Notes › Tech Notes › Putting a Bitmap Into SVG Only Swaps the Box: the 232-Byte Container Constant, a base64 Embed Checkbox That Never Fires, and the 142064 Characters That Do Not Move Across Four Zoom Levels

Putting a Bitmap Into SVG Only Swaps the Box: the 232-Byte Container Constant, a base64 Embed Checkbox That Never Fires, and the 142064 Characters That Do Not Move Across Four Zoom Levels

This page wraps bitmaps instead of tracing them: the container costs a constant 232 bytes, the base64 embed checkbox yields byte-identical output in all three states, 142064 characters never move across the four zoom levels, and the download URL inflates once more on the way out.

Pinning the conclusion to bytes

There is no <path> anywhere in what this page produces: one <image> line simply wraps its arms around the whole data URL and that is the end of it. Grepping the entire template returns canvas, drawImage, toDataURL, trace, quantiz and posterize 0 times each, and the bitmap has a single door in, readAsDataURL, used 1 time at L144. The 28 images I made myself plus 3 more files from the same batch come to 31 files; fetched back and decoded from base64 they match their inputs byte for byte, 31/31. That is the whole claim of this page: it ships a wrapper, and everything below is the bookkeeping behind that sentence.

The subject is the template behind the Image to SVG page, 10270 bytes of it, and its script section was lifted verbatim into node:vm and run exactly as it ships. Nothing was rewritten for the test, so every value below is something the page produced on its own.

The container overhead is a constant

The fixed part of buildSvg (L157-L198) is 15 literals added together: 39+40+48+12+10+2+18+1+3+16+10+2+21+4+6 = 232 bytes, which include 7 newlines and 27 indentation spaces. So svgB = 232 + 3*len(w) + 3*len(h) + prefixLen + b64Len: w and h are each written three times, every character of those numbers costs 3 bytes, and prefixLen is only ever 22 or 23. Across the 31 rows of the container identity table resid is 0 everywhere; the jpg with n=581743 reads 775660 + 21 + 23 + 232 = 775936. So the wrapper is not a share of what you feed in; it is a fixed charge plus a toll on every digit you declare.

base64 only reports the reconciliation

ceil(n/3)*4 hits 31/31. Padding comes in three shapes: remainder 0 leaves 0 pad characters (8 of the 28 files), remainder 1 adds 2 (12 files), remainder 2 adds 1 (8 files). For the samples that divide evenly b64/n is exactly 1.333333, and the deviation is only visible at the scale of tens or hundreds of bytes: a_1x1.png sits at 1.369863, while by the 100 KB level only 0.000025 is left. The ratio says nothing about the picture; it only says how much the encoding stretches what was already there.

The "base64 embed" checkbox that never fires

The checkbox at L21 appears 1 time in the whole file and ships checked; the script block looks it up by id 0 times. Building d_101x67.png once per checkbox state gives outputs whose sha starts with the same 12 characters, 5d037f0d8a4b, and whose length is 30249 every time, so unchecking changes nothing byte for byte. The caption at L22, 使用 base64 嵌入(生成独立 SVG 文件), that is embed with base64 so the SVG file stands alone, has nothing to land on here, because base64 is the only embedding this page knows. It is the kind of control that reads as a choice in the interface and as a no-op in the code.

Source B → base64 chars → SVG B a_1x1.png 73 100 360 urlLen/svgB 1.6694 h_1200x900.png 1030404 1373872 1374147 whole 1.3336 1.1440 28 files total 3808141 5085119 1.3353 Floor 1.0552 belongs to n_123x45.bmp

Figure 1: three lengths on one chain.

Read the columns as the chain itself: the bytes the decoder produced, the characters those bytes become, and the document that finally carries them.

Four zoom levels change three declared numbers

Math.round at L159/L160 fills only 6 number slots, and the base64 at L168 does not care which level is picked. In that 24-row zoom table the b64Len of 241×137 is a constant 142064, while the declaration runs 60×34, 121×69, 241×137, 482×274; the 101×67 PNG holds 29980 and the BMP holds 27232. Rounding leaves traces at 120.5 → 121 and 60.25 → 60, and choosing 25% for a 1×1 image collapses into width="0" height="0" and image_0x0.svg.

LevelDeclared w×hsvgBThe "SVG size" readout
25%60×34142330139.0 KB
50%121×69142333139.0 KB
100%241×137142336139.0 KB
200%482×274142336139.0 KB

The bytes between levels come from nothing but digit counts; the bitmap data itself does not move. The zoom control decides how large the picture draws, not how much data it carries.

The label can stay on the old level

Three factory values disagree with each other: active at L26 sits on 25%, imgSvgScale=100 is the default at L85, and the level label ships written 100%. After an upload, L131-L134 reset scale=100 and move active to index 2 but never touch the label text, and the only assignment to that id in the whole file is L152. Measured by clicking 25% and then uploading a different image: the label stays "25%", imgSvgScale is 100 in fact, and both the declaration and the intrinsic size are 241×137, which is an image built at 100% under an interface that reads 25%. The text beside the control is the one place on this page that can be a step behind, so the sizes written into the file are the honest witness.

The download URL inflates a second time

The encodeURIComponent at L171 touches only the pluses, slashes and equals signs inside base64, and each of those grows from 1 character into 3. The closed form urlLen = 33 + svgB + 2*esc came out OK on all 18 rows of that table, and the fixed skeleton contributes 100 escaped characters on its own. urlLen/svgB falls from 1.6694 toward 1.0552, with large files settling between 1.055 and 1.072; h_1200x900.png is pushed out to 1.1440 because slashes are 4.5669% of it and pluses 2.6251%. Preview and download both eat this layer, while the interface displays the layer above it. The extra width is the price of handing a document over as an address instead of as a file.

The display convention and the format gate

formatSize (L226-L230) picks the unit first and applies toFixed(1) afterwards, which is how 1048575 shows up as 1024.0 KB, and how the two neighboring rows of h_1200x900.png read 1006.3 KB in one and 1.3 MB in the other. That is this page's display convention, not a difference on the filesystem. Both readings describe one artifact, so they should not be tallied as two sizes.

The regex at L103, /image\//, has no anchor and no i flag: of 17 type values it passes 11 and blocks 6. It stops the empty string, text/plain, application/pdf, IMAGE/PNG, png and image, while ximage/png, foo/image/bar, not/image/ and text/html,image/png all get through, so the 格式 row, the line naming the detected format, prints XPNG, FOO/BAR, NOT/ and TEXT/HTML,PNG. Grepping onerror returns 0, so when decoding fails the whole block never runs and the panel stays dark. The test is only whether that substring shows up somewhere, which is exactly why the odd inputs above behave the way they do.

Animated GIFs and the wording audit

j_anim2.gif (900 bytes, loop=0) carries two frames delayed 20cs and 50cs. Wrapped, decoded back to bytes and re-parsed, it returns the same frame fields one by one, the MIME is still image/gif, and svgB is 1472. What a 0-size SVG renders as, Chrome's " (1)" suffix on a repeated download name, the data URL length ceiling, the cost of putting 6.4 MB through innerHTML and frame-by-frame playback all stayed unmeasured, because Node has neither a rendering nor a download stack. The JPG and WEBP byte streams were encoded by headless Chrome and checked back against prefix and length. The animation is carried rather than re-rendered, which is the distinction worth keeping. The copy was judged against entry 46 of config/tools.php sentence by sentence, 17 in all: 6 agree, 4 need a qualifier or agree only partly, 7 conflict, and 5 of those point at the same fact in the code rather than to scattered slips.

Where wrapping ends and tracing begins was settled in the older piece on bitmap to vector conversion, so it is not repeated here. Moving image data between containers belongs to SVG format conversion, and this article measures none of that; the two pages answer different questions, one about content and one about packaging.

Common questions

Q: Does unchecking "base64 embed" change the output?

No. All three states give the same first 12 sha characters and the same length, 30249.

Q: Does picking 25% make the file smaller?

For 241×137 the b64Len stays 142064 across all four levels, svgB moves by 6 bytes at most, and every level displays 139.0 KB.

Q: Does an animated GIF survive being wrapped?

The bytes and the frame structure do: the 20cs and 50cs delays come back field by field after a reverse parse. Playback was not measured.

📎 Related articles

Drag to 4 and drag to 8, both preview 43890 blocks: two block grids for preview and export, three stops that crash and the 6px misclick threshold 2026-11-10 271 Characters Quietly Missing After the Conversion: The Hole Protocol, the 64-Round Refill Cap and the 20-Entry Warning Limit of the Markdown to HTML Page 2026-11-10 Decoding Runs Four Steps in a Row: Of 1194 Corpus Lines on the unicode-convert Page Only 798 Come Back Unchanged 2026-11-09 The Keep-Set Shrinks to 66 Characters: Character-by-Character Readings of Three Encoding Modes over 128 Code Points, Plus a 9-Cell Encode/Decode Matrix 2026-11-09 The Interface Order of the Eight Checkboxes Runs Backwards from the Code: Hard-Coded Eight-Step Order, Pairwise Swap Truth Table, and Code-Unit Metrics on the Text Cleaner Page 2026-11-09