Перейти к содержимому
Поиск
Разработка

Cropper.js: кадрирование изображений — мастерская и 8 примеров

Выбираем кадр прямо в браузере: аватар, карточка товара, обложка, поворот и круглый PNG. Восемь живых задач, локальные файлы, настоящий экспорт и готовые исходники.

Александр Некрасов
Cropper.js — кадрирование изображений в браузере: рамка, поворот и готовый результат

Пользователь загрузил фотографию для профиля, но лицо оказалось у края. Редактор добавил обложку статьи, а в широком блоке исчез главный объект. В каталоге одинаковые товары занимают разное место, и карточки выглядят случайным набором картинок. Эти проблемы решаются не очередным ограничением «загрузите правильный размер», а понятным инструментом подготовки изображения прямо в интерфейсе.

Cropper.js позволяет выбрать нужный фрагмент, закрепить пропорции, повернуть исходник и получить готовый файл в браузере. Ниже — рабочая мастерская на версии 2.3.1: восемь задач, собственные учебные иллюстрации, локальные изображения, настоящий PNG/JPEG и исходники. Разберём не только вызовы библиотеки, но и путь от кнопки выбора файла до размещения редактора на Linux VPS.

Где пригодится кадрирование в интерфейсе

В личном кабинете человеку нужен узнаваемый аватар. Полезный интерфейс показывает рамку заранее, позволяет приблизить лицо и объясняет, какой результат сохранится. В редакционной системе задача другая: из одного исходника нужно получить широкую обложку для статьи и квадратную картинку для списка материалов. Пользователь выбирает смысловой центр, а система обеспечивает одинаковые технические параметры.

В интернет-магазине кадрирование помогает выровнять фотографии товаров. В CRM его можно применять к фотографиям сотрудников, логотипам клиентов и иллюстрациям проектов. В конструкторе мероприятий — к обложке события и бейджу участника. В справочном разделе — к фрагменту скриншота, на котором видно конкретную настройку, без лишних панелей вокруг.

У всех этих ситуаций есть общее требование: человек должен понимать последствия действия до сохранения. Поле с размерами само по себе этого не даёт. Рамка, предпросмотр, понятные пропорции и возможность сброса делают результат предсказуемым. Поэтому мастерская построена вокруг задач пользователя, а технические параметры доступны рядом, когда они действительно нужны.

Что делает Cropper.js, а что остаётся приложению

Cropper.js управляет графическим редактором в браузере: изображением, областью выбора и преобразованиями. В нашем проекте библиотека получает источник, пользователь меняет кадр, затем выделенная область рисуется в canvas. Браузер кодирует этот canvas в PNG или JPEG, а страница предлагает скачать файл. Сервер не получает фотографию и не участвует в расчёте кадра.

Но кадрирование не решает все задачи медиасистемы. Проверку прав, привязку к записи, хранение оригинала, удаление старого файла, серверную перепроверку и выпуск нескольких размеров нужно проектировать отдельно. Если пользователю разрешено менять аватар, это не означает, что сервер может принять произвольный файл под произвольным именем. Полученный Blob остаётся недоверенным входом для вашего API.

Полезно разделить роли: человек выбирает композицию в Cropper.js, интерфейс загрузки показывает передачу и ошибки, сервер готовит окончательные варианты. На сайте уже есть материалы про FilePond и загрузку файлов и про обработку изображений через Sharp. Их можно соединить в один процесс, сохранив понятную ответственность каждого этапа.

Мини-мастерская: восемь задач и настоящий результат

Начните с аватара: передвиньте квадрат кнопками или мышью и скачайте PNG. Затем переключитесь на баннер и сравните, как меняется композиция при широком кадре. В сценарии размера попробуйте две настройки качества JPEG и смотрите на вес реально созданного файла. Это не расчёт по приблизительной формуле: справа показан размер Blob, из которого построен предпросмотр и который будет скачан.

Сценарии используют общий полноценный редактор. Переключение задачи загружает соответствующий учебный исходник и начальные настройки. Можно изменить иллюстрацию, открыть свой файл, повернуть его, задать координаты, выбрать формат и получить код текущего экспорта. Восьмой сценарий сразу раскрывает выбор локального файла; кнопка проверки ошибки позволяет увидеть отказ без потери текущего кадра.

Встроенный блок имеет собственную прокрутку. На телефоне удобнее открыть мастерскую отдельно: рабочее поле и кнопки займут доступную ширину. Все изображения ниже созданы для этого материала из геометрических фигур. Для проверки настоящих фотографий воспользуйтесь выбором файла: его содержимое остаётся в браузере.

Версия 2.x: отдельные элементы вместо старого общего API

В примерах из поиска легко встретить Cropper.js 1.x с методами вроде getCroppedCanvas. Этот материал использует другую ветку: 2.3.1. Редактор собран из веб-компонентов, а нужная операция вызывается у соответствующего элемента. Изображение отвечает за преобразования, выделение — за рамку, рабочее поле — за размещение элементов.

Для прикладного кода удобно сначала создать экземпляр Cropper, а затем получить ссылки на изображение и выделение. Не смешивайте старую конфигурацию с новыми методами: похожие названия задач не означают совместимость примеров. В архиве закреплены версии всей установки через lock-файл; итоговый JavaScript содержит компоненты из этой установки и не обращается к внешнему CDN.

import Cropper from 'cropperjs';
const image = new Image();
image.src = '/images/source.png';
await image.decode();
const cropper = new Cropper(image, { container: '#editor' });
const selection = cropper.getCropperSelection();
const cropperImage = cropper.getCropperImage();
await cropperImage.$ready();

Подробнее: конструктор и элементы Cropper.js.

Установка и устройство проекта

Для изменения исходников нужен Node.js 24. В готовом архиве уже есть собранная папка public, поэтому разместить редактор можно и на обычном статическом хостинге. Node.js используется для установки зависимостей, сборки и проверок; постоянно работающий Node-процесс публичному редактору не требуется.

Исходный app.js управляет интерфейсом, а model.js хранит сценарии, ограничения, проверку заголовков изображения и генератор примера. Сборка помещает локальный bundle в public/app.js, копирует Bootstrap и точные исходники в public/source. Это позволяет прочитать реализацию прямо из мастерской и затем взять весь проект для своего приложения.

Если начинаете отдельный проект, установите зависимости явно. Для повторения приложенного примера используйте npm ci: эта команда ориентируется на уже включённый package-lock.json. Не заменяйте её произвольным обновлением пакетов в середине проверки — иначе у вас будет другая сборка, которую нужно заново испытать.

npm install --save-exact cropperjs@2.3.1 bootstrap@5.3.8
npm install --save-dev --save-exact esbuild@0.28.2

# В скачанном проекте вместо новой установки:
npm ci --ignore-scripts
npm run build
npm test

Сценарий 1: квадратный аватар для личного кабинета

Аватар часто показывается небольшим, поэтому важнее выбрать лицо и характерные детали, чем сохранить весь исходник. Квадрат 1:1 даёт стабильный результат для списка сотрудников, комментариев и профиля. В мастерской этот сценарий выпускает файл шириной 512 пикселей; рамка остаётся квадратной при изменении размера.

Попробуйте сместить кадр на десять пикселей кнопкой вправо. Затем измените поле X и примените координаты. Оба способа работают с одним выделением. Так человек может сначала примерно выбрать композицию перетаскиванием, а затем аккуратно поправить её без точного попадания по маленькому маркеру.

Круглое отображение в интерфейсе и круглое содержимое файла — разные решения. Для обычного профиля часто достаточно квадратного изображения и CSS border-radius на элементе img. Это сохраняет универсальный файл для других экранов. Если нужен именно PNG с прозрачными углами, используйте отдельный сценарий круглого экспорта, который рассмотрим ниже.

selection.aspectRatio = 1;
selection.$change(30, 20, 180, 180, 1);
const avatarCanvas = await selection.$toCanvas({ width: 512, height: 512 });

Сценарий 2: карточка товара с пропорциями 3:4

Для каталога важно, чтобы соседние карточки воспринимались как одна система. Вертикальный кадр 3:4 подходит для многих предметных фотографий: остаётся место над и под объектом, а сетка не прыгает от разных пропорций исходников. При этом универсального правильного кадра для любого товара нет: длинному инструменту и компактной кружке могут потребоваться разные композиции.

В примере выберите рамку так, чтобы у предмета сохранилась ручка. Затем приблизьте изображение и обратите внимание, как легко потерять полезную деталь. Это хорошая иллюстрация для администратора каталога: технически корректное соотношение сторон ещё не означает удачную фотографию.

В готовом магазине полезно хранить исходник и отдельные параметры выбранной области, а после подтверждения создавать нужные размеры для списка и карточки. Не заставляйте человека повторно выбирать кадр только потому, что дизайнер добавил ещё один размер превью. Перед сохранением покажите результат именно в том контексте, где его увидит покупатель.

selection.aspectRatio = 3 / 4;
selection.$change(70, 15, 180, 240, selection.aspectRatio);
const productCanvas = await selection.$toCanvas({ width: 720, height: 960 });

Сценарий 3: широкая обложка и смысловой центр

В обложке статьи часто важен не центр файла, а конкретный объект: здание, устройство, лицо или интерфейс на экране. Автоматическое центральное обрезание может убрать именно то, ради чего выбрана картинка. В сценарии 16:9 сдвиньте рамку между зданием и небом. Один исходник даст разные визуальные акценты без изменения содержимого.

Для лендинга оставляйте пространство там, где будет заголовок. Лучше выводить заголовок отдельным HTML поверх изображения: он останется доступным поиску, адаптивной вёрстке и экранному диктору. Саму картинку можно подготовить так, чтобы важный объект не попадал под текст.

В мастерской ширина баннера по умолчанию 1280 пикселей. Это учебное значение, а не обязательный размер любой обложки. Выбирайте размер по реальному контейнеру, плотности экрана и бюджету загрузки. Для узкой карточки можно сделать другую производную, сохранив тот же смысловой центр, но проверив её композицию отдельно.

selection.aspectRatio = 16 / 9;
const cover = await selection.$toCanvas({ width: 1280, height: 720 });

Сценарий 4: свободный фрагмент для инструкции

Когда изображение поясняет настройку, фиксированные пропорции могут мешать. Нужно показать поле и соседнюю подпись, а лишняя панель только отвлекает. Свободное выделение позволяет подобрать рамку по содержимому. В мастерской у этого сценария доступны независимые ширина и высота рамки; размер результата по вертикали следует получившейся пропорции.

Задайте узкий фрагмент, примените координаты и посмотрите на высоту экспортируемого файла. Затем расширьте рамку. Вы увидите связь между формой выбранной области и итоговым размером: ширина вывода сама по себе не описывает весь результат.

Для справочного материала оставляйте достаточно контекста. Если вырезать только кнопку без названия окна и соседних элементов, новичку может быть трудно найти её у себя. Кадрирование должно помогать понять действие, а не просто уменьшать картинку. Если скриншот содержит персональные данные, сначала удалите или надёжно заретушируйте их в предназначенном для этого процессе: выбор фрагмента не заменяет проверку содержимого.

selection.aspectRatio = NaN; // свободные пропорции
selection.$change(20, 30, 260, 110, NaN);
const fragment = await selection.$toCanvas({ width: 900 });

Сценарий 5: поворот, отражение и пустые области

Поворот полезен для выравнивания фотографии, а отражение — для некоторых дизайнерских композиций. Но отражать документы, подписи и маркировку обычно нельзя: текст станет зеркальным, а смысл изображения изменится. В интерфейсе лучше называть эти действия явно, чем прятать их за похожими безымянными значками.

В мастерской поверните предмет на пятнадцать градусов и посмотрите на края. Рамка остаётся в рабочем поле, однако после преобразования часть её площади может оказаться вне изображения. Шахматный фон показывает пустоту. Увеличьте изображение или уменьшите кадр, если прозрачные углы не нужны.

Кнопка сброса возвращает исходные преобразования и заново вписывает рамку, сохраняя открытый файл. Это важнее, чем кажется: пользователь должен спокойно экспериментировать и иметь понятный путь назад. Наш стенд ограничивает масштаб относительно начального вписывания, чтобы случайное многократное нажатие не превратило изображение в почти невидимую точку или огромный фрагмент.

cropperImage.rotatable = true;
cropperImage.scalable = true;
cropperImage.$rotate('15deg');
cropperImage.$scale(-1, 1); // горизонтальное отражение
cropperImage.$scale(1.1); // увеличение относительно текущего состояния
// Для сброса изображения:
cropperImage.$resetTransform();
cropperImage.$center('contain');

Подробнее: преобразования CropperImage.

Сценарий 6: круглый PNG и фон JPEG

Для бейджа, аватарки участника или графической наклейки бывает нужен файл с прозрачными углами. В версии 2.3 у выделения есть borderRadius, который применяется и при экспорте через canvas. В нашей мастерской круг разрешён для квадратной рамки: так результат соответствует названию и не превращается неожиданно в овал.

Сначала скачайте PNG. Затем выберите JPEG и задайте светлый или тёмный фон. Сравните углы: PNG сохраняет прозрачность, JPEG хранит сплошное изображение, поэтому приложение явно подкладывает выбранный цвет. Если не продумать этот этап, результат может оказаться с нежелательным фоном.

Этот пример пригодится при создании карточек участников мероприятия и изображений для презентаций. Для обычного веб-аватара всё ещё может быть удобнее квадратный файл с круглым CSS-отображением. Выбирайте вариант по тому, где файл будет использоваться после скачивания: внутри вашего интерфейса или в стороннем редакторе.

selection.aspectRatio = 1;
selection.borderRadius = '50%';
const canvas = await selection.$toCanvas({ width: 512, height: 512 });
// Прозрачные углы сохраняются при кодировании в PNG.
canvas.toBlob(blob => { /* сохранить полученный Blob */ }, 'image/png');

Подробнее: рамка, ограничения и экспорт выделения.

Сценарий 7: качество, разрешение и реальный вес

Число пикселей и качество JPEG влияют на результат по-разному. Уменьшение ширины сокращает количество деталей и общий объём изображения. Изменение качества меняет характер сжатия при тех же размерах. Для фотографии небольшое снижение качества часто незаметно, а на тексте, тонких линиях и резких цветовых границах дефекты могут проявиться раньше.

В мастерской выберите JPEG, оставьте ширину 1280 и сравните качество 90 и 50. Затем уменьшите ширину до 640. Смотрите одновременно на изображение и на вес. Нельзя заранее обещать одинаковую экономию для любых картинок: однотонная иллюстрация, шумная фотография и скриншот с текстом сжимаются по-разному.

Для PNG ползунок качества отключён, чтобы не создавать ложного ощущения контроля. Размер файла здесь зависит от содержимого, разрешения и кодирования браузера. Если нужна централизованная политика WebP/AVIF, несколько адаптивных вариантов и стабильный серверный конвейер, подключайте обработку после загрузки. Браузерный редактор прежде всего помогает человеку выбрать нужный кадр.

const blob = await new Promise((resolve, reject) => {
  canvas.toBlob(value => value ? resolve(value) : reject(new Error('Нет результата')),
    'image/jpeg', 0.85);
});
console.log(blob.type, blob.size); // фактический MIME и вес в байтах

Подробнее: кодирование canvas в Blob.

Сценарий 8: свой файл, ошибка и восстановление

Выбор локального файла должен быть безопасным для текущей работы. Если человек случайно указал текстовый документ, редактор не должен очищать уже настроенное изображение. В мастерской неверный вход показывает сообщение, а текущий источник и рамка сохраняются. Кнопка теста ошибки позволяет проверить это поведение без подготовки отдельного файла.

Принимаются PNG, JPEG и WebP в пределах указанных ограничений. SVG, GIF и анимация в этом учебном редакторе не поддерживаются. Расширение и значение file.type не считаются достаточным доказательством: код читает сигнатуру и объявленные размеры, а затем проверяет успешное декодирование и реальные размеры изображения.

Файл не отправляется в WordPress или на какой-либо внешний API. После чтения браузер рисует декодированное изображение в промежуточный canvas и создаёт нормализованный источник для редактора. Оригинал на диске не изменяется. Пользователь может скачать результат как новый файл и сам решить, где его использовать дальше.

import { LIMITS, inspectImage } from './model.js';
if (file.size < 1 || file.size > LIMITS.bytes) {
  throw new Error('Файл пустой или больше 8 МиБ');
}
const declared = inspectImage(await file.arrayBuffer());
// Далее: декодировать, проверить фактические размеры и нормализовать.
// Полная обработка с освобождением URL находится в src/app.js.
console.log(declared.mime, declared.width, declared.height);

Координаты рамки и размер исходника — разные величины

Поля X, Y, ширина и высота в мастерской относятся к рабочему полю редактора в CSS-пикселях. Исходное изображение может иметь намного больше пикселей и отображаться уменьшенным. Кроме того, у него есть матрица преобразования: масштаб, поворот, отражение и перемещение. Поэтому нельзя бездумно взять координаты рамки и применить их на сервере как координаты оригинала.

Для переноса выбора между экранами полезно хранить нормализованную модель состояния и явно определять её систему координат. Если сохраняется матрица и рамка текущего редактора, нужны также размеры рабочего поля и правила восстановления. Если сервер должен вырезать прямоугольник исходника, потребуется корректное преобразование координат, особенно после поворота.

Код текущего экспорта в мастерской честно показывает размеры поля, рамку и матрицу. Это фрагмент для уже созданного экземпляра, а не универсальный файл, который сам найдёт любую фотографию и воспроизведёт её на другом экране. Для самостоятельного запуска скачайте полный проект, где есть создание редактора, обработка загрузки и управление состоянием.

const transform = cropperImage.$getTransform(); // [a, b, c, d, e, f]
const draft = {
  transform,
  selection: { x: selection.x, y: selection.y,
    width: selection.width, height: selection.height },
  canvas: { width: cropper.getCropperCanvas().clientWidth,
    height: cropper.getCropperCanvas().clientHeight }
};

EXIF, ориентация и данные после экспорта

Фотография с телефона может храниться в одном расположении пикселей, а правильное отображение задаваться метаданными ориентации. Это одна из причин, почему картинка в старом обработчике иногда поворачивается неожиданно. Не переносите из примеров 1.x предположение, что checkOrientation автоматически решает эту задачу в 2.x: архитектура и доступные настройки изменились.

Наш стенд использует декодирование современным браузером и затем рисует полученный результат в новый canvas. В архив включён собственный JPEG с Orientation=6: заголовок содержит размеры 900 × 1200, а ориентационно-корректное отображение показывает горизонтальный городской рисунок. Такой файл полезен для проверки именно вашего браузера и пути загрузки.

Новый файл создаётся из пикселей, исходный EXIF не копируется как часть приложения. Однако это не обещание универсальной очистки любого формата, цветового профиля и служебных данных во всех браузерах. Если у сервиса есть строгие требования к метаданным, цвету или конфиденциальности, закрепите отдельную серверную политику обработки и проверьте реальные выходные файлы.

Подробнее: различия веток и миграция API.

Ограничения памяти: почему недостаточно проверить мегабайты

Сжатый файл может быть небольшим, но после декодирования занимать значительный объём памяти. Изображение на шестнадцать миллионов пикселей требует десятки мегабайт только для обычного растрового буфера; рядом существуют исходник, промежуточный canvas и результат. На телефоне такая нагрузка заметнее, чем на рабочей станции.

Поэтому мастерская ограничивает и размер файла, и количество пикселей: восемь МиБ, шестнадцать миллионов пикселей и 8192 пикселя по стороне. Заголовки проверяются до декодирования. Для вывода действует отдельный бюджет: до 2048 по стороне и четырёх миллионов пикселей. Квадрат 2048 × 2048 немного превышает этот бюджет, а 2000 × 2000 помещается.

Это выбранные ограничения учебного приложения, а не заявленные пределы Cropper.js. В своём сервисе определите их по реальной аудитории и измерениям. Показывайте пределы рядом с выбором файла, объясняйте отказ и предлагайте понятное действие: например, выбрать уменьшенную копию. Молчаливое сильное уменьшение исходника может испортить ожидаемый результат и лишить человека контроля.

Экспорт: прозрачность, ошибки и освобождение ресурсов

Экспорт состоит из двух операций: выделение создаёт canvas, затем браузер кодирует его в Blob. Обе операции могут завершиться неуспешно, поэтому кнопка скачивания должна становиться доступной только после получения пригодного результата. В нашем интерфейсе один Blob используется и для предпросмотра, и для скачивания: человек видит именно тот файл, который получит.

Для JPEG фон подкладывается после рисования изображения через destination-over. Так цвет заполняет прозрачные участки, включая углы круглого кадра. Для PNG этот шаг пропускается. После кодирования проверяется фактический MIME: если браузер вернул другой формат, приложение сообщает об этом вместо сохранения файла с неверным расширением.

Объектный URL удобно связать с изображением предпросмотра и ссылкой скачивания, но он удерживает ресурс. Освобождайте старый URL после замены, а не сразу после назначения src. В проекте также отменяется право устаревшего асинхронного результата обновлять интерфейс: более медленный предыдущий экспорт не должен затереть новый кадр.

const context = canvas.getContext('2d');
context.save();
context.globalCompositeOperation = 'destination-over';
context.fillStyle = '#ffffff';
context.fillRect(0, 0, canvas.width, canvas.height);
context.restore();

const objectURL = URL.createObjectURL(blob);
preview.src = objectURL;
// Когда этот предпросмотр заменён и больше не нужен:
// URL.revokeObjectURL(objectURL);

Подробнее: временные адреса Blob.

CORS: изображение видно, но скачать кадр не получается

Чужая картинка может отображаться в img, но это не означает, что браузер разрешит прочитать результат её рисования в canvas. При источнике с другого origin действуют правила CORS. Если необходимые разрешения отсутствуют, canvas становится непригодным для чтения, и экспорт завершается ошибкой безопасности.

В мастерской внешние адреса не вводятся: учебные файлы лежат рядом со страницей, а собственный файл выбран пользователем локально. Это делает поведение понятным. Если вы добавляете удалённую медиатеку, настройте разрешённый источник, заголовки сервера и crossorigin до назначения src. Не обещайте, что одна строка на клиенте исправит произвольный сторонний сайт.

const image = new Image();
image.crossOrigin = 'anonymous'; // установить до src
image.src = 'https://media.example.com/approved-image.jpg';
// Сервер media.example.com должен разрешать нужный origin через CORS.
await image.decode();

Подробнее: CORS и изображения в canvas.

Асинхронная работа, сброс и жизненный цикл

Декодирование файла, создание canvas и кодирование результата не происходят мгновенно. Пользователь может выбрать другую задачу раньше, чем завершится предыдущая операция. Поэтому полезно присваивать операциям номер версии состояния: результат применяется только в том случае, если он относится к текущему состоянию. Мастерская использует такой подход и для открытия изображения, и для предпросмотра.

При движении рамки не нужно создавать тяжёлый файл на каждый отдельный пиксель перемещения. Здесь обновление отложено на короткую паузу, а кнопка скачивания временно блокируется. Интерфейс сохраняет отзывчивость, но не предлагает старый файл под видом уже изменённого результата.

При замене редактора освобождается прежний экземпляр и временные URL. При изменении ширины рабочего поля наш стенд заново вписывает изображение и рамку, явно сообщая об этом. Для редактора с длительными черновиками лучше реализовать восстановление нормализованного выбора после resize. Важно заранее определить такое поведение, чтобы поворот телефона не приводил к необъяснимой потере работы.

let revision = 0;
async function updatePreview() {
  const ownRevision = ++revision;
  const canvas = await selection.$toCanvas({ width: 800 });
  const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
  if (ownRevision !== revision || !blob) return;
  // Только актуальный результат получает право заменить предпросмотр.
}

Как добавить отправку в PHP или Node.js приложение

В этом проекте отправки нет, но место её подключения вполне определённое: после подтверждения кадра у вас есть Blob. Его можно положить в FormData и передать существующему обработчику загрузки. Показывайте отдельный статус передачи и не смешивайте «кадр готов» с «изображение сохранено в профиле». При сетевой ошибке человек должен иметь возможность повторить передачу, сохранив выбранный кадр.

Сервер проверяет сессию, право менять конкретную запись, CSRF-защиту, допустимый размер и фактическое содержимое. Имя файла лучше выдавать на сервере, а не использовать переданный путь. Хранилище должно исключать исполнение загруженного содержимого. Для публичных изображений создавайте подготовленные производные; оригиналы и служебные данные не обязаны быть доступны напрямую.

Ниже показана только точка интеграции с уже существующим защищённым API. Это не готовый PHP-обработчик и не маршрут данной мастерской. Реализация приёма зависит от приложения: кому принадлежит изображение, где оно хранится, какие размеры нужны и как удаляется предыдущая версия. Клиентские ограничения повторяются на сервере, потому что HTTP-запрос можно отправить и без нашего интерфейса.

// Пример интеграции; маршрут и CSRF-токен предоставляет ваше приложение.
const form = new FormData();
form.append('image', blob, 'avatar.png');
const response = await fetch('/api/profile/avatar', {
  method: 'POST', credentials: 'same-origin',
  headers: { 'X-CSRF-Token': csrfToken }, body: form
});
if (!response.ok) throw new Error('Сервер не сохранил изображение');

Удобство на мобильном и доступность управления

На узком экране инструменты должны оставаться подписанными и доступными пальцу. В мастерской сценарии перестраиваются в две колонки, рабочее поле занимает всю ширину, результат располагается ниже, а группы кнопок переносятся на следующие строки. Поля координат дополняют перетаскивание: пользователю не обязательно ловить маленький угол рамки.

Текстовые сообщения сообщают о загрузке, ошибке и готовом результате. Кнопки имеют видимое состояние фокуса, а смещения доступны обычным нажатием с клавиатуры. При этом визуальная композиция остаётся графической задачей. Наличие подписей и клавиатурных кнопок не является доказательством полной доступности редактора для незрячего человека.

Для продукта проверьте реальный телефон, масштаб браузера, длинное имя файла, локальный снимок с камеры и восстановление после ошибочного выбора. Убедитесь, что прокрутка страницы не мешает жестам в редакторе, а интерфейс объясняет, где заканчивается рабочее поле. Режим узкого окна на компьютере полезен для вёрстки, но не заменяет физическое устройство и проверку экранным диктором.

Запуск на Windows без изменения политики PowerShell

Скачайте ZIP, распакуйте его в отдельную папку и откройте терминал внутри cropper-project. Установленный Node.js должен быть версии 24. Команды npm.cmd обходят типичную проблему с запретом запуска npm.ps1, не требуя менять политику выполнения PowerShell для всей системы.

После установки выполните сборку и тесты, затем запустите локальный сервер. Откройте адрес 127.0.0.1:8143 в браузере. Сервер читает только public и не принимает POST. Для остановки используйте Ctrl+C. Если порт занят, задайте другой через переменную PORT и откройте соответствующий адрес.

Не открывайте index.html двойным щелчком как file://: модульному JavaScript нужен корректный HTTP-контекст. Если Node.js нужен только для сборки, а PHP уже установлен, готовую папку можно проверить встроенным PHP-сервером. Это локальный способ просмотра статических файлов, а не производственная конфигурация и не обработчик изображений.

Expand-Archive -LiteralPath .\cropper-project.zip -DestinationPath .\cropper-demo
Set-Location .\cropper-demo\cropper-project
node --version
npm.cmd ci --ignore-scripts
npm.cmd run build
npm.cmd test
npm.cmd start
# Альтернатива для уже собранной public при установленном PHP:
# php -S 127.0.0.1:8143 -t public

Запуск на Linux и проверка готовой сборки

На Linux процесс сборки такой же: распакуйте проект в рабочий каталог обычного пользователя, установите зависимости из lock-файла и выполните проверки. npm не требуется запускать от root. Если Node.js ещё не установлен, подготовьте версию 24 из доверенного источника и сначала проверьте node –version.

Локальный сервер по умолчанию слушает loopback. Это удобно для разработки на той же машине или подключения через SSH-туннель. Не открывайте его наружу просто ради демонстрации: для постоянного публичного адреса проще обслуживать готовую статику через нормальный веб-сервер с HTTPS.

Проверьте заголовки JavaScript и доступность иллюстраций. Ошибка MIME часто выглядит как «ничего не работает», хотя файл физически существует: браузер отказывается исполнять модуль, который сервер отдал с неподходящим типом. В проекте используются обычные расширения .js, чтобы не зависеть от настройки .mjs на конкретном хостинге.

mkdir -p "$HOME/cropper-demo"
unzip cropper-project.zip -d "$HOME/cropper-demo"
cd "$HOME/cropper-demo/cropper-project"
npm ci --ignore-scripts
npm run build
npm test
npm start
# В другом терминале:
curl -I http://127.0.0.1:8143/app.js
curl http://127.0.0.1:8143/health

Linux VPS: HTTPS и публикация только public

Для размещения редактора на VPS достаточно готовой папки public. В примере используется отдельный домен и Caddy. Установите веб-сервер по официальной инструкции, направьте DNS на VPS и откройте необходимые порты для HTTPS. Node.js может вообще отсутствовать на сервере, если сборка выполнена заранее в проверенном окружении.

Размещайте релизы в отдельных каталогах, а текущую версию выбирайте символьной ссылкой. Пользователю веб-сервера достаточно читать статические файлы. Не публикуйте корень проекта с package-lock, тестами и рабочими конфигурациями как каталог сайта. В архиве есть готовый Caddyfile.example с ограничением методов, MIME-защитой и политикой содержимого для этого редактора.

Если страница предназначена сотрудникам, добавьте аутентификацию существующего приложения или basic_auth с хешем пароля. Секреты не должны попадать в public и исходники. Для iframe на другом домене отдельно согласуйте frame-ancestors. Подробный разбор HTTPS и размещения приложений есть в материале о Caddy.

crop.example.com {
    root * /srv/cropper/current/public
    encode zstd gzip
    @write not method GET HEAD
    respond @write 405
    header X-Content-Type-Options nosniff
    file_server
}
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
curl -I https://crop.example.com/
curl -I https://crop.example.com/app.js
sudo journalctl -u caddy --since '10 minutes ago'
# Полная подготовка каталогов, прав и логов — в README архива.

Подробнее: статические файлы в Caddy.

Обновление, кеш и возможность отката

Обновляйте библиотеку в отдельной копии проекта, затем пересобирайте bundle и проходите сценарии заново. Особое внимание уделяйте круглому PNG, поворотам, загрузке JPEG с ориентацией и качеству JPEG. Автоматический тест методов не заметит, что конкретный браузер нарисовал неожиданный фон или иначе обработал изображение.

Выпускайте новую папку релиза целиком. Если заменить только HTML или только JavaScript при долгом кешировании, пользователь может получить несовместимое сочетание файлов. Для серьёзного приложения удобны имена сборок с хешем и короткое кеширование HTML; в простом стенде достаточно контролировать согласованную замену и обновление кеша.

Сохраняйте предыдущую проверенную сборку до завершения приёмки. В README показана замена символьной ссылки current на GNU/Linux и обратное переключение на прошлый релиз. Это обратимое действие в специально выделенном каталоге, без очистки чужих файлов и без вмешательства в другие сайты на сервере. После обновления проверьте не только главную страницу, но и скачанный результат.

Что проверять перед использованием в своём сервисе

В архиве есть автоматические тесты пресетов, сигнатур, объявленных размеров, анимации, выходных ограничений и координат рамки. Отдельный тест создаёт настоящие элементы Cropper.js в jsdom, проверяет преобразования и вызовы рисования с круглой маской. HTTP-проверка подтверждает раздачу public, правильный тип JavaScript и отказ на POST.

У этой проверки есть ясная граница: jsdom не является настоящим растровым браузером. В тесте Canvas API регистрируются вызовы, а не сравниваются реальные пиксели. Поэтому браузерная приёмка должна включать настоящий экспорт, открытие скачанного файла, поворот, качество, локальный JPEG, мобильную ширину и понятные ошибки. Проверки проекта не заменяют ваши ограничения загрузки и права доступа.

После интеграции пройдите полный пользовательский путь: открыть профиль, выбрать изображение, изменить кадр, подтвердить, дождаться сохранения, обновить страницу и увидеть ту же картинку. Искусственно вызовите сетевую ошибку и проверьте повтор. Практическая ценность редактора появляется тогда, когда человек получает предсказуемый результат и может исправить ошибку, не начиная всё сначала.

Исходники и дальнейшее развитие

Скачиваемый проект содержит готовую страницу, исходный JavaScript, локальные зависимости в сборке, учебные изображения, тесты и инструкции для Windows и Linux. Собственный код и иллюстрации распространяются по MIT; сведения о Cropper.js и Bootstrap приложены отдельно. Внешние фотографии, аккаунты и платные сервисы для повторения не нужны.

Начните с одной задачи своего продукта: аватара, обложки или карточки товара. Зафиксируйте ожидаемые пропорции и размер, объясните их в интерфейсе и подключите подтверждение результата. Затем добавляйте хранение черновика, несколько производных и серверную обработку, если они действительно нужны. Такая последовательность помогает сохранить удобство: пользователь выбирает кадр, а технические правила приложения поддерживают этот выбор.

Официальные источники ниже помогут проверить детали API при обновлении. Сопоставляйте документацию с установленной версией: ветки 1.x и 2.x отличаются, а отдельные возможности вроде скругления экспортируемой рамки появились внутри второй ветки.