I converted a logo that is nothing but an outline into JPG, sent it to a partner, and got one line back: why is the background white. The region in the source image really was transparent, and the trouble sits in the conversion step itself, because a JPG export cannot keep transparency: it picks a background colour and fills every transparent pixel with it. The awkward half of the story is the rest. On one and the same image, changing only that fill colour moves the file by a few hundred bytes, while dragging the quality slider to 90 or down to 10 changes not a single byte on the PNG outlet.
The white background is not the encoder, it is a layer painted on the canvas first
The starting point is not compression, it is the canvas. Lines 230 to 232 of the template paint the whole canvas with the chosen background colour through fillRect before the source image is pushed on top of it through drawImage. A pixel at alpha=0 never gets a chance to stay empty, so what lands there is the background colour itself. In the sampled image those pixels are recorded as RGB (255,0,0): filled with white they read back as (255,255,255), filled with #ff0080 they read back as (255,0,128), and across 24 sample points the output alpha is 255 every single time, byte for byte exact.
The tick box changes the moment the target switches to 「转为 JPG」: it reports checked=false and disabled=true, and the hint underneath becomes 「JPG 不支持透明,透明区域自动填充下方底色」. Clicking the greyed-out box anyway still reads back checked=false, because shouldFillBg() is true unconditionally at that point. Switch to 「转为 PNG」 and the background picker is disabled=true again. Of the three outlets, one situation leaves that control operable: WebP with transparency not preserved.
One image, four file sizes, and the only thing that changed was the colour behind it
Filling transparency also rewrites the size. That banded image starts at 2576 B, and in the image format converter I parked quality on 90, touched nothing else, and sent it out through the JPG outlet once per background colour. What came back were four exports with four byte counts that never repeat.
| Fill colour | JPG export size |
|---|---|
#ffffff | 5023 B |
#7f7f7f | 4840 B |
#000000 | 4769 B |
#ff0080 | 4196 B |
The direction is inverted as well. At quality 90 I ran six real PNGs through JPG and five of them grew: 2083 B to 3160 B, 5445 B to 8586 B. The one that shrank went from 899899 B to 216635 B, a factor of 4.15, no match for the cut to a fifth or a tenth the copy promises. A batch of 16 images sharing one name in different suffixes printed an aggregate bar carrying its own verdict, 32.5 KB going in and 49.4 KB coming out, labelled 「体积增加」, that is 52% bigger.
Fig. 1: the source size of three PNGs against the size they come out at through JPG with quality on 90, bars scaled to 8586 B.
91 slider positions, and not one of them takes part in PNG encoding
The slider is declared min=10 and max=100, and what the DOM hands back is step="1", so the usable integer positions run from 10 to 100, 91 in all, and setting value to 5 is simply clamped back up to 10 by the browser. Select 「转为 PNG」 and the entire quality row disappears as display=none, because line 235 swaps the quality passed to toBlob for undefined. The solid image comes back at 10606 B on positions 10, 55, 90 and 100 alike, and the photo comes back at 26190 B on all four.
Fig. 2: the JPG byte curve for the photo, which starts at 5445 B, enumerated from 10 to 100, with the constant PNG outlet size drawn as the dashed line.
The JPG side is the only side with a curve at all. Enumerating all 91 positions yields 91 byte counts that are different from each other: 2040 B at 10, 8586 B at 90, 67341 B at 100, and the biggest single jump sits between 99 and 100, where the file adds 38192 B in one step. The solid image behaves the opposite way, since every position from 10 to 60 gives 3159 B and every position from 70 to 90 gives 3160 B, so 91 positions produce 3 sizes in total. WebP is not monotonic either: 1392 B at 10 is larger than 1294 B at 90, and the 520 B at 100 is the smallest of the set.
Ask it for HEIC and it quietly hands you a PNG
Three target format buttons, nothing else: data-mime is image/png, image/jpeg or image/webp, and the suffix map on line 65 carries three entries, png, jpg, webp. Reach past the interface and set targetMime outside that map and canvas.toBlob neither throws nor warns. Ask image/heic, image/avif or image/tiff and each falls back to image/png in complete silence, the same solid image returning 10606 B with a file header of 89 50 4e 47, while only image/webp really produces WebP, 1294 B of RIFF. A MIME outside the map shows up in the export name itself, solid_640.undefined, and the support list on line 9 reads PNG / JPG / WebP / BMP / GIF / SVG, HEIC not among it.
The entry point reads the declaration and never the contents. The single gate is line 167, file.type.startsWith('image/'). Declare image/heic and put the bytes of that 2083 B PNG inside it, and it is accepted without complaint and read in at 640×640. A perfectly ordinary PNG whose type is an empty string is the one that gets dropped, leaving only a toast, 「已跳过非图片文件:no_type」. Static scanning finds no MAX, no maxSize, no limit and no comparison against blob.size anywhere in the template, so there is neither a size ceiling nor a count ceiling.
Two images collide on one name and the archive keeps only one
The export name is built by replacing only the part after the last dot, so archive.tar.gz becomes archive.tar.jpg, and characters that look like markup are kept exactly as they were: <b>&"x".png comes out as <b>&"x".jpg. Combine that habit with the three suffixes above and same.png plus same.jpg from one batch both export as same.jpg, so zip.file() is called twice while the finished archive holds one entry. The 8948 B written later overwrites the 3160 B written earlier, the whole package measures 9062 B where the two member images together should have been 12108 B, and the swallowed image leaves no message behind. With a single image there is no ZIP at all, and when the typeof JSZip check on line 274 cannot resolve the dependency, pack everything degrades into downloading the first file only.
Dimensions come from the decoder rather than from the tool. For the nine inputs I fed in, every output width and height matched naturalWidth and naturalHeight one by one, and there is no scaling path in the code at all, so a real resize has to go through the image resize tool. When the container header disagrees with the decoder, the decoder wins: exif6_400x300.jpg writes 400×300 in its SOF, the browser decodes it as 300×400, and all three outlets therefore produce 300×400.
Three questions that come back a second time
Q: Which outlet should a transparent logo take?
Only PNG, or WebP with transparency kept, can leave the background absent. On the JPG side some background colour always ends up inside the transparent region, there is no way to decline it. If JPG is mandatory, set the fill to whatever the destination page shows behind the image, which hides the seam far better than the default white does.
Q: Does choosing PNG mean nothing happened to my file?
The encoding layer ignores the slider and fills nothing, and the source alpha is written out position by position as it arrived. That still is not the same as untouched. The RGB of a fully transparent pixel has already been cleared to (0,0,0) by the decoder on the canvas, so the colour the source file recorded is gone before encoding starts; every outlet writes RGBA, and a source without an alpha channel gets one added anyway. Most of the jump from 2083 B to 10606 B on the solid image is that container overhead rather than any new pixel data.
Q: What should I guard against before a batch JPG run?
Separate files whose names differ only by suffix before you start, because once they collide the archive keeps only the file written later and reports nothing. Then read the aggregate bar when the run finishes: whenever it says 「体积增加」, the batch really did get bigger, and that is the honest number to judge the conversion by.