Cut a region out of an image into a new file, turned, flipped, scaled or round.
@ui-sdk/core/image cuts a region out of an image and returns it as a new image file. Use it after someone picks a crop, like an avatar from a photo, and submit or upload the file it returns.
Crop an Image
cropImage takes an image, the region to keep and options for the file it writes. The region is in the image's own pixels, whatever size the image shows at on the page:
import { cropImage } from "@ui-sdk/core/image"
const file = await cropImage(photo, { x: 120, y: 40, width: 600, height: 600 })
file.name // "photo.png", from the photo's name
file.type // "image/png"The image can be a file from an <input type="file">, a URL, an <img>, a <canvas> or an ImageBitmap. A URL from another origin needs CORS, or the browser won't let the page read its pixels. The promise resolves with a File, so FormData and fetch take it as it is.
Turn and Flip
Set rotation to turn the image a quarter turn clockwise at a time, and flipX or flipY to mirror it. The region's x and y count from the top left of the image as it shows after it's turned and flipped:
const file = await cropImage(photo, {
x: 0,
y: 0,
width: 300,
height: 400,
rotation: 90,
flipX: true,
})So a 400 by 300 photo turned by 90 is 300 wide and 400 tall, and the region above keeps all of it.
import * as React from "react"
import { cropImage, type ImageRotation } from "@ui-sdk/core/image"
import { useLog } from "./log"
const quarters = [
["#ef4444", 0, 0],
["#22c55e", 200, 0],
["#3b82f6", 0, 150],
["#eab308", 200, 150],
] as const
function paint(canvas: HTMLCanvasElement) {
const context = canvas.getContext("2d")!
for (const [color, x, y] of quarters) {
context.fillStyle = color
context.fillRect(x, y, 200, 150)
}
}
function Example() {
const log = useLog()
const source = React.useRef<HTMLCanvasElement>(null)
const [rotation, setRotation] = React.useState<ImageRotation>(0)
const [flipX, setFlipX] = React.useState(false)
const [circle, setCircle] = React.useState(false)
const [result, setResult] = React.useState<{ url: string; file: File }>()
React.useEffect(() => paint(source.current!), [])
React.useEffect(
() => () => result && URL.revokeObjectURL(result.url),
[result]
)
async function crop() {
const file = await cropImage(
source.current!,
{ x: 0, y: 0, width: 150, height: 150, rotation, flipX },
{ width: 96, shape: circle ? "circle" : "rectangle" }
)
setResult({ url: URL.createObjectURL(file), file })
log(`${file.name}, ${file.type}, ${file.size} bytes`)
}
return (
<div data-slot="stack">
<div data-slot="controls">
<select
aria-label="Rotation"
value={rotation}
onChange={(event) =>
setRotation(Number(event.target.value) as ImageRotation)
}
>
{[0, 90, 180, 270].map((degrees) => (
<option key={degrees} value={degrees}>
Turn {degrees}°
</option>
))}
</select>
<label data-slot="label">
<input
type="checkbox"
checked={flipX}
onChange={(event) => setFlipX(event.target.checked)}
/>
Flip
</label>
<label data-slot="label">
<input
type="checkbox"
checked={circle}
onChange={(event) => setCircle(event.target.checked)}
/>
Circle
</label>
</div>
<div data-slot="image-crop-stage">
<canvas
ref={source}
width={400}
height={300}
aria-label="Four colored quarters"
role="img"
style={{
transform: `scale(${flipX ? -1 : 1}, 1) rotate(${rotation}deg)`,
}}
/>
</div>
<div data-slot="image-crop-output">
<button type="button" data-variant="solid" onClick={crop}>
Crop
</button>
{result && (
<img
data-slot="image-crop-result"
src={result.url}
alt="The cropped image"
/>
)}
</div>
</div>
)
}
export { Example }The File
| Option | What it sets |
|---|---|
type | "image/png" by default, or "image/jpeg" or "image/webp" |
quality | From 0 to 1, for JPEG and WebP |
width | The width in pixels. Left out, it follows height at the region's ratio, or the region's width |
height | The height in pixels. Left out, it follows width at the region's ratio, or the region's height |
shape | "circle" keeps the ellipse inside the region and leaves the corners transparent |
name | The file's name, instead of the source's name with the new extension |
Pass width alone to scale the crop, like a 256 pixel avatar from a much larger photo:
const avatar = await cropImage(photo, crop, {
type: "image/webp",
quality: 0.9,
width: 256,
shape: "circle",
})A browser that can't write the type you ask for writes PNG instead, and the file's type and name say which it wrote. JPEG has no transparency, so a circle's corners turn black there. Use PNG or WebP for round crops.