hq-cropper: Image Cropper без зависимостей для JavaScript
- среда, 17 декабря 2025 г. в 00:00:05
Привет! Хочу рассказать о своей библиотеке hq-cropper — инструменте для обрезки изображений на чистом TypeScript без единой зависимости.
Когда искал cropper для своего проекта, столкнулся с двумя проблемами. Во-первых, большинство популярных решений тянут за собой кучу зависимостей и весят 100+ KB. Во-вторых, мало кто работает с большими изображениями.
Решил написать своё решение: лёгкое, без зависимостей, с умным алгоритмом масштабирования.
Ситуация: пользователь загружает фото 4000×3000 пикселей, а вам нужен аватар 200×200. Большинство кропперов справляются с этим плохо:
Наивный подход: обрезать в полном разрешении, потом уменьшить → жрёт память, тормозит
Простое уменьшение: сначала downscale, потом crop → теряем качество
Фиксированный размер: всегда одинаковые dimensions на выходе → нет гибкости
Главная сложность — найти баланс: нужны маленькие файлы на выходе, но нельзя убивать качество когда исходник и так небольшой.
Библиотека использует логарифмический алгоритм масштабирования, управляемый параметром 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-cropperimport { 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()
})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)
)Библиотека существует уже несколько лет и используется в продакшене в ряде проектов. Но до этого релиза накопился технический долг: утечки памяти, баги в resize логике, отсутствие валидации. В версии 3.2.0 провёл основательный рефакторинг и закрыл все известные проблемы.
Исправления:
Утечки памяти (корректная очистка при закрытии)
Race conditions в canvas операциях
Resize хэндлы во всех углах
Новые возможности:
onError callback для обработки ошибок
maxFileSize и allowedTypes для валидации файлов
minPortalSize против слишком маленьких выделений
Производительность:
Кэширование DOM элементов
requestAnimationFrame throttling для плавного 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 и вопросам в комментариях!