syscycle/PNG-Vectorizer-Algorithm
0
1/** 2 * @license 3 * SPDX-License-Identifier: Apache-2.0 4 */ 5 6import React, { useState, useRef, useEffect, useCallback } from 'react'; 7import { UploadCloud, Download, Image as ImageIcon, Sparkles, Settings2, Loader2 } from 'lucide-react'; 8// @ts-ignore 9import ImageTracer from 'imagetracerjs'; 10import { cn } from './lib/utils'; 11 12export default function App() { 13 const [originalImage, setOriginalImage] = useState<string | null>(null); 14 const [svgString, setSvgString] = useState<string | null>(null); 15 const [isProcessing, setIsProcessing] = useState(false); 16 17 // Vectorization parameters 18 const [colorCount, setColorCount] = useState<number>(16); 19 20 // 1. Pre-Processing (Anti-Noise & Oversampling) 21 const [upscale, setUpscale] = useState<number>(1); // 1 to 4 22 const [preBlur, setPreBlur] = useState<number>(0); // 0 to 5px Gaussian Blur 23 const [pixelatedUpscale, setPixelatedUpscale] = useState<boolean>(false); // Use nearest-neighbor instead of smooth 24 25 // 2. Tracing & Curve Fitting 26 const [curveFitting, setCurveFitting] = useState<number>(1); // qtres 27 const [lineFitting, setLineFitting] = useState<number>(1); // ltres 28 const [pathOmit, setPathOmit] = useState<number>(8); // Remove small segments 29 const [cornerDetection, setCornerDetection] = useState<boolean>(true); // rightangleenhance 30 31 // 3. Rendering 32 const [strokeWidth, setStrokeWidth] = useState<number>(1); // 0 to 5 33 const [layering, setLayering] = useState<number>(0); // 0 (sequential) or 1 (parallel) 34 35 const [isDragging, setIsDragging] = useState(false); 36 37 const fileInputRef = useRef<HTMLInputElement>(null); 38 39 const processImage = useCallback(() => { 40 if (!originalImage) return; 41 setIsProcessing(true); 42 43 // Give UI time to update processing state 44 setTimeout(() => { 45 try { 46 const options = { 47 numberofcolors: colorCount, 48 qtres: curveFitting, 49 ltres: lineFitting, 50 pathomit: pathOmit, 51 blurradius: 0, // Using canvas pre-blur instead for exact gaussian control 52 blurdelta: 20, // default 53 strokewidth: strokeWidth, 54 layering: layering, 55 linefilter: true, 56 rightangleenhance: cornerDetection, 57 colorsampling: 2, 58 roundcoords: 2 59 }; 60 61 const img = new Image(); 62 img.crossOrigin = "Anonymous"; 63 img.onload = () => { 64 let scale = upscale; 65 66 // Safety: avoid browser crashes on huge images 67 const maxDim = Math.max(img.width, img.height); 68 if (maxDim * scale > 2500) { 69 scale = Math.max(1, 2500 / maxDim); 70 } 71 72 const canvas = document.createElement('canvas'); 73 const finalWidth = Math.round(img.width * scale); 74 const finalHeight = Math.round(img.height * scale); 75 canvas.width = finalWidth; 76 canvas.height = finalHeight; 77 const ctx = canvas.getContext('2d'); 78 79 if (ctx) { 80 if (scale > 1) { 81 if (pixelatedUpscale) { 82 ctx.imageSmoothingEnabled = false; 83 } else { 84 // Smooth interpolation to blur the stair-stepped pixels of low-res images 85 ctx.imageSmoothingEnabled = true; 86 ctx.imageSmoothingQuality = 'high'; 87 } 88 } 89 90 // Makaledeki 1. Teknik: Gaussian Blur (Anti-Noise) 91 if (preBlur > 0) { 92 ctx.filter = `blur(${preBlur}px)`; 93 } 94 95 // Draw the source image onto the upscaled canvas 96 ctx.drawImage(img, 0, 0, finalWidth, finalHeight); 97 98 // Reset filter so we can read it safely 99 ctx.filter = 'none'; 100 const imgData = ctx.getImageData(0, 0, finalWidth, finalHeight); 101 102 // Vectorize 103 const svgstr = ImageTracer.imagedataToSVG(imgData, options); 104 105 // Critical Fix: ImageTracer's `strokewidth` creates sharp "miter" spikes on curved boundaries. 106 // We force rounded stroke joins globally in the SVG to eliminate these spikes. 107 const fixedSvg = svgstr.replace('<svg ', '<svg stroke-linejoin="round" stroke-linecap="round" '); 108 setSvgString(fixedSvg); 109 } 110 setIsProcessing(false); 111 }; 112 img.onerror = () => { 113 setIsProcessing(false); 114 console.error("Failed to load image"); 115 }; 116 img.src = originalImage; 117 118 } catch (error) { 119 console.error("Vectorization failed:", error); 120 alert("Failed to vectorize image."); 121 setIsProcessing(false); 122 } 123 }, 50); 124 }, [originalImage, colorCount, curveFitting, lineFitting, pathOmit, cornerDetection, strokeWidth, layering, upscale, preBlur, pixelatedUpscale]); 125 126 // Re-process when parameters change 127 useEffect(() => { 128 // Add a small debounce if needed, but for now just process 129 const timeout = setTimeout(() => { 130 processImage(); 131 }, 300); 132 return () => clearTimeout(timeout); 133 }, [processImage]); 134 135 const handleFileUpload = (file: File) => { 136 if (!file.type.startsWith('image/')) return; 137 const reader = new FileReader(); 138 reader.onload = (e) => { 139 if (e.target?.result && typeof e.target.result === 'string') { 140 const imageUrl = e.target.result; 141 setOriginalImage(imageUrl); 142 } 143 }; 144 reader.readAsDataURL(file); 145 }; 146 147 const onDragOver = (e: React.DragEvent) => { 148 e.preventDefault(); 149 setIsDragging(true); 150 }; 151 152 const onDragLeave = (e: React.DragEvent) => { 153 e.preventDefault(); 154 setIsDragging(false); 155 }; 156 157 const onDrop = (e: React.DragEvent) => { 158 e.preventDefault(); 159 setIsDragging(false); 160 if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { 161 handleFileUpload(e.dataTransfer.files[0]); 162 } 163 }; 164 165 const handleDownload = () => { 166 if (!svgString) return; 167 const blob = new Blob([svgString], { type: 'image/svg+xml' }); 168 const url = URL.createObjectURL(blob); 169 const link = document.createElement('a'); 170 link.href = url; 171 link.download = 'vectorized_image.svg'; 172 document.body.appendChild(link); 173 link.click(); 174 document.body.removeChild(link); 175 URL.revokeObjectURL(url); 176 }; 177 178 const hasImage = !!originalImage; 179 180 return ( 181 <div className="min-h-screen bg-[#09090B] text-[#E4E4E7] font-sans flex flex-col"> 182 {/* Header */} 183 <header className="h-16 border-b border-white/10 flex items-center justify-between px-6 bg-[#09090B] sticky top-0 z-10"> 184 <div className="flex items-center gap-3 text-white"> 185 <div className="w-8 h-8 bg-indigo-600 rounded flex items-center justify-center font-bold text-white shadow-lg shadow-indigo-500/20"> 186 <Sparkles className="w-4 h-4" /> 187 </div> 188 <h1 className="font-semibold text-lg tracking-tight">Vectorify <span className="text-indigo-400 font-normal">Pro</span></h1> 189 </div> 190 <div className="text-sm font-medium text-gray-400 hidden sm:block"> 191 Flawless Image Vectorization 192 </div> 193 </header> 194 195 <main className="flex-1 max-w-7xl w-full mx-auto p-4 sm:p-6 lg:p-8 flex flex-col lg:flex-row gap-6"> 196 197 {/* Main Content Area */} 198 <div className="flex-1 flex flex-col min-w-0"> 199 {!hasImage ? ( 200 <div 201 className={cn( 202 "flex-1 flex flex-col items-center justify-center border-2 border-dashed rounded-xl transition-all duration-200 bg-[#121214]", 203 isDragging ? "border-indigo-500 bg-indigo-500/10" : "border-white/10 hover:border-white/20" 204 )} 205 onDragOver={onDragOver} 206 onDragLeave={onDragLeave} 207 onDrop={onDrop} 208 onClick={() => fileInputRef.current?.click()} 209 > 210 <div className="p-8 text-center cursor-pointer flex flex-col items-center"> 211 <div className="w-16 h-16 bg-indigo-500/20 rounded border border-indigo-500/30 flex items-center justify-center mb-6 shadow-[0_0_15px_rgba(99,102,241,0.2)]"> 212 <UploadCloud className="w-8 h-8 text-indigo-400" /> 213 </div> 214 <h2 className="text-xl font-bold text-gray-200 mb-2">Upload an Image</h2> 215 <p className="text-gray-400 mb-8 max-w-sm text-sm"> 216 Drag and drop a PNG, JPG, or GIF here, or click to browse. We'll automatically convert it to a crisp SVG. 217 </p> 218 <button className="px-6 py-2.5 bg-indigo-600 text-white rounded-full font-bold text-sm shadow-lg shadow-indigo-600/20 hover:bg-indigo-500 transition w-full sm:w-auto"> 219 Browse Files 220 </button> 221 </div> 222 </div> 223 ) : ( 224 <div className="flex-1 grid grid-rows-2 lg:grid-rows-1 lg:grid-cols-2 gap-4 lg:gap-6"> 225 226 {/* Original Image Box */} 227 <div className="bg-[#18181B] rounded-xl border border-white/5 shadow-sm overflow-hidden flex flex-col"> 228 <div className="px-4 py-3 border-b border-white/5 bg-black/20 flex items-center justify-between"> 229 <div className="flex items-center text-gray-400 font-bold text-[10px] uppercase tracking-widest"> 230 <ImageIcon className="w-3.5 h-3.5 mr-2 text-gray-500" /> 231 Original 232 </div> 233 <button 234 onClick={() => { setOriginalImage(null); setSvgString(null); }} 235 className="text-[10px] font-bold uppercase tracking-widest text-gray-400 hover:text-white bg-white/5 hover:bg-white/10 px-2 py-1 rounded border border-white/10 transition-colors" 236 > 237 Start Over 238 </button> 239 </div> 240 <div className="flex-1 p-4 flex items-center justify-center bg-[#121214] bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4IiBoZWlnaHQ9IjgiIHZpZXdCb3g9IjAgMCA4IDgiPjxyZWN0IHdpZHRoPSI4IiBoZWlnaHQ9IjgiIGZpbGw9IiMxYjFiMWUiLz48cGF0aCBkPSJNMCAwaDR2NEgwem00IDRoNHY0SDR6IiBmaWxsPSIjMjEyMTI2Ii8+PC9zdmc+')] shadow-inner relative"> 241 <img src={originalImage} alt="Original uploaded image" className="max-w-full max-h-[500px] object-contain drop-shadow-2xl rounded" /> 242 </div> 243 </div> 244 245 {/* Vectorized Result Box */} 246 <div className="bg-[#18181B] rounded-xl border border-white/5 shadow-sm overflow-hidden flex flex-col relative"> 247 <div className="px-4 py-3 border-b border-white/5 bg-black/20 flex items-center justify-between"> 248 <div className="flex items-center text-indigo-400 font-bold text-[10px] uppercase tracking-widest"> 249 <Sparkles className="w-3.5 h-3.5 mr-2" /> 250 Vectorized (SVG) 251 </div> 252 {isProcessing && ( 253 <div className="flex items-center text-[10px] font-bold uppercase tracking-widest text-indigo-300"> 254 <Loader2 className="w-3 h-3 mr-1.5 animate-spin" /> 255 Processing... 256 </div> 257 )} 258 </div> 259 <div className="flex-1 p-4 flex items-center justify-center bg-[#121214] bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4IiBoZWlnaHQ9IjgiIHZpZXdCb3g9IjAgMCA4IDgiPjxyZWN0IHdpZHRoPSI4IiBoZWlnaHQ9IjgiIGZpbGw9IiMxYjFiMWUiLz48cGF0aCBkPSJNMCAwaDR2NEgwem00IDRoNHY0SDR6IiBmaWxsPSIjMjEyMTI2Ii8+PC9zdmc+')] shadow-inner relative"> 260 {svgString ? ( 261 <div 262 className="max-w-full max-h-[500px] flex items-center justify-center drop-shadow-2xl [&>svg]:w-full [&>svg]:max-h-[500px] [&>svg]:h-auto" 263 dangerouslySetInnerHTML={{ __html: svgString }} 264 /> 265 ) : ( 266 <div className="text-gray-600 flex flex-col items-center"> 267 <ImageIcon className="w-8 h-8 mb-2 opacity-50" /> 268 <span className="text-xs font-medium uppercase tracking-widest">Preview Area</span> 269 </div> 270 )} 271 </div> 272 {/* Overlay while processing */} 273 {isProcessing && ( 274 <div className="absolute inset-0 top-11 bg-[#09090B]/60 backdrop-blur-sm flex items-center justify-center z-10"> 275 <div className="bg-[#18181B] px-6 py-4 rounded border border-white/10 shadow-2xl flex flex-col items-center"> 276 <Loader2 className="w-8 h-8 text-indigo-500 animate-spin mb-3" /> 277 <span className="text-[10px] font-bold uppercase tracking-widest text-indigo-400">Tracing paths...</span> 278 </div> 279 </div> 280 )} 281 </div> 282 283 </div> 284 )} 285 286 <input 287 type="file" 288 accept="image/*" 289 className="hidden" 290 ref={fileInputRef} 291 onChange={(e) => { 292 if (e.target.files && e.target.files.length > 0) { 293 handleFileUpload(e.target.files[0]); 294 } 295 }} 296 /> 297 </div> 298 299 {/* Sidebar Settings (Disabled if no image) */} 300 <div className={cn( 301 "w-full lg:w-72 flex flex-col transition-opacity duration-300", 302 !hasImage ? "opacity-30 pointer-events-none" : "opacity-100" 303 )}> 304 <div className="bg-[#09090B] rounded-xl border border-white/10 p-6 flex flex-col h-full lg:h-auto sticky top-24"> 305 <div className="flex items-center gap-2 mb-6 pb-4 border-b border-white/5"> 306 <Settings2 className="w-4 h-4 text-indigo-500" /> 307 <h3 className="text-[10px] uppercase tracking-widest text-gray-400 font-bold">Vectorization Settings</h3> 308 </div> 309 310 <div className="mb-6 flex-1 space-y-5 overflow-y-auto pr-1"> 311 {/* 1. Low Res Curve Fixes */} 312 <div className="p-3 bg-indigo-500/10 border border-indigo-500/20 rounded-lg space-y-4"> 313 <h4 className="text-[10px] uppercase tracking-widest text-indigo-400 font-bold mb-3">1. Görüntü Ön İşleme (Anti-Noise)</h4> 314 315 <div> 316 <div className="flex justify-between items-center mb-2"> 317 <label className="text-[10px] uppercase tracking-widest text-gray-500 font-bold">Oversampling (Upscale)</label> 318 <span className="text-xs font-mono text-indigo-400">{upscale}x</span> 319 </div> 320 <input 321 type="range" min="1" max="4" step="1" value={upscale} 322 onChange={(e) => setUpscale(parseInt(e.target.value))} 323 className="w-full accent-indigo-500 opacity-80 hover:opacity-100 cursor-pointer" 324 /> 325 <p className="text-[9px] text-gray-400 mt-1">Teknik 7: İnce detayları yakalamak için çözünürlüğü artırır.</p> 326 </div> 327 328 <div> 329 <label className="flex items-center justify-between cursor-pointer group"> 330 <span className="text-[10px] uppercase tracking-widest text-gray-400 group-hover:text-white transition">Pürüzlü Büyüt (Halo Önleyici)</span> 331 <div className="relative inline-block w-8 h-4 rounded-full bg-white/10 transition-colors group-hover:bg-white/20"> 332 <input 333 type="checkbox" 334 className="peer absolute opacity-0 w-0 h-0" 335 checked={pixelatedUpscale} 336 onChange={(e) => setPixelatedUpscale(e.target.checked)} 337 /> 338 <span className="absolute left-[2px] top-[2px] w-3 h-3 rounded-full bg-gray-400 transition-all peer-checked:bg-indigo-400 peer-checked:translate-x-4"></span> 339 </div> 340 </label> 341 <p className="text-[9px] text-gray-400 mt-2 leading-relaxed">Etkinken, büyütme işleminde yumuşatma (anti-aliasing) yapmaz. <strong>Büyütme veya Blur işleminden dolayı oluşan renk katmanlarını (Halo/aura etkisi) önlemek için bunu açın veya Renk Sayısını (menünün altında) azaltın!</strong></p> 342 </div> 343 344 <div> 345 <div className="flex justify-between items-center mb-2"> 346 <label className="text-[10px] uppercase tracking-widest text-gray-500 font-bold">Gaussian Blur</label> 347 <span className="text-xs font-mono text-indigo-400">{preBlur}px</span> 348 </div> 349 <input 350 type="range" min="0" max="5" step="0.5" value={preBlur} 351 onChange={(e) => setPreBlur(parseFloat(e.target.value))} 352 className="w-full accent-indigo-500 opacity-80 hover:opacity-100 cursor-pointer" 353 /> 354 <p className="text-[9px] text-gray-400 mt-1">Teknik 1: Pütürleri/stair-step pikselleri yumuşatıp hatları belli eder.</p> 355 </div> 356 </div> 357 358 {/* 2. Curve Fitting & Tracing */} 359 <div className="p-3 bg-indigo-500/10 border border-indigo-500/20 rounded-lg space-y-4"> 360 <h4 className="text-[10px] uppercase tracking-widest text-indigo-400 font-bold mb-3">2. Vector Tracing & Curve Fitting</h4> 361 362 <div> 363 <div className="flex justify-between items-center mb-2"> 364 <label className="text-[10px] uppercase tracking-widest text-gray-500 font-bold">Bezier Curve Fitting Toleransı</label> 365 <span className="text-xs font-mono text-indigo-400">{curveFitting}</span> 366 </div> 367 <input 368 type="range" min="0.1" max="10" step="0.1" value={curveFitting} 369 onChange={(e) => setCurveFitting(parseFloat(e.target.value))} 370 className="w-full accent-indigo-500 opacity-80 hover:opacity-100 cursor-pointer" 371 /> 372 <p className="text-[9px] text-gray-400 mt-1">Teknik 3 & 4: Eğrileri bezier olarak uydurur. Yüksek değer zigzagları yok edip daha pürüzsüz sonuç verir.</p> 373 </div> 374 375 <div> 376 <div className="flex justify-between items-center mb-2"> 377 <label className="text-[10px] uppercase tracking-widest text-gray-500 font-bold">Düz Çizgi Toleransı</label> 378 <span className="text-xs font-mono text-indigo-400">{lineFitting}</span> 379 </div> 380 <input 381 type="range" min="0.1" max="10" step="0.1" value={lineFitting} 382 onChange={(e) => setLineFitting(parseFloat(e.target.value))} 383 className="w-full accent-indigo-500 opacity-80 hover:opacity-100 cursor-pointer" 384 /> 385 </div> 386 387 <div> 388 <div className="flex justify-between items-center mb-2"> 389 <label className="text-[10px] uppercase tracking-widest text-gray-500 font-bold">Küçük Segmentleri Sil</label> 390 <span className="text-xs font-mono text-indigo-400">{pathOmit}</span> 391 </div> 392 <input 393 type="range" min="0" max="64" step="1" value={pathOmit} 394 onChange={(e) => setPathOmit(parseInt(e.target.value))} 395 className="w-full accent-indigo-500 opacity-80 hover:opacity-100 cursor-pointer" 396 /> 397 <p className="text-[9px] text-gray-400 mt-1">Teknik 5: Yüksek frekanslı noise'ları ve gereksiz noktaları çöpe atar.</p> 398 </div> 399 400 <div> 401 <label className="flex items-center justify-between cursor-pointer group"> 402 <span className="text-[10px] uppercase tracking-widest text-gray-400 group-hover:text-white transition">Köşe Koruma (Corner Detect)</span> 403 <div className="relative inline-block w-8 h-4 rounded-full bg-white/10 transition-colors group-hover:bg-white/20"> 404 <input 405 type="checkbox" 406 className="peer absolute opacity-0 w-0 h-0" 407 checked={cornerDetection} 408 onChange={(e) => setCornerDetection(e.target.checked)} 409 /> 410 <span className="absolute left-[2px] top-[2px] w-3 h-3 rounded-full bg-gray-400 transition-all peer-checked:bg-indigo-400 peer-checked:translate-x-4"></span> 411 </div> 412 </label> 413 <p className="text-[9px] text-gray-400 mt-2 leading-relaxed">Teknik 6: Keskin köşeleri tespit edip muhafaza eder, kalan yerleri curve smoothing ile yumuşatır.</p> 414 </div> 415 </div> 416 417 <div className="h-px bg-white/5 w-full my-4"></div> 418 419 {/* Colors */} 420 <div> 421 <div className="flex justify-between items-center mb-2"> 422 <label className="text-[10px] uppercase tracking-widest text-gray-500 font-bold"> 423 Colors 424 </label> 425 <span className="text-xs font-mono text-indigo-400">{colorCount}</span> 426 </div> 427 <input 428 type="range" min="2" max="64" value={colorCount} 429 onChange={(e) => setColorCount(parseInt(e.target.value))} 430 className="w-full accent-indigo-500 opacity-80 hover:opacity-100 cursor-pointer" 431 /> 432 <p className="text-[9px] text-gray-600 mt-1">Fewer colors = simpler output.</p> 433 </div> 434 435 {/* Layering (Sequential vs Parallel) */} 436 <div> 437 <div className="flex justify-between items-center mb-2"> 438 <label className="text-[10px] uppercase tracking-widest text-gray-500 font-bold"> 439 Layer Stacking 440 </label> 441 </div> 442 <div className="flex bg-white/5 rounded-md p-1 gap-1"> 443 <button 444 onClick={() => setLayering(0)} 445 className={cn("flex-1 text-[10px] py-1.5 rounded transition", layering === 0 ? "bg-indigo-500/20 text-indigo-400 font-semibold" : "text-gray-500 hover:text-white")} 446 > 447 Overlapping (Fills Gaps) 448 </button> 449 <button 450 onClick={() => setLayering(1)} 451 className={cn("flex-1 text-[10px] py-1.5 rounded transition", layering === 1 ? "bg-indigo-500/20 text-indigo-400 font-semibold" : "text-gray-500 hover:text-white")} 452 > 453 Cutout (No Overlap) 454 </button> 455 </div> 456 </div> 457 458 {/* Stroke Width / Anti-Gap */} 459 <div> 460 <div className="flex justify-between items-center mb-2"> 461 <label className="text-[10px] uppercase tracking-widest text-gray-500 font-bold"> 462 Stroke (Anti-Gap) 463 </label> 464 <span className="text-xs font-mono text-indigo-400">{strokeWidth}px</span> 465 </div> 466 <input 467 type="range" min="0" max="5" step="0.5" value={strokeWidth} 468 onChange={(e) => setStrokeWidth(parseFloat(e.target.value))} 469 className="w-full accent-indigo-500 opacity-80 hover:opacity-100 cursor-pointer" 470 /> 471 <p className="text-[9px] text-gray-600 mt-1">If using Cutout mode, slightly increase stroke to hide gaps.</p> 472 </div> 473 474 </div> 475 476 <div className="mt-auto pt-6 border-t border-white/5"> 477 <button 478 disabled={!svgString || isProcessing} 479 onClick={handleDownload} 480 className="w-full bg-indigo-600 hover:bg-indigo-500 py-3 rounded-lg font-bold text-sm text-white shadow-lg shadow-indigo-600/20 transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2" 481 > 482 <Download className="w-4 h-4" /> 483 <span>Download Vectors</span> 484 </button> 485 </div> 486 </div> 487 </div> 488 489 </main> 490 </div> 491 ); 492}