Preview and export

Use a preview for continuous visual feedback and request a canvas when you need output pixels.

Live preview

Pass the instance bound with bind:this straight to CropperPreview. It follows the cropper’s reactive getters without an update callback.

Give the preview a size in CSS. It fits the crop’s ratio inside that box. Snapshot inputs (state, image, transitions, loading, loaded) and imperative adapters are also supported; see customize previews.

Export on an action

getCanvas() returns the cropped bitmap, or null if export is disabled or the current source is not ready. onReady guarantees the initial rendered export source is ready with the default configuration.

Continuous results

Reading coordinates is cheap; drawing and encoding pixels on every drag is not. If your workflow needs continuous exports, debounce them and cancel pending work on destruction. This demo includes that pattern.

Resize the result

Pass width and height to request dimensions, or maxWidth, maxHeight and maxArea to cap them. Export keeps the crop’s aspect ratio, so a differently shaped width/height request does not force distortion.

For example, getCanvas({ maxWidth: 2048, maxHeight: 2048, fillColor: 'white' }) limits memory and fills transparent areas before JPEG encoding. A circle stencil still exports a rectangular canvas; create a circular mask yourself if required.

Compare the selected pixel dimensions with the output requirement: a 4-inch square at 300 DPI needs 1200 × 1200 pixels. Upscaling cannot restore missing detail. A warning lets the user decide; minWidth and minHeight can enforce a minimum instead.

Blob export and memory

Use canvas.toBlob() for binary output. Treat its null result as a failure and report it; a large image or memory pressure can prevent encoding. This helper retries at a smaller size and releases the output canvas’s pixels after encoding.

import type { CropperInstance } from 'svelte-advanced-cropper';

// The longest side to export at, largest first. A phone that can't make the first may manage
// the second.
const MAX_SIDES = [2048, 1280];

export interface ExportedCrop {
	blob: Blob;
	width: number;
	height: number;
	/** The size limit the export succeeded at. */
	maxSide: number;
}

function toJpeg(canvas: HTMLCanvasElement): Promise<Blob | null> {
	return new Promise((resolve) => canvas.toBlob(resolve, 'image/jpeg', 0.9));
}

/**
 * Exports the crop as a JPEG of at most 2048 px a side, trying again at 1280 px when the
 * browser can't make the larger one. Transparent areas are painted `fillColor`. Resolves
 * null when neither size works, or when no photo is loaded.
 */
export async function exportCrop(
	cropper: CropperInstance,
	{ fillColor = 'white' }: { fillColor?: string } = {}
): Promise<ExportedCrop | null> {
	for (const maxSide of MAX_SIDES) {
		const canvas = cropper.getCanvas({ maxWidth: maxSide, maxHeight: maxSide, fillColor });
		if (!canvas) {
			return null;
		}

		const { width, height } = canvas;
		// A canvas that is too big, or a tab short of memory, gives null here: no error is thrown.
		const blob = await toJpeg(canvas);
		// getCanvas() draws into the same canvas every time. Shrinking it frees its pixels
		// until the next export.
		canvas.width = 0;
		canvas.height = 0;
		if (blob) {
			return { blob, width, height, maxSide };
		}
	}

	return null;
}

getCanvas() reuses the same output canvas on later calls. Encode or copy its pixels before another export. The library releases the spare canvas used for transforms, but the returned canvas stays allocated until it is resized or the cropper is destroyed. Serialize exports if you also shrink that shared canvas.

CORS failures may throw a SecurityError instead of returning null; see cross-origin images.

Upload integration

Convert the canvas to a Blob and append it to FormData. The request URL, authentication and response handling belong to your application. This demo prepares the upload and simulates submission locally; it does not contact a server.