<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: hashem alharhashi</title>
    <description>The latest articles on DEV Community by hashem alharhashi (@hashem_alharhashi_3777a84).</description>
    <link>https://dev.to/hashem_alharhashi_3777a84</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4074715%2Fd022af9f-daab-41e5-8d4c-0bbfcc3aead3.png</url>
      <title>DEV Community: hashem alharhashi</title>
      <link>https://dev.to/hashem_alharhashi_3777a84</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hashem_alharhashi_3777a84"/>
    <language>en</language>
    <item>
      <title>I benchmarked my own vectorizer against potrace. It lost two rows out of four.</title>
      <dc:creator>hashem alharhashi</dc:creator>
      <pubDate>Mon, 07 Sep 2026 13:12:00 +0000</pubDate>
      <link>https://dev.to/hashem_alharhashi_3777a84/i-benchmarked-my-own-vectorizer-against-potrace-it-lost-two-rows-out-of-four-4c2o</link>
      <guid>https://dev.to/hashem_alharhashi_3777a84/i-benchmarked-my-own-vectorizer-against-potrace-it-lost-two-rows-out-of-four-4c2o</guid>
      <description>&lt;p&gt;I build a browser-based image-to-SVG vectorizer. Every tool in this category publishes a comparison table where it wins every row, which is why nobody believes any of them.&lt;/p&gt;

&lt;p&gt;So I published the opposite: a reproducible benchmark, scripts and raw data included, where my own tool loses on speed, loses on file size, and loses outright on monochrome work.&lt;/p&gt;

&lt;p&gt;Repository: &lt;strong&gt;&lt;a href="https://github.com/hashem1983jordan-cmd/raster-to-vector-benchmark" rel="noopener noreferrer"&gt;raster-to-vector-benchmark&lt;/a&gt;&lt;/strong&gt; — MIT, re-runnable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;One real logo, 1535x621 RGBA, 11,697 distinct colours. Two tracers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;potrace 1.16&lt;/strong&gt; — the standard, and the engine behind a lot of things you have used&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;12pixa&lt;/strong&gt; — my tool. VTracer compiled from Rust to WebAssembly, running client-side in a Web Worker&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;potrace 1.16&lt;/th&gt;
&lt;th&gt;12pixa (colour)&lt;/th&gt;
&lt;th&gt;12pixa (B&amp;amp;W)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;time&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.10 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.21 s&lt;/td&gt;
&lt;td&gt;not measured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SVG bytes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3,331&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;24,119&lt;/td&gt;
&lt;td&gt;6,503&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;colours&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;224&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;MAE vs original&lt;/strong&gt; (lower is better)&lt;/td&gt;
&lt;td&gt;10.34&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.47&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;13.31&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;potrace is roughly 70x faster and produces a file 7x smaller.&lt;/strong&gt; That is not a rounding difference, it is two orders of magnitude on time.&lt;/p&gt;

&lt;p&gt;And the row that hurts: &lt;strong&gt;my own black-and-white preset is the worst result in the table.&lt;/strong&gt; MAE 13.31 against potrace's 10.34 on the same monochrome job. potrace beats me at the thing potrace is for.&lt;/p&gt;

&lt;p&gt;Where my tool wins is colour — MAE 3.47, the most accurate reproduction of the three, and the only one that produces a palette at all. But "wins the row it was built for" is a much smaller claim than the marketing table usually makes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The methodology mistake I made first — and why it matters
&lt;/h2&gt;

&lt;p&gt;My first attempt compared each output against a &lt;strong&gt;thresholded&lt;/strong&gt; version of the original. potrace scored 97%. My tool scored 21%.&lt;/p&gt;

&lt;p&gt;That result was garbage, and the reason is worth internalising: potrace only accepts bitonal input, so &lt;code&gt;prepare.py&lt;/code&gt; thresholds the image before handing it over. Comparing potrace's output to the thresholded reference compares it &lt;strong&gt;to its own input&lt;/strong&gt;. It cannot lose. The benchmark was measuring "did potrace faithfully reproduce the file we gave it", which is a question with a known answer.&lt;/p&gt;

&lt;p&gt;The fix is to score every candidate against the &lt;strong&gt;original colour raster&lt;/strong&gt; — the thing a user actually had before they started. That single change flipped the outcome and produced the table above.&lt;/p&gt;

&lt;p&gt;If you write a benchmark, the first question to ask is: &lt;em&gt;does one contestant get to be measured against a reference derived from its own preprocessing?&lt;/em&gt; If yes, throw the numbers away.&lt;/p&gt;

&lt;h2&gt;
  
  
  A detail that costs people an hour
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;potrace 1.16&lt;/code&gt; reads PBM/PGM/PPM and BMP. &lt;strong&gt;It cannot open a PNG.&lt;/strong&gt; Every "potrace failed on my file" thread reduces to this. &lt;code&gt;scripts/prepare.py&lt;/code&gt; converts first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. prepare the input (potrace cannot read PNG)&lt;/span&gt;
python scripts/prepare.py corpus/logo-1535x621.png

&lt;span class="c"&gt;# 2. trace&lt;/span&gt;
potrace &lt;span class="nt"&gt;--svg&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; results/potrace--logo.svg out/logo-1535x621.gray.pgm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Timing a browser tool has its own problem: there is no binary to wrap in &lt;code&gt;time&lt;/code&gt;. The page is reloaded per run and measured in-page, so the number includes WASM instantiation. That is the honest way to report it — it is what the user waits through.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would actually tell you to use
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monochrome art, logos, line work, laser or vinyl cutting&lt;/strong&gt; → &lt;strong&gt;potrace&lt;/strong&gt;. Faster, smaller, and more accurate than my own B&amp;amp;W preset. Use it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything with colour&lt;/strong&gt; → potrace is not a candidate; it has no mechanism for colour at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;7.2 seconds is slow&lt;/strong&gt;, and I am not going to describe it as "instant".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why publish a benchmark you lose
&lt;/h2&gt;

&lt;p&gt;Two reasons.&lt;/p&gt;

&lt;p&gt;The first is that an unfalsifiable claim is worth nothing. "Best vectorizer" is not a testable statement. "MAE 3.47 versus 10.34 on this file, here is the script" is, and you can prove me wrong in ten minutes.&lt;/p&gt;

&lt;p&gt;The second is that this is how I would want to be sold to. When a tool tells me where it is weak, I trust the rest of the page.&lt;/p&gt;

&lt;p&gt;The corpus, the scripts, the raw JSON and the SVG outputs are all in the repo. If you re-run it and get different numbers, open an issue — I would rather fix the benchmark than defend it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The tool is &lt;a href="https://12pixa.com" rel="noopener noreferrer"&gt;12pixa&lt;/a&gt; — free, no upload, no account, runs entirely in the browser through WebAssembly. Built by &lt;a href="https://aijolabs.com/en/" rel="noopener noreferrer"&gt;AiJoLabs&lt;/a&gt; in Amman, Jordan.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webassembly</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I shipped 65 tools. Then I deleted 64 of them.</title>
      <dc:creator>hashem alharhashi</dc:creator>
      <pubDate>Wed, 12 Aug 2026 12:44:56 +0000</pubDate>
      <link>https://dev.to/hashem_alharhashi_3777a84/i-shipped-65-tools-then-i-deleted-64-of-them-506o</link>
      <guid>https://dev.to/hashem_alharhashi_3777a84/i-shipped-65-tools-then-i-deleted-64-of-them-506o</guid>
      <description>&lt;p&gt;A year ago I shipped a site with 65 small image tools. Resize, crop, compress, convert, rotate — the usual grid of utilities. It looked productive. It wasn't.&lt;/p&gt;

&lt;p&gt;Analytics said something uncomfortable: almost nobody used 64 of them. One tool had returning users, real session time, and people coming back with different files. It was the image-to-SVG vectorizer.&lt;/p&gt;

&lt;p&gt;So I deleted the other 64.&lt;/p&gt;

&lt;p&gt;Here's what I learned rebuilding around the one thing that worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grouping traced paths by exact hex is useless on real images
&lt;/h2&gt;

&lt;p&gt;The vectorizer has a colour/layer editor — pick a colour, hide it or recolour it. The obvious implementation groups SVG paths by their exact fill value.&lt;/p&gt;

&lt;p&gt;That works beautifully on the synthetic test logo I built it against. Then a user sent me a real file: a gold emblem, photographed.&lt;/p&gt;

&lt;p&gt;The trace produced &lt;strong&gt;3,428 paths across 3,122 distinct hex values.&lt;/strong&gt; Almost every path had its own colour. The palette showed the top 24 by frequency, which together covered &lt;strong&gt;83 paths — 2% of the image.&lt;/strong&gt; Clicking a swatch did nothing you could see. The feature was technically working and practically broken.&lt;/p&gt;

&lt;p&gt;The fix was to stop treating colours as identities and start treating them as positions in a space. Seed from the heaviest colours, reject seeds that are too close to an existing one, then assign every colour to its nearest seed using a weighted-RGB distance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;dist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;dg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rm&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dr&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dg&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dg&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;rm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That weighting is a cheap approximation of human colour perception — green gets the heaviest coefficient because we discriminate green far better than blue.&lt;/p&gt;

&lt;p&gt;Result: 3,122 colours collapsed to &lt;strong&gt;18 layers covering 100% of paths.&lt;/strong&gt; Hiding the top layer now removes 610 paths and you watch the background disappear.&lt;/p&gt;

&lt;p&gt;One detail that mattered more than expected: when recolouring, don't flatten the layer to a single value. Store each path's original colour, compute its offset from the cluster centre, and re-apply that offset to the new colour. Otherwise every gradient and shadow in that layer turns into a flat blob.&lt;/p&gt;

&lt;h2&gt;
  
  
  I shipped a 26MB WASM model and had to rip it out
&lt;/h2&gt;

&lt;p&gt;Small inputs trace badly, so I added an ONNX upscaling model to pre-enlarge them. Locally it was great.&lt;/p&gt;

&lt;p&gt;In production, the WASM binary ran 13–26MB depending on the build, and on some machines its &lt;strong&gt;compilation&lt;/strong&gt; — not inference, compilation — locked the main thread for minutes. On my machine, and in a clean browser profile, it was fine. That's the worst kind of bug: it only exists for other people.&lt;/p&gt;

&lt;p&gt;I removed it entirely. The site went from 18MB to 1.4MB, and the quality difference on real inputs was smaller than I'd told myself it was.&lt;/p&gt;

&lt;p&gt;If you're shipping a model to the browser, budget for compile time on cold, weak, extension-loaded machines — not just inference time on yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture that made deleting 64 tools worth it
&lt;/h2&gt;

&lt;p&gt;Everything runs client-side. VTracer (Rust) compiled to WebAssembly, executed in a Web Worker with OffscreenCanvas so the UI stays responsive during a multi-second trace.&lt;/p&gt;

&lt;p&gt;That's not a purity thing, it's what makes the product viable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No upload&lt;/strong&gt; — files never leave the machine, which matters when the file is a client's unreleased logo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No rate limit&lt;/strong&gt; — there's no server cost to ration, so there's no reason to cap anyone&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works offline&lt;/strong&gt; after the page loads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No account&lt;/strong&gt;, because there's nothing to store&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One parameter tuning note, since it cost me real time to measure: &lt;code&gt;colorPrecision&lt;/code&gt; at 8 produced 3,590 paths / 1,577KB. At 6 it produced 2,856 paths / 1,139KB — &lt;strong&gt;28% smaller files and 32% faster&lt;/strong&gt;, with output I genuinely could not tell apart on logos. Default it lower than you think.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual lesson
&lt;/h2&gt;

&lt;p&gt;Shipping 65 things felt like 65 chances to win. It was really one diluted product with 65 half-maintained surfaces, no clear reason to return, and nothing anyone would describe to a friend.&lt;/p&gt;

&lt;p&gt;The surviving tool is here if you want to break it: &lt;strong&gt;&lt;a href="https://12pixa.com" rel="noopener noreferrer"&gt;https://12pixa.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Thin script fonts and scanned line drawings are where tracers fall apart — if you find a file it handles badly, I'd genuinely rather hear that than a compliment.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>webassembly</category>
      <category>javascript</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
