Привет! Хочу рассказать о своей библиотеке hq-cropper — инструменте для обрезки изображений на чистом TypeScript без единой зависимости.
Когда искал cropper для своего проекта, столкнулся с двумя проблемами. Во-первых, большинство популярных решений тянут за собой кучу зависимостей и весят 100+ KB. Во-вторых, мало кто работает с большими изображениями.
Решил написать своё решение: лёгкое, без зависимостей, с умным алгоритмом масштабирования.
Проблема больших изображений
Ситуация: пользователь загружает фото 4000×3000 пикселей, а вам нужен аватар 200×200. Большинство кропперов справляются с этим плохо:
Наивный подход: обрезать в полном разрешении, потом уменьшить → жрёт память, тормозит
Простое уменьшение: сначала downscale, потом crop → теряем качество
Фиксированный размер: всегда одинаковые dimensions на выходе → нет гибкости
Главная сложность — найти баланс: нужны маленькие файлы на выходе, но нельзя убивать качество когда исходник и так небольшой.
Как решает hq-cropper
Библиотека использует логарифмический алгоритм масштабирования, управляемый параметром quality:
Маленькие исходники → минимальное или нулевое уменьшение (сохраняем качество)
Большие исходники → пропорциональное уменьшение (снижаем размер файла)
Параметр quality (по умолчанию 1.01) — это основание логарифма для расчёта выходных размеров:
outputSize = log(cropSelectionSize) / log(quality)
quality: 1.01→ большой выход (почти 1:1 с выде��ением)quality: 1.5→ средний выход (хороший баланс)quality: 2.0→ маленький выход (агрессивное сжатие)
Практические примеры
Аватары (баланс качества и размера)
const cropper = HqCropper(onSubmit, { quality: 1.5, compression: 0.85, type: 'jpeg', })
Результат: выделение 500px → ~180px на выходе. Выделение 200px → ~150px. Маленькие выделения остаются чёткими, большие разумно сжимаются.
Превью для галереи (минимальный размер)
const cropper = HqCropper(onSubmit, { quality: 2.0, compression: 0.7, type: 'jpeg', })
Результат: агрессивное уменьшение. 500px → ~130px. Идеально для превью где важен размер файла.
Высокое качество (сохранить детали)
const cropper = HqCropper(onSubmit, { quality: 1.01, compression: 1, type: 'png', })
Результат: почти 1:1. 500px → ~490px. Максимальное качество, большие файлы.
Исходник | Выделение | quality: 1.01 | quality: 1.5 | quality: 2.0 |
4000×3000 | 800px | ~780px | ~210px | ~130px |
1200×800 | 400px | ~390px | ~170px | ~120px |
400×400 | 200px | ~195px | ~150px | ~110px |
Обратите внимание: маленькие выделения сохраняют больше относительного размера — это защищает качество когда пользователь работает с небольшими изображениями.
Дополнительные настройки
const cropper = HqCropper(onSubmit, { // Логарифмический коэффициент quality: 1.5, // JPEG компрессия (0-1, где 1 — лучшее качество) compression: 0.85, // Формат type: 'jpeg', // или 'png' })
Комбинации:
quality: 1.5+compression: 0.85→ Баланс (рекомендую для аватаров)quality: 2.0+compression: 0.7→ Минимальные файлыquality: 1.01+compression: 1+type: 'png'→ Максимальное качество
Возможности
Ноль зависимостей — чистый TypeScript, ~22KB minified
Framework agnostic — работает с любым стеком
Умное масштабирование — логарифмический алгоритм
Drag & resize — интуитивный UI с угловыми хэндлами
Валидация файлов — проверка типа и размера
Обработка ошибок — callback-based error reporting
Полная типизация — TypeScript из коробки
Быстрый старт
npm install hq-cropper
import { HqCropper } from 'hq-cropper' const cropper = HqCropper((base64, blob, state) => { document.querySelector('img').src = base64 console.log(`Обрезано ${state.fileName}: ${blob?.size} байт`) }) document.querySelector('button').addEventListener('click', () => { cropper.open() })
Пример для React
import { useRef, useState } from 'react' import { HqCropper } from 'hq-cropper' function AvatarUpload() { const [avatar, setAvatar] = useState('') const cropperRef = useRef( HqCropper( (base64) => setAvatar(base64), { portalSize: 200, quality: 1.5, compression: 0.85, }, undefined, (error) => console.error(error) ) ) return ( <div> {avatar && <img src={avatar} alt="Avatar" />} <button onClick={() => cropperRef.current.open()}> Загрузить аватар </button> </div> ) }
Все параметры конфигурации
const cropper = HqCropper( onSubmit, { // Настройки портала (область выделения) portalSize: 150, minPortalSize: 50, portalPosition: 'center', // Настройки выхода type: 'jpeg', quality: 1.5, compression: 0.85, // Валидация maxFileSize: 5 * 1024 * 1024, // 5MB allowedTypes: ['image/jpeg', 'image/png'], // Локализация applyButtonLabel: 'Применить', cancelButtonLabel: 'Отмена', }, // Кастомные css стили { root: ['my-cropper-modal'], portal: ['my-crop-area'], applyButton: ['btn', 'btn-primary'], cancelButton: ['btn', 'btn-outline'], }, (error) => alert(error) )
Что нового в v3.2.0
Библиотека существует уже несколько лет и используется в продакшене в ряде проектов. Но до этого релиза накопился технический долг: утечки памяти, баги в resize логике, отсутствие валидации. В версии 3.2.0 провёл основательный рефакторинг и закрыл все известные проблемы.
Исправления:
Утечки памяти (корректная очистка при закрытии)
Race conditions в canvas операциях
Resize хэндлы во всех углах
Новые возможности:
onErrorcallback для обработки ошибокmaxFileSizeиallowedTypesдля валидации файловminPortalSizeпротив слишком маленьких выделений
Производительность:
Кэширование DOM элементов
requestAnimationFramethrottling для плавного drag
Кастомизация стилей
Библиотека позволяет полностью переопределить CSS-классы для любого элемента. Передайте свои классы через третий параметр:
const cropper = HqCropper( onSubmit, config, { root: ['my-cropper-modal'], portal: ['my-crop-area'], applyButton: ['btn', 'btn-primary'], cancelButton: ['btn', 'btn-outline'], } )
Доступные элементы для кастомизации: root, header, body, footer, portal, portalArea, sourceImage, preview, previewImage, applyButton, cancelButton, и хэндлы для resize (handlerResizeTopLeft, handlerResizeTopRight, handlerResizeBottomLeft, handlerResizeBottomRight).
Это удобно когда нужно вписать cropper в существующую дизайн-систему или использовать свой CSS-фреймворк.
Ссылки
Это моя первая статья на habr, буду рад звёздочкам на GitHub и вопросам в комментариях!
