UNPKG

compote-ui

Version:

An opinionated UI component library for Svelte, built on top of [Ark UI](https://ark-ui.com) with additional components and features not available in the core Ark UI library.

147 lines (146 loc) 5.31 kB
/** * Client-side image processing utilities using the Canvas API. * These are browser-only utilities — they will throw if called during SSR. */ const defaults = { maxWidth: 1000, maxHeight: 1000, quality: 0.85, format: 'image/webp', trim: false, trimThreshold: 10 }; function canvasToBlob(canvas, format, quality) { return new Promise((resolve, reject) => { canvas.toBlob((blob) => { if (blob) resolve(blob); else reject(new Error('canvas.toBlob failed')); }, format, quality); }); } /** Returns the bounding box of non-white/non-transparent pixels */ function getTrimBounds(ctx, width, height, threshold) { const { data } = ctx.getImageData(0, 0, width, height); function isBackground(i) { const a = data[i + 3]; if (a < threshold) return true; // transparent const r = data[i]; const g = data[i + 1]; const b = data[i + 2]; return r >= 255 - threshold && g >= 255 - threshold && b >= 255 - threshold; } let top = 0; outer: for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { if (!isBackground((y * width + x) * 4)) { top = y; break outer; } } } let bottom = height - 1; outer: for (let y = height - 1; y >= 0; y--) { for (let x = 0; x < width; x++) { if (!isBackground((y * width + x) * 4)) { bottom = y; break outer; } } } let left = 0; outer: for (let x = 0; x < width; x++) { for (let y = top; y <= bottom; y++) { if (!isBackground((y * width + x) * 4)) { left = x; break outer; } } } let right = width - 1; outer: for (let x = width - 1; x >= 0; x--) { for (let y = top; y <= bottom; y++) { if (!isBackground((y * width + x) * 4)) { right = x; break outer; } } } return { x: left, y: top, width: right - left + 1, height: bottom - top + 1 }; } function applyTrim(sourceCanvas, threshold) { const ctx = sourceCanvas.getContext('2d'); const { x, y, width, height } = getTrimBounds(ctx, sourceCanvas.width, sourceCanvas.height, threshold); const trimmed = document.createElement('canvas'); trimmed.width = width; trimmed.height = height; trimmed.getContext('2d').drawImage(sourceCanvas, x, y, width, height, 0, 0, width, height); return trimmed; } function scaleCanvas(src, maxWidth, maxHeight) { let { width, height } = src; if (width > maxWidth || height > maxHeight) { const ratio = Math.min(maxWidth / width, maxHeight / height); width = Math.round(width * ratio); height = Math.round(height * ratio); } if (width === src.width && height === src.height) return src; const scaled = document.createElement('canvas'); scaled.width = width; scaled.height = height; scaled.getContext('2d').drawImage(src, 0, 0, width, height); return scaled; } /** Load an image element from a src URL (data URL, blob URL, or regular URL) */ export function loadImage(src) { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => resolve(img); img.onerror = () => reject(new Error('Failed to load image')); img.src = src; }); } /** Convert a File to a base64 data URL */ export function fileToDataUrl(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = () => reject(new Error('Failed to read file')); reader.readAsDataURL(file); }); } /** * Crop a full-resolution source image using natural pixel coordinates, then resize and convert. * Use this instead of getCroppedImage() from Ark UI which outputs at CSS/display resolution. */ export async function cropImage(src, crop, opts) { const { maxWidth, maxHeight, quality, format, trim, trimThreshold } = { ...defaults, ...opts }; const img = await loadImage(src); let canvas = document.createElement('canvas'); canvas.width = crop.width; canvas.height = crop.height; canvas .getContext('2d') .drawImage(img, crop.x, crop.y, crop.width, crop.height, 0, 0, crop.width, crop.height); if (trim) canvas = applyTrim(canvas, trimThreshold); canvas = scaleCanvas(canvas, maxWidth, maxHeight); return canvasToBlob(canvas, format, quality); } /** Resize and convert an image without cropping, returns a Blob */ export async function processImage(src, opts) { const { maxWidth, maxHeight, quality, format, trim, trimThreshold } = { ...defaults, ...opts }; const img = await loadImage(src); let canvas = document.createElement('canvas'); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; canvas.getContext('2d').drawImage(img, 0, 0); if (trim) canvas = applyTrim(canvas, trimThreshold); canvas = scaleCanvas(canvas, maxWidth, maxHeight); return canvasToBlob(canvas, format, quality); }