Ir al contenido

Image Cropper

Esta página aún no está disponible en tu idioma.

A ready-to-use cropping workflow that combines AsterUI layout and form components with a lightweight crop selection flow.

Image Cropper

Upload an image, adjust the crop, preview the result, and download the cropped file.

Image Cropper

Crop

Image Source
Aspect Ratio
Crop source

Preview

Drag to draw a crop.
The preview updates as you adjust the crop.
import { type ChangeEvent, type SyntheticEvent, useEffect, useRef, useState } from 'react'
import ReactCrop, { centerCrop, makeAspectCrop } from 'react-image-crop'
import 'react-image-crop/dist/ReactCrop.css'
import { Button, Card, Col, FileInput, Row, Select, Space, Typography } from 'asterui'

function App() {
  const defaultImage = '/valley.png'
  const aspectOptions = [
    { label: 'Free', value: 'free' },
    { label: '1:1', value: '1' },
    { label: '4:3', value: String(4 / 3) },
    { label: '16:9', value: String(16 / 9) },
  ]
  
  const centerAspectCrop = (mediaWidth: number, mediaHeight: number, aspect: number) =>
    centerCrop(
      makeAspectCrop(
        {
          unit: '%',
          width: 80,
        },
        aspect,
        mediaWidth,
        mediaHeight
      ),
      mediaWidth,
      mediaHeight
    )
  
  const [imageSrc, setImageSrc] = useState(defaultImage)
  const [crop, setCrop] = useState<Crop>()
  const [completedCrop, setCompletedCrop] = useState<PixelCrop | null>(null)
  const [aspectValue, setAspectValue] = useState('1')
  const imageRef = useRef<HTMLImageElement | null>(null)
  const previewCanvasRef = useRef<HTMLCanvasElement | null>(null)
  
  const aspect = aspectValue === 'free' ? undefined : Number(aspectValue)
  
  const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0]
    if (!file) return
  
    const reader = new FileReader()
    reader.onload = () => {
      const nextSource = typeof reader.result === 'string' ? reader.result : defaultImage
      setImageSrc(nextSource)
    }
    reader.readAsDataURL(file)
  }
  
  const handleImageLoad = (event: SyntheticEvent<HTMLImageElement>) => {
    if (!aspect) return
    const { naturalWidth, naturalHeight } = event.currentTarget
    setCrop(centerAspectCrop(naturalWidth, naturalHeight, aspect))
  }
  
  const handleAspectChange = (event: ChangeEvent<HTMLSelectElement>) => {
    const value = event.target.value
    setAspectValue(value)
    if (value === 'free' || !imageRef.current) return
    const { naturalWidth, naturalHeight } = imageRef.current
    setCrop(centerAspectCrop(naturalWidth, naturalHeight, Number(value)))
  }
  
  const handleReset = () => {
    setImageSrc(defaultImage)
    setAspectValue('1')
    setCrop(undefined)
    setCompletedCrop(null)
  }
  
  const handleDownload = () => {
    if (!previewCanvasRef.current) return
    previewCanvasRef.current.toBlob((blob) => {
      if (!blob) return
      const url = URL.createObjectURL(blob)
      const link = document.createElement('a')
      link.href = url
      link.download = 'crop.png'
      link.click()
      URL.revokeObjectURL(url)
    })
  }
  
  useEffect(() => {
    if (!completedCrop || !previewCanvasRef.current || !imageRef.current) return
    const canvas = previewCanvasRef.current
    const image = imageRef.current
    const scaleX = image.naturalWidth / image.width
    const scaleY = image.naturalHeight / image.height
    const ctx = canvas.getContext('2d')
    if (!ctx) return
  
    const pixelRatio = window.devicePixelRatio || 1
    canvas.width = Math.floor(completedCrop.width * scaleX * pixelRatio)
    canvas.height = Math.floor(completedCrop.height * scaleY * pixelRatio)
    ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0)
    ctx.imageSmoothingQuality = 'high'
    ctx.clearRect(0, 0, canvas.width, canvas.height)
    ctx.drawImage(
      image,
      completedCrop.x * scaleX,
      completedCrop.y * scaleY,
      completedCrop.width * scaleX,
      completedCrop.height * scaleY,
      0,
      0,
      completedCrop.width * scaleX,
      completedCrop.height * scaleY
    )
  }, [completedCrop])
  
  return (
      <Card className="w-full">
        <Space direction="vertical" size="lg" className="w-full">
          <Space justify="between" align="center" className="w-full">
            <Typography.Title level={4}>Image Cropper</Typography.Title>
            <Space size="sm">
              <Button variant="outline" onClick={handleReset}>
                Reset
              </Button>
              <Button color="primary" onClick={handleDownload} disabled={!completedCrop?.width}>
                Download Crop
              </Button>
            </Space>
          </Space>
          <Row gutter={16}>
            <Col xs={24} lg={14}>
              <Card title="Crop" className="h-full">
                <Space direction="vertical" size="md" className="w-full">
                  <Space direction="vertical" size="xs" className="w-full">
                    <Typography.Text className="text-sm font-medium">Image Source</Typography.Text>
                    <FileInput accept="image/*" onChange={handleFileChange} />
                  </Space>
                  <Space direction="vertical" size="xs" className="w-full">
                    <Typography.Text className="text-sm font-medium">Aspect Ratio</Typography.Text>
                    <Select value={aspectValue} onChange={handleAspectChange}>
                      {aspectOptions.map((option) => (
                        <option key={option.value} value={option.value}>
                          {option.label}
                        </option>
                      ))}
                    </Select>
                  </Space>
                  <div className="rounded-box border border-base-300 bg-base-100 p-3">
                    <ReactCrop
                      crop={crop}
                      onChange={(nextCrop) => setCrop(nextCrop)}
                      onComplete={(nextCrop) => setCompletedCrop(nextCrop)}
                      aspect={aspect}
                      ruleOfThirds
                    >
                      <img
                        ref={imageRef}
                        src={imageSrc}
                        alt="Crop source"
                        onLoad={handleImageLoad}
                      />
                    </ReactCrop>
                  </div>
                </Space>
              </Card>
            </Col>
            <Col xs={24} lg={10}>
              <Card title="Preview" className="h-full">
                <Space direction="vertical" size="md">
                  <div className="aspect-square w-full rounded-box border border-base-300 bg-base-200 p-3 flex items-center justify-center">
                    {completedCrop?.width ? (
                      <canvas ref={previewCanvasRef} className="max-h-full max-w-full" />
                    ) : (
                      <Typography.Text className="text-sm" type="secondary">
                        Drag to draw a crop.
                      </Typography.Text>
                    )}
                  </div>
                  <Typography.Text className="text-sm" type="secondary">
                    The preview updates as you adjust the crop.
                  </Typography.Text>
                </Space>
              </Card>
            </Col>
          </Row>
        </Space>
      </Card>
    )
}

export default App
  • Upload and replace - Swap the source image without leaving the flow
  • Aspect presets - Switch between freeform, square, and wide crops
  • Live preview - Review the cropped output as you adjust the selection
  • Download action - Save the cropped image as a PNG
  • Grid layout - Ready for dashboard or profile settings pages
  • Wrap the cropper in a Modal or Drawer for focused editing
  • Add a circular crop preset for avatars
  • Persist crop state in a form or profile API
  • Add size limits and guidance for upload formats