'use client'; import { useRef, useEffect, useState, useCallback } from 'react'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Upload, Download, Eraser, Brush, RotateCcw, Link as LinkIcon, Loader2 } from 'lucide-react'; import { fileToBase64 } from '@/lib/utils'; import { validateImageUrlWithBase64 } from '@/lib/image-validation'; import { apiClient } from '@/lib/api'; interface InpaintingCanvasProps { onSourceImageChange: (image: string) => void; onMaskImageChange: (image: string) => void; className?: string; targetWidth?: number; targetHeight?: number; } export function InpaintingCanvas({ onSourceImageChange, onMaskImageChange, className, targetWidth, targetHeight }: InpaintingCanvasProps) { const canvasRef = useRef(null); const maskCanvasRef = useRef(null); // Keep for mask generation const fileInputRef = useRef(null); const [sourceImage, setSourceImage] = useState(null); const [originalSourceImage, setOriginalSourceImage] = useState(null); const [isDrawing, setIsDrawing] = useState(false); const [brushSize, setBrushSize] = useState(20); const [isEraser, setIsEraser] = useState(false); const [canvasSize, setCanvasSize] = useState({ width: 512, height: 512 }); const [inputMode, setInputMode] = useState<'file' | 'url'>('file'); const [urlInput, setUrlInput] = useState(''); const [isLoadingUrl, setIsLoadingUrl] = useState(false); const [urlError, setUrlError] = useState(null); const [isResizing, setIsResizing] = useState(false); // Initialize canvases useEffect(() => { const canvas = canvasRef.current; const maskCanvas = maskCanvasRef.current; if (!canvas || !maskCanvas) return; const ctx = canvas.getContext('2d'); const maskCtx = maskCanvas.getContext('2d'); if (!ctx || !maskCtx) return; // Set canvas size canvas.width = canvasSize.width; canvas.height = canvasSize.height; maskCanvas.width = canvasSize.width; maskCanvas.height = canvasSize.height; // Initialize mask canvas with black (no inpainting) maskCtx.fillStyle = 'black'; maskCtx.fillRect(0, 0, canvasSize.width, canvasSize.height); // Update mask image updateMaskImage(); }, [canvasSize]); const updateMaskImage = useCallback(() => { const maskCanvas = maskCanvasRef.current; if (!maskCanvas) return; const maskDataUrl = maskCanvas.toDataURL(); onMaskImageChange(maskDataUrl); }, [onMaskImageChange]); const loadImageToCanvas = useCallback((base64Image: string) => { // Store original image for resizing setOriginalSourceImage(base64Image); setSourceImage(base64Image); onSourceImageChange(base64Image); // Load image to get dimensions and update canvas size const img = new Image(); img.onload = () => { // Use target dimensions if provided, otherwise fit within 512x512 let width: number; let height: number; if (targetWidth && targetHeight) { width = targetWidth; height = targetHeight; } else { // Calculate scaled dimensions to fit within 512x512 while maintaining aspect ratio const maxSize = 512; width = img.width; height = img.height; if (width > maxSize || height > maxSize) { const aspectRatio = width / height; if (width > height) { width = maxSize; height = maxSize / aspectRatio; } else { height = maxSize; width = maxSize * aspectRatio; } } } const newCanvasSize = { width: Math.round(width), height: Math.round(height) }; setCanvasSize(newCanvasSize); // Draw image on main canvas const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; canvas.width = width; canvas.height = height; ctx.drawImage(img, 0, 0, width, height); // Update mask canvas size const maskCanvas = maskCanvasRef.current; if (!maskCanvas) return; const maskCtx = maskCanvas.getContext('2d'); if (!maskCtx) return; maskCanvas.width = width; maskCanvas.height = height; maskCtx.fillStyle = 'black'; maskCtx.fillRect(0, 0, width, height); updateMaskImage(); }; img.src = base64Image; }, [onSourceImageChange, updateMaskImage, targetWidth, targetHeight]); // Auto-resize image when target dimensions change useEffect(() => { const resizeImage = async () => { if (!originalSourceImage || !targetWidth || !targetHeight) { return; } // Don't resize if we're already resizing if (isResizing) { return; } try { setIsResizing(true); const result = await apiClient.resizeImage(originalSourceImage, targetWidth, targetHeight); loadImageToCanvas(result.image); } catch (err) { console.error('Failed to resize image:', err); } finally { setIsResizing(false); } }; resizeImage(); }, [targetWidth, targetHeight, originalSourceImage]); const handleImageUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; try { const base64 = await fileToBase64(file); loadImageToCanvas(base64); } catch (err) { console.error('Failed to load image:', err); } }; const handleUrlLoad = async () => { if (!urlInput.trim()) { setUrlError('Please enter a URL'); return; } setIsLoadingUrl(true); setUrlError(null); try { const result = await validateImageUrlWithBase64(urlInput); if (!result.isValid) { setUrlError(result.error || 'Failed to load image from URL'); setIsLoadingUrl(false); return; } // Use base64 data if available, otherwise use the URL directly const imageData = result.base64Data || urlInput; loadImageToCanvas(imageData); setIsLoadingUrl(false); } catch (err) { setUrlError(err instanceof Error ? err.message : 'Failed to load image from URL'); setIsLoadingUrl(false); } }; const startDrawing = (e: React.MouseEvent) => { if (!sourceImage) return; setIsDrawing(true); draw(e); }; const stopDrawing = () => { setIsDrawing(false); }; const draw = (e: React.MouseEvent) => { if (!isDrawing || !sourceImage) return; const canvas = canvasRef.current; const maskCanvas = maskCanvasRef.current; if (!canvas || !maskCanvas) return; const ctx = canvas.getContext('2d'); const maskCtx = maskCanvas.getContext('2d'); if (!ctx || !maskCtx) return; const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; const x = (e.clientX - rect.left) * scaleX; const y = (e.clientY - rect.top) * scaleY; // Draw on mask canvas (for API) maskCtx.globalCompositeOperation = 'source-over'; maskCtx.fillStyle = isEraser ? 'black' : 'white'; maskCtx.beginPath(); maskCtx.arc(x, y, brushSize, 0, Math.PI * 2); maskCtx.fill(); // Draw visual overlay directly on main canvas ctx.save(); ctx.globalCompositeOperation = 'source-over'; if (isEraser) { // For eraser, just redraw the image at that position const img = new Image(); img.onload = () => { // Clear the area and redraw ctx.save(); ctx.globalCompositeOperation = 'destination-out'; ctx.beginPath(); ctx.arc(x, y, brushSize, 0, Math.PI * 2); ctx.fill(); ctx.restore(); // Redraw the image in the cleared area ctx.save(); ctx.globalCompositeOperation = 'destination-over'; ctx.drawImage(img, 0, 0, canvas.width, canvas.height); ctx.restore(); updateMaskImage(); }; img.src = sourceImage; } else { // For brush, draw a visible overlay ctx.globalAlpha = 0.6; ctx.fillStyle = 'rgba(255, 105, 180, 0.8)'; // Bright pink for visibility ctx.beginPath(); ctx.arc(x, y, brushSize, 0, Math.PI * 2); ctx.fill(); // Also draw a border for better visibility ctx.globalAlpha = 1.0; ctx.strokeStyle = 'rgba(255, 0, 0, 0.9)'; // Red border ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(x, y, brushSize, 0, Math.PI * 2); ctx.stroke(); ctx.restore(); updateMaskImage(); } }; const clearMask = () => { const canvas = canvasRef.current; const maskCanvas = maskCanvasRef.current; if (!canvas || !maskCanvas) return; const ctx = canvas.getContext('2d'); const maskCtx = maskCanvas.getContext('2d'); if (!ctx || !maskCtx) return; // Clear mask canvas maskCtx.fillStyle = 'black'; maskCtx.fillRect(0, 0, maskCanvas.width, maskCanvas.height); // Redraw source image on main canvas if (sourceImage) { const img = new Image(); img.onload = () => { ctx.drawImage(img, 0, 0, canvas.width, canvas.height); }; img.src = sourceImage; } updateMaskImage(); }; const downloadMask = () => { const canvas = maskCanvasRef.current; if (!canvas) return; const link = document.createElement('a'); link.download = 'inpainting-mask.png'; link.href = canvas.toDataURL(); link.click(); }; return (
setInputMode(value as 'file' | 'url')}> Upload File From URL
{ setUrlInput(e.target.value); setUrlError(null); }} placeholder="https://example.com/image.png" disabled={isLoadingUrl} />

Enter a URL that ends with an image extension (.jpg, .png, .gif, etc.)

{urlError && (

{urlError}

)}
{isResizing && (
Resizing image...
)} {sourceImage && ( <>

Draw on the image to mark areas for inpainting. White areas will be inpainted, black areas will be preserved.

setBrushSize(Number(e.target.value))} min={1} max={100} className="w-full" />
)}
); }