Работа с изображениями в PHP: чек-лист перед продакшеном
Загрузка картинок кажется тривиальной, пока сервер не падает от 40-мегапиксельного PNG или пока в аплоаде не оказывается PHP-скрипт под видом JPEG. Разбираем, что проверить до релиза.

Форма загрузки аватара работает на тесте, проходит ревью, уезжает в прод — и через неделю память процесса упирается в лимит, потому что кто-то загрузил PNG 12000×12000. Или хуже: файл с именем avatar.jpg и содержимым <?php ... ?> лёг в публичную папку и выполнился. Работа с изображениями в PHP полна мест, где очевидный код ведёт себя не так, как ожидаешь. Ниже — чек-лист того, что стоит проверить до продакшена: расширения, память, валидация, обработка, хранение.
Материал рассчитан на PHP 8.1+ и предполагает, что вы уже пишете на PHP и упёрлись в конкретные вопросы загрузки и обработки картинок. Формат — практика: не пошаговый гайд для новичка, а разбор точек, где решение имеет цену.
Первое: какое расширение вы вообще используете
В PHP исторически три способа работать с растром, и выбор влияет на всё остальное.
| Расширение | Плюсы | Минусы |
|---|---|---|
| GD | Идёт в комплекте почти везде, простой API | Держит всё изображение в памяти несжатым, слабее по качеству ресайза |
| Imagick (ImageMagick) | Больше форматов, лучше качество, ограничители ресурсов | Отдельная установка, историческая цепочка CVE, тяжелее |
libvips (через jcupitt/vips) | Резко ниже потребление памяти на больших файлах, потоковая обработка | Нужна библиотека и PHP-расширение FFI, меньше примеров |
Для типичного веб-приложения с аватарами и превью GD достаточно. Если обрабатываете пользовательские фото высокого разрешения пачками — присмотритесь к libvips: разница в пиковой памяти на большом изображении измеряется разами, а не процентами.
Проверьте, что расширение вообще собрано с нужными форматами
GD может быть собран без поддержки WebP или AVIF. Не полагайтесь на предположения — спросите у самого PHP:
<?php
// Проверка возможностей GD в текущей сборке
$info = gd_info();
var_dump($info['WebP Support'] ?? false); // true, если WebP доступен
var_dump($info['AVIF Support'] ?? false); // AVIF добавлен в GD в PHP 8.1
Если в проде поддержки нет, а код зовёт imagewebp(), вы получите фатальную ошибку на живом трафике. Проверяйте наличие функции перед вызовом.
Валидация: расширение файла ничего не гарантирует
Самая частая ошибка — доверять полю type из $_FILES или расширению в имени файла. И то, и другое приходит от клиента и подделывается тривиально.
Минимальная честная проверка — определить реальный тип по содержимому и убедиться, что PHP смог прочитать изображение как изображение:
<?php
$path = $_FILES['photo']['tmp_name'];
// 1. Реальный MIME по содержимому, а не по имени
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($path);
$allowed = ['image/jpeg', 'image/png', 'image/webp'];
if (!in_array($mime, $allowed, true)) {
throw new RuntimeException('Недопустимый тип файла');
}
// 2. getimagesize вернёт false, если это не растровое изображение
$size = getimagesize($path);
if ($size === false) {
throw new RuntimeException('Файл не является изображением');
}
[$width, $height] = $size;
Проверка расширения защищает от опечаток пользователя. Проверка содержимого защищает от атакующего. Это разные задачи, и первая не заменяет вторую.
Ограничьте размеры до обработки, а не после
Так называемая decompression bomb — маленький по весу PNG, который разворачивается в гигантский битмап. Файл 2 КБ на диске может дать 20000×20000 пикселей, а это около 1,2 ГБ в памяти для GD (4 байта на пиксель). Проверяйте геометрию до того, как отдать файл в imagecreatefrom*:
- Отсекайте по
$width * $height, а не только по весу файла на диске. - Задайте разумный потолок — например, 50 мегапикселей — и отклоняйте всё выше.
- В Imagick есть встроенные ограничители через
Imagick::setResourceLimit()— используйте их как второй рубеж.
Память: где GD убивает процесс
GD распаковывает изображение в память целиком и в несжатом виде. Формула для оценки пика: ширина × высота × 4 байта, плюс столько же на копию при ресайзе. Для 6000×4000 (24 Мп) это примерно 96 МБ только на исходник, и вдвое больше во время операции.
Отсюда практические следствия:
memory_limitв 128 МБ не выдержит обработку фото с современного телефона. Либо поднимайте лимит для конкретного скрипта, либо переходите на libvips.- Освобождайте ресурсы явно после работы. В PHP 8
imagedestroy()уже не обязателен (объектGdImageсобирается сборщиком мусора), но при обработке в цикле не держите ссылки на обработанные изображения дольше нужного. - Не обрабатывайте изображения синхронно в HTTP-запросе, если файлы крупные. Тяжёлый ресайз — кандидат в фоновую очередь.
Пример: безопасный ресайз с проверкой памяти
<?php
// Оценка потребности в памяти перед загрузкой в GD
function estimateMemory(int $width, int $height): int
{
// 4 байта на пиксель + запас на копию при ресайзе
return (int) ($width * $height * 4 * 2.1);
}
$size = getimagesize($sourcePath);
if ($size === false) {
throw new RuntimeException('Не изображение');
}
[$w, $h] = $size;
$needed = estimateMemory($w, $h);
$limit = ini_get('memory_limit');
// -1 означает без ограничения; иначе сравниваем с оценкой
if ($limit !== '-1' && $needed > 100 * 1024 * 1024) {
throw new RuntimeException('Изображение слишком велико для обработки');
}
$src = imagecreatefromstring(file_get_contents($sourcePath));
$target = imagescale($src, 800); // высота подберётся пропорционально
imagewebp($target, $destPath, 82);
Функция imagescale() доступна с PHP 5.5 и сама держит пропорции, если передать только ширину. Качество ресемплинга у неё приличное — вручную городить imagecopyresampled() ради базового превью не нужно.
EXIF, ориентация и метаданные
Фото с телефона часто приходит в «лежачем» пиксельном буфере с флагом ориентации в EXIF. Браузер повернёт его при показе, а ваш ресайз — нет, и превью выйдет боком. Читайте ориентацию через exif_read_data() (расширение exif должно быть включено) и поворачивайте вручную через imagerotate() перед ресайзом.
Отдельно: EXIF содержит GPS-координаты съёмки. Если вы публикуете пользовательские фото, вы можете нечаянно раскрыть, где человек живёт. После обработки GD не переносит EXIF в результат — это, как ни странно, полезно с точки зрения приватности. Imagick метаданные сохраняет, их нужно чистить через stripImage() явно.
Хранение: куда класть загруженное
Правило одно и жёсткое: каталог с пользовательскими загрузками не должен исполнять PHP.
- Храните загрузки вне
DocumentRootи отдавайте через контроллер, либо в отдельной публичной папке, где веб-сервер не запускает PHP. - В Nginx не проксируйте
.phpиз папки аплоадов в PHP-FPM. В Apache отключите обработку через конфиг, а не через.htaccessвнутри самой папки (её тоже могут перезаписать). - Генерируйте имя файла сами. Не используйте оригинальное имя от пользователя — там может быть
../, нулевой байт или двойное расширениеphoto.php.jpg. - Пересохраняйте изображение через GD/Imagick, а не кладите оригинальный байт-поток. Пересохранённый файл гарантированно является картинкой, а не полиглотом с исполняемым хвостом.
Когда что выбирать
| Ситуация | Инструмент |
|---|---|
| Аватары, мелкие превью, стандартный хостинг | GD, синхронно в запросе |
| Пользовательские фото высокого разрешения | libvips или GD в фоновой очереди с поднятым лимитом |
| Нужны редкие форматы, PDF, сложные фильтры | Imagick с включёнными resource limits |
Что проверить перед релизом
- Расширение собрано с поддержкой всех форматов, которые вы вызываете (
gd_info()илиImagick::queryFormats()). - Тип файла определяется по содержимому через
finfo, а не по$_FILES['type']. - Есть потолок по мегапикселям, а не только по весу файла.
- Оценка памяти против
memory_limitдля крупных изображений или обработка вынесена в очередь. - EXIF-ориентация учтена, GPS-метаданные вычищены при публикации.
- Имя файла генерируется на сервере, оригинал пересохраняется, папка аплоадов не исполняет PHP.
Следующий шаг после релиза — повесить метрику на пиковую память процесса обработки и на время ресайза. Если 95-й перцентиль времени поплыл — значит, к вам пошли крупные файлы, и пора выносить обработку из HTTP-запроса в фон. Отдачу готовых картинок и генерацию превью на лету — отдельная тема, здесь мы разбирали только приём и сохранение.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: PHP Manual: Image Processing and GD, PHP Manual: Handling file uploads
Частые вопросы
У меня GD, а не Imagick. Хватит ли его для продакшена?
Проверки finfo достаточно, чтобы отсеять вредоносный файл?
А если у меня старая версия PHP, например 7.4?
Загрузка иногда падает с ошибкой памяти, хотя файл маленький. В чём дело?
Нужно ли удалять EXIF из загруженных фото?
Почему нельзя просто сохранить оригинальный файл как есть?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.