Когда мы публикуем статьи на сайте, про оптимизацию изображений часто забывают.
А зря: сжатие и ресайз заметно ускоряют загрузку страниц. Хорошая новость — это можно автоматизировать программно.
Класс для преобразования изображений
/**
* /local/php_interface/PictureWrapper.php
*
* Самодостаточный класс:
* - Оборачивает в с
* - Генерирует WebP и делает ресайз по длинной стороне до 1000 (по умолчанию)
* - Сохраняет прозрачность PNG/GIF, учитывает EXIF-ориентацию JPEG
* - Работает с URL вида /images/... и с путями, содержащими %20
*
* Требования: PHP-GD с поддержкой imagewebp().
*/
class PictureWrapper
{
/**
* Преобразовать HTML: найти , сгенерировать webp (и ресайз), обернуть в .
*/
public static function wrapImagesWithPicture(string $html, int $maxSide = 1000, int $quality = 85): string
{
if ($html === '' || stripos($html, ''
. $html . '';
$dom->loadHTML($wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
libxml_clear_errors();
$xpath = new \DOMXPath($dom);
/** @var \DOMElement $img */
foreach ($xpath->query('//img') as $img) {
// Пропускаем, если уже внутри
if ($img->parentNode && strtolower($img->parentNode->nodeName) === 'picture') {
continue;
}
$src = trim((string)$img->getAttribute('src'));
if ($src === '' || strpos($src, 'data:') === 0) {
continue;
}
$relative = self::toRelativePath($src);
$webpSrc = null;
if ($relative && self::isLocalPath($relative)) {
// Генерируем WebP с ресайзом
$webp = self::makeWebpAndResize($relative, /*rewrite*/ false, $quality, $maxSide);
if ($webp) {
$webpSrc = $webp;
}
}
// Собираем
$picture = $dom->createElement('picture');
if ($webpSrc) {
$source = $dom->createElement('source');
$source->setAttribute('type', 'image/webp');
$source->setAttribute('srcset', $webpSrc);
$picture->appendChild($source);
}
// Улучшения для
if (!$img->hasAttribute('loading')) {
$img->setAttribute('loading', 'lazy');
}
if (!$img->hasAttribute('decoding')) {
$img->setAttribute('decoding', 'async');
}
// Вставляем на место
$img->parentNode->replaceChild($picture, $img);
$picture->appendChild($img);
}
// Вернём содержимое
$body = $dom->getElementsByTagName('body')->item(0);
$result = '';
foreach ($body->childNodes as $child) {
$result .= $dom->saveHTML($child);
}
return $result;
}
/**
* Генерация WebP + ресайз по длинной стороне.
* Вернёт относительный путь к .webp или false.
*/
public static function makeWebpAndResize(string $src, bool $rewrite = false, int $quality = 85, int $maxSide = 1000)
{
if (!$src || !function_exists('imagewebp')) {
return false;
}
$docRoot = rtrim($_SERVER['DOCUMENT_ROOT'] ?? '', '/');
// Снимаем query и декодируем %XX
$srcClean = explode('?', $src, 2)[0];
$srcFS = $docRoot . urldecode($srcClean);
if (!is_file($srcFS)) {
return false;
}
$info = getimagesize($srcFS);
if ($info === false) {
return false;
}
[$w, $h, $type] = $info;
// Куда сохраняем .webp (регистронезависимая замена)
$webpRel = preg_replace('~\\.(jpe?g|png|gif)$~i', '.webp', $srcClean);
if (!$webpRel) {
$webpRel = $srcClean . '.webp';
}
$webpFS = $docRoot . urldecode($webpRel);
if (is_file($webpFS) && !$rewrite) {
return $webpRel;
}
// Загружаем исходник
switch ($type) {
case IMAGETYPE_JPEG:
$im = @imagecreatefromjpeg($srcFS);
// EXIF-ориентация
if (function_exists('exif_read_data')) {
$exif = @exif_read_data($srcFS);
if (!empty($exif['Orientation'])) {
$o = (int)$exif['Orientation'];
if ($o === 3) { $im = imagerotate($im, 180, 0); }
elseif ($o === 6) { $im = imagerotate($im, -90, 0); $tmp=$w; $w=$h; $h=$tmp; }
elseif ($o === 8) { $im = imagerotate($im, 90, 0); $tmp=$w; $w=$h; $h=$tmp; }
}
}
break;
case IMAGETYPE_PNG:
$im = @imagecreatefrompng($srcFS);
imagepalettetotruecolor($im);
imagealphablending($im, false);
imagesavealpha($im, true);
break;
case IMAGETYPE_GIF:
$im = @imagecreatefromgif($srcFS);
break;
default:
return false;
}
if (!$im) {
return false;
}
// Ресайз (contain) до $maxSide по длинной стороне
if ($maxSide && max($w, $h) > $maxSide) {
if ($w >= $h) {
$tw = $maxSide;
$th = (int)round($h * ($maxSide / $w));
} else {
$th = $maxSide;
$tw = (int)round($w * ($maxSide / $h));
}
$dst = imagecreatetruecolor($tw, $th);
// Прозрачность для PNG/GIF
if ($type === IMAGETYPE_PNG || $type === IMAGETYPE_GIF) {
imagealphablending($dst, false);
imagesavealpha($dst, true);
$transparent = imagecolorallocatealpha($dst, 0, 0, 0, 127);
imagefilledrectangle($dst, 0, 0, $tw, $th, $transparent);
}
imagecopyresampled($dst, $im, 0, 0, 0, 0, $tw, $th, $w, $h);
imagedestroy($im);
$im = $dst;
}
// Гарантируем наличие директории
$dir = dirname($webpFS);
if (!is_dir($dir)) {
@mkdir($dir, 0755, true);
}
// Сохраняем webp
$ok = imagewebp($im, $webpFS, $quality);
imagedestroy($im);
return ($ok && is_file($webpFS)) ? $webpRel : false;
}
/**
* Конвертирует абсолютный URL на наш домен → относительный путь; оставляет относительные как есть.
*/
protected static function toRelativePath(string $src): ?string
{
// Относительный — сразу возвращаем
if (strpos($src, 'http://') !== 0 && strpos($src, 'https://') !== 0) {
return $src;
}
$host = $_SERVER['HTTP_HOST'] ?? '';
$parsed = parse_url($src);
if (!$parsed || empty($parsed['host'])) {
return null;
}
if ($host && $parsed['host'] !== $host) {
return null; // внешние не трогаем
}
$path = ($parsed['path'] ?? '');
$query = isset($parsed['query']) ? ('?' . $parsed['query']) : '';
return $path . $query;
}
/**
* Разрешаем локальные пути. Добавлен /images/.
*/
protected static function isLocalPath(string $relative): bool
{
return strpos($relative, '/upload/') === 0
|| strpos($relative, '/bitrix/') === 0
|| strpos($relative, '/content/') === 0
|| strpos($relative, '/images/') === 0; // важно для вашего кейса
}
}
Подключение
Подключите класс, например, в /local/php_interface/init.php:
Находим компонент, в котором хотим оптимизировать контент, например: /local/templates/site/components/bitrix/news/articles/bitrix/news.detail/.default/
Добавляем код в result_modifier.php:
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();
if (class_exists(PictureWrapper::class)) {
$arResult['~DETAIL_TEXT'] = PictureWrapper::wrapImagesWithPicture($arResult['~DETAIL_TEXT']);
}
Мы используем файлы cookie, разработанные нашими специалистами и третьими лицами, для анализа событий на нашем веб-сайте,
что позволяет нам улучшать взаимодействие с пользователями и обслуживание. Продолжая просмотр страниц нашего сайта,
вы принимаете условия его использования. Более подробные сведения смотрите в нашей
Политике в отношении файлов Cookie.