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 (
Brush
setBrushSize(Number(e.target.value))}
style={{ width: 110 }}
/>
{brushSize}
);
}
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',
};
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',
}}
>
)}
{hasImage && (
<>
Drop an image or click to choose one
PNG or JPG
{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.'}
> )}
Comments
Post a Comment