Автоматическая оптимизация изображений в Bitrix: WebP и ресайз без лишних движений

Автоматическая оптимизация изображений в Bitrix: WebP и ресайз без лишних движений
10 сентября 2025
3 мин.
312
28 сентября 2025

Зачем всё это

Когда мы публикуем статьи на сайте, про оптимизацию изображений часто забывают. А зря: сжатие и ресайз заметно ускоряют загрузку страниц. Хорошая новость — это можно автоматизировать программно.

Класс для преобразования изображений

/**
 * /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:

require $_SERVER['DOCUMENT_ROOT'] . '/local/php_interface/PictureWrapper.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']);
}