First put the three layers of the bill on the table
There is one JPG and the receiving side wants PNG. When the conversion finishes, three things happen at once: 27,760,000 pixels keep every bit they had, the file size grows to 15.881 times, and the blocking artifacts plus the chroma repetition that JPEG left behind are locked, exactly as they were, inside a lossless container. Every figure here is measured: the probe page injects the 8908 byte inline script of image-convert.html as it is, headless Chrome calls addFiles() and convertAll() directly, and what is read back is the output of the real export function.
Identity: a three way cross comparison with zero difference per channel
Decode the source JPEG, take the template canvas pixels, decode the PNG product back, and compare the three routes against each other: pixelsDiffering, maxChannelDiff and the per channel rDiff/gDiff/bDiff/aDiff are all 0. That covers 3,760,000 pixels for the group of 7 files, plus the 24,000,000 pixels of the 6000x4000 one. For all 8 products the IHDR reads bitDepth 8, colorType 6, interlace 0, 8 against 8 with no second quantization. The magic number 89504e470d0a1a0a, blob.type and the download name agree on PNG in three separate places, and the 39587 byte JPEG wearing a fake .webp suffix still comes out as a 504277 byte .png.
The price: growth only becomes readable in bytes per pixel
| Sample | Source bytes | Product bytes | Multiple |
|---|---|---|---|
| exif6_400x300 | 4802 | 7515 | 1.565 |
| jp_flat flat colour | 1698 | 4785 | 2.818 |
| jp_detail high detail | 107934 | 513021 | 4.753 |
| photo_400 | 9712 | 116541 | 12.0 |
| fake_webp_is_jpeg | 39587 | 504277 | 12.738 |
| jp_grad gradient | 7185 | 114102 | 15.881 |
On one and the same 400x400 canvas flat colour grows 2.818 times while the gradient grows 15.881 times, and the whole gap is entropy: the source costs 0.0449 bytes per pixel, the product costs 0.7131 bytes per pixel, and the quotient is exactly 15.881. Container wrapping is only the minor term: 8 bytes of signature plus 25 for IHDR plus 12 for IEND is 45 bytes, and each IDAT slice collects 12 more, so 45+360+116136=116541 and 45+348+113709=114102 can both be recomputed item by item. Every byte that grew sits in the entropy coded body itself.
Fig. 1 | Bar heights are log proportional to byte count: the dim bar is the source, the bright bar is the product.
The distortion marks of JPEG are frozen in, losslessly
Take the 8x8 block statistics of the pixels decoded from the product and of the pixels decoded from the source JPEG. The 42 fields, once serialized, are equal character for character, and that equality is itself the bad news: not one blocking artifact was smoothed away. For photo_1600 the block boundary gradient hRatio is 9.286; the very same gradient image saved straight from canvas to PNG measures only 0.136, and if it passes through JPEG first the figure becomes 1.319.
The 4:2:0 chroma subsampling survives unchanged as well. The share of neighbouring pixels whose Cb is exactly identical climbs from 38.15% in the control group to 72.93%, cbRatio moves from 0.698 to 4.606, and cbByResidue changes from a flat line near 28.7 into an alternation of 6.283 and 3.379. All of the loss happened in the JPEG encode. Original canvas against the JPEG decode gives the high detail sample 159994 differing pixels, a largest single channel difference of 142 and sumAbsDiff 12524610; the same canvas against the PNG product returns the same set of numbers; and the JPEG decode against the product stays at 0. So the PNG hop means no further quality is spent, it does not mean quality is recovered.
Round trip: only low entropy images converge
Run jpg to png to jpg over the group of 7 samples and the return bytes are 0.622, 0.79, 0.921, 0.926, 0.941 of what went in, plus two that land on exactly 1; the opposite direction, PNG to JPG, is its own account. The PNG hop in the middle does not touch a single pixel and only lifts 8948 bytes back to 116078. Photos keep losing on every pass: photo_400 shows 12185 differing pixels and sumAbs 45085 on the first pass, then 13762 and 59824 on the second. Low entropy images converge instead. From the second hop jp_grad settles at 7185 bytes with 70 differing pixels and sumAbs 93, its third hop PNG is 114131 bytes and jitters by 29 bytes, and jp_flat comes back byte for byte equal to the source at 1698. On its third pass jp_detail falls from 101576 to 99952, only 1.6% lower, while damaging 412 more pixels.
Fig. 2 | The return byte ratios of the 7 round trips; the dashed line marks a ratio of one.
Metadata has nowhere to live and the orientation is already baked
In exif6_400x300.jpg the source segment table holds a 34 byte APP1 EXIF block whose orientation is 6, yet the 7515 byte product keeps no eXIf chunk at all: tEXt, iTXt, zTXt, iCCP, bKGD, pHYs, sRGB, gAMA and cHRM all count 0 and metaBytes is 0. The 472 byte APP2 ICC profile in the jp_ samples does not get in either. The orientation was not damaged by the conversion: the decoded width and height and the product IHDR all say 300x400, because the browser had already baked orientation into the pixels, so what is lost is only the 34 byte record of it.
The template sets no ceiling and the failure branch never fired
It is the same toBlob call as convertOne: on 16384x16384, where the input handed to the encoder is 1073741824 bytes, it produces 5012121 bytes in 2320 milliseconds, and on 12000x12000 it produces 2707968 bytes in 1203 milliseconds. The conversion failure branch on line 244 fired 0 times under measurement. The branches that really refuse input are only the non image/ prefix on line 167 and the unreadable image on line 178, and out of 15 inputs 11 succeeded while 4 were refused. Line 143 is what sets the quality row to display none for the PNG direction.
A batch of 10: the summary bar and byte level idempotence
For a batch of 10 files loadMs is 21 and convertMs is 162, and the summary bar reads "Converted 10 / 10 files · total 281.2 KB → 1.94 MB (size up 606%)". Running convertAll() a second time leaves the product sha prefixes unchanged at d72921d5dd519312 and 1f85ef0821cd4944. Zipping the 7 files gives 1584975 bytes under both DEFLATE and STORE against 1584197 bytes of members, a compression gain of 0 bytes, because PNG has already pushed the entropy as far as it goes. The entry point is JPG to PNG.
Common questions
Q: The size grows by more than ten times, did the conversion ruin the image?
It did not. The product is pixel for pixel the same as the JPEG decode, and the only thing that changed is the bookkeeping: JPEG trades its block boundary gradients against bytes, while PNG writes every pixel down honestly through lossless entropy coding, which turns 0.0449 bytes per pixel into 0.7131 bytes per pixel.
Q: Can converting to PNG remove the blocking artifacts of JPEG?
No. The 42 block statistics fields matching character for character is the answer: the blocking artifacts and the 72.93% chroma repetition are preserved together, losslessly, with nothing averaged out.
Q: Does an alpha channel mean a transparent background?
colorType 6 does carry an alpha slot, but alphaMin and alphaMax are both 255, and among 3,760,000 pixels 0 are anything other than fully opaque. JPG carries no transparency information at all, and switching to another container does not grow any.