Intervention Image: обработка изображений в PHP — мастерская и 9 задач
Подготовка изображений для сайта: сравнение до/после, реальные PNG/JPEG/WebP, ошибки и пакетная обработка. Девять задач и PHP-исходники.

Изображения для сайта в PHP: scale, crop, contain, EXIF, WebP, прозрачность и пакетная обработка. Девять задач с реальными файлами и исходниками.
Intervention Image: превратить загрузку в готовую картинку для сайта
Пользователь загрузил изображение, но для интерфейса оно ещё не готово. Каталогу нужна одинаковая рамка карточек, списку — маленькое превью, странице товара — подробная версия, а логотипу — сохранённая прозрачность. Если просто уменьшить фотографию стилем CSS, браузер всё равно может скачать большой оригинал. Если растянуть её до нужного прямоугольника, круг станет овалом, а предмет потеряет пропорции.
Intervention Image даёт PHP-приложению понятный способ читать растровое изображение, менять геометрию, накладывать другой слой и сохранять результат в выбранном формате. В статье работаем без Laravel: обычный Composer-проект, PHP и драйвер GD. Разбираем не только успешное преобразование, но и повреждённый файл, неверное качество, лимит пикселей и частичный результат пакета.
В мастерской девять задач и двадцать четыре варианта с настоящими выходными файлами. Можно сравнить изображение до и после, увидеть размеры и число байтов, переключиться между результатами Windows и Linux, скачать картинку, JSON и исходный PHP. Практическая цель — выбрать рецепт обработки под свой интерфейс и затем воспроизвести его на своих материалах.
Где использовать: каталог, публикации, кабинет и импорт
В интернет-магазине нужны ровные карточки, но важный объект нельзя случайно отрезать. Для предмета на прозрачном фоне часто подходит помещение целиком в рамку. Для фотографий интерьера удобнее заполненная карточка с осмысленным кадрированием. Это разные требования; один универсальный вызов для всего медиакаталога обычно приводит к компромиссам, которые заметны посетителю.
В личном кабинете обработчик создаёт аватар, миниатюру вложения или обложку проекта. В редакции сайта он формирует несколько размеров публикации. При импорте поставщика проверяет вход, нормализует ориентацию и строит производные заранее, чтобы страница не ждала тяжёлую обработку при каждом открытии. Результат хранится отдельно от оригинала и получает собственное имя.
Сначала определите потребителя файла: где он отображается, сколько места занимает, допустима ли обрезка и нужен ли прозрачный фон. Затем задайте ограниченный набор пресетов. Названия card, thumb и fit понятнее десятков произвольных параметров в URL. Версия пресета позволяет позже изменить качество или рамку, сохранив воспроизводимость уже выданных изображений.
Версия 4, PHP 8.3 и проверка окружения
Проект использует Intervention Image 4.4.1 и intervention/gif 5.0.1; точный набор хранится в composer.lock. Для версии 4 требуется PHP не ниже 8.3. Нужны mbstring и графический драйвер; в нашей лаборатории это GD с JPEG, PNG и WebP, дополнительно EXIF и fileinfo. Оболочка мастерской использует локальный Bootstrap 5.3.9. Проверка версий выполнена 13 ноября 2026 года.
Не смешивайте примеры разных поколений библиотеки. Здесь используются ImageManager, decodePath(), Format и именованные параметры версии 4. Старый материал с другим методом создания менеджера или сохранения может относиться к другой ветке. Сравнивайте код с официальной установкой v4, а не только с похожим названием пакета.
Для нового приложения Composer выбирает разрешённую версию по ограничению. Для скачанного проекта install восстанавливает lock, поэтому запуск лучше начинать именно с него. Сам пакет не устанавливает системную поддержку JPEG или WebP: это свойства расширения GD. Наличие PHP в консоли ещё не означает, что нужные кодеки доступны этому процессу.
composer install --no-interaction --no-plugins --no-scripts
composer check-platform-reqs
php -v
php --ini
php -m
php -r 'var_export(gd_info());'
Мини-мастерская: что выполнено на серверной стороне
Каждый показанный результат действительно создан PHP с Intervention Image и GD. В публичной мастерской сохранены отдельные наборы Windows и Linux. Посетитель выбирает готовый вариант и рассматривает соответствующий файл; новая обработка фотографии в браузере или на хостинге не запускается. Это прямо обозначено в верхней части интерфейса. Для своих файлов предусмотрен CLI в скачиваемом архиве.
Выберите задачу, затем вариант. У пакетного опыта дополнительно переключается конкретный выходной файл. Рядом с изображениями видны реальные размеры, MIME и байты. Подложка помогает проверить прозрачность, а режим ста процентов показывает настоящие пиксели с прокруткой. В режиме «Уместить» интерфейс масштабирует только просмотр: маленькое превью не становится файлом высокого разрешения.
Для отрицательного варианта картинка не подменяется старым успешным результатом: показано отсутствие выходного файла и фактическая ошибка. Под изображениями раскрывается JSON, ниже открывается исходник сценария. Скачайте нужный результат и сравните его хеш с записью. SHA-256 подтверждает целостность конкретного файла, но не является цифровой подписью или доказательством доверия к автору.
Менеджер, драйвер и сохранность оригинала
Менеджер определяет используемый графический движок и настройки чтения. Для лаборатории выбраны автоматическая ориентация, отключение декодирования анимации и явный фон для форматов без прозрачности. Это решение для небольшого набора статических растровых примеров. Оно не означает, что любой GIF, SVG, TIFF или файл с цветовым профилем будет обработан по требованиям вашей системы.
Каждый независимый результат следует получать из оригинала. Если сначала уменьшить изображение до миниатюры, а затем из изменённого объекта делать большую карточку, потерянные детали не вернутся. Поэтому наши сценарии заново читают fixture. В рабочем каталоге полезно отдельно хранить оригиналы, производные и версию рецепта, связывающую их между собой.
Не сохраняйте результат поверх загруженного оригинала без осознанного решения. В CLI проекта существующее назначение отклоняется, а готовые данные сначала пишутся во временный файл той же директории. Такая дисциплина упрощает повтор обработки и откат неудачного пресета. Детали выбора движка доступны в описании драйверов и конфигурации.
use Intervention\Image\ImageManager;
use Intervention\Image\Drivers\Gd\Driver;
use Intervention\Image\Format;
$manager = new ImageManager(
Driver::class,
autoOrientation: true,
decodeAnimation: false,
backgroundColor: '133e39',
strip: true,
);
$image = $manager->decodePath('fixtures/card.png');
Опыт 1. Масштабирование без искажения и без увеличения
В первой задаче карточка размером 1200×800 уменьшается до ширины 320. Результат получает высоту 213 пикселей с округлением к целой сетке. Центральный круг остаётся визуально круглым. Это хороший вариант для превью, где важнее показать весь исходник, чем заполнить конкретную рамку без полей.
Во втором варианте запрашивается ширина 1600 через scaleDown. Исходник не увеличивается: итог остаётся 1200×800. Такая политика помогает не выдавать растянутую маленькую картинку за подробную версию. Она не запрещает интерфейсу увеличить её стилем, поэтому собственные размеры файла всё равно нужно учитывать в разметке и выборе srcset.
Третий вариант намеренно использует resize(320, 320). Прямоугольник становится квадратом, но содержание меняет пропорции. Это полезный отрицательный пример: одинаковые числа в CSS или параметрах функции ещё не означают корректную подготовку фотографии. Для большинства товарных изображений такое растяжение нежелательно; оставьте его задачам, где изменение геометрии действительно задумано.
$image = $manager->decodePath('fixtures/card.png');
$image->scaleDown(width: 320); // 320 × 213
$original = $manager->decodePath('fixtures/card.png');
$original->scaleDown(width: 1600); // остаётся 1200 × 800
$distorted = $manager->decodePath('fixtures/card.png');
$distorted->resize(320, 320); // намеренное изменение пропорций
Опыт 2. Cover: одинаковые карточки и выбор области
Для ряда карточек часто нужен строго одинаковый размер. В опыте cover создаёт квадрат 360×360, который полностью заполнен содержимым. Цена такого заполнения — потеря части исходного кадра. Это видно на нашей тестовой карточке: слева и справа находятся разные объекты, а в центре контрольный круг.
Переключите центр, левый и правый край. Размер выходного файла одинаковый, но выбранный сюжет меняется. Пример особенно полезен для каталога новостей: центральная обрезка может убрать человека, заголовок на фотографии или важную деталь товара. Размер сам по себе не описывает хороший результат; нужно определить смысловой центр кадра.
Если приложение хранит точку интереса, можно заранее выбрать подходящее выравнивание или рассчитать собственные координаты кадрирования. Не обещайте автоматическое понимание содержания: наш сценарий не распознаёт лица и не выбирает главный предмет нейросетью. Он показывает детерминированную геометрию. Для маленьких оригиналов отдельно решите, допустимо ли увеличение, и рассмотрите ограничивающие варианты методов.
use Intervention\Image\Alignment;
$image = $manager->decodePath('fixtures/card.png');
$image->cover(360, 360, alignment: Alignment::RIGHT);
$image->encodeUsingFormat(Format::PNG)->save('output/right.png');
Опыт 3. Contain: сохранить весь объект и добавить поля
Задача contain решает другую проблему: весь исходник должен остаться внутри заданной рамки. В квадрате 360×360 наша прямоугольная карточка занимает центральную область, а свободное пространство превращается в поля. Это полезно для технических изображений, логотипов, схем и товаров, у которых нельзя отрезать края.
В мастерской доступны светлый и прозрачный фон. У прозрачного варианта переключите подложку: поля примут цвет окружающего интерфейса, потому что в файле на этом месте нет непрозрачной заливки. У светлого варианта цвет полей останется прежним. На белой странице оба результата могут показаться похожими, поэтому проверка только на одном фоне легко пропускает проблему.
Прозрачность задаётся корректным цветовым значением, а не произвольным словом. В проекте используется rgba(0,0,0,0), проверенное фактическим запуском версии 4.4.1. Для визуальной проверки есть шахматная сетка; в JSON дополнительно указан alpha контрольного пикселя. Так различие подтверждается содержимым файла, а не только внешним видом браузера.
$image = $manager->decodePath('fixtures/card.png');
$image->contain(360, 360, background: 'rgba(0,0,0,0)');
$image->encodeUsingFormat(Format::PNG)->save('output/contained.png');
Опыт 4. Crop: прямоугольник по точным координатам
Иногда область уже известна: пользователь выделил прямоугольник, импорт передал координаты или приложение вырезает стандартный фрагмент шаблона. Тогда полезно отделить crop от масштабирования. В нашей задаче выбирается область 400×400 со смещением y=100. Варианты x=0 и x=800 показывают разные части исходной карточки.
Координаты относятся к пикселям изображения на том этапе, когда выполняется операция. Если интерфейс показывал уменьшенное превью, координаты выделения надо пересчитать в исходную систему. Если сначала применена EXIF-ориентация или масштабирование, размеры и оси могли измениться. Сохраняйте вместе с выделением сведения о том, относительно какой версии изображения оно рассчитано.
Проверяйте границы прямоугольника до обработки: ширину, высоту, отрицательные смещения и выход за пределы. В учебных вариантах используются заранее допустимые значения. Для произвольного запроса нужна отдельная валидация. Формат «пользователь прислал четыре числа» не гарантирует корректную область и не определяет, нужно ли обрезать по границе или добавить фон.
$image = $manager->decodePath('fixtures/card.png');
$image->crop(400, 400, x: 800, y: 100);
$image->encodeUsingFormat(Format::PNG)->save('output/fragment.png');
Опыт 5. EXIF: почему фотография иногда лежит на боку
JPEG может хранить пиксели в одной ориентации, а метаданные — указывать способ отображения. Тогда разные инструменты показывают тот же файл по-разному. В нашей авторской фикстуре сырые пиксели имеют размер 800×1200, а EXIF Orientation=6 предписывает поворот. Это не чужая фотография: тестовый файл создан из геометрической карточки и не содержит координат GPS.
Отключённая автоматическая ориентация оставляет 800×1200. Включённая даёт правильную горизонтальную карточку 1200×800. Для честного сравнения слева показан PNG сырых пикселей без EXIF: иначе сам браузер мог бы исправить ориентацию JPEG до того, как посетитель увидит различие. Исходный JPEG также включён в архив.
Ориентацию удобно нормализовать перед расчётом размеров и crop. Не применяйте поворот второй раз только потому, что сохранили старое значение метаданных отдельно. После изменения пикселей информация о преобразовании должна соответствовать результату. Драйвер GD при кодировании не сохраняет EXIF; для задачи сохранения профилей и метаданных нужен отдельный проверенный процесс.
$raw = new ImageManager(Driver::class, autoOrientation: false);
$correct = new ImageManager(Driver::class, autoOrientation: true);
$a = $raw->decodePath('fixtures/orientation-6.jpg'); // 800 × 1200
$b = $correct->decodePath('fixtures/orientation-6.jpg'); // 1200 × 800
Опыт 6. JPEG, WebP и качество: измерять конкретный результат
Три варианта используют один размер 600×400: JPEG с quality 85, WebP с quality 80 и WebP с quality 35. Каждый результат действительно закодирован нужным кодеком, а MIME проверен по содержимому. В интерфейсе выводится число байтов полученного файла. Разница между изображением на экране и его размером на диске становится наглядной.
Качество не является общей шкалой для всех форматов. Значение 80 у JPEG и WebP не обещает одинаковую потерю деталей. Маленький вес также не равен хорошему результату: проверьте мелкий текст, контрастные границы, шум, градиенты и артефакты возле линий. Авторская карточка удобна для таких границ, но не заменяет испытание на фотографиях вашего проекта.
В наших данных более низкое качество WebP даёт меньший файл. Это результат конкретного опыта, а не обещание фиксированного процента экономии. Версии GD и кодеков могут изменить размер и байты между Windows и Linux. Публикуйте размеры фактических файлов; не рассчитывайте их по абстрактной формуле от качества.
$image = $manager->decodePath('fixtures/card.png')->scaleDown(width: 600);
$image->encodeUsingFormat(Format::JPEG, quality: 85)->save('output/card.jpg');
$image->encodeUsingFormat(Format::WEBP, quality: 80)->save('output/card.webp');
// encodeUsingFormat выбирает кодек; расширение должно ему соответствовать.
Опыт 7. PNG/WebP с alpha и JPEG с явным фоном
Прозрачный логотип нельзя бездумно сохранить в JPEG: у этого формата нет такого альфа-канала. Приложение должно решить, какой фон появится на месте прозрачных областей. В мастерской PNG и WebP сохраняют прозрачный угол, а JPEG смешивается с тёмным фоном #133e39, заданным менеджеру.
Откройте опыт и переключите светлую, тёмную и шахматную подложки. У PNG/WebP внешний фон просвечивает. У JPEG окрашенный прямоугольник остаётся частью самого файла. Такое поведение особенно важно для логотипов в светлой и тёмной теме: заранее запечённый белый фон на тёмной странице выглядит как посторонняя наклейка.
В JSON контрольный alpha измерен через GD: значение 127 означает полную прозрачность, а 0 — непрозрачность. Это шкала представления GD, её не следует путать с CSS opacity от нуля до единицы. Для JPEG тест дополнительно проверяет цвет угла с небольшим допуском на сжатие, поэтому настройка фона подтверждена фактическими пикселями.
$manager = new ImageManager(Driver::class, backgroundColor: '133e39');
$image = $manager->decodePath('fixtures/transparent.png');
$image->encodeUsingFormat(Format::PNG)->save('output/logo.png');
$image->encodeUsingFormat(Format::JPEG, quality: 85)->save('output/logo.jpg');
Опыт 8. Водяной знак: размер, отступы и прозрачность
Водяной знак полезен для обозначения источника, статуса черновика или принадлежности внутренней подборке. В примере используется собственная плашка AE LAB, которую PHP уменьшает до ширины 144 и размещает в правом нижнем углу с отступом 16. Основное изображение перед этим получает ширину 600.
Два варианта отличаются прозрачностью вставки: .25 и 1. В API параметр называется transparency, но его границы описывают видимость слоя: ноль полностью прозрачен, единица непрозрачна. На практике это стоит проверять на светлом и тёмном участке кадра. Слишком слабая маркировка исчезнет, а слишком крупная закроет полезный объект.
Масштаб водяного знака лучше связывать с итоговой картинкой, а не с произвольным размером исходного логотипа. Создавайте его после выбора конечной геометрии, чтобы отступы были предсказуемыми. Такая вставка не мешает скачать изображение и не гарантирует защиту авторских прав: её задача в этом примере — понятная визуальная маркировка.
$image = $manager->decodePath('fixtures/card.png')->scaleDown(width: 600);
$mark = $manager->decodePath('fixtures/watermark.png')->scale(width: 144);
$image->insert($mark, x: 16, y: 16,
alignment: Alignment::BOTTOM_RIGHT, transparency: .25);
$image->encodeUsingFormat(Format::PNG)->save('output/marked.png');
Опыт 9. Пакет и частичная ошибка: не потерять хорошие результаты
Обычно обработчик получает не одну идеальную картинку, а набор файлов. Первый вариант формирует три производные шириной 160, 320 и 640 из одного оригинала. Во втором на месте среднего элемента находится намеренно повреждённый JPEG. Настоящий декодер Intervention отклоняет его, а остальные два результата сохраняются.
Это важная граница обработки ошибки. Если исключение перехвачено только вокруг всего пакета, один дефект может остановить работу остальных элементов. Если исключение проглочено внутри цикла без отчёта, пользователь получит неполную коллекцию без объяснения. Здесь каждый элемент имеет собственный статус, а пакет явно обозначен как частичный результат.
Последние два варианта показывают ошибочное quality=101 и отказ нашего ограничителя пикселей. Первая ошибка принадлежит библиотеке, вторая — политике приложения. Они различаются классом и причиной. В production полезно сохранять эту разницу в журнале и сообщении пользователю, чтобы повтор задачи не маскировал исправимую ошибку параметров или неподходящий вход.
foreach ([160, 320, 640] as $width) {
try {
$image = $manager->decodePath($input)->scaleDown(width: $width);
$image->encodeUsingFormat(Format::WEBP, quality: 80)
->save("output/item-{$width}.webp");
} catch (\Intervention\Image\Exceptions\ImageException $e) {
$errors[] = ['width' => $width, 'type' => $e::class];
}
}
Входные данные: байты, MIME, пиксели и допустимые форматы
В рабочем CLI разрешены только локальные читаемые JPEG, PNG и WebP. Сначала проверяются существование файла, размер в байтах, MIME через fileinfo и размеры заголовка. Только затем данные передаются декодеру. Ограничение в 8 MiB и двадцать миллионов пикселей — пример политики приложения; под свой сервис его нужно подобрать по нагрузке и доступной памяти.
Расширение .jpg не доказывает, что внутри JPEG. Наш broken.jpg специально содержит обычный текст. И наоборот, успешное чтение заголовка не гарантирует, что весь поток корректно декодируется. Поэтому предварительная проверка и обработка исключения после неё нужны одновременно. Это разные стадии, которые отвечают на разные вопросы.
Здесь не принимаются сетевые URL и произвольные форматы. Добавление загрузки по адресу потребует отдельной политики доступа, ограничений размера, перенаправлений и защиты от обращений к внутренним сервисам. Библиотека изображений не выполняет эту работу за приложение. Аналогично небольшой сжатый файл может потребовать заметную память после декодирования; лимит байтов не заменяет лимит пикселей.
$path = realpath($input);
if ($path === false || !is_file($path)) {
throw new InvalidArgumentException('Local file required');
}
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($path);
if (!in_array($mime, ['image/jpeg', 'image/png', 'image/webp'], true)) {
throw new InvalidArgumentException('Unsupported input format');
}
// Полный guard с byte/pixel budget находится в src/Pipeline.php.
Сохранение результата: имя, временный файл и параллельность
Путь назначения — часть контракта, а не случайная строка из формы. CLI требует расширение .webp, существующую доступную папку и отсутствие готового файла с таким именем. Результат сначала кодируется во временный файл в той же директории, затем перемещается на целевое место. При ошибке собственный временный файл удаляется.
Это защищает от обычной ситуации, когда посетитель увидел наполовину записанное изображение. Однако пример не реализует межпроцессную блокировку одного назначения. Два одновременно запущенных задания могут конкурировать между проверкой существования и переименованием. Для сервиса используйте уникальные имена, идентификатор версии или внешнюю сериализацию задач.
Оригинал не должен менять имя только потому, что обновился производный формат. Храните связь: ID исходного объекта, версия рецепта, размер, MIME, путь, состояние обработки. Тогда можно заново сформировать одну производную, удалить устаревшую версию или объяснить, почему картинка не появилась. Для таких операций удобно сочетать обработчик с очередью и журналом задач.
php bin/convert.php fixtures/card.png output/card.webp card
php bin/convert.php fixtures/card.png output/thumb.webp thumb
php bin/convert.php fixtures/card.png output/fit.webp fit
# Назначения должны различаться; существующий файл не перезаписывается.
Windows: установка и первый полезный запуск
На Windows начните с версии PHP и используемого ini. Частая причина ошибки — расширение включено для другого экземпляра PHP, а терминал запускает старый путь. Команды php -v и php –ini помогают увидеть конкретную сборку. GD, mbstring, EXIF и fileinfo должны быть доступны именно ей; поддержка WebP проверяется через gd_info().
После Composer install запустите тесты, создайте папку output и выполните CLI на готовой карточке. Проверьте код завершения. Затем повторите команду с собственным локальным изображением и новым именем назначения. Так вы проверите не только наличие библиотеки, но и фактическую запись результата в нужную директорию.
Просмотр мастерской требует обычного HTTP, потому что она загружает JSON. Открытие index.html через file:// может блокировать эти запросы. В README есть пример локального сервера PHP на loopback. Он нужен для разработки и останавливается Ctrl+C; переносить такой сервер как публичный production-сервис не следует.
composer install --no-interaction --no-plugins --no-scripts
php tests/run.php
New-Item -ItemType Directory -Force output
php bin/convert.php fixtures/card.png output/card.webp card
$LASTEXITCODE
# Повреждённый файл: ожидаемый exit 2
php bin/convert.php fixtures/broken.jpg output/broken.webp card
$LASTEXITCODE
Linux: системные расширения и воспроизводимый контейнер
Для Linux важна та же проверка, но пакеты устанавливаются средствами дистрибутива. Ниже приведён пример Ubuntu 24.04 с PHP 8.3 из её репозиториев. В другой системе названия и доступные версии могут отличаться. После установки нужно проверить фактические расширения CLI и требования Composer, а не считать успешный apt достаточным доказательством работоспособности.
В архиве есть Dockerfile, который собирает GD с JPEG и WebP, включает EXIF и mbstring, устанавливает зависимости из lock и запускает тесты. Это удобный способ воспроизвести окружение, когда на рабочей машине другая конфигурация. Он не требует подключать существующую базу, каталог приложения или рабочие фотографии.
Базовый тег PHP может обновляться; запись хранит реальную версию использованного runtime. Поэтому одинаковый composer.lock не обещает одинаковые байты сжатых файлов на любых системах. Если нужны строго воспроизводимые бинарные результаты, фиксируйте также образ и версии кодеков. Для обычной приёмки проверяйте геометрию, MIME, прозрачность и визуальное качество.
sudo apt update
sudo apt install php8.3-cli php8.3-gd php8.3-mbstring php8.3-xml php8.3-curl php8.3-zip unzip
composer install --no-interaction --no-plugins --no-scripts
composer check-platform-reqs
php tests/run.php
# Альтернатива из каталога скачанного проекта:
docker build -t aen-image-lab:1 .
docker run --rm aen-image-lab:1
VPS: где хранить файлы и от какого пользователя запускать
На сервере отделите исходники приложения от публичной выдачи. Например, app и originals находятся под /srv/image-worker вне document root, а готовые производные лежат в отдельной статической папке. Процессу обработки нужна запись только в назначенные output и временную директорию. Запуск от root и chmod 777 не являются нормальным способом исправить владельца файлов.
Если обработка выполняется через CLI, проверяйте его PHP отдельно от PHP-FPM. На одном сервере могут одновременно существовать несколько версий и конфигураций. Наличие GD в панели сайта не означает, что его увидит /usr/bin/php из задания очереди. Сохраните путь бинарника в настройках worker и повторите тест из-под того же пользователя.
Публичной мастерской достаточно каталога public: это изображения, JSON, CSS и JavaScript. Ей не нужен исполняемый PHP endpoint. Vendor, тесты и CLI не следует публиковать рядом с загружаемыми файлами. Внешнее развёртывание на VPS здесь не заявляется выполненным; реальные Linux-проверки проведены в локальном контейнере, а серверная схема дана для практического переноса.
Ресурсы: ограничить один процесс и число параллельных задач
Растровая обработка расходует память на распакованные пиксели и промежуточные копии. Размер JPEG на диске поэтому плохо предсказывает максимальный расход памяти. Поворот, изменение размеров и наложение слоя могут временно удерживать несколько буферов. Несколько одновременно работающих процессов умножают эту нагрузку.
Для первого серверного запуска задайте ограничения времени и памяти, измерьте потребление на типичных и предельных входных данных. Приведённые 256M и 60 секунд — пример начальной конфигурации, а не обещание обработать любую картинку в рамках нашего входного лимита. Процессные или контейнерные ограничения дополняют проверки приложения, особенно при работе с файлами пользователей.
При превышении timeout Linux-команда вернёт отдельный код 124. Сам CLI проекта возвращает 0 при успехе и 2 при отказе. Не превращайте все ненулевые коды в бесконечный retry: повреждение файла требует замены данных, а нехватка ресурсов — пересмотра лимита, формата или архитектуры обработки.
timeout 60s /usr/bin/php -d memory_limit=256M bin/convert.php \
/srv/image-worker/originals/item.png \
/srv/image-worker/derived/item-card-v1.webp card
code=$?
printf 'exit=%s\n' "$code"
Очередь, повторная обработка и наблюдаемость
Для тяжёлого импорта разумно разделить приём файла и создание производных. Первый шаг сохраняет оригинал и ставит задачу. Worker получает ID, нужный пресет и версию рецепта, проверяет данные и записывает результат. Пользовательский интерфейс показывает состояние «обрабатывается», успех или понятный отказ, а не держит HTTP-запрос открытым до завершения всего каталога.
Мини-мастерская выполняет последовательные PHP-примеры и не выдаёт их за готовую очередь. Она показывает основу: независимые входы, отдельный результат каждого элемента и отсутствие разрушения оригинала. В реальном сервисе добавьте ограничение числа workers, повторяемый идентификатор задания и правило, что делать при уже существующей производной.
В журнал полезно писать ID задачи, пресет, размеры входа и выхода, длительность, класс ошибки и код завершения. Не нужно автоматически сохранять содержимое файлов или личные метаданные пользователя. Для PHP-журналирования пригодится наш материал о Monolog; точный контракт обработки остаётся частью вашего приложения.
Как выдать результат браузеру: размеры, srcset и кеш
Обработка полезна только тогда, когда интерфейс действительно использует подходящую производную. Укажите width и height, чтобы браузер заранее резервировал место. Для нескольких размеров сформируйте srcset и корректное sizes, соответствующее сетке сайта. Не отправляйте большой оригинал во все карточки только потому, что CSS визуально делает их маленькими.
Версионное имя результата упрощает кеширование: когда меняется содержимое или рецепт, меняется URL. Длинный immutable cache подходит именно такому ресурсу. Если под прежним именем постоянно перезаписывается новая картинка, клиент может продолжить показывать старую версию. Решение должно быть согласовано с CDN и правилами вашего веб-сервера.
При выборе WebP проверяйте MIME и реальную поддержку вашей цепочки: кодек сервера, выдачу, оптимизатор и потребляющий клиент. Статическое расширение имени не конвертирует изображение. В примерах преобразование выполняет encodeUsingFormat, а тест проверяет содержимое результата отдельно. Не смешивайте размер CSS, размер пиксельного массива и размер файла в байтах: каждый отвечает за свою часть поведения.
<img src="/images/item-320-v1.webp"
srcset="/images/item-160-v1.webp 160w,
/images/item-320-v1.webp 320w,
/images/item-640-v1.webp 640w"
sizes="(max-width: 600px) 90vw, 320px"
width="320" height="213" alt="Описание товара" loading="lazy">
Что подтверждено тестами и какие задачи остаются отдельно
Windows и Linux выполнили по 167 проверок. Они проверяют девять тем, двадцать четыре варианта, реальные размеры и MIME выходных файлов, прозрачность контрольного угла, ориентацию EXIF, положение водяного знака, различие crop и обработку ошибок. Четыре отдельные CLI-команды проверяют успех, сохранность существующего назначения, повреждённый вход и неверный пресет.
Исходники и фикстуры связаны с записями через SHA-256. Выходные изображения имеют собственные хеши и реальные размеры. Эти сведения позволяют обнаружить, что файл или PHP-код после записи изменился. Они не означают криптографическую подпись, гарантированное отсутствие уязвимостей декодера или одинаковые сжатые байты на любой версии библиотек.
Отдельно остаются качество на вашей коллекции фотографий, цветовой профиль, анимация, большие TIFF, доступность интерфейса для конкретной аудитории и нагрузка production-сервера. Здесь выбран понятный ограниченный набор статических JPEG/PNG/WebP. Его можно расширять постепенно, добавляя новые реальные фикстуры и проверки, а не объявляя поддержку формата по одному успешному файлу.
Исходники, документация и первый шаг в своём проекте
В архиве находятся PHP-код, Composer lock, Dockerfile, авторские тестовые изображения, сценарии, тесты и готовая статическая мастерская. Python нужен только для необязательного повторного создания EXIF-фикстуры; готовый JPEG уже включён. Для обычного запуска достаточно указанного PHP-окружения и Composer install. Команды и ограничения собраны также в README и отдельном руководстве Linux/VPS.
Начните с одного практического пресета: например, превью товара без искажения или квадратной обложки с контролируемым crop. Сравните результат на нескольких характерных изображениях, включая прозрачное, маленькое и повреждённое. Затем подключите хранение, очередь и выдачу. Так новый инструмент решает конкретную задачу сайта, а не остаётся набором непроверенных вызовов.
Первичные источники: Intervention Image v4, изменение геометрии, кодирование результата, вставка изображений, ошибки. Если нужна помощь с зависимостями и lock-файлом, начните с нашей статьи о Composer.


