Перейти к содержанию

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 этого документа. До утверждения реализация нарезки не начинается.

Вопросы, на которые таблица должна ответить:

  1. Какое максимальное соотношение сторон нужно? (определяет требования к исходнику)
  2. Есть ли места, где картинка обрезается по высоте (object-fit: cover)? Там нужна точка фокуса.
  3. Где нужен прозрачный фон, а где картинка ложится на цветную подложку?
  4. Поддерживаем 3x или ограничиваемся 2x? (объём хранилища ×2)
  5. Одна и та же картинка товара во всех местах или разные (например, обрезанная сверху для узкой плитки)?

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. Что нужно от клиента и фронтенда

  1. Заполненная таблица §7.1 — от фронтенд-разработчика витрины по макетам.
  2. Утверждение списка пресетов — совместно.
  3. Требования к исходникам, которые получит фотограф/дизайнер: минимальное разрешение, соотношение, фон, отступы (чтобы кроп в квадрат не срезал край блюда).
  4. Ответ, нужна ли поддержка 3x и AVIF (влияет на объём хранилища и время генерации).