<?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: Usman Bashir</title>
    <description>The latest articles on DEV Community by Usman Bashir (@usman_basheers).</description>
    <link>https://dev.to/usman_basheers</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%2F4065175%2F505f679e-e978-4c9a-9070-6a534301d15f.png</url>
      <title>DEV Community: Usman Bashir</title>
      <link>https://dev.to/usman_basheers</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/usman_basheers"/>
    <language>en</language>
    <item>
      <title>How SVG-to-PNG Conversion Actually Works in the Browser</title>
      <dc:creator>Usman Bashir</dc:creator>
      <pubDate>Thu, 06 Aug 2026 07:55:03 +0000</pubDate>
      <link>https://dev.to/usman_basheers/how-svg-to-png-conversion-actually-works-in-the-browser-5307</link>
      <guid>https://dev.to/usman_basheers/how-svg-to-png-conversion-actually-works-in-the-browser-5307</guid>
      <description>&lt;p&gt;If you've ever tried to convert an SVG to a PNG entirely in the browser (no server round-trip, no ImageMagick, just JavaScript), you've probably hit a point where the output looked almost right and you couldn't figure out why it wasn't quite right. The mechanics are simple in outline and full of small traps in practice. This is a walkthrough of how the conversion actually works, and where it usually breaks.&lt;/p&gt;

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

&lt;p&gt;There's no native "SVG to PNG" API. The browser gives you two lower-level primitives and expects you to combine them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Load the SVG as an &lt;code&gt;Image&lt;/code&gt; object (or draw it via a data URL / object URL).&lt;/li&gt;
&lt;li&gt;Draw that image onto a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; element.&lt;/li&gt;
&lt;li&gt;Read the canvas back out as a PNG via &lt;code&gt;canvas.toBlob()&lt;/code&gt; or &lt;code&gt;canvas.toDataURL()&lt;/code&gt;.
&lt;/li&gt;
&lt;/ol&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;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;svgString&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="s2"&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="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;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="s2"&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;targetWidth&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;targetHeight&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="s2"&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="nx"&gt;targetWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetHeight&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="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="c1"&gt;// blob is your PNG&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole mechanism. Everything else in this post is about the ways this innocent-looking code produces a wrong-looking image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rasterization: canvas doesn't know about "vector"
&lt;/h2&gt;

&lt;p&gt;The moment &lt;code&gt;drawImage&lt;/code&gt; runs, the SVG stops being a vector graphic. Canvas is a bitmap surface: it rasterizes the image at whatever pixel dimensions the canvas has, right then, and every pixel is committed. There's no going back to redraw it sharper later without re-running the whole pipeline from the source SVG at a new size. This is why "export at 2x" isn't a checkbox on the PNG after the fact; it has to happen before rasterization, by drawing into a larger canvas from the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scale factors and devicePixelRatio
&lt;/h2&gt;

&lt;p&gt;This is the single most common cause of blurry exports. A CSS pixel is not a physical pixel. On a standard display, &lt;code&gt;devicePixelRatio&lt;/code&gt; is 1 and they match. On most modern laptop and phone screens, it's 2 or 3, meaning the OS is rendering 2-3 physical pixels for every logical CSS pixel to get a sharp image.&lt;/p&gt;

&lt;p&gt;If you size your canvas using the SVG's logical width and height directly, then display or save that canvas at "actual size," you'll get an export that looks soft on any high-density display, because you only rasterized at 1x worth of detail.&lt;/p&gt;

&lt;p&gt;The fix is to multiply the canvas dimensions by your target scale factor before drawing, then scale the drawing context to match:&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;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;devicePixelRatio&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="c1"&gt;// or a fixed export scale like 2 or 3&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;targetWidth&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="nx"&gt;targetHeight&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;scale&lt;/span&gt;&lt;span class="p"&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;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;targetWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetHeight&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that &lt;code&gt;devicePixelRatio&lt;/code&gt; reflects the &lt;em&gt;viewing&lt;/em&gt; device, not necessarily the scale you want for an &lt;em&gt;exported&lt;/em&gt; file. For export tooling, it's usually better to expose scale as an explicit user choice (1x / 2x / 3x, or a target pixel width) rather than silently inheriting whatever screen the export happened to run on.&lt;/p&gt;

&lt;h2&gt;
  
  
  viewBox vs width/height: which dimensions actually apply
&lt;/h2&gt;

&lt;p&gt;SVG has two, occasionally conflicting, ideas of "size":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;viewBox&lt;/code&gt; attribute defines the internal coordinate system: the drawing's own units.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;width&lt;/code&gt;/&lt;code&gt;height&lt;/code&gt; attributes (or equivalent CSS) define how large the SVG renders in the document.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When both are present, &lt;code&gt;viewBox&lt;/code&gt; sets the aspect ratio and coordinate space, and &lt;code&gt;width&lt;/code&gt;/&lt;code&gt;height&lt;/code&gt; set the actual rendered size: the SVG scales its contents to fit. When an SVG has a &lt;code&gt;viewBox&lt;/code&gt; but no explicit &lt;code&gt;width&lt;/code&gt;/&lt;code&gt;height&lt;/code&gt;, browsers typically fall back to a default intrinsic size (often 300×150), which is rarely what you want for an export. This is a common source of "why did my PNG come out as 300x150" bug reports: the source SVG had a &lt;code&gt;viewBox&lt;/code&gt; but nothing telling the rasterizer what pixel size to target, so something upstream picked the default.&lt;/p&gt;

&lt;p&gt;For predictable exports, it's worth explicitly setting the target width and height on the canvas yourself, calculated from the &lt;code&gt;viewBox&lt;/code&gt; aspect ratio, rather than trusting the SVG's own &lt;code&gt;width&lt;/code&gt;/&lt;code&gt;height&lt;/code&gt; attributes to be present or correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  External image references don't resolve
&lt;/h2&gt;

&lt;p&gt;An SVG can reference external raster images via &lt;code&gt;&amp;lt;image href="..."&amp;gt;&lt;/code&gt;. This works fine when the SVG renders directly in an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag or inline in the DOM, because the browser fetches the reference normally. It breaks in the canvas pipeline for a specific reason: once you draw an image containing external references onto a canvas, the canvas becomes "tainted" if any of those references were loaded cross-origin without proper CORS headers, and &lt;code&gt;toDataURL()&lt;/code&gt;/&lt;code&gt;toBlob()&lt;/code&gt; will throw a security error rather than silently failing.&lt;/p&gt;

&lt;p&gt;Even when CORS isn't the issue, external references add a dependency on network availability and load timing that a self-contained SVG doesn't have: if the referenced image hasn't finished loading when the SVG is rasterized, you get a blank spot where it should be. The most reliable fix for browser-side conversion is to inline external raster references as base64 data URIs before rasterizing, so the SVG carries everything it needs with no additional fetch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fonts: embedded vs linked
&lt;/h2&gt;

&lt;p&gt;Text in an SVG is the other common failure point. If an SVG uses &lt;code&gt;&amp;lt;text&amp;gt;&lt;/code&gt; with a font that isn't embedded in the SVG itself (just referenced by font-family name, or linked via an external stylesheet/&lt;code&gt;@font-face&lt;/code&gt;), the rasterizer falls back to whatever font is available on the system running the conversion, which may not match what the SVG's author intended, and will definitely not match if the conversion runs somewhere the font isn't installed at all (a different machine, a headless environment, etc.).&lt;/p&gt;

&lt;p&gt;The robust options are converting text to outlined paths before export (so there's no font dependency left at all), or embedding the font data directly in the SVG as a base64 &lt;code&gt;@font-face&lt;/code&gt; inside a &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; block. Linked fonts are the fragile option: they depend on the environment doing the rasterizing having network access and the same font available, which is exactly the kind of thing that works on your machine and breaks the moment the conversion runs somewhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;None of these problems are exotic. They're all consequences of the same basic fact: canvas rasterization is a one-shot, environment-dependent process, and an SVG can carry dependencies (external images, linked fonts, ambiguous dimensions) that don't survive that process cleanly. Handling them means: pick an explicit scale factor rather than trusting device defaults, resolve the &lt;code&gt;viewBox&lt;/code&gt;/&lt;code&gt;width&lt;/code&gt;/&lt;code&gt;height&lt;/code&gt; ambiguity explicitly rather than trusting fallbacks, inline external image references, and either outline or embed fonts before rasterizing.&lt;/p&gt;

&lt;p&gt;If you'd rather not wire this pipeline up by hand, SVG Lab's &lt;a href="https://svglab.app/svg-to-png" rel="noopener noreferrer"&gt;SVG-to-PNG tool&lt;/a&gt; handles the scale factor, dimension, and font-embedding cases above automatically, worth a look as a working example of the same pipeline described here, running entirely in the browser.&lt;/p&gt;

</description>
      <category>svg</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>css</category>
    </item>
  </channel>
</rss>
