Сжатие картинок на лету в PHP: GD, WebP и миниатюры

0

Картинки ♡

Сжатие картинок на лету в PHP: GD, WebP и миниатюры
1,9 МБ
стало 150 КБ ♡

Сайт с тяжёлыми картинками это сайт, который закрывают, не дождавшись. У меня загрузка картинок была настроена и вроде как сжимала. Но две PNG по 1,9 МБ лежали на сервере как есть. Причина смешная: на сервере не стояло расширение GD, и сжатие молча пропускалось. Ни ошибки, ни предупреждения, просто «ну я пошёл». Расскажу, как всё устроено и как я это починила.

Что делает загрузка

Класс "ImageStore" принимает загруженный файл и по очереди:

  • декодирует картинку через GD;
  • поворачивает по данным EXIF (фото с телефона иначе оказываются лежащими на боку);
  • ужимает до 1600 пикселей по ширине;
  • сохраняет в WebP с качеством 82;
  • по желанию делает уменьшенную копию шириной 800 пикселей для карточек.

Карточки и списки берут маленькую копию, а полная картинка открывается только на странице записи. Разница по трафику огромная, а глазами почти незаметная.

Главное: не падать на плохих файлах

Самое интересное в таком классе это защита. GD держит картинку в памяти, примерно по 4 байта на пиксель, поэтому фото на 60 мегапикселей легко съест всю память процесса, и привет, ошибка. Я поставила предел в 20 миллионов пикселей:

private const MAX_PIXELS = 20_000_000;

$info = @getimagesizefromstring($contents);
if (! $info || $info[0] * $info[1] > self::MAX_PIXELS) {
    return null;
}

Если файл слишком большой или GD его не понял, класс не падает, а сохраняет оригинал как есть. Анимированные GIF я тоже не трогаю: после декодирования у них остался бы только первый кадр, и гифка превратилась бы в грустную картинку.

Поворот по EXIF: почему фото лежит на боку

Телефон хранит поворот фотографии не в самих пикселях, а в метаданных. После пересохранения метаданные теряются, и снимок оказывается лежащим на боку, как будто его уронили. Поэтому поворачиваю вручную:

$exif = @exif_read_data('data://image/jpeg;base64,'.base64_encode($contents));
$angle = match ($exif['Orientation'] ?? 1) {
    3 => 180,
    6 => -90,
    8 => 90,
    default => 0,
};

$rotated = $angle === 0 ? $image : imagerotate($image, $angle, 0);

Мелочь для PHP 8.5

Функция "imagedestroy()" объявлена устаревшей: объекты GD теперь освобождаются сами. Если в старом коде она есть, PHP 8.5 начнёт на неё жаловаться. Удаляй смело, никто не заплачет.

Старые файлы и результат

Новые загрузки сжимаются сразу, а для тех, что уже лежат на диске, у меня есть команда "php artisan images:optimize". Она конвертирует старые PNG в WebP, обновляет записи в базе и удаляет оригиналы. Есть ключ "--dry-run", чтобы сначала посмотреть, что будет, и не рыдать потом.

Результат на моих двух картинках: 1,9 МБ каждая превратились в WebP примерно по 150 КБ, плюс миниатюры по 64 КБ. Всего освободилось около 3,5 МБ. Неплохо для одной команды и одного установленного расширения.

Вывод простой: GD на сервере ставь сразу, иначе сжатие молча превращается в «сохранить как есть», и ты не узнаешь об этом, пока не заглянешь в каталог. Как выкладывать проект на сервер целиком, я рассказывала в статье про деплой Laravel.

Комментарии

Буду рада вашим мыслям, идеям и вопросам!
Давайте обсуждать ♡

Пока никто не написал.
Станьте первым! ♡

Поддержать проект

Угостить кофе по СБП

Перевод через Систему быстрых платежей, сумма любая. Спасибо, что вы здесь ♡

QR-код для перевода по СБП
Или другим способом
Дневник на почте

Подписаться на блог

Когда выйдет новая запись, я пришлю письмо со ссылкой. Без рассылок «просто так» и без спама. Отписаться можно одним нажатием.

Или следить в Telegram