Автоматическая конвертация изображений в WebP при загрузке в 1С-Битрикс

6 мин чтения
Формат WebP позволяет уменьшить размер изображений и сократить объём данных, загружаемых пользователем при открытии страниц сайта.

В 1С-Битрикс изображения можно автоматически преобразовывать в WebP ещё до их сохранения в файловой системе.

Рассмотрим вариант для стандартных полей элемента инфоблока:

PREVIEW_PICTURE — изображение для анонса;
DETAIL_PICTURE — детальное изображение.

Для обработки будем использовать события OnBeforeIBlockElementAdd и OnBeforeIBlockElementUpdate. Они вызываются до сохранения элемента и позволяют изменить данные загружаемого файла.

Как работает конвертация

При добавлении или изменении элемента обработчик проверяет поля PREVIEW_PICTURE и DETAIL_PICTURE.

Если пользователь загрузил JPEG или PNG, изображение открывается средствами библиотеки GD и сохраняется во временный файл WebP.

После этого в массиве файла изменяются:

name — имя файла;
type — MIME-тип;
tmp_name — путь к временному файлу;
size — размер нового файла.

Далее 1С-Битрикс сохраняет WebP штатным файловым механизмом.

Если изображение невозможно преобразовать, исходный файл остаётся без изменений.

Код для init.php

Код можно разместить, например, в файле:


/local/php_interface/init.php


<?php

use Bitrix\Main\EventManager;

$eventManager = EventManager::getInstance();

$eventManager->addEventHandlerCompatible(
    'iblock',
    'OnBeforeIBlockElementAdd',
    ['ProjectWebpConverter', 'handle']
);

$eventManager->addEventHandlerCompatible(
    'iblock',
    'OnBeforeIBlockElementUpdate',
    ['ProjectWebpConverter', 'handle']
);

final class ProjectWebpConverter
{
    private const QUALITY = 80;

    private const IMAGE_FIELDS = [
        'PREVIEW_PICTURE',
        'DETAIL_PICTURE',
    ];

    public static function handle(array &$fields): void
    {
        foreach (self::IMAGE_FIELDS as $fieldName) {
            if (
                empty($fields[$fieldName])
                || !is_array($fields[$fieldName])
            ) {
                continue;
            }

            $fields[$fieldName] = self::convertToWebp(
                $fields[$fieldName]
            );
        }
    }

    private static function convertToWebp(array $file): array
    {
        if (!empty($file['del'])) {
            return $file;
        }

        if (
            isset($file['error'])
            && (int)$file['error'] !== UPLOAD_ERR_OK
        ) {
            self::log(
                'Ошибка загрузки файла, код: '
                . (int)$file['error']
            );

            return $file;
        }

        $tmpName = (string)($file['tmp_name'] ?? '');

        if (
            $tmpName === ''
            || !is_file($tmpName)
            || !is_readable($tmpName)
        ) {
            return $file;
        }

        if (!self::isWebpSupported()) {
            self::log(
                'Конвертация пропущена: GD не поддерживает WebP'
            );

            return $file;
        }

        $imageInfo = @getimagesize($tmpName);

        if ($imageInfo === false) {
            self::log(
                'Не удалось определить параметры изображения: '
                . $tmpName
            );

            return $file;
        }

        $mimeType = (string)($imageInfo['mime'] ?? '');

        if ($mimeType === 'image/webp') {
            return $file;
        }

        if (!in_array(
            $mimeType,
            ['image/jpeg', 'image/png'],
            true
        )) {
            return $file;
        }

        $image = null;
        $webpTempFile = null;

        try {
            switch ($mimeType) {
                case 'image/jpeg':
                    $image = @imagecreatefromjpeg($tmpName);
                    break;

                case 'image/png':
                    $image = @imagecreatefrompng($tmpName);
                    break;
            }

            if (!$image) {
                self::log(
                    'GD не удалось открыть изображение: '
                    . $tmpName
                );

                return $file;
            }

            if ($mimeType === 'image/png') {
                if (function_exists('imagepalettetotruecolor')) {
                    imagepalettetotruecolor($image);
                }

                imagealphablending($image, false);
                imagesavealpha($image, true);
            }

            $webpTempFile = tempnam(
                sys_get_temp_dir(),
                'bx_webp_'
            );

            if ($webpTempFile === false) {
                self::log(
                    'Не удалось создать временный WebP-файл'
                );

                return $file;
            }

            $result = @imagewebp(
                $image,
                $webpTempFile,
                self::QUALITY
            );

            imagedestroy($image);
            $image = null;

            if (
                !$result
                || !is_file($webpTempFile)
                || filesize($webpTempFile) === 0
            ) {
                @unlink($webpTempFile);

                self::log(
                    'Не удалось сохранить изображение в WebP'
                );

                return $file;
            }

            $originalName = (string)($file['name'] ?? 'image');

            $baseName = pathinfo(
                $originalName,
                PATHINFO_FILENAME
            );

            if ($baseName === '') {
                $baseName = 'image';
            }

            $file['name'] = $baseName . '.webp';
            $file['type'] = 'image/webp';
            $file['tmp_name'] = $webpTempFile;
            $file['size'] = filesize($webpTempFile);
            $file['error'] = UPLOAD_ERR_OK;

            register_shutdown_function(
                static function () use ($webpTempFile): void {
                    if (is_file($webpTempFile)) {
                        @unlink($webpTempFile);
                    }
                }
            );

            self::log(
                sprintf(
                    'Изображение %s преобразовано в %s',
                    $originalName,
                    $file['name']
                )
            );

            return $file;

        } catch (\Throwable $e) {

            if ($image) {
                @imagedestroy($image);
            }

            if (
                $webpTempFile
                && is_file($webpTempFile)
            ) {
                @unlink($webpTempFile);
            }

            self::log(
                'Ошибка конвертации WebP: '
                . $e->getMessage()
            );

            return $file;
        }
    }

    private static function isWebpSupported(): bool
    {
        if (
            !extension_loaded('gd')
            || !function_exists('imagewebp')
            || !function_exists('gd_info')
        ) {
            return false;
        }

        $gdInfo = gd_info();

        return !empty($gdInfo['WebP Support']);
    }

    private static function log(string $message): void
    {
        if (function_exists('AddMessage2Log')) {
            AddMessage2Log(
                $message,
                'webp-converter'
            );
        }
    }
}

Какие форматы обрабатываются

В данном примере автоматически преобразуются:

JPEG → WebP
PNG → WebP

Если загружается уже готовый WebP, повторная конвертация не выполняется.

Другие форматы остаются без изменений.

Почему GIF не конвертируется

GIF намеренно исключён из автоматической обработки.

Стандартная библиотека GD при открытии анимированного GIF работает только с одним кадром. Поэтому обычная конвертация такого файла может превратить анимацию в статичное изображение.

Если на сайте используются только статичные GIF, их обработку можно добавить отдельно.

Проверка поддержки WebP в GD

Одного наличия расширения GD недостаточно. В установленной на сервере сборке должна быть включена поддержка WebP.

Для проверки можно временно выполнить:


<?php

echo '<pre>';
print_r(gd_info());
echo '</pre>';

В полученном массиве должно присутствовать значение:


[WebP Support] => 1

После проверки диагностический код необходимо удалить.

Качество WebP

Качество итогового изображения задаётся константой:


private const QUALITY = 80;

Значение 80 является хорошей отправной точкой для большинства фотографий.

При необходимости качество можно увеличить или уменьшить в зависимости от требований проекта.

Пример изменения массива файла

До обработки файл может выглядеть примерно так:


[
    'name' => 'product-photo.jpg',
    'type' => 'image/jpeg',
    'tmp_name' => '/tmp/php12345',
    'size' => 245812,
    'error' => 0,
    'MODULE_ID' => 'iblock',
]

После успешной конвертации:


[
    'name' => 'product-photo.webp',
    'type' => 'image/webp',
    'tmp_name' => '/tmp/bx_webp_xxxxx',
    'size' => 128540,
    'error' => 0,
    'MODULE_ID' => 'iblock',
]

Остальные служебные значения массива файла при этом сохраняются.

Что необходимо проверить на сервере

Перед использованием решения необходимо проверить наличие GD и поддержки WebP.

Также стоит обратить внимание на параметры PHP:


upload_max_filesize
post_max_size
memory_limit

Особенно важен memory_limit.

JPEG и PNG хранятся в сжатом виде, но при обработке через GD изображение полностью распаковывается в оперативную память. Поэтому большая фотография может потребовать значительно больше памяти, чем занимает исходный файл на диске.

Также PHP должен иметь возможность записывать файлы во временный каталог, возвращаемый функцией:


sys_get_temp_dir()

Особенность JPEG и EXIF

При повторном сохранении JPEG через GD исходные EXIF-метаданные не сохраняются.

Это особенно важно для фотографий со смартфонов. В некоторых файлах правильная ориентация изображения хранится в EXIF, а само изображение физически записано в другом положении.

Если пользователи сайта часто загружают фотографии непосредственно с телефонов, необходимо отдельно протестировать вертикальные снимки.

При необходимости перед конвертацией следует добавить обработку EXIF Orientation.

Какие поля обрабатываются

Приведённый обработчик работает только со стандартными полями элемента инфоблока:


PREVIEW_PICTURE
DETAIL_PICTURE

Он не преобразует автоматически:
  • изображения разделов;
  • свойства инфоблока типа «Файл»;
  • пользовательские поля;
  • дополнительные изображения товара;
  • другие файловые поля.
Для таких файлов необходимо использовать соответствующие события или отдельную обработку.

Как проверить работу

После подключения обработчика создайте тестовый элемент инфоблока и загрузите JPEG или PNG в изображение анонса или детальное изображение.

После сохранения проверьте:
  1. Расширение сохранённого файла.
  2. MIME-тип изображения.
  3. Наличие WebP-файла в каталоге upload.
  4. Корректность отображения изображения на сайте.
  5. Сохранение прозрачности PNG.
  6. Загрузку уже готового WebP.
  7. Удаление существующего изображения.
  8. Загрузку большого изображения.
Также полезно протестировать изменение уже существующего элемента инфоблока.

Логирование

В примере используется стандартная функция 1С-Битрикс:


AddMessage2Log()

Она позволяет зафиксировать ошибки конвертации и успешную обработку файлов.

После проверки решения подробное логирование успешных операций можно удалить и оставить только запись ошибок.

Итог

Обработчики OnBeforeIBlockElementAdd и OnBeforeIBlockElementUpdate позволяют автоматически преобразовывать JPEG и PNG в WebP ещё до сохранения изображения в 1С-Битрикс.

Пользователь при этом продолжает работать с обычной формой загрузки файлов, а преобразование выполняется автоматически на стороне сервера.

Если конвертация невозможна или сервер не поддерживает WebP, обработчик возвращает исходный файл без изменений.

Такой подход позволяет внедрить WebP без необходимости изменять стандартный процесс загрузки изображений в административной части сайта.
Вернуться к блогу