Работа с изображениями в PHP: типичные ошибки и как их избежать
Загрузка аватарки в 8 мегапикселей кладёт PHP-процесс по памяти, а «проверка по расширению» пускает на сервер PHP-шелл под видом картинки. Разбираем ошибки, на которые натыкаются почти все.

Пользователь загружает фото с телефона: 4032×3024, JPEG весом 4 МБ. Ваш скрипт вызывает imagecreatefromjpeg() — и процесс падает с Allowed memory size of 134217728 bytes exhausted. При этом сам файл всего 4 мегабайта. Дальше — как обработка изображений в PHP ломается на ровном месте и что с этим делать. Ориентируюсь на PHP 8.1–8.3 и расширение GD; где важно, отмечаю разницу с Imagick.
Ошибка 1. Считать, что 4 МБ файла — это 4 МБ памяти
GD распаковывает JPEG в несжатый растр в памяти. Формула простая: ширина × высота × 4 байта на пиксель (RGBA), плюс накладные расходы. Для 4032×3024 это уже около 48 МБ только на один буфер. А при ресайзе в памяти живут одновременно исходное и целевое изображения — считайте вдвое.
Отсюда вывод, который экономит вам ночные алерты: проверять габариты до распаковки, а не размер файла.
<?php
// Проверяем разрешение до загрузки в память (PHP 8.x, GD)
$info = getimagesize($sourcePath);
if ($info === false) {
throw new RuntimeException('Файл не является изображением');
}
[$width, $height] = $info;
$megapixels = ($width * $height) / 1_000_000;
// Ограничение: например, 25 мегапикселей
if ($megapixels > 25) {
throw new RuntimeException('Изображение слишком большое');
}
Проверка как понять, что сработало: на файле 4032×3024 переменная $megapixels будет около 12.2, на скриншоте 4K — около 8.3. Если у вас в мониторинге всё равно всплывают падения по памяти — значит, кто-то грузит панорамы и надо снижать порог.
Ошибка 2. Проверять тип файла по расширению или по имени
Классика, которая до сих пор встречается в проде. Расширение .jpg не значит ничего: атакующий переименует PHP-скрипт в avatar.jpg и, если сервер сконфигурирован неудачно, добьётся исполнения. MIME-тип из $_FILES['file']['type'] тоже приходит от клиента и подделывается тривиально.
Доверять можно только проверке содержимого. getimagesize() читает сигнатуру файла и возвращает реальный тип в элементе [2] (константы IMAGETYPE_JPEG, IMAGETYPE_PNG и так далее).
<?php
$info = getimagesize($sourcePath);
$allowed = [IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_WEBP];
if ($info === false || !in_array($info[2], $allowed, true)) {
throw new RuntimeException('Недопустимый формат');
}
Проверка содержимого отсеивает подделку сигнатуры, но не гарантирует, что внутри «чистый» файл. Второй рубеж — пересохранение изображения через GD/Imagick: это стирает посторонние данные, приклеенные к валидному растру.
И отдельно: папку с загрузками нужно закрыть от исполнения PHP на уровне веб-сервера (в Nginx — не отдавать её в fastcgi_pass, в Apache — php_admin_flag engine off для директории). Проверка типа в коде и запрет исполнения на сервере работают только вместе.
Ошибка 3. Терять прозрачность PNG и качество при ресайзе
Самая частая визуальная беда: после уменьшения PNG прозрачный фон становится чёрным. Причина в том, что новый холст в GD по умолчанию залит непрозрачным чёрным, а копирование пикселей поверх него смешивает альфу.
Лечится тремя строчками до копирования:
<?php
$dst = imagecreatetruecolor($newWidth, $newHeight);
// Сохраняем альфа-канал для PNG/WebP
imagealphablending($dst, false);
imagesavealpha($dst, true);
$transparent = imagecolorallocatealpha($dst, 0, 0, 0, 127);
imagefilledrectangle($dst, 0, 0, $newWidth, $newHeight, $transparent);
imagecopyresampled(
$dst, $src,
0, 0, 0, 0,
$newWidth, $newHeight,
$srcWidth, $srcHeight
);
Второй нюанс — imagecopyresized() против imagecopyresampled(). Первый работает быстрее, но масштабирует без интерполяции: диагонали превращаются в лесенку. Для превью почти всегда нужен imagecopyresampled() — качество заметно выше, разница по скорости на аватарках несущественна.
Ошибка 4. Не сбрасывать EXIF-ориентацию
Фото с телефона часто снято «в портрет», но пиксели в файле лежат горизонтально, а правильный поворот задан только тегом Orientation в EXIF. Браузер этот тег читает, GD — нет. После ресайза через GD EXIF теряется, и картинка оказывается повёрнутой на 90 градусов.
Нужно применить поворот вручную до ресайза, прочитав ориентацию через exif_read_data() (расширение ext-exif должно быть включено):
<?php
$exif = @exif_read_data($sourcePath);
if ($exif !== false && !empty($exif['Orientation'])) {
$src = match ((int) $exif['Orientation']) {
3 => imagerotate($src, 180, 0),
6 => imagerotate($src, -90, 0),
8 => imagerotate($src, 90, 0),
default => $src,
};
}
Проверка: снимите фото телефоном вертикально, загрузите — превью должно быть вертикальным, а не лежащим на боку. У Imagick для этого есть autoOrient() начиная с ImageMagick 6.5.6 — там ручная возня не нужна.
Ошибка 5. Не освобождать ресурсы в цикле
До PHP 8.0 функции GD возвращали ресурс, который сборщик мусора не подхватывал автоматически, и без imagedestroy() в цикле обработки батча память утекала. С PHP 8.0 GD возвращает объект GdImage, который освобождается сборщиком мусора при потере ссылок — явный imagedestroy() перестал быть обязательным.
Но при обработке большого списка изображений в одном процессе полагаться на GC рискованно: пики памяти всё равно копятся. Освобождайте буферы явно, как только они больше не нужны:
<?php
foreach ($files as $file) {
$src = imagecreatefromjpeg($file);
// ... ресайз, сохранение ...
imagedestroy($src); // на PHP 8.x необязательно, но полезно в батче
imagedestroy($dst);
}
GD или Imagick: когда что
| Критерий | GD | Imagick |
|---|---|---|
| Наличие | Обычно есть из коробки | Ставится отдельно (ext-imagick + ImageMagick) |
| Форматы | JPEG, PNG, WebP, GIF, AVIF (с новыми версиями) | Шире: TIFF, PSD, PDF, HEIC при поддержке в сборке |
| Качество ресайза | Достаточное | Обычно выше, больше фильтров |
| EXIF-ориентация | Вручную | autoOrient() |
| Память на больших файлах | Требует контроля вручную | Есть механизмы ограничения ресурсов |
Для аватарок и превью товаров хватает GD. Если нужны нестандартные форматы, точная цветопередача или обработка тяжёлых файлов — берите Imagick. Точный список поддерживаемых форматов зависит от того, как собрана ваша ImageMagick: сверяйте вывод Imagick::queryFormats(), а не документацию наугад.
Что делать дальше
Соберите обработку в один сервисный класс, чтобы проверки не расползались по контроллерам, и настройте:
- лимит мегапикселей на входе — до распаковки;
- проверку типа по сигнатуре плюс пересохранение файла;
- запрет исполнения PHP в папке загрузок на уровне веб-сервера;
- вынос ресайза в очередь (например, через воркер), если изображений много — синхронная обработка в HTTP-запросе упирается в
max_execution_time; - мониторинг пиков памяти PHP-FPM: если видите скачки — ищите, кто загрузил панораму мимо лимита.
Дальше стоит посмотреть в сторону генерации WebP/AVIF для отдачи и <picture> с fallback — но это уже отдельная тема про фронтенд-доставку, здесь мы её не разворачиваем.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: PHP Manual: Image Processing and GD, PHP Manual: getimagesize
Частые вопросы
У меня PHP 7.4, а не 8.x — примеры подойдут?
Скрипт всё равно падает по памяти, хотя я поставил лимит мегапикселей. Что не так?
getimagesize() достаточно для безопасности загрузок?
PNG после ресайза стал с чёрным фоном вместо прозрачного — почему?
Нужно ли расширение ext-exif для поворота фото с телефона?
Стоит ли обрабатывать изображения прямо в HTTP-запросе?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.