Сжатие картинок на лету в PHP: GD, WebP и миниатюры
Картинки ♡
стало 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.
Комментарии
Буду рада вашим мыслям, идеям и вопросам!
Давайте обсуждать ♡
Делитесь
своими мыслями
— мне очень
это важно ♡