How SVG-to-PNG Conversion Actually Works in the Browser
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. The basic pipeline There's no native "SVG to PNG" API. The browser gives you two lower-level primitives and expects you to combine them: Load the SVG as an Image object (or draw it via a data URL / object URL). Draw that image onto a element. Read the canvas back out as a PNG via canvas.toBlob() or canvas.toDataURL(). const img = new Image(); const svgBlob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" }); const url = URL.createObjectURL(svgBlob); img.onload = () => { const canvas = document.createElement("canvas"); canvas.width = targetWidth; canvas.height = targetHeight; const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, targetWidth, targetHeight); canvas.toBlob((blob) => { // blob is your PNG URL.revokeObjectURL(url); }, "image/png"); }; img.src = url; That's the whole mechanism. Everything else in this post is about the ways this innocent-looking code produces a wrong-looking image. Rasterization: canvas doesn't know about "vector" The moment drawImage 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. Scale factors and devicePixelRatio This is the single most common cause of blurry exports. A CSS pixel is not a physical pixel. On a standard display, devicePixelRatio 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. 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. The fix is to multiply the canvas dimensions by your target scale factor before drawing, then scale the drawing context to match: const scale = window.devicePixelRatio || 2; // or a fixed export scale like 2 or 3 canvas.width = targetWidth * scale; canvas.height = targetHeight * scale; ctx.scale(scale, scale); ctx.drawImage(img, 0, 0, targetWidth, targetHeight); Note that devicePixelRatio reflects the viewing device, not necessarily the scale you want for an exported 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. viewBox vs width/height: which dimensions actually apply SVG has two, occasionally conflicting, ideas of "size": The viewBox attribute defines the internal coordinate system: the drawing's own units. The width/height attributes (or equivalent CSS) define how large the SVG renders in the document. When both are present, viewBox sets the aspect ratio and coordinate space, and width/height set the actual rendered size: the SVG scales its contents to fit. When an SVG has a viewBox but no explicit width/height, 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 viewBox but nothing telling the rasterizer what pixel size to target, so something upstream picked the default. For predictable exports, it's worth explicitly setting the target width and height on the canvas yourself, calculated from the viewBox aspect ratio, rather than trusting the SVG's own width/height attributes to be present or correct. External image references don't resolve An SVG can reference external raster images via . This works fine when the SVG renders directly in an 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 toDataURL()/toBlob() will throw a security error rather than silently failing. 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. Fonts: embedded vs linked Text in an SVG is the other common failure point. If an SVG uses with a font that isn't embedded in the SVG itself (just referenced by font-family name, or linked via an external stylesheet/@font-face), 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.). 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 @font-face inside a 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. Putting it together 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 viewBox/width/height ambiguity explicitly rather than trusting fallbacks, inline external image references, and either outline or embed fonts before rasterizing. If you'd rather not wire this pipeline up by hand, SVG Lab's SVG-to-PNG tool 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.
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to