Image Crop and Edit Before Upload Implementation

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1212
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    815

Image Crop and Edit Before Upload Implementation

Uploading images without client-side processing is a pain for the backend: incorrect aspect ratios for avatars, multi-megabyte originals in the database, mismatched layout requirements. Client-side cropping solves this before sending the file.

Stack and Library Choice

Two main players: Cropper.js (vanilla, ~34 KB gzip) and react-image-crop (React-specific, ~6 KB). For Vue there's vue-cropper. For complex scenarios with filters and text — Fabric.js or Konva.js, but that's overkill for most tasks.

Choice is simple:

  • Need React — react-image-crop or react-cropper (wrapper over Cropper.js)
  • Need vanilla/jQuery — Cropper.js directly
  • Need rotations, filters, text overlay — Cropper.js with extensions

Basic Implementation with react-image-crop

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}>Upload</button>
      )}
    </div>
  )
}

Key point — scale through naturalWidth / width. Browser displays the image in reduced view, but you need to crop from the original.

Sending Through FormData to Server

async function uploadCroppedImage(blob: Blob): Promise<void> {
  const formData = new FormData()
  // Filename with extension — important for mime-type on server
  formData.append('avatar', blob, `avatar-${Date.now()}.webp`)

  const response = await fetch('/api/users/avatar', {
    method: 'POST',
    headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')!.getAttribute('content')! },
    body: formData,
  })

  if (!response.ok) throw new Error(`Upload failed: ${response.status}`)
}

On the Laravel side, the file arrives as a regular $request->file('avatar'). Additional server-side cropping is not needed — dimensions are already correct.

Advanced Scenario: Cropper.js with Rotation and Zoom

<div>
  <img id="image" src="/original.jpg">
  <button onclick="cropper.rotate(90)">Rotate</button>
  <button onclick="cropper.zoom(0.1)">+</button>
  <button onclick="cropper.zoom(-0.1)">-</button>
  <button onclick="uploadCropped()">Upload</button>
</div>

<script>
const cropper = new Cropper(document.getElementById('image'), {
  aspectRatio: 16 / 9,
  viewMode: 2,           // don't go beyond image bounds
  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 Orientation Handling

Mobile photos often arrive rotated due to EXIF metadata. For reliability — use exifr library:

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
        // Apply transformation according to EXIF table
        applyExifRotation(canvas, img, rotation)
        resolve(canvas.toDataURL())
      }
      img.src = e.target!.result as string
    }
    reader.readAsDataURL(file)
  })
}

Size Limits Before Crop

Uploading a 20 MB original to crop 200×200 is wasteful. Check before showing crop:

const MAX_SIZE_MB = 10
const MAX_DIMENSION = 4096

function validateImage(file: File): string | null {
  if (file.size > MAX_SIZE_MB * 1024 * 1024) {
    return `File too large. Maximum ${MAX_SIZE_MB} MB`
  }
  return null
}

// To check dimensions — only after loading in img
function checkDimensions(img: HTMLImageElement): string | null {
  if (img.naturalWidth > MAX_DIMENSION || img.naturalHeight > MAX_DIMENSION) {
    return `Image too large. Maximum ${MAX_DIMENSION}px`
  }
  return null
}

If you need to accept large files — resize to reasonable dimensions before crop via canvas.drawImage with reduced canvas.width/height.

Before/After Preview

function CropPreview({ crop, imgRef }: { crop: PixelCrop; imgRef: React.RefObject<HTMLImageElement> }) {
  const previewRef = useRef<HTMLCanvasElement>(null)

  useEffect(() => {
    if (!crop || !imgRef.current || !previewRef.current) return

    const img = imgRef.current
    const canvas = previewRef.current
    const ctx = canvas.getContext('2d')!

    const scaleX = img.naturalWidth / img.width
    const scaleY = img.naturalHeight / img.height
    const pixelRatio = window.devicePixelRatio

    canvas.width = 150 * pixelRatio
    canvas.height = 150 * pixelRatio
    ctx.scale(pixelRatio, pixelRatio)

    ctx.drawImage(img,
      crop.x * scaleX, crop.y * scaleY,
      crop.width * scaleX, crop.height * scaleY,
      0, 0, 150, 150
    )
  }, [crop, imgRef])

  return <canvas ref={previewRef} style={{ width: 150, height: 150, borderRadius: '50%' }} />
}

Implementation Timeline

Basic crop integration with upload — 4–6 hours. With rotation, EXIF correction, preview, validation, and responsive modal — 1–2 days. If you need multi-upload with individual crop for each file — a separate task for 3–5 days.