Notes › Tech Notes › Linked Images Vanish When SVG Turns to PNG: A 594 Byte File Shows 3 Pictures in Preview and 1 in the Export

Linked Images Vanish When SVG Turns to PNG: A 594 Byte File Shows 3 Pictures in Preview and 1 in the Export

One 594-byte SVG keeps all 3 linked images in preview, yet its exported PNG holds only the embedded one: preview and export are two loading paths with different ink. Also measured: the size boxes versus viewBox, the silent empty payload past the canvas limit, and the width gap across 12 font families.

Preview Shows 3 Images, the Export Keeps One

It starts from one file, sv_image_rel.svg: 594 bytes holding 3 <image> elements. Two pull the same 64 x 64 tile in through a relative path; the third is inline as a data: URI. All three are drawn in the preview pane, where the probe logs 3 load events. The PNG that comes back weighs 2,576 bytes, and the region where the linked tiles belong measures px=0 of ink; only the inline element survives, worth 4,096 px. The file did not change in between: the page reads it through two mechanisms that were never the same.

Inline DOM Versus Blob, Image and drawImage

On the SVG Converter page, "load" and "convert and download" are two chains with no shared segment. The preview chain (lines 121-168 of the template) hands the string to DOMParser, takes the first <svg> and places it inline in the document, building no Image object and no Blob URL, so an external reference is fetched like any other resource of an ordinary page. The export chain (lines 200-255) is a pipeline instead: string, Blob, objectURL, new Image, and only once that image fires onload does drawImage copy it into a canvas. Decoding the same markup as an image puts it in an opaque origin, where internal references follow a second set of rules and the canvas keeps only what that resolution produced.

Four Internal Dependencies, Two Sets of Numbers

A cross-file <use> is the cleanest case: the 249 byte sample fires load on the DOM side, so the preview shows the referenced shape, while the raster side returns px=0 for the whole frame. An external @import fails differently but splits the same way: the 284 byte sample carries a rule written for a 64px text, the rule never reaches the rasterized document, and the computed value falls back to -apple-system at 14px. Text length measures 85.83 there against 475.06 once the identical rule is inlined in a 332 byte file, because the fallback face is smaller and narrower. The export matches: the linked version writes out 4,972 bytes whose ink is only 692 px.

An external @font-face shows the split without changing a letter. The 322 byte sample and a file embedding the same face as a data: URI, 612,547 bytes of it, come out the same width in the DOM column: both measure 400.55, so the previews are indistinguishable. In the raster column they are not one drawing: one ends at px=5401 with a bbox of 315 x 49, the other at px=4855 with a bbox of 314 x 43, because only one of the two resolves its font through the document.

The fourth gives no warning: the 256 byte sample paints a rect with fill:url(...) at an external image, the DOM keeps that url verbatim as the computed fill, and previewing shows nothing. On the raster side the whole canvas measures px=0, the export hands back a 2,544 byte picture with nothing in it, and the toast still reports success.

None of this supports the sentence "external references are measured as unsupported". This Chrome was launched with --allow-file-access-from-files, and the two samples rewritten to absolute file:/// and https:// targets were rasterized after all: ink px=3327, the same 8,075 byte PNG twice. What they support is narrower: preview and export draw different ink from one and the same file, the headless setup never triggered a block here, and this does not extrapolate to the live site.

Four dependency types: DOM column vs rasterDOM sideRaster sideLinked image (594 B)3 loadspx=0 · 2576 BLinked @import85.83 vs 475.06692 px · 4972 BLinked @font-faceboth read 400.555401 vs 4855 pxExternal image as fillrect 221x221px=0 · 2544 Bsame machine, same code, different ink

Figure 1: the four dependencies read on both paths.

Two Size Boxes, Overwritten by viewBox as a Pair

convert-out-w and convert-out-h hold canvas pixels, and they meet the SVG's own geometry at one moment only, the step reading the "original size". The rule there is paired rather than per field. If a viewBox exists and splits into 4 numbers while either the width or the height is missing from the root, both boxes are overwritten together by its third and fourth numbers. If both are present, viewBox takes no part, and the export path reads only the boxes. max="10000" is a plain HTML attribute the code never reads, which is why the arithmetic walks past it: with keep-ratio on, typing 10000 into the height makes the tool back-calculate a width of 13333 into the other box, and a probe writing 100000 reads back unchanged with the error field still null.

svg root markup and box readings (parsed on lines 131-141)
Written on the svg rootBoth boxes readWhich side wins
viewBox="0 0 320 240" only320 x 240viewBox fills a missing side
width="100" plus viewBox320 x 240the typed 100 drops with its pair
width="320" height="240" plus viewBox="0 0 100 50"320 x 240both present, viewBox loses

Rounding splits the same way: origin and the keep-ratio back-calculation use Math.round, the export reads the box values with parseInt, so in one and the same box "333.7" truncates to 333 and "1e3" collapses to 1. A field written as 0 or left empty counts as falsy and falls back to origin.

Past the Canvas Limit, a Silent Empty Payload

Asking for 16385 x 16385, or 20000 x 20000, raises no error and produces no picture. The data URL is down to 6 characters, the payload is literally "data:,", and the toast still announces "Converted to PNG (20000x20000) and started downloading". So the byte count is the only check left. Inside the limit the bytes are real: the 320 x 240 reference image exports 35,990 bytes, 6000 x 6000 gives 697,011, 10000 x 10000 gives 1,889,260, all opening with the PNG magic number 89 50 4e 47 0d 0a 1a 0a.

The Width Account: the Raster Clipped to 320

Text width is the third axis of disagreement. The string "VECTOR-0123" was measured at 64 against the 12 font-family values on this machine, and DOM text length runs from 352.00 (monospace and SimSun) to 484.97 (Arial Black), a spread of 132.97 units over the same eleven characters. A name that does not exist warns nobody: it falls through to inheritance, landing on the 392.44 that Segoe UI measures. Two further steps hide it in preview. The rect in the DOM is multiplied by the preview's own zoom, so an over-wide run still fits the pane; a canvas 320 wide clips every raster run to 320 or less.

DOM text length at 64px across 12 font-family valuesArial Black484.97Arial429.22Microsoft YaHei426.63Courier New422.47Times New Roman404.16Segoe UI392.44NoSuchFace 9000392.44Consolas387.07monospace352.00SimSun352.00Dashed line = the 320 ceiling of the raster canvas

Figure 2: family widths against the 320 ceiling of the canvas.

One File at a Time, and object URL Reclaim

There is no batch entry here. fileInputMultiple is false, and both the drop handler and the change handler (lines 104 and 107) take only files[0]: a second file is never read. What is created is released cleanly, and a full 13-step run records object URL made=11 and revoked=11, one created and one revoked per export. How large to convert and which format to keep is a separate account, settled in the piece on SVG to PNG export size and format choice.

Common Questions

Q: Preview and export disagree, what do I check first?

Check every reference that points outside the file: an <image> with a relative path, a cross-file <use>, an external @import or @font-face, and fill:url(...) as a paint source. All four live in the DOM, and the rasterizing chain resolves them again on its own, so a clean preview says nothing about the exported pixels.

Q: The size box took a huge number without complaining, is that safe?

No. Neither the max attribute nor the error field blocks a write, so 13333 lands in the box anyway; past the canvas limit there is still no error, only a 6-character empty payload wearing a success toast. Verify the exported bytes and the magic number first.

Q: Why is the text in the export narrower than in the preview?

font-family resolves against the fonts installed on the machine, so the readings across 12 families spread by 132.97, and a canvas 320 wide clips the raster to 320 or less. Inline the CSS rules and embed the font as a data: URI.

📎 Related articles

I Tested 16 Switch Combinations of an HTML Tag Stripper: It Reports 19 Tags but Deletes 148 Characters 2026-10-30 Handing a Base64 string to someone else: 4 edits tried, 3 of them throw an error 2026-10-30 App Store Screenshots Export at 1242×2688: 8 Canvas Presets, Noise at 24.83 Times the Size, and a Phone Frame Cropped 134.93 px by Default 2026-10-30 16 Keys In, 4 Columns Out: How json-to-excel Builds CSV Headers 2026-10-30 Untick Keep Comments and format-js Compresses a 24 Character URL Down to 13 Broken Characters 2026-10-30