<?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: sharefun2023</title>
    <description>The latest articles on DEV Community by sharefun2023 (@sharefun2023).</description>
    <link>https://dev.to/sharefun2023</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%2F4038465%2F0efe338c-5e60-4e6d-ae98-f674cfd4a893.png</url>
      <title>DEV Community: sharefun2023</title>
      <link>https://dev.to/sharefun2023</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sharefun2023"/>
    <language>en</language>
    <item>
      <title>Base64 decoding: padding, UTF-8, and the atob() trap</title>
      <dc:creator>sharefun2023</dc:creator>
      <pubDate>Mon, 21 Sep 2026 12:06:14 +0000</pubDate>
      <link>https://dev.to/sharefun2023/base64-decoding-padding-utf-8-and-the-atob-trap-11hf</link>
      <guid>https://dev.to/sharefun2023/base64-decoding-padding-utf-8-and-the-atob-trap-11hf</guid>
      <description>&lt;p&gt;Most Base64 bugs I have debugged were not crypto bugs. They were text-encoding bugs wearing a Base64 costume — or decoders being far more permissive than anyone assumed. Here are the five that keep showing up, each one measured rather than remembered.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;atob()&lt;/code&gt; does not return text
&lt;/h2&gt;

&lt;p&gt;Base64 encodes &lt;em&gt;bytes&lt;/em&gt;, not characters. &lt;code&gt;atob()&lt;/code&gt; returns a "binary string": one character per byte, char codes 0–255. Non-ASCII text (i.e. anything UTF-8) comes back as mojibake:&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="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;5paH5Lu2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// "æä»¶"  ← not what you meant&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is one extra step, and it is always the same step:&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;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// "文件"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Newer engines are getting a native helper (&lt;code&gt;Uint8Array.fromBase64()&lt;/code&gt;), but the split is real: Chrome (v153) has it and Node 24 does not — I checked both on this machine, side by side. Until every runtime you target agrees, &lt;code&gt;TextDecoder&lt;/code&gt; is the form that works everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Padding is structure, not payload
&lt;/h2&gt;

&lt;p&gt;Base64 packs 3 bytes into 4 characters. When the byte count is not divisible by 3, &lt;code&gt;=&lt;/code&gt; fills out the last group. Two useful consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browsers accept missing padding: &lt;code&gt;atob('aGVsbG8')&lt;/code&gt; returns &lt;code&gt;'hello'&lt;/code&gt; without complaint.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;length % 4 === 1&lt;/code&gt; is impossible. If you compute that, the string was truncated somewhere upstream — not "recovered" by adding &lt;code&gt;=&lt;/code&gt;. &lt;code&gt;atob('a')&lt;/code&gt; throws &lt;code&gt;InvalidCharacterError&lt;/code&gt;, which is the correct behaviour being politely demonstrated.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. URL-safe Base64 is a different alphabet
&lt;/h2&gt;

&lt;p&gt;JWT segments, S3 presigned URLs, anything passed through a query string: &lt;code&gt;+&lt;/code&gt; becomes &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;/&lt;/code&gt; becomes &lt;code&gt;_&lt;/code&gt;, and the &lt;code&gt;=&lt;/code&gt; padding is usually stripped. &lt;code&gt;atob()&lt;/code&gt; rejects those two characters outright:&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="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a-b_&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// InvalidCharacterError&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So convert the alphabet &lt;em&gt;before&lt;/em&gt; decoding, and re-add the padding:&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;b64urlToText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/-/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/_/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Node's decoder never complains, and that is the trap
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Buffer.from(s, 'base64')&lt;/code&gt; is deliberately lenient. It ignores characters outside the alphabet, it ignores truncation, and it never throws. Measured on Node 24:&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="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aGVs*bG8=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// 'hello'  ← the * was silently dropped&lt;/span&gt;
&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aGVsb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// 'hel'    ← truncated input, no error&lt;/span&gt;
&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a-b_&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// 'k\ufffd\ufffd' ← URL-safe chars, decoded as garbage&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which means "it decoded fine in Node" proves nothing at all: you can be handed a mangled string, get a plausible-looking result, and only find out much later. If the input can come from anywhere you do not control, validate before decoding:&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;A-Za-z0-9+&lt;/span&gt;&lt;span class="se"&gt;/]&lt;/span&gt;&lt;span class="sr"&gt;*=&lt;/span&gt;&lt;span class="se"&gt;{0,2}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not valid Base64&lt;/span&gt;&lt;span class="dl"&gt;'&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;h2&gt;
  
  
  5. Whitespace is a non-issue (PEM, MIME, emails)
&lt;/h2&gt;

&lt;p&gt;Both browser &lt;code&gt;atob()&lt;/code&gt; and Node strip ASCII whitespace, so line-wrapped PEM bodies and MIME blobs decode as-is: &lt;code&gt;atob('aGVs\n bG8=')&lt;/code&gt; → &lt;code&gt;'hello'&lt;/code&gt;. The classic failure is never whitespace — it is everything in section 4.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the "free online decoder" pattern actually leaks
&lt;/h2&gt;

&lt;p&gt;Look at what gets pasted into a Base64 decoder. It is rarely a toy string: it is a session cookie, a signed token, an auth header copied out of a log line. If it is a JWT, the payload is a live credential until it expires, and it has just been POSTed to someone's server. That is also why token decoders make an attractive target — their request logs are full of credentials that still work.&lt;/p&gt;

&lt;p&gt;Browser devtools will decode for you, but the fiddly cases (URL-safe input, invalid bytes, known-truncation, a hex view of the decoded bytes) are what made me keep a decoder open in a browser tab. Mine is at &lt;a href="https://23232322.xyz/base64-decode" rel="noopener noreferrer"&gt;23232322.xyz/base64-decode&lt;/a&gt; — client-side only, nothing uploaded, and it explains &lt;em&gt;why&lt;/em&gt; an input is rejected instead of just failing. The encoder pair is at &lt;a href="https://23232322.xyz/base64-encode" rel="noopener noreferrer"&gt;base64-encode&lt;/a&gt;, and if you are pasting actual tokens, the one that verifies signatures (HS256/384/512 secrets and RS/ES256–512 public keys) is the &lt;a href="https://23232322.xyz/tools/jwt-debugger" rel="noopener noreferrer"&gt;JWT decoder &amp;amp; debugger&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;æä»¶&lt;/code&gt; instead of real text&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;atob()&lt;/code&gt; returns bytes-as-chars&lt;/td&gt;
&lt;td&gt;&lt;code&gt;new TextDecoder('utf-8').decode(Uint8Array.from(atob(s), c =&amp;gt; c.charCodeAt(0)))&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;InvalidCharacterError&lt;/code&gt; on &lt;code&gt;-&lt;/code&gt; / &lt;code&gt;_&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;URL-safe alphabet&lt;/td&gt;
&lt;td&gt;swap &lt;code&gt;-_&lt;/code&gt; → &lt;code&gt;+/&lt;/code&gt;, re-pad&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Throws on a 5-char segment&lt;/td&gt;
&lt;td&gt;&lt;code&gt;length % 4 === 1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;input is truncated; fix upstream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node decodes garbage without error&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Buffer.from&lt;/code&gt; silently drops junk&lt;/td&gt;
&lt;td&gt;regex-validate first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Line-wrapped PEM block&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;nothing to do, whitespace is ignored&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>base64</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>debugging</category>
    </item>
    <item>
      <title>I Counted How Many Free Dev Tools Quietly Upload Your Data. Then I Built 80 That Don't.</title>
      <dc:creator>sharefun2023</dc:creator>
      <pubDate>Sun, 13 Sep 2026 12:05:30 +0000</pubDate>
      <link>https://dev.to/sharefun2023/i-counted-how-many-free-dev-tools-quietly-upload-your-data-then-i-built-80-that-dont-d89</link>
      <guid>https://dev.to/sharefun2023/i-counted-how-many-free-dev-tools-quietly-upload-your-data-then-i-built-80-that-dont-d89</guid>
      <description>&lt;p&gt;Paste a JWT into a "free online JWT debugger." Paste a config file into a "free JSON formatter."&lt;/p&gt;

&lt;p&gt;Then open DevTools, hit the Network tab, and watch it POST your payload to &lt;code&gt;api.something-saas.com/v1/parse&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I got annoyed enough to check. Of the first dozen "free" tools I tried, &lt;strong&gt;eight&lt;/strong&gt; shipped data off the device. Some had a privacy policy. Some had a privacy policy that said "we may share data with partners." Your production tokens, in a partner's S3 bucket.&lt;/p&gt;

&lt;p&gt;Most of them weren't even doing it to be evil. They just built a Next.js app, needed the logic somewhere, and the server was already there. Formatter on the server. Base64 decode on the server. For a 3-line &lt;code&gt;JSON.stringify&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that actually costs you
&lt;/h2&gt;

&lt;p&gt;It's not just privacy theater. Server-side tooling has real costs you pay every day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency.&lt;/strong&gt; Round-trip to a region that isn't yours, on a connection that isn't yours. My regex tester took ~400ms to say "no match."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits and paywalls.&lt;/strong&gt; Free tier hits you at 10 operations. The tool that was "free" is now a $9/mo subscription for &lt;code&gt;btoa&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability.&lt;/strong&gt; Their server goes down, your tool goes down. Great during an incident, when you actually need to decode something.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline.&lt;/strong&gt; On a plane, on a locked-down corporate network, the tool is gone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those problems disappears if the code runs in the browser tab you already have open.&lt;/p&gt;

&lt;h2&gt;
  
  
  What client-side actually means
&lt;/h2&gt;

&lt;p&gt;There is no API. There is no fetch to a backend. &lt;code&gt;Network&lt;/code&gt; stays empty except for the static assets. Concretely:&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="c1"&gt;// Base64, no server. 2 lines and no data leaves the tab.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&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;decode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// JWT verify with a real signature check, in-page.&lt;/span&gt;
&lt;span class="c1"&gt;// WebCrypto handles HS256/384/512 and RS/ES via importKey.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;importKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;raw&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HMAC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verify&lt;/span&gt;&lt;span class="dl"&gt;"&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;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/-/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/_/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&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="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HMAC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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's the whole "backend." The &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; is the API.&lt;/p&gt;

&lt;p&gt;The interesting cases are where people assume you &lt;em&gt;need&lt;/em&gt; a server:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Why people think it needs a server&lt;/th&gt;
&lt;th&gt;How it's actually done client-side&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SQL formatting&lt;/td&gt;
&lt;td&gt;"Parsing is hard"&lt;/td&gt;
&lt;td&gt;Tokenizer + formatter in a Web Worker — keeps the main thread at 60fps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image conversion&lt;/td&gt;
&lt;td&gt;"Encoding is server work"&lt;/td&gt;
&lt;td&gt;Canvas + &lt;code&gt;toBlob('image/webp')&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SVG to PNG&lt;/td&gt;
&lt;td&gt;"Rasterization needs a service"&lt;/td&gt;
&lt;td&gt;Draw the SVG into a canvas via a data-URI image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regex testing&lt;/td&gt;
&lt;td&gt;"Sandboxing"&lt;/td&gt;
&lt;td&gt;It's a regex. &lt;code&gt;RegExp&lt;/code&gt; has been in the engine forever&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hashing (SHA/MD5)&lt;/td&gt;
&lt;td&gt;"Performance"&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;crypto.subtle&lt;/code&gt;, native speed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;WebCrypto also gives you something the SaaS tools don't: &lt;strong&gt;the key never crosses the wire.&lt;/strong&gt; A server-side JWT verifier has to receive your signing secret to check a signature. That's a much bigger deal than the payload leak.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://23232322.xyz" rel="noopener noreferrer"&gt;23232322.xyz&lt;/a&gt; — 80+ tools, one page each, no build step. JSON formatter and validator, Base64 encode/decode, regex tester, JWT debugger with real HS256/384/512 and RS/ES256-512 verification, hash generators, UUID, URL encode/decode, diff checker, HTML minifier, and a bunch more.&lt;/p&gt;

&lt;p&gt;No account. No upload. No analytics on your input. Source for each page is plain HTML + vanilla JS, so you can read it and confirm nothing is being sent anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check any tool yourself in 20 seconds
&lt;/h2&gt;

&lt;p&gt;Before you trust any "free online X":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open DevTools → &lt;strong&gt;Network&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Filter to &lt;strong&gt;Fetch/XHR&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Use the tool with obvious dummy data (&lt;code&gt;{"test":1}&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;If a request appears with your input in the body — that data left your machine.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do that to my tools too. It's the point.&lt;/p&gt;

&lt;p&gt;If a tool can't be written this way, it's because it needs a real backend. Most of them don't. They just needed somewhere to put the code.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The whole set is at &lt;a href="https://23232322.xyz" rel="noopener noreferrer"&gt;23232322.xyz&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>privacy</category>
      <category>tools</category>
    </item>
    <item>
      <title>Why Your SVG-to-PNG Conversion Looks Wrong (And How to Fix It in the Browser)</title>
      <dc:creator>sharefun2023</dc:creator>
      <pubDate>Thu, 10 Sep 2026 12:34:26 +0000</pubDate>
      <link>https://dev.to/sharefun2023/why-your-svg-to-png-conversion-looks-wrong-and-how-to-fix-it-in-the-browser-1k8n</link>
      <guid>https://dev.to/sharefun2023/why-your-svg-to-png-conversion-looks-wrong-and-how-to-fix-it-in-the-browser-1k8n</guid>
      <description>&lt;h1&gt;
  
  
  Why Your SVG-to-PNG Conversion Looks Wrong (And How to Fix It in the Browser)
&lt;/h1&gt;

&lt;p&gt;You export an icon to PNG and it comes back with the wrong font, a black background instead of transparency, or soft, blurry edges. Nine times out of ten the bug is not in your export code — it's in the assumption that a browser rasterizes SVG the same way a design tool does.&lt;/p&gt;

&lt;p&gt;Converting SVG to PNG in the browser is really just three steps: turn the markup into an image, draw that image onto a canvas, and read the canvas back as a PNG. The canvas is doing all the work. Everything that looks wrong comes from what the browser &lt;em&gt;refuses&lt;/em&gt; to load while doing it.&lt;/p&gt;

&lt;p&gt;Here is the full pipeline plus the seven failure modes I hit while building a client-side converter with no server, no uploads, and no dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;svgToPng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;svgText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scale&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="nx"&gt;background&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Parse and normalise the SVG&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DOMParser&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;parseFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;svgText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/svg+xml&lt;/span&gt;&lt;span class="dl"&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;svg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;parsererror&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid SVG&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Resolve the intrinsic size&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;viewBox&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;[,&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vb&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0 0 300 150&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\s&lt;/span&gt;&lt;span class="sr"&gt;,&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;width&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;height&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Force explicit pixel dimensions (see pitfall 3)&lt;/span&gt;
  &lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;width&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;height&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xmlns&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://www.w3.org/2000/svg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Serialise to a blob URL the &amp;lt;img&amp;gt; can load&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;XMLSerializer&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;serializeToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/svg+xml;charset=utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&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;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. Draw at scale for a crisp result&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&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;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;background&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;background&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillRect&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 5. Export&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBlob&lt;/span&gt;&lt;span class="p"&gt;(&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="o"&gt;=&amp;gt;&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="nf"&gt;resolve&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="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Canvas is tainted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revokeObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SVG failed to rasterise&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;;&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;Roughly 40 lines, and it works — for a subset of SVG. The rest of this post is about that subset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 1: external resources are silently blocked
&lt;/h2&gt;

&lt;p&gt;This is the big one. When an SVG is loaded through an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;, the browser treats it as an &lt;strong&gt;image&lt;/strong&gt;, not a document. Images do not get to fetch subresources. So these all fail silently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;image href="logo.png"&amp;gt;&lt;/code&gt; pointing at a file or a URL&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@font-face&lt;/code&gt; rules inside a &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; block&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@import&lt;/code&gt; of a stylesheet&lt;/li&gt;
&lt;li&gt;anything behind &lt;code&gt;url(#…)&lt;/code&gt; that resolves to a network request&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no error. No console warning. The element just renders as nothing, and you get a PNG with a hole in it. The fix is to inline everything first — convert referenced images to &lt;code&gt;data:&lt;/code&gt; URIs, and turn text into paths before export if the font matters (see next pitfall).&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 2: fonts fall back to whatever is installed
&lt;/h2&gt;

&lt;p&gt;Same root cause, different symptom. Your SVG says &lt;code&gt;font-family: "Inter"&lt;/code&gt;, the browser rasterizing it has never heard of Inter, and you get Helvetica or DejaVu instead. Text reflows, line breaks move, and a logo turns into something slightly wrong that nobody can name.&lt;/p&gt;

&lt;p&gt;Two reliable options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Convert text to paths in the source file.&lt;/strong&gt; The output is then font-independent everywhere, forever. This is what &lt;code&gt;text-to-path&lt;/code&gt; / &lt;code&gt;object-to-path&lt;/code&gt; does in your editor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use only fonts you can guarantee&lt;/strong&gt; — generic families (&lt;code&gt;sans-serif&lt;/code&gt;, &lt;code&gt;serif&lt;/code&gt;, &lt;code&gt;monospace&lt;/code&gt;) or system stacks. They render, just never the exact file you designed with.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you keep live text, expect the browser's metrics, not your design tool's metrics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 3: no width/height, no pixels
&lt;/h2&gt;

&lt;p&gt;An SVG with only a &lt;code&gt;viewBox&lt;/code&gt; has no intrinsic size:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's perfectly valid SVG, and Chrome will happily display it when sized by CSS. But when you load the same markup into an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;, several browsers report &lt;code&gt;naturalWidth&lt;/code&gt; as &lt;code&gt;0&lt;/code&gt; — and &lt;code&gt;drawImage&lt;/code&gt; with a zero-sized source throws or draws nothing. The export "succeeds" and produces an empty or 1×1 PNG.&lt;/p&gt;

&lt;p&gt;Fix: always set explicit &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; attributes on the root &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt; before serialising, derived from the &lt;code&gt;viewBox&lt;/code&gt;. That's the &lt;code&gt;svg.setAttribute('width', w)&lt;/code&gt; line above, and it's the single most common cause of "my converter only works on some files."&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 4: blurry output on retina
&lt;/h2&gt;

&lt;p&gt;A canvas has two sizes: its CSS size and its backing-store size. Only the backing store defines how many real pixels you get. If you set the canvas to 300×300 and draw a 300×300 icon into it, you get a 300×300 PNG — which is correct, and looks soft on a 2× display because there simply aren't enough pixels.&lt;/p&gt;

&lt;p&gt;Set the backing store bigger and scale the drawing:&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="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// scale = 2 for 2x&lt;/span&gt;
&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because SVG is resolution-independent, you can go as high as you like — &lt;code&gt;scale = 4&lt;/code&gt; on a 24px icon gives you 96×96 real pixels. Just remember the dimensions you multiply here are the &lt;em&gt;output&lt;/em&gt; pixel dimensions; a 4000px export of a complex icon is a 4000px PNG, not a free lunch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 5: "transparent" isn't
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;canvas&lt;/code&gt; starts fully transparent, so &lt;code&gt;toBlob('image/png')&lt;/code&gt; gives you real alpha by default. The surprise is the other direction: designers export SVGs with a white &lt;code&gt;&amp;lt;rect&amp;gt;&lt;/code&gt; covering the artboard, so your "transparent" PNG arrives with a white box painted over the background. Nothing to fix in code — it's in the source file.&lt;/p&gt;

&lt;p&gt;If you &lt;em&gt;do&lt;/em&gt; want a flat background (say for a JPG or for email), paint it before drawing, since &lt;code&gt;drawImage&lt;/code&gt; composites rather than replaces:&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;background&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;background&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillRect&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pitfall 6: filters and &lt;code&gt;foreignObject&lt;/code&gt; are renderer-specific
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;feGaussianBlur&lt;/code&gt;, &lt;code&gt;feTurbulence&lt;/code&gt;, &lt;code&gt;mix-blend-mode&lt;/code&gt;, and &lt;code&gt;&amp;lt;foreignObject&amp;gt;&lt;/code&gt; are the parts of SVG that every engine implements a little differently. A blur that looks right in Chrome can look banded in Firefox and glossier in Safari. &lt;code&gt;&amp;lt;foreignObject&amp;gt;&lt;/code&gt; — the trick for rendering HTML inside SVG — is the least portable feature in the spec, and it's also the fastest way to get a canvas that won't export (next pitfall).&lt;/p&gt;

&lt;p&gt;If your target is a PNG that looks identical for everyone, flatten these effects in the source file or accept per-browser drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 7: the tainted canvas
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;canvas.toBlob()&lt;/code&gt; throws &lt;code&gt;SecurityError&lt;/code&gt; when the canvas is "tainted" — meaning something cross-origin was drawn into it. With pure SVG-as-image this is mostly moot because external resources are blocked anyway, but it comes straight back when you compose a design: draw a user's cross-origin photo onto the canvas, then try to export, and the export dies even though the pixels are right there on screen.&lt;/p&gt;

&lt;p&gt;Two ways out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Serve the images with permissive CORS headers and set &lt;code&gt;img.crossOrigin = 'anonymous'&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; setting &lt;code&gt;src&lt;/code&gt;. (Order matters; setting it after triggers a second, uncached request or an outright failure.)&lt;/li&gt;
&lt;li&gt;Do the compositing server-side, which is exactly the dependency a client-side tool is trying to avoid.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is also why the classic "paste a URL and convert" trick fails: the remote SVG has to be fetched with CORS enabled, or the browser blocklists it before a single pixel is drawn.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;p&gt;Before you blame your code, check the source file:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Everything inlined — images as &lt;code&gt;data:&lt;/code&gt; URIs, fonts as paths or generic families.&lt;/li&gt;
&lt;li&gt;Explicit &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; on the root &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt;, matching the &lt;code&gt;viewBox&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;xmlns="http://www.w3.org/2000/svg"&lt;/code&gt; present after serialisation.&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;currentColor&lt;/code&gt; and no &lt;code&gt;var(--…)&lt;/code&gt; — outside a page context there is nothing to inherit from, so they resolve to defaults (usually black).&lt;/li&gt;
&lt;li&gt;No page CSS assumptions — styles must live inside the SVG's own &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; or as attributes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And in your code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Backing store scaled up for retina, not just CSS size.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;URL.revokeObjectURL&lt;/code&gt; in a &lt;code&gt;finally&lt;/code&gt; block, or you leak the blob on every conversion.&lt;/li&gt;
&lt;li&gt;Paint a background &lt;em&gt;before&lt;/em&gt; &lt;code&gt;drawImage&lt;/code&gt; if the format has no alpha channel.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  I built one of these
&lt;/h2&gt;

&lt;p&gt;These are the exact edge cases I hit while building &lt;strong&gt;&lt;a href="https://imgloft.com" rel="noopener noreferrer"&gt;imgloft.com&lt;/a&gt;&lt;/strong&gt; — a free browser-side image toolset for SVG→PNG, PNG compression, resizing and cropping. It does the rasterisation on your device with the pipeline above, so nothing is uploaded and there's no queue to wait in. If you just want to convert a file rather than debug one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://imgloft.com/svg-to-png" rel="noopener noreferrer"&gt;SVG to PNG converter&lt;/a&gt;&lt;/strong&gt; — scale 1×–4×, transparent or flat background&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://imgloft.com/free-online-svg-to-png-converter" rel="noopener noreferrer"&gt;The longer guide version of this post&lt;/a&gt;&lt;/strong&gt; — including a comparison table of browser-side vs server-side conversion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've hit a rasterisation bug that isn't on this list, I want to hear it — drop it in the comments and I'll reproduce it.&lt;/p&gt;

</description>
      <category>svg</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How I Built a Free SVG to PNG Converter in 1 Hour</title>
      <dc:creator>sharefun2023</dc:creator>
      <pubDate>Tue, 04 Aug 2026 08:05:54 +0000</pubDate>
      <link>https://dev.to/sharefun2023/how-i-built-a-free-svg-to-png-converter-in-1-hour-4ci9</link>
      <guid>https://dev.to/sharefun2023/how-i-built-a-free-svg-to-png-converter-in-1-hour-4ci9</guid>
      <description>&lt;p&gt;I needed a quick way to convert SVG icons to PNG for a project. Every online converter I found required uploading files to a server — slow, privacy-invasive, and overkill for a 2KB icon.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;a href="https://imgloft.com" rel="noopener noreferrer"&gt;imgloft.com&lt;/a&gt;&lt;/strong&gt; — a free, client-side SVG to PNG converter that works entirely in your browser. No uploads, no server, 100% private.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Most online SVG-to-PNG tools follow the same pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Upload your SVG to their server&lt;/li&gt;
&lt;li&gt;Server runs a conversion library (ImageMagick, librsvg, etc.)&lt;/li&gt;
&lt;li&gt;Download the result&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is fine for occasional use, but it breaks down when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're working with sensitive design assets&lt;/li&gt;
&lt;li&gt;The file is large and upload takes forever&lt;/li&gt;
&lt;li&gt;You're offline or behind a restrictive firewall&lt;/li&gt;
&lt;li&gt;You just want to convert a tiny icon without waiting&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The 1-Hour Solution
&lt;/h2&gt;

&lt;p&gt;The entire converter is built with three browser APIs:&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="c1"&gt;// 1. Read the SVG file&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FileReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;svgText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Draw it on a canvas at the desired resolution&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Image&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;svgBlob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;svgText&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/svg+xml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;svgBlob&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&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;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Export as PNG&lt;/span&gt;
    &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBlob&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// Trigger download&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readAsText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Three APIs, zero dependencies, zero server cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Only Took 1 Hour
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No backend.&lt;/strong&gt; The entire tool runs in the browser using the Canvas API. No Express, no ImageMagick, no Docker container. Just a static HTML file served from Cloudflare Pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No auth.&lt;/strong&gt; Users don't need accounts — they come, convert, and leave. No database, no user management, no session tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No framework.&lt;/strong&gt; Plain HTML, vanilla JavaScript, and a few lines of CSS. No React, no build step, no node_modules.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Client-side is underrated.&lt;/strong&gt; The browser can do way more than we give it credit for. SVG rendering, image encoding, even basic image manipulation — all built in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ship the MVP.&lt;/strong&gt; I could have spent weeks adding batch conversion, format options, color filters. Instead I shipped the core feature in an afternoon and let users tell me what they need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free tools attract real users.&lt;/strong&gt; Within days of going live, Google started indexing the site. People are searching for "svg to png converter" — 40,000 times a month. The demand is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://imgloft.com" rel="noopener noreferrer"&gt;imgloft.com&lt;/a&gt;&lt;/strong&gt; — drag any SVG, get a PNG instantly. Also includes compress, resize, and crop tools.&lt;/p&gt;

&lt;p&gt;If you're thinking about building a micro-tool site, just do it. Ship the simplest version first. The browser is your backend.&lt;/p&gt;

</description>
      <category>svg</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Building a Client-Side SQL Formatter with Vanilla JS and Web Workers</title>
      <dc:creator>sharefun2023</dc:creator>
      <pubDate>Mon, 20 Jul 2026 15:35:46 +0000</pubDate>
      <link>https://dev.to/sharefun2023/building-a-client-side-sql-formatter-with-vanilla-js-and-web-workers-888</link>
      <guid>https://dev.to/sharefun2023/building-a-client-side-sql-formatter-with-vanilla-js-and-web-workers-888</guid>
      <description>&lt;p&gt;Most SQL formatters process queries server-side. That means every time you click "Format", your SQL gets sent to a random backend — with no guarantee of what happens to it.&lt;/p&gt;

&lt;p&gt;I wanted something different: &lt;strong&gt;a formatter where your data never leaves your browser.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://sqlformat.io" rel="noopener noreferrer"&gt;sqlformat.io&lt;/a&gt;&lt;/strong&gt; is built with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vanilla JavaScript&lt;/strong&gt; — no framework, no build step&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/sql-formatter-org/sql-formatter" rel="noopener noreferrer"&gt;sql-formatter&lt;/a&gt;&lt;/strong&gt; — the formatting engine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web Workers&lt;/strong&gt; — formatting runs off the main thread so the UI stays snappy even on 1000+ line queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Pages&lt;/strong&gt; — zero-cost hosting with global edge caching&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User pastes SQL
  ↓
Main thread sends to Web Worker
  ↓
Worker calls sql-formatter with dialect-specific options
  ↓
Formatted SQL sent back to main thread
  ↓
Rendered with syntax highlighting (no library needed — just regex + &amp;lt;span&amp;gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; the sql-formatter library alone is ~50KB gzipped. Running it in a Web Worker means the main thread never blocks, and the user can keep typing while large queries format in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why No Server?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy&lt;/strong&gt; — zero bytes leave the user browser&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline&lt;/strong&gt; — works after the page loads, no internet needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale&lt;/strong&gt; — no server costs, no rate limits, no API keys&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt; — no round trip, instant formatting&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Web Workers are underrated&lt;/strong&gt; — they turn a potentially laggy text processor into a buttery-smooth UX&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL dialects are deceptively complex&lt;/strong&gt; — MySQL vs PostgreSQL vs BigQuery all have different quoting rules, and the formatter needs to handle inline comments and dollar-quoted strings that span multiple lines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regex-based syntax highlighting works surprisingly well&lt;/strong&gt; for SQL — you do not need a parser, just a few well-placed regex patterns for keywords, strings, numbers, and comments&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Try it: &lt;a href="https://sqlformat.io" rel="noopener noreferrer"&gt;sqlformat.io&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Source: &lt;a href="https://github.com/sharefun2023/sqlformat.io" rel="noopener noreferrer"&gt;github.com/sharefun2023/sqlformat.io&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Got Tired of Slow SQL Formatters That Upload My Queries — So I Built a Free One</title>
      <dc:creator>sharefun2023</dc:creator>
      <pubDate>Mon, 20 Jul 2026 15:35:08 +0000</pubDate>
      <link>https://dev.to/sharefun2023/i-got-tired-of-slow-sql-formatters-that-upload-my-queries-so-i-built-a-free-one-129o</link>
      <guid>https://dev.to/sharefun2023/i-got-tired-of-slow-sql-formatters-that-upload-my-queries-so-i-built-a-free-one-129o</guid>
      <description>&lt;p&gt;I write a lot of SQL every day. And every online formatter I tried had the same problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Uploads queries to their server&lt;/strong&gt; — privacy nightmare when you are working with production schema&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiny text area&lt;/strong&gt; — cannot handle 500+ line queries without lag&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3 ads&lt;/strong&gt; before you even see the formatted output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;a href="https://sqlformat.io" rel="noopener noreferrer"&gt;sqlformat.io&lt;/a&gt;&lt;/strong&gt; — a no-BS SQL formatter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client-side only&lt;/strong&gt; — everything runs in YOUR browser via Web Worker. Zero bytes leave your device&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;15+ SQL dialects&lt;/strong&gt; — MySQL, PostgreSQL, SQLite, BigQuery, Snowflake, T-SQL, and more&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No signup, no ads, no upload&lt;/strong&gt; — ever&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instant&lt;/strong&gt; — paste, click Format, done&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I Built It
&lt;/h2&gt;

&lt;p&gt;I got tired of copy-pasting production queries into random websites and wondering where they ended up. With sqlformat.io, you can disconnect your internet after the page loads and keep formatting offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extra Tools Included
&lt;/h2&gt;

&lt;p&gt;It is not just a formatter — the site has several free SQL tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Diff&lt;/strong&gt; — compare two SQL queries side by side&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minify/Compress&lt;/strong&gt; — strip whitespace for embedding in code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate&lt;/strong&gt; — syntax check across dialects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ER Diagram&lt;/strong&gt; — visualize table relationships&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL to JSON&lt;/strong&gt; — convert query results&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Generator&lt;/strong&gt; — generate test data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explain Visualizer&lt;/strong&gt; — MySQL/PostgreSQL EXPLAIN output as a tree&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;Vanilla JS + &lt;a href="https://github.com/sql-formatter-org/sql-formatter" rel="noopener noreferrer"&gt;sql-formatter&lt;/a&gt; library, hosted on Cloudflare Pages. All formatting runs in a Web Worker so the UI stays responsive even on huge queries.&lt;/p&gt;




&lt;p&gt;Open source: &lt;a href="https://github.com/sharefun2023/sqlformat.io" rel="noopener noreferrer"&gt;github.com/sharefun2023/sqlformat.io&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Would love feedback from folks who format SQL daily!&lt;/p&gt;

</description>
      <category>sql</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>privacy</category>
    </item>
  </channel>
</rss>
