Team Ai
Apppublic

syscycle/PNG-Vectorizer-Algorithm

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
App.tsx492 linesDownload Raw Back to src
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}