Прямой ответ
Обработка изображений на сервере решает три задачи: привести файл к нужному размеру, выбрать эффективный формат и удалить лишние метаданные. Все три выполняются одной библиотекой, но каждая имеет подводные камни.
Базовый ресайз на Pillow:
from PIL import Image, ImageOps
def make_thumbnail(src: str, dst: str, size: int = 512) -> None:
with Image.open(src) as im:
im = ImageOps.exif_transpose(im) # учёт ориентации из EXIF
im.thumbnail((size, size), Image.LANCZOS)
if im.mode in ("RGBA", "P"):
im = im.convert("RGB")
im.save(dst, "WEBP", quality=82, method=6)
Три важных детали в этих строках:
exif_transposeдо ресайза. Камера часто хранит изображение повёрнутым, а поворот задаётся тегом ориентации. Без этой строки миниатюра получится боком, хотя в браузере оригинал выглядит нормально.thumbnailсохраняет пропорции. Методresizeс явными размерами растягивает изображение, если соотношение сторон не совпадает.- Конвертация в RGB перед JPEG/WebP. Формат JPEG не поддерживает альфа-канал, и сохранение RGBA приводит к ошибке или чёрному фону.
Выбор формата:
| Формат | Когда применять |
|---|---|
| WebP | Фотографии и графика для веба: заметно меньше JPEG при сопоставимом качестве |
| AVIF | Ещё сильнее сжимает, но медленнее кодируется и поддерживается не везде |
| JPEG | Совместимость по умолчанию, фотографии без прозрачности |
| PNG | Графика с прозрачностью, скриншоты, логотипы |
| SVG | Векторная графика, иконки |
Список поддерживаемых браузерами форматов приведён в MDN.
EXIF нужно удалять. Метки времени, модель устройства и, что важнее, координаты GPS сохраняются в файле и уходят вместе с ним. Пересохранение изображения без копирования метаданных убирает их:
im.save(dst, "WEBP", quality=82) # EXIF не переносится, если не передан явно
Практика: безопасная и предсказуемая обработка
1. Остерегайтесь image bomb. Небольшой по объёму файл может описывать изображение огромных размеров, и при распаковке память процесса исчерпывается. Pillow ограничивает это через MAX_IMAGE_PIXELS:
from PIL import Image
Image.MAX_IMAGE_PIXELS = 100_000_000 # 100 млн пикселей
При превышении Pillow выдаёт предупреждение DecompressionBombWarning, а при двукратном превышении — исключение. Проверять размеры нужно до открытия файла.
2. Проверяйте тип по содержимому, а не по расширению. Файл с именем photo.jpg может быть чем угодно. Определение реального формата даёт Image.open(...).format или проверка магических байтов — см. статью про безопасность загрузок.
3. Выносите обработку в очередь. Кодирование изображения — задача с высокой нагрузкой на CPU. Обработка в обработчике HTTP-запроса блокирует воркер; при нескольких одновременных загрузках задержка растёт у всех. Очередь разгружает приём — см. статью про надёжность очередей.
4. Учитывайте расход памяти. Изображение 8000×6000 пикселей в RGB занимает около 144 МБ в распакованном виде независимо от размера файла. Лимит памяти контейнера должен это учитывать — см. статью про requests и limits.
5. Не храните оригинал в публичном доступе. Пользователю нужна миниатюра; оригинал остаётся приватным и отдаётся по подписанной ссылке.
6. Сохраняйте несколько размеров сразу. Генерация миниатюры по требованию при каждом запросе тратит CPU; предварительная подготовка наборов размеров дешевле.
7. Фиксируйте качество кодирования. Значение quality влияет на объём сильнее, чем размер в пикселях. Диапазон 80–85 для WebP даёт хорошее соотношение.
Рекомендации:
- Нормализуйте вход. Приводите всё к одному формату и цветовому пространству, иначе результат зависит от того, чем снят оригинал.
- Обрабатывайте ошибки формата явно. Повреждённый файл должен давать понятный ответ 422, а не 500.
- Используйте
LANCZOSдля уменьшения. Метод влияет на резкость результата;NEARESTдаёт заметные артефакты. - Логируйте размеры и объём до и после. Это единственный способ заметить, что качество кодирования изменилось после обновления библиотеки.
- Проверяйте результат на реальных файлах. Тест на одном изображении не выявляет проблемы с CMYK, палитрой или анимированными форматами.
Практический сценарий: часть миниатюр получалась повёрнутой и с чёрным фоном
Контекст: сервис генерировал миниатюры загруженных фотографий. Пользователи жаловались, что часть изображений отображается боком, а часть имеет чёрный фон вместо белого.
- Диагностика поворота. Проблемные файлы имели тег ориентации EXIF, отличный от 1. Исходный код открывал изображение и сразу уменьшал его, поэтому поворот не применялся, а браузер при показе оригинала поворачивал его сам — отсюда расхождение.
- Диагностика фона. Файлы с альфа-каналом сохранялись в JPEG, и прозрачные области заполнялись чёрным.
- Действия. (а) Добавили
ImageOps.exif_transposeдо ресайза; (б) для форматов без поддержки прозрачности ввели наложение на белый фон вместо простогоconvert("RGB"); (в) перевели вывод на WebP, который поддерживает прозрачность; (г) установилиImage.MAX_IMAGE_PIXELSи проверку размеров до обработки; (д) перенесли генерацию миниатюр в фоновую очередь, поскольку кодирование занимало до двух секунд и блокировало обработчик запросов. - Результат. Поворот и фон стали корректными для всех проверенных форматов; время ответа на загрузку сократилось с двух секунд до 120 миллисекунд, потому что обработка перестала выполняться синхронно.
Вывод: проблемы обработки изображений почти всегда связаны с метаданными и цветовыми моделями, а не с самим алгоритмом ресайза. Ориентация из EXIF и альфа-канал требуют явной обработки, иначе результат зависит от исходного файла.
FAQ
Нужно ли хранить оригинал, если есть миниатюра? Да, если возможны повторная обработка или показ в большем размере. Миниатюра необратимо теряет данные.
Почему WebP лучше JPEG для веба? При сопоставимом визуальном качестве WebP даёт меньший объём, что сокращает время загрузки страницы. Поддержка в браузерах стала повсеместной.
Удаляет ли ресайз метаданные автоматически? Пересохранение без явной передачи метаданных их не копирует, поэтому координаты GPS исчезают. Полагаться на это стоит только после проверки конкретного формата.
Можно ли обрабатывать изображения без сервера? Да, часть операций выполняется на стороне клиента, но проверка содержимого и защита от вредоносных файлов остаются задачей сервера.
Источники: Pillow docs, MDN (image types), Exiv2 (теги EXIF) — ссылки в шапке.