Spaces:
Runtime error
Runtime error
| "use client"; | |
| import React, { useEffect, useMemo, useRef, useState } from 'react'; | |
| type ImageEditorPaneProps = { | |
| imageBase64: string; | |
| onUpdate: (base64: string) => void; | |
| onClose: () => void; | |
| prompt?: string; | |
| }; | |
| function loadImage(base64: string): Promise<HTMLImageElement> { | |
| return new Promise((resolve, reject) => { | |
| const img = new Image(); | |
| img.onload = () => resolve(img); | |
| img.onerror = reject; | |
| img.src = `data:image/png;base64,${base64}`; | |
| }); | |
| } | |
| function canvasToBase64(canvas: HTMLCanvasElement): string { | |
| return canvas.toDataURL("image/png").split(",")[1] || ""; | |
| } | |
| export default function ImageEditorPane({ imageBase64, onUpdate, onClose, prompt }: ImageEditorPaneProps) { | |
| const [working, setWorking] = useState(false); | |
| const [brightness, setBrightness] = useState(0); // -100..100 | |
| const [contrast, setContrast] = useState(0); // -100..100 | |
| const previewRef = useRef<HTMLImageElement>(null); | |
| const applyAndUpdate = async (fn: (img: HTMLImageElement, ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => void | Promise<void>) => { | |
| setWorking(true); | |
| try { | |
| const img = await loadImage(imageBase64); | |
| const canvas = document.createElement("canvas"); | |
| canvas.width = img.naturalWidth; | |
| canvas.height = img.naturalHeight; | |
| const ctx = canvas.getContext("2d", { willReadFrequently: true }); | |
| if (!ctx) return; | |
| ctx.drawImage(img, 0, 0); | |
| await fn(img, ctx, canvas); | |
| const b64 = canvasToBase64(canvas); | |
| onUpdate(b64); | |
| } finally { | |
| setWorking(false); | |
| } | |
| }; | |
| const cropCenter = (aspect: number) => applyAndUpdate((img, ctx, canvas) => { | |
| const w = img.naturalWidth; const h = img.naturalHeight; | |
| let cw = w; let ch = Math.round(w / aspect); | |
| if (ch > h) { ch = h; cw = Math.round(h * aspect); } | |
| const sx = Math.round((w - cw) / 2); | |
| const sy = Math.round((h - ch) / 2); | |
| const out = document.createElement("canvas"); | |
| out.width = cw; out.height = ch; | |
| const octx = out.getContext("2d")!; | |
| octx.imageSmoothingEnabled = true; | |
| octx.drawImage(img, sx, sy, cw, ch, 0, 0, cw, ch); | |
| canvas.width = cw; canvas.height = ch; | |
| ctx.clearRect(0,0,canvas.width, canvas.height); | |
| ctx.drawImage(out, 0, 0); | |
| }); | |
| const applyAdjust = () => applyAndUpdate((img, ctx, canvas) => { | |
| const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
| const data = imgData.data; | |
| const b = brightness / 100 * 255; // shift | |
| const c = Math.pow((contrast + 100) / 100, 2); // gamma-like | |
| for (let i = 0; i < data.length; i += 4) { | |
| for (let k = 0; k < 3; k++) { | |
| let v = data[i + k]; | |
| v = v + b; // brightness | |
| v = ((v - 128) * c) + 128; // contrast | |
| data[i + k] = Math.max(0, Math.min(255, v)); | |
| } | |
| } | |
| ctx.putImageData(imgData, 0, 0); | |
| }); | |
| const upscale2x = () => applyAndUpdate((img, ctx, canvas) => { | |
| const outW = img.naturalWidth * 2; | |
| const outH = img.naturalHeight * 2; | |
| const out = document.createElement("canvas"); | |
| out.width = outW; out.height = outH; | |
| const octx = out.getContext("2d")!; | |
| octx.imageSmoothingEnabled = true; | |
| octx.imageSmoothingQuality = 'high'; | |
| octx.drawImage(img, 0, 0, outW, outH); | |
| canvas.width = outW; canvas.height = outH; | |
| ctx.clearRect(0,0,canvas.width, canvas.height); | |
| ctx.drawImage(out, 0, 0); | |
| }); | |
| return ( | |
| <div className="h-full w-full p-4"> | |
| <div className="relative h-full rounded-2xl overflow-hidden bg-white/5 dark:bg-white/5 backdrop-blur-xl border border-white/10 shadow-2xl"> | |
| <div className="flex items-center justify-between px-4 py-3 border-b border-white/10"> | |
| <div className="text-sm text-muted-foreground truncate">{prompt || 'Image Editor'}</div> | |
| <div className="flex items-center gap-2"> | |
| <button disabled={working} onClick={() => cropCenter(1)} className="px-2 py-1 rounded bg-white/10 hover:bg-white/20">1:1</button> | |
| <button disabled={working} onClick={() => cropCenter(16/9)} className="px-2 py-1 rounded bg-white/10 hover:bg-white/20">16:9</button> | |
| <button disabled={working} onClick={() => cropCenter(4/3)} className="px-2 py-1 rounded bg-white/10 hover:bg-white/20">4:3</button> | |
| <div className="flex items-center gap-2 pl-3"> | |
| <span className="text-xs opacity-70">B</span> | |
| <input disabled={working} type="range" min={-100} max={100} value={brightness} onChange={(e) => setBrightness(parseInt(e.target.value))} /> | |
| <span className="text-xs opacity-70">C</span> | |
| <input disabled={working} type="range" min={-100} max={100} value={contrast} onChange={(e) => setContrast(parseInt(e.target.value))} /> | |
| <button disabled={working} onClick={applyAdjust} className="px-2 py-1 rounded bg-white/10 hover:bg-white/20">Apply</button> | |
| </div> | |
| <button disabled={working} onClick={upscale2x} className="px-2 py-1 rounded bg-white/10 hover:bg-white/20">Upscale 2×</button> | |
| <button disabled={working} onClick={onClose} className="px-2 py-1 rounded bg-white/10 hover:bg-white/20">Close</button> | |
| </div> | |
| </div> | |
| <div className="p-4 h-[calc(100%-56px)] overflow-auto flex items-center justify-center"> | |
| <img ref={previewRef} src={`data:image/png;base64,${imageBase64}`} alt="Editor" className="max-w-full max-h-full rounded-lg shadow" /> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |