import React, { useRef, useState, useCallback, useEffect } from 'react'; import { Upload, Eraser, Undo2, Download, RotateCcw, Wand2 } from 'lucide-react'; export default function WatermarkRemover() { const imgCanvasRef = useRef(null); const maskCanvasRef = useRef(null); const wrapRef = useRef(null); const fileInputRef = useRef(null); const [hasImage, setHasImage] = useState(false); const [brushSize, setBrushSize] = useState(28); const [isDrawing, setIsDrawing] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const [dims, setDims] = useState({ w: 0, h: 0 }); const [history, setHistory] = useState([]); const [hint, setHint] = useState('upload'); const MAX_DIM = 1400; const loadImage = useCallback((file) => { const url = URL.createObjectURL(file); const img = new Image(); img.onload = () => { let w = img.width; let h = img.height; if (w > MAX_DIM || h > MAX_DIM) { const scale = MAX_DIM / Math.max(w, h); w = Math.round(w * scale); h = Math.round(h * scale); } setDims({ w, h }); requestAnimationFrame(() => { const ic = imgCanvasRef.current; const mc = maskCanvasRef.current; ic.width = w; ic.height = h; mc.width = w; mc.height = h; const ctx = ic.getContext('2d'); ctx.clearRect(0, 0, w, h); ctx.drawImage(img, 0, 0, w, h); const mctx = mc.getContext('2d'); mctx.clearRect(0, 0, w, h); setHasImage(true); setHint('mark'); setHistory([]); URL.revokeObjectURL(url); }); }; img.src = url; }, []); const onFileChange = (e) => { const f = e.target.files && e.target.files[0]; if (f) loadImage(f); }; const onDrop = (e) => { e.preventDefault(); const f = e.dataTransfer.files && e.dataTransfer.files[0]; if (f && f.type.startsWith('image/')) loadImage(f); }; const getPos = (e) => { const mc = maskCanvasRef.current; const rect = mc.getBoundingClientRect(); const scaleX = mc.width / rect.width; const scaleY = mc.height / rect.height; const clientX = e.touches ? e.touches[0].clientX : e.clientX; const clientY = e.touches ? e.touches[0].clientY : e.clientY; return { x: (clientX - rect.left) * scaleX, y: (clientY - rect.top) * scaleY }; }; const pushHistory = () => { const mc = maskCanvasRef.current; const snap = mc.getContext('2d').getImageData(0, 0, mc.width, mc.height); setHistory((h) => [...h.slice(-9), snap]); }; const drawDot = (x, y) => { const mctx = maskCanvasRef.current.getContext('2d'); mctx.fillStyle = 'rgba(232,163,61,0.55)'; mctx.beginPath(); mctx.arc(x, y, brushSize / 2, 0, Math.PI * 2); mctx.fill(); }; const handleStart = (e) => { if (!hasImage) return; e.preventDefault(); pushHistory(); setIsDrawing(true); const { x, y } = getPos(e); drawDot(x, y); }; const handleMove = (e) => { if (!isDrawing) return; e.preventDefault(); const { x, y } = getPos(e); drawDot(x, y); }; const handleEnd = () => setIsDrawing(false); const undo = () => { if (history.length === 0) return; const prev = history[history.length - 1]; maskCanvasRef.current.getContext('2d').putImageData(prev, 0, 0); setHistory((h) => h.slice(0, -1)); }; const clearMask = () => { const mc = maskCanvasRef.current; mc.getContext('2d').clearRect(0, 0, mc.width, mc.height); setHistory([]); }; const startOver = () => { setHasImage(false); setHint('upload'); setHistory([]); if (fileInputRef.current) fileInputRef.current.value = ''; }; const removeWatermark = () => { const ic = imgCanvasRef.current; const mc = maskCanvasRef.current; const w = ic.width, h = ic.height; setIsProcessing(true); requestAnimationFrame(() => { const ictx = ic.getContext('2d'); const mctx = mc.getContext('2d'); const imgData = ictx.getImageData(0, 0, w, h); const maskData = mctx.getImageData(0, 0, w, h); const px = imgData.data; const mpx = maskData.data; const masked = new Uint8Array(w * h); let any = false; for (let i = 0, p = 0; i < mpx.length; i += 4, p++) { if (mpx[i + 3] > 40) { masked[p] = 1; any = true; } } if (!any) { setIsProcessing(false); return; } // seed masked pixels with nearest average of unmasked ring, then diffuse const work = new Float32Array(w * h * 3); for (let p = 0; p < w * h; p++) { work[p * 3] = px[p * 4]; work[p * 3 + 1] = px[p * 4 + 1]; work[p * 3 + 2] = px[p * 4 + 2]; } const idx = (x, y) => y * w + x; const iterations = 120; for (let it = 0; it < iterations; it++) { for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { const p = idx(x, y); if (!masked[p]) continue; let rs = 0, gs = 0, bs = 0, n = 0; if (x > 0) { const q = idx(x - 1, y); rs += work[q*3]; gs += work[q*3+1]; bs += work[q*3+2]; n++; } if (x < w - 1) { const q = idx(x + 1, y); rs += work[q*3]; gs += work[q*3+1]; bs += work[q*3+2]; n++; } if (y > 0) { const q = idx(x, y - 1); rs += work[q*3]; gs += work[q*3+1]; bs += work[q*3+2]; n++; } if (y < h - 1) { const q = idx(x, y + 1); rs += work[q*3]; gs += work[q*3+1]; bs += work[q*3+2]; n++; } if (n > 0) { work[p*3] = rs / n; work[p*3+1] = gs / n; work[p*3+2] = bs / n; } } } } for (let p = 0; p < w * h; p++) { if (masked[p]) { px[p*4] = work[p*3]; px[p*4+1] = work[p*3+1]; px[p*4+2] = work[p*3+2]; } } ictx.putImageData(imgData, 0, 0); mctx.clearRect(0, 0, w, h); setHistory([]); setIsProcessing(false); setHint('done'); }); }; const download = () => { const ic = imgCanvasRef.current; const link = document.createElement('a'); link.download = 'cleaned.png'; link.href = ic.toDataURL('image/png'); link.click(); }; return (

Spot removal

{hasImage ? `${dims.w}×${dims.h}` : 'no image'}

Brush over a watermark or unwanted mark and the pixels underneath get rebuilt from the surrounding image. Works best on logos and text sitting over plain or gently textured backgrounds.

{!hasImage && (
e.preventDefault()} onClick={() => fileInputRef.current.click()} style={{ border: '1px dashed #3A362E', borderRadius: 4, padding: '64px 24px', textAlign: 'center', cursor: 'pointer', background: '#181B20', }} >
Drop an image or click to choose one
PNG or JPG
)} {hasImage && ( <>
Brush setBrushSize(Number(e.target.value))} style={{ width: 110 }} /> {brushSize}
{isProcessing && (
rebuilding pixels…
)}

{hint === 'mark' && 'Paint over the watermark area, then hit "Remove marked area". You can mark and remove more than once.'} {hint === 'done' && 'Done — check the result and mark any leftover edges if needed, or download.'}

)}
); } const btnStyle = (disabled) => ({ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, color: disabled ? '#4A473F' : '#ECE7DD', background: 'transparent', border: '1px solid #2E2B25', borderRadius: 4, padding: '7px 12px', cursor: disabled ? 'default' : 'pointer', }); const primaryBtnStyle = { display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, fontWeight: 500, color: '#12151a', background: '#E8A33D', border: 'none', borderRadius: 4, padding: '10px 16px', cursor: 'pointer', };

Comments

Popular posts from this blog

පාට තෝරන ගේම් ගහලා සල්ලි හොයමු.