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

Пользователь загрузил фотографию с телефона: 8000×6000 пикселей, JPEG на 4 МБ. Ваш скрипт делает imagecreatefromjpeg() — и процесс падает с Allowed memory size exhausted, потому что распакованный битмап занял почти 200 МБ. Файл был 4 мегабайта, а память съел так, будто их было пятьдесят. Это первая из ошибок, на которые натыкаются, когда впервые делают ресайз аватарок или превью для каталога.
Разберём, как обрабатывать загруженные изображения в PHP так, чтобы не ронять процесс по памяти, не пускать под видом картинки чужой код и получать предсказуемый результат. Ориентируемся на PHP 8.1+ и два расширения: GD (почти всегда есть из коробки) и Imagick (обёртка над ImageMagick, ставится отдельно). Про генерацию картинок с нуля и наложение текста тут не говорим — только про приём, проверку и преобразование того, что прислал пользователь.
Три ошибки, которые ломают всё
Прежде чем показывать рабочий код, зафиксируем, где именно обычно течёт кровь.
- Память считают по размеру файла, а не по разрешению. JPEG сжат в 10–20 раз. Распакованный битмап в GD занимает примерно
ширина × высота × 4байта плюс накладные расходы. Картинка 8000×6000 — это около 192 МБ только на один буфер, а при ресайзе их два. - Доверяют MIME-типу из
$_FILES. Полеtypeв массиве загрузки берётся из заголовка, который прислал браузер. Его подделывает кто угодно. Файл с расширением.jpgи типомimage/jpegможет быть PHP-скриптом. - Забывают освобождать ресурсы. В цикле по сотне файлов без
imagedestroy()(для GD) память копится до конца запроса.
Шаг первый: проверить, что это вообще изображение
Единственная надёжная проверка — прочитать сам файл, а не то, что о нём сообщил браузер. Для этого есть getimagesize(): она читает сигнатуру и возвращает реальные размеры и тип, либо false, если это не картинка.
<?php
// PHP 8.1+
// $tmpPath — путь из $_FILES['photo']['tmp_name']
$info = getimagesize($tmpPath);
if ($info === false) {
// Файл не распознан как изображение — отклоняем
throw new RuntimeException('Это не изображение');
}
[$width, $height] = $info; // реальные размеры в пикселях
$mime = $info['mime']; // реальный MIME, например image/jpeg
// Белый список форматов, а не чёрный
$allowed = ['image/jpeg', 'image/png', 'image/webp'];
if (!in_array($mime, $allowed, true)) {
throw new RuntimeException('Формат не поддерживается: ' . $mime);
}
getimagesize() не защищает от вредоносного содержимого на 100 процентов, но отсекает файлы, которые вообще не являются картинками. Дополнительно храните загруженные файлы вне корня сайта или в директории с запретом на выполнение PHP — тогда даже подделанное расширение ничего не даст.
Шаг второй: посчитать память до распаковки
Разрешение вы уже знаете из getimagesize(). Проверьте его до того, как вызвать imagecreatefromjpeg(), иначе проверять будет нечем — процесс упадёт раньше.
<?php
// Грубая оценка: 4 байта на пиксель + запас ×1.7 на промежуточные буферы
$estimatedBytes = $width * $height * 4 * 1.7;
$limitBytes = 96 * 1024 * 1024; // 96 МБ — задайте под свой memory_limit
if ($estimatedBytes > $limitBytes) {
throw new RuntimeException(
"Слишком большое разрешение: {$width}x{$height}"
);
}
Множитель 1.7 — эмпирический запас на второй буфер при ресайзе. Он не заменяет замеры на вашей нагрузке, но спасает от очевидного перерасхода. Верхнюю границу разрешения лучше ограничивать сразу: превью каталога редко нужно больше 4000 пикселей по стороне.
Шаг третий: ресайз без потери пропорций
Самая частая визуальная ошибка новичка — imagecopyresized() вместо imagecopyresampled(). Первая масштабирует «в лоб», без интерполяции, и края получаются ступенчатыми. Вторая сглаживает — её и берём.
<?php
// GD, PHP 8.1+. Ресайз JPEG в превью с сохранением пропорций
function makeThumbnail(
string $srcPath,
string $dstPath,
int $maxSide = 800,
int $quality = 82
): void {
$info = getimagesize($srcPath);
if ($info === false) {
throw new RuntimeException('Не изображение');
}
[$srcW, $srcH] = $info;
// Коэффициент так, чтобы длинная сторона стала maxSide
$ratio = min($maxSide / $srcW, $maxSide / $srcH, 1.0);
$dstW = (int) round($srcW * $ratio);
$dstH = (int) round($srcH * $ratio);
$src = imagecreatefromjpeg($srcPath);
$dst = imagecreatetruecolor($dstW, $dstH);
imagecopyresampled(
$dst, $src,
0, 0, 0, 0,
$dstW, $dstH, $srcW, $srcH
);
imagejpeg($dst, $dstPath, $quality);
// Освобождаем оба буфера сразу, не дожидаясь конца запроса
imagedestroy($src);
imagedestroy($dst);
}
Условие 1.0 в min() не даёт увеличивать маленькие картинки — апскейл в PHP выглядит мыльно и почти никогда не нужен.
Прозрачность PNG и WebP
Для JPEG код выше работает. Но если вы делаете превью PNG с прозрачностью и не включите альфа-канал, фон станет чёрным. Перед копированием в целевой холст нужно два вызова:
<?php
// Только для PNG и WebP с прозрачностью
imagealphablending($dst, false);
imagesavealpha($dst, true);
Порядок важен: imagealphablending(false) отключает смешивание, чтобы прозрачные пиксели копировались как есть, а imagesavealpha(true) велит сохранить альфа-канал в итоговый файл. Забудете второй вызов — прозрачность потеряется при imagepng().
GD или Imagick
Оба расширения решают задачу, но по-разному ведут себя с памятью и качеством. GD держит распакованный битмап в памяти PHP-процесса, поэтому крупные изображения бьют по memory_limit напрямую. Imagick умеет работать с диском и лучше справляется с большими файлами и цветовыми профилями, но требует установленного ImageMagick на сервере.
| Критерий | GD | Imagick |
|---|---|---|
| Наличие на хостинге | Почти всегда из коробки | Ставится отдельно, есть не везде |
| Память на большие файлы | Весь битмап в RAM процесса | Ограничивается через resource limits |
| Качество ресайза | Достаточное для превью | Больше фильтров, тоньше настройка |
| Форматы | JPEG, PNG, WebP, GIF, AVIF (с версии PHP 8.1 при сборке с поддержкой) | Десятки, включая TIFF, HEIC, PDF |
| Метаданные EXIF | Не читает при обработке | Читает и умеет чистить |
Если картинки — превью аватарок и товаров до пары тысяч пикселей, а хостинг обычный shared, берите GD: он есть везде и его хватает. Imagick оправдан, когда на входе бывают тяжёлые файлы, важен цветовой профиль или нужны форматы, которых GD не знает.
Ограничение памяти в Imagick
У Imagick своя система лимитов, независимая от memory_limit PHP. Установить их можно до открытия файла:
<?php
// Imagick: ограничить память и заставить использовать диск при превышении
$img = new Imagick();
$img->setResourceLimit(Imagick::RESOURCETYPE_MEMORY, 128 * 1024 * 1024);
$img->setResourceLimit(Imagick::RESOURCETYPE_MAP, 256 * 1024 * 1024);
$img->readImage($srcPath);
$img->thumbnailImage(800, 800, true); // true — сохранить пропорции
$img->stripImage(); // убрать EXIF и прочие метаданные
$img->writeImage($dstPath);
$img->clear();
Константы ресурсов и их точное поведение проверяйте в документации к вашей версии php-imagick — набор доступных типов зависит от сборки ImageMagick.
EXIF: поворот и утечка данных
Две проблемы, о которых забывают. Первая: фотографии с телефона несут в EXIF ориентацию. Браузер её учитывает, а GD после ресайза — нет, и портретное фото ложится набок. Ориентацию читают через exif_read_data() и поворачивают картинку вручную по тегу Orientation.
Вторая: EXIF хранит GPS-координаты съёмки, модель камеры, иногда серийный номер. Если вы публикуете загруженные фото, вы публикуете и эти данные. Imagick чистит их через stripImage(). В GD метаданные не переносятся в новый холст при imagecreatetruecolor(), поэтому пересохранённая картинка чистая сама по себе — но только если вы её действительно пересохраняете, а не отдаёте оригинал.
Если не получилось
Типичные ошибки, которые вы увидите в логах, и что с ними делать.
Allowed memory size of N bytes exhausted— распаковка съела большеmemory_limit. Не поднимайте лимит бездумно: сначала проверьте разрешение до распаковки (шаг второй) и отклоняйте гигантские картинки.Call to undefined function imagecreatefromwebp()— GD собран без поддержки WebP. Проверьтеgd_info()— там видно, какие форматы включены. Либо ставьте сборку с поддержкой, либо не принимайте WebP.imagejpeg(): Unable to open '...' for writing— нет прав на запись в целевую директорию или её вовсе нет. Проверьте существование папки и владельца процесса веб-сервера.- Фон стал чёрным вместо прозрачного — забыли
imagesavealpha(true)перед сохранением PNG. - Класс
Imagickне найден — расширение не установлено. На shared-хостинге может быть только GD; уточните у поддержки или проверьтеextension_loaded('imagick').
Что настроить дальше
Базовый пайплайн — проверка, лимит по памяти, ресайз с пересохранением — закрывает основные риски. Дальше стоит посмотреть в сторону трёх вещей. Вынесите тяжёлую обработку из веб-запроса в очередь (например, генерацию нескольких размеров превью), чтобы пользователь не ждал. Настройте отдачу WebP или AVIF браузерам, которые их поддерживают, — это заметно экономит трафик. И проверьте на проде реальное потребление памяти под пиковой нагрузкой: memory_get_peak_usage(true) в конце запроса покажет, насколько верны ваши оценки по разрешению.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: PHP Manual: Image Processing and GD, PHP Manual: Imagick
Частые вопросы
А если у меня GD без поддержки WebP или AVIF?
getimagesize() полностью защищает от загрузки вредоносных файлов?
Почему процесс падает по памяти на файле всего в несколько мегабайт?
Чем imagecopyresampled отличается от imagecopyresized?
Что делать с EXIF-данными загруженных фото?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.