Обрізка та редагування зображень перед завантаженням: готове рішення

Завантажуєте фото з телефона й отримуєте важкі файли, які перевантажують сервер і сповільнюють сайт? Ми розробляємо клієнтський редактор зображень з обрізанням і поворотом до відправлення, використовуючи перевірені бібліотеки Cropper.js та react-image-crop. Наша команда реалізує рішення під ключ — від інтеграції в інтерфейс до оптимізації під мобільні пристрої, забезпечуючи швидке завантаження та подальшу підтримку.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Обрізка та редагування зображень перед завантаженням: готове рішення
Середній
~2-3 дні
Часті запитання

Клієнтська обрізка зображень: чому це важливо?

Більшість користувачів завантажують фотографії прямо з телефону: величезний оригінал 4000×3000 та 8 МБ. Сервер має прийняти, обробити та зберегти — навантаження на I/O та CPU зростає. Клієнтська обрізка вирішує це до відправлення: вирізаємо потрібний фрагмент, повертаємо, коригуємо яскравість — на сервер іде вже оптимізований файл. Ми використовуємо перевірені бібліотеки Cropper.js та react-image-crop, щоб вписати кроп у будь-який інтерфейс. Навіщо ганяти мегабайти, якщо можна відправити тільки те, що потрібно? Досвід 7+ років та 50+ проєктів з обробкою зображень підтверджує: такий підхід знижує навантаження на сервер у 3–5 разів та прискорює завантаження сторінок. Крім того, клієнтська обробка дозволяє заощадити до 70% коштів на зберігання та скоротити витрати на трафік у 5 разів. А ще це позитивно впливає на Core Web Vitals — особливо LCP, адже браузеру не потрібно чекати серверної обробки.

Які проблеми вирішує кроп на клієнті?

  • Неправильні пропорції: користувачі завантажують горизонтальні фото для вертикальних аватарів — доводиться обрізати вручну.
  • Величезна вага: вихідник 10 МБ перетворюється на 200 KB після кропу в WebP 0.9.
  • EXIF-поворот: мобільні знімки відображаються перевернутими до виправлення метаданих.
  • Зайві елементи: рекламні банери, водяні знаки — все це видаляється до завантаження.
  • Завантаження на повільному з'єднанні: користувачі з 3G можуть чекати хвилини, а після кропу відправляється тільки 200 KB.

Як вибрати бібліотеку для кропу?

Бібліотека Розмір (gzip) Фічі Для яких задач
Cropper.js ~34 KB Поворот, зум, обрізка, аспект Vanilla JS, jQuery, будь-який стек
react-image-crop ~6 KB Обрізка з аспектом, кастомний UI React (18+, Next.js)
vue-cropper ~15 KB Обрізка, поворот, зум Vue 2/3, Nuxt
Fabric.js ~200 KB Повноцінний редактор, шари, фільтри, текст Якщо потрібен редактор з накладанням тексту/фільтрів

Cropper.js у 2 рази швидше завантажується, ніж Fabric.js, для простого кропу. Якщо задача — тільки кадрування з фіксованим аспектом, react-image-crop легший у 5 разів.

Базова реалізація на React

import { useState, useRef, useCallback } from 'react'
import ReactCrop, { Crop, PixelCrop, centerCrop, makeAspectCrop } from 'react-image-crop'
import 'react-image-crop/dist/ReactCrop.css'

function centerAspectCrop(width: number, height: number, aspect: number): Crop {
  return centerCrop(
    makeAspectCrop(
      {
        unit: '%',
        width: 90,
      },
      aspect,
      width,
      height
    ),
    width,
    height
  )
}

export function ImageCropUploader({
  aspect = 1,
  onUpload,
}: {
  aspect?: number
  onUpload: (blob: Blob) => Promise<void>
}) {
  const [imgSrc, setImgSrc] = useState('')
  const [crop, setCrop] = useState<Crop>()
  const [completedCrop, setCompletedCrop] = useState<PixelCrop>()
  const imgRef = useRef<HTMLImageElement>(null)

  function onSelectFile(e: React.ChangeEvent<HTMLInputElement>) {
    if (e.target.files?.[0]) {
      const reader = new FileReader()
      reader.addEventListener('load', () => setImgSrc(reader.result?.toString() ?? ''))
      reader.readAsDataURL(e.target.files[0])
    }
  }

  function onImageLoad(e: React.SyntheticEvent<HTMLImageElement>) {
    const { width, height } = e.currentTarget
    setCrop(centerAspectCrop(width, height, aspect))
  }

  const getCroppedBlob = useCallback((): Promise<Blob> => {
    const image = imgRef.current
    if (!image || !completedCrop) throw new Error('No crop data')

    const canvas = document.createElement('canvas')
    const scaleX = image.naturalWidth / image.width
    const scaleY = image.naturalHeight / image.height
    canvas.width = completedCrop.width * scaleX
    canvas.height = completedCrop.height * scaleY
    const ctx = canvas.getContext('2d')!

    ctx.drawImage(
      image,
      completedCrop.x * scaleX,
      completedCrop.y * scaleY,
      completedCrop.width * scaleX,
      completedCrop.height * scaleY,
      0,
      0,
      canvas.width,
      canvas.height
    )

    return new Promise((resolve, reject) => {
      canvas.toBlob(
        blob => blob ? resolve(blob) : reject(new Error('Canvas empty')),
        'image/webp',
        0.9
      )
    })
  }, [completedCrop])

  async function handleSubmit() {
    const blob = await getCroppedBlob()
    await onUpload(blob)
  }

  return (
    <div>
      <input type="file" accept="image/*" onChange={onSelectFile} />
      {imgSrc && (
        <ReactCrop
          crop={crop}
          onChange={setCrop}
          onComplete={setCompletedCrop}
          aspect={aspect}
          minWidth={100}
        >
          <img ref={imgRef} src={imgSrc} onLoad={onImageLoad} />
        </ReactCrop>
      )}
      {completedCrop && (
        <button onClick={handleSubmit}>Завантажити</button>
      )}
    </div>
  )
}

Ключовий момент — масштабування через naturalWidth / width. Браузер відображає зображення в зменшеному вигляді, а вирізати потрібно з оригіналу.

Як реалізувати кроп за 5 кроків?

  1. Вибір бібліотеки залежно від фреймворку (таблиця вище).
  2. Завантаження файлу через FileReader та відображення в <img>.
  3. Ініціалізація кроп-інтерфейсу з параметрами аспекту та мінімальним розміром.
  4. Отримання обрізаного фрагменту через canvas.toBlob() у формат WebP з якістю 0.9.
  5. Відправлення Blob через FormData на сервер.

Додатково: валідація розміру до кропу (обмеження 10 МБ), перевірка EXIF-орієнтації та попередній перегляд результату.

Cropper.js: поворот, зум та завантаження

<div>
  <img id="image" src="/original.jpg">
  <button onclick="cropper.rotate(90)">Повернути</button>
  <button onclick="cropper.zoom(0.1)">+</button>
  <button onclick="cropper.zoom(-0.1)">-</button>
  <button onclick="uploadCropped()">Завантажити</button>
</div>
<script>
  const cropper = new Cropper(document.getElementById('image'), {
    aspectRatio: 16 / 9,
    viewMode: 2, // не виходити за межі зображення
    dragMode: 'move',
    autoCropArea: 0.8,
    restore: false,
    guides: true,
    center: true,
    highlight: false,
    cropBoxMovable: true,
    cropBoxResizable: true,
    toggleDragModeOnDblclick: false,
  })

  async function uploadCropped() {
    cropper.getCroppedCanvas({
      maxWidth: 1920,
      maxHeight: 1080,
      imageSmoothingEnabled: true,
      imageSmoothingQuality: 'high',
    }).toBlob(async (blob) => {
      const fd = new FormData()
      fd.append('image', blob, 'cover.webp')
      await fetch('/api/upload', {
        method: 'POST',
        body: fd
      })
    }, 'image/webp', 0.85)
  }
</script>

Що робити з EXIF-орієнтацією?

Мобільні фото часто повернуті через метадані EXIF. Браузер до недавнього часу не враховував це автоматично. Ось як це виправити:

img { image-orientation: from-image; /* підтримується в сучасних браузерах */ } 

Для надійності використовуємо бібліотеку exifr:

import { parse } from 'exifr'
async function fixOrientation(file: File): Promise<string> {
  const exif = await parse(file, { orientation: true })
  return new Promise(resolve => {
    const reader = new FileReader()
    reader.onload = (e) => {
      const img = new Image()
      img.onload = () => {
        const canvas = document.createElement('canvas')
        const rotation = exif?.Orientation ?? 1
        // застосовуємо трансформацію за таблицею EXIF
        applyExifRotation(canvas, img, rotation)
        resolve(canvas.toDataURL())
      }
      img.src = e.target!.result as string
    }
    reader.readAsDataURL(file)
  })
}

Докладніше про властивість image-orientation читайте в MDN.

Валідація та обмеження

Завантажувати 20 МБ оригінал, щоб потім вирізати 200×200 — марнотратство. Перевіряємо перед показом кропу:

const MAX_SIZE_MB = 10
const MAX_DIMENSION = 4096

function validateImage(file: File): string | null {
  if (file.size > MAX_SIZE_MB * 1024 * 1024) {
    return `Файл занадто великий. Максимум ${MAX_SIZE_MB} МБ`
  }
  return null
}

// Для перевірки розмірів — тільки після завантаження в img
function checkDimensions(img: HTMLImageElement): string | null {
  if (img.naturalWidth > MAX_DIMENSION || img.naturalHeight > MAX_DIMENSION) {
    return `Зображення занадто велике. Максимум ${MAX_DIMENSION}px`
  }
  return null
}

Якщо потрібно прийняти великі файли — ресайзити до розумних розмірів перед кропом через canvas.drawImage зі зменшеними canvas.width/height.

Що входить в роботу

Компонент Опис
Вибір бібліотеки Підбір оптимального рішення під ваш стек (React, Vue, vanilla)
Інтеграція кропу Налаштування компонента з підтримкою аспекту, повороту, зуму
EXIF-корекція Виправлення орієнтації мобільних знімків
Валідація Перевірка розміру, розширення та типу файлу до обробки
Оптимізація Стиснення в WebP з контрольованою якістю
Попередній перегляд Живий перегляд результату до завантаження
Документація Інструкція з інтеграції та підтримки

Довірити це завдання професіоналам

Наш досвід: 7+ років у веб-розробці, 50+ проєктів з обробкою зображень на клієнті. Ми гарантуємо, що інтеграція буде виконана вчасно, код буде покритий тестами та документований. Отримайте консультацію нашого інженера — вона займе 30 хвилин. Замовте впровадження клієнтського кропу та скоротіть витрати на серверну інфраструктуру.

Підсумок: клієнтська обрізка знижує навантаження на сервер, покращує UX та економить трафік. Впровадьте її за 2–5 днів з нашою допомогою.

Наші компетенції:

Останні роботи

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1542
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1394
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1081
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1369
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1142
  • Розробка веб-сайту для компанії ФІКСПЕР
    Розробка веб-сайту для компанії ФІКСПЕР
    1060