07. Изображения — ТЗ на загрузку, нарезку и отдачу¶
Отдельный документ, потому что это исторически проблемная зона: изображения загружались, но нарезались неправильно — обрезались не по центру композиции, показывались в пропорциях, отличных от заложенных при загрузке, теряли резкость. Все три дефекта имеют одну общую причину: набор целевых размеров не был зафиксирован до реализации, и нарезка подгонялась под вёрстку задним числом.
7.1. Пункт первый: собрать все размеры изображений¶
Это блокирующая задача этапа. Без неё пресеты нарезки задать нельзя.
Нужно пройти по всем экранам витрины и админки и выписать каждое место, где показывается
картинка, с реальными размерами. Заполняется совместно с фронтенд-разработчиком витрины по
макетам и по свёрстанным страницам (инструмент — DevTools: реальный width/height элемента на
каждой точке перелома, а не размеры из макета).
Форма таблицы, которую нужно заполнить:
| Где используется | Экран / компонент | Отображаемый размер, px | Соотношение | Точки перелома | Ретина | Прозрачность | Пресет |
|---|---|---|---|---|---|---|---|
| Карточка товара в списке | каталог, плитка | ? × ? | ? | 360 / 768 / 1200 | 2x | нет | card |
| Карточка товара, страница товара | детальная | ? × ? | ? | 2x | detail |
||
| Товар в корзине | корзина | ? × ? | 2x | thumb |
|||
| Товар в поиске / подсказках | поиск | ? × ? | |||||
| Модификатор (тесто, соус, топпинг) | выбор модификаторов | ? × ? | да? | modifier |
|||
| Иконка категории | навигация | ? × ? | да | icon |
|||
| Баннер категории | шапка категории | ? × ? | hero |
||||
| Бейдж | плашка на карточке | ? × ? | да | badge |
|||
| Превью в админке, список | таблица товаров | ? × ? | admin-thumb |
||||
| Превью в админке, карточка | форма товара | ? × ? | admin-preview |
||||
| Открытый граф (соцсети, мессенджеры) | og:image | 1200 × 630 | 1.91:1 | — | — | нет | og |
| Мобильное приложение (если есть) | — | 2x, 3x |
Колонки «Соотношение» и «Прозрачность» — ключевые: именно из-за них ломалось. Если в одном месте товар показывается квадратом, а в другом — прямоугольником 4:3, это два разных пресета с разным кропом, а не один растянутый.
Результат этой работы — заполненная таблица и утверждённый список пресетов, который попадает в
конфигурацию (media.presets в application.yml) и в §7.3 этого документа. До утверждения
реализация нарезки не начинается.
Вопросы, на которые таблица должна ответить:
- Какое максимальное соотношение сторон нужно? (определяет требования к исходнику)
- Есть ли места, где картинка обрезается по высоте (
object-fit: cover)? Там нужна точка фокуса. - Где нужен прозрачный фон, а где картинка ложится на цветную подложку?
- Поддерживаем 3x или ограничиваемся 2x? (объём хранилища ×2)
- Одна и та же картинка товара во всех местах или разные (например, обрезанная сверху для узкой плитки)?
7.2. Причины прошлых дефектов и как они закрываются¶
| Дефект | Причина | Решение |
|---|---|---|
| Обрезано не по центру композиции | автоматический кроп по геометрическому центру | точка фокуса: при загрузке админ ставит точку на изображении, все кропы считаются относительно неё |
| Картинка растянута / сплюснута | пресет не совпадает с реальным соотношением блока в вёрстке | пресеты берутся из §7.1, а не назначаются произвольно; тест верстки проверяет соответствие |
| Размыто на ретине | нарезка только в 1x | обязательные @2x варианты, srcset в API |
| Размыто на любых экранах | апскейл маленького исходника | запрет апскейла: если исходник меньше требуемого — загрузка отклоняется с понятным сообщением |
| «Загрузил — на сайте старая» | кеш CDN по одному и тому же URL | имя файла содержит хеш содержимого, URL меняется при перезаливке |
| Тяжёлые страницы | отдача JPEG-оригиналов | webp/avif + фиксированные размеры + loading="lazy" на витрине |
| Разный вид одного товара в разных местах | ручная обрезка при загрузке | загружается один исходник максимального качества, все производные генерируются системой |
7.3. Пресеты (заполняется после §7.1)¶
Формат описания каждого пресета:
media:
presets:
card:
width: 400
height: 400
fit: COVER # COVER (кроп по точке фокуса) | CONTAIN (вписать) | WIDTH (по ширине)
background: none # none | #FFFFFF — для CONTAIN и прозрачных PNG
formats: [webp, jpeg]
densities: [1, 2]
quality: { webp: 82, jpeg: 85 }
upscale: false
Правила именования: <preset>@<density>.<format>, ключ в S3 —
products/{sha256[0:2]}/{sha256}/{preset}@{density}.{format}.
7.4. Требования к загрузке¶
| Параметр | Значение |
|---|---|
| Форматы исходника | JPEG, PNG, WebP, HEIC (конвертируется), не SVG для контентных картинок |
| Максимальный размер файла | 20 МБ |
| Минимальное разрешение | вычисляется как максимум по всем пресетам × максимальная плотность; при загрузке проверяется и сообщается конкретно: «нужно не менее 1600×1600, у файла 800×800» |
| Рекомендуемое разрешение | указывается прямо в интерфейсе загрузки рядом с полем |
| Цветовой профиль | приводится к sRGB (иначе на витрине «выцветшие» фото из-за Adobe RGB) |
| EXIF | ориентация применяется и вычищается; геоданные и прочий EXIF удаляются |
| Дедупликация | по sha256 содержимого: повторная загрузка того же файла не создаёт копию |
| Антивирус / валидация | проверка, что файл действительно изображение (по сигнатуре, не по расширению) |
7.5. Процесс обработки¶
загрузка → валидация (формат, размер, разрешение, сигнатура)
→ нормализация (EXIF-ориентация, sRGB, удаление метаданных)
→ сохранение оригинала в S3 (никогда не удаляется — источник для перегенерации)
→ генерация всех пресетов × плотностей × форматов
→ запись media.file + media.rendition
→ ответ админке с готовыми URL
Генерация — синхронная при загрузке (админ должен сразу увидеть результат), с ограничением по времени; при большом количестве пресетов — параллельно в пуле. Если генерация части ренditions упала, файл сохраняется, недостающие догенерируются фоновой задачей, в админке — индикатор.
Перегенерация: при изменении пресета или добавлении нового — фоновая задача проходит по всем файлам и догенерирует недостающее. Оригиналы для этого и хранятся. Экран «Медиа» показывает прогресс перегенерации.
7.6. Интерфейс загрузки в админке¶
- Drag & drop, вставка из буфера, выбор файла, массовая загрузка.
- Предпросмотр во всех пресетах сразу после загрузки: админ видит, как картинка будет выглядеть в плитке, на детальной странице и в корзине, до сохранения. Это главное средство против «загрузил — а на сайте обрезано не так».
- Установка точки фокуса мышью с живым обновлением предпросмотров.
- Явное сообщение о причине отказа: не «ошибка загрузки», а «разрешение 800×800 меньше требуемого 1600×1600».
- Замена изображения сохраняет точку фокуса и alt.
7.7. Отдача¶
- Прямо из S3 через CDN/nginx, backend в раздаче не участвует.
Cache-Control: public, max-age=31536000, immutable— URL иммутабельны за счёт хеша.- API отдаёт готовый
srcsetиsizes, витрина не занимается подбором:
"image": {
"id": 900,
"url": "/media/ab/ab3f…/card@1.webp",
"width": 400, "height": 400,
"srcset": "/media/ab/ab3f…/card@1.webp 1x, /media/ab/ab3f…/card@2.webp 2x",
"fallback": "/media/ab/ab3f…/card@1.jpeg",
"alt": "Пицца Панам",
"blurhash": "L6PZfSi_.AyE…"
}
blurhash(или доминирующий цвет) — для плейсхолдера при ленивой загрузке; считается один раз при загрузке.- Формат выбирается через
<picture>на витрине (avif → webp → jpeg), а не поAcceptна сервере: так работает CDN-кеш безVary.
7.8. Изображения из iiko не используются¶
В выгрузке есть buttonImageUrl у позиций и категорий (в текущих данных — null почти везде).
Даже когда они заполнены, это ссылки на файлы iiko неизвестного размера и качества, вне нашего
контроля и вне CDN. Витрина использует только изображения из медиабиблиотеки. buttonImageUrl
сохраняется в зеркале iiko как справочная информация и может показываться в админке подсказкой
(«в iiko есть картинка — посмотреть»), но не попадает в публичное API.
7.9. Что нужно от клиента и фронтенда¶
- Заполненная таблица §7.1 — от фронтенд-разработчика витрины по макетам.
- Утверждение списка пресетов — совместно.
- Требования к исходникам, которые получит фотограф/дизайнер: минимальное разрешение, соотношение, фон, отступы (чтобы кроп в квадрат не срезал край блюда).
- Ответ, нужна ли поддержка 3x и AVIF (влияет на объём хранилища и время генерации).