Заметки команды о мастерстве, форматах и небольших решениях, стоящих за хорошей круглой обрезкой.
Библиотека cropperjs v2 и что изменилось по сравнению с v1
Этот инструмент построен на cropperjs версии 2.1.1, полностью переписанной библиотеке оригинального cropperjs. Релиз v2 заменяет старый API на основе конструктора (new Cropper(img, { aspectRatio: 1 })) на архитектуру Web Components. Холст обрезки, наложение выделения и сам элемент изображения теперь являются пользовательскими HTML-элементами, зарегистрированными через браузерный CustomElementRegistry. Это значит, что Shadow DOM отвечает за все внутренние стили: отдельного файла cropper.css импортировать не нужно, что устраняет один запрос ассета Vite по сравнению с v1. Блокировка соотношения сторон теперь задаётся на самом элементе выделения (cropperSelection.aspectRatio = 1), а не на конструкторе, поэтому соотношение можно менять в рантайме без переинициализации всего интерфейса обрезки. Библиотека весит 41 960 байт в исходном виде и 12 175 байт в gzip, измерено напрямую по заголовку content-length с CDN jsDelivr. Эти 12 КБ платятся один раз за сессию и переиспользуются на всех пресет-маршрутах обрезки благодаря разбиению чанков Vite.
Как рассчитываются пиксельные размеры вывода (без увеличения)
Когда вы перетаскиваете выделение обрезки на экране, координаты, которые вы видите, заданы в пиксельном пространстве исходного изображения, а не в пространстве CSS-пикселей. Это различие важно для экранов с высоким DPI: телефон с коэффициентом плотности 3x показывает изображение шириной 300 CSS-пикселей как 900 реальных пикселей, но координаты выделения отражают именно 900 реальных пикселей, а не 300 CSS. Скачиваемый файл вывода содержит ровно столько пикселей, сколько прямоугольник выделения занимает в исходном изображении. Операция обрезки вызывает selection.$toCanvas() из API cropperjs v2. Это асинхронный метод, возвращающий свежий HTMLCanvasElement, размер которого равен пиксельным размерам выделения. Затем этот холст передаётся в canvas.toBlob() для PNG, JPG или WebP, либо в кодировщик @jsquash/avif для AVIF. Шага увеличения не вставляется. Если ваше выделение шириной 800 пикселей, то и вывод будет шириной 800 пикселей. Пресеты социальных платформ (1080x1080 для Instagram, 1280x720 для YouTube) выдаются в этих размерах только тогда, когда в исходном изображении достаточно пикселей, чтобы заполнить выделение.
Удаление метаданных EXIF и что при этом теряется
Каждый экспорт удаляет данные EXIF, IPTC, XMP и ICC-профиль. Это происходит на уровне канвас-конвейера: браузер рисует декодированные пиксели изображения на HTMLCanvasElement, и холст кодирует это в новый файл. У холста нет понятия контейнеров метаданных, он хранит только значения пикселей и параметры кодирования. К удаляемому относятся координаты GPS, производитель и модель камеры, фокусное расстояние объектива, настройки экспозиции, поля авторских прав и теги цветовых профилей. Цветовой профиль sRGB фактически является допущением холста: цвета рендерятся через конвейер отображения браузера и перекодируются без прикреплённого профиля. Для большинства задач веб-публикации и социальных сетей удалённые EXIF предпочтительны, потому что данные GPS могут раскрыть информацию о местоположении, которой пользователь не собирался делиться. Для профессиональных фотографических процессов, где точность цветового профиля важна, обратите внимание, что точность ICC-профиля не сохраняется, и для доставки в печать или публикацию следует использовать инструмент экспорта с управлением цветом.
Что происходит при обрезке анимированного GIF
Когда вы перетаскиваете анимированный GIF в инструмент обрезки, браузер декодирует в элемент изображения только первый кадр, используемый холстом обрезки. Это поведение браузера, а не ограничение cropperjs: HTMLImageElement не предоставляет JavaScript отдельные кадры анимированного GIF так, чтобы API канваса мог их перебрать. В результате для выделения обрезки доступен только первый кадр, и итоговый вывод представляет собой неподвижное изображение в любом формате, который вы выберете на финальном экране. Если на входе GIF и вы выбираете GIF на выходе, реализация canvas.toBlob в браузере не включает кодировщик GIF, поэтому вывод молча откатывается на PNG. Инструмент явно отклоняет GIF как опцию формата вывода, чтобы избежать этого тихого понижения. Если нужно обрезать все кадры анимации, понадобится инструмент, который работает покадрово, а этот инструмент такое не поддерживает.
Клавиатурная навигация и доступность
Элемент выделения cropperjs v2 реализует клавиатурную навигацию как часть своей спецификации Web Component. Как только вы переходите Tab на выделение обрезки, клавиши стрелок перемещают рамку на один пиксель за раз в нажатом направлении. Shift вместе со стрелкой перемещает рамку на десять пикселей. Это нативно доступно без обходных решений со скринридером или пользовательских JavaScript-наложений: обработчик клавиатуры живёт внутри shadow root элемента выделения и срабатывает на keydown. Это важно для пользователей, которые полагаются на навигацию только с клавиатуры, включая некоторые конфигурации вспомогательных технологий, устройства моторной доступности и точные процессы, где трекпад не может надёжно достичь субпиксельного выравнивания. Живой счётчик пикселей, привязанный к выделению, также обновляется с каждым нажатием клавиши, поэтому пользователи скринридеров с aria-live областями на счётчике могут слышать, как меняются текущие размеры. Доступность с клавиатуры остаётся одной из областей, где этот инструмент опережает iLoveIMG и img2go, в которых не реализована навигация обрезки стрелками.
Компромиссы выбора формата после обрезки
Селектор формата на финальном экране даёт четыре варианта. PNG идёт без потерь: каждый пиксель в области обрезки хранится ровно так, как был декодирован из исходника. Размер файла больше, чем у JPG для того же содержимого, обычно в три-пять раз больше для фотографии. JPG применяет сжатие с потерями при качестве 0.9 (по шкале от 0 до 1), что примерно соответствует пресету высокого качества в большинстве настольных редакторов. При q0.9 разница между оригиналом и выводом обычно не видна на стандартном мониторе, но файл заметно меньше. WebP при том же качестве даёт файл примерно на 25 до 35 процентов меньше, чем JPG, в наших тестах кодирования, с тем же воспринимаемым качеством на фотографиях. WebP также поддерживает режим без потерь и сохраняет прозрачность. AVIF при качестве 0.9 через библиотеку @jsquash/avif выдаёт самый маленький вывод из четырёх форматов, но первое кодирование в сессии требует загрузки WASM-модуля 870 КБ. Последующие кодирования AVIF в той же вкладке быстрые, потому что модуль уже загружен.