Notes › Tech Notes › A BMP Goes In, a PNG Comes Out: 12342 Bytes Become 281, but the File Is Still Named .bmp

A BMP Goes In, a PNG Comes Out: 12342 Bytes Become 281, but the File Is Still Named .bmp

Re-export at the Same Size: 12342 Bytes In, 281 Bytes Back

I have wanted for a while to check the claim that a bitmap is never compressed and therefore heavy, and the check I picked is crude: take one 64x64 BMP (12342 bytes), type 64 back into both the width box and the height box, and re-export it at its own size. What came down was 281 bytes, and its first 12 bytes were 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d — the PNG magic number followed by IHDR — while the file was named bmp_64_resized.bmp. Reopening that blob: address gives type=image/png and size=281, byte-for-byte identical to resultBlob, so the bytes that reached the disk are themselves a PNG.

The Name and the Bytes Are Produced by Two Separate Pieces of Code

Line 227 of the template image-resize.html lets mimeType inherit the uploaded file.type, and line 228 if (mimeType === 'image/bmp') mimeType = 'image/png'; carries the comment that Canvas cannot emit BMP. The name belongs elsewhere: line 246 originalFile.name.replace(/\.([^.]+)$/, '_resized.$1') only copies the original extension into _resized.$1, and nobody looks at whether the container changed. So on the Resize BMP path the two halves each write their own rule: among the 22 finished exports not a single one has a BM header, 21 are PNG and one is WebP, and the 16 that entered as BMP all download under a name ending in .bmp.

The boundary needs stating: in the pre-probe, requesting image/bmp already returns image/png, both products being 1062 bytes, and deleting line 228 changes nothing. But the primary evidence in this article is the first 12 bytes of the template's own output together with its download name.

The Decoder Is Generous, the Gate Only Reads the Extension

All 13 structurally complete BMP samples decoded to the width and height their header fields promise: 1/4/8/16/24/32 bpp, the BI_RLE8 one at 5436, the BI_RLE4 one at 2428 bytes, a negative biHeight top-down file, and a 63x64 whose row width is not 4-aligned. The only rejection was the biSize=12 sample at 3096 bytes; the other two rejections were a 16-byte truncation and a 0-byte file. The 24bpp sample is exactly 14+40+192*64=12342 bytes, so "uncompressed" does add up on the BI_RGB branch.

What the gate reads is the file.type that the extension hands it: the same BMP renamed to .png or .webp gets through and still decodes as BMP pixels, while renaming it to .dat or .txt draws 不支持的图片格式. JPEG bytes hung on a .bmp name (9712 bytes) are treated as a BMP and turned into a PNG of 116541 bytes, 12 times the source. Only when the extension is not .bmp do all three agree: the copy renamed .webp really does request image/webp, really returns 570 bytes, and really downloads with .webp in the name. The site article on why an extension and the real format disagree talks about the general case of users renaming files; the mismatch here is one that line 228 and line 246 create between themselves.

Getting Smaller Is a Container Change, Not a Resize

Across the 12 same-size re-exports every IHDR reads bitDepth=8 colorType=6: palettes and 565 bit-fields are all expanded into RGBA8 and only then compressed. On the 32bpp sample I wrote alpha=0 explicitly into the top-left 16x16; after resizing, the canvas alpha is min=255 max=255, so transparency was already gone at the decoding stage. As a control, a fully transparent 1x1 PNG comes back with alpha min=0 max=0 after enlargement.

StructureSource bytesSame-size output bytes
24bpp BI_RGB, 64x6412342281
8bpp palette, 64x645174577
16bpp BI_BITFIELDS(565)82581442
BI_RLE4, 64x6424281953

Send the same picture in at another bit depth and the byte count that leaves almost follows the pixel count alone: the 16438 and the 12342 files both come out at 529 bytes.

4 bpp 87.6% RLE4 80.4% 1 bpp 60.5% 8 bpp 11.2% 24 bpp 2.28% JPEG as .bmp = 1199.8%

Fig. 1: output bytes as a share of source bytes

The drawImage on line 224 runs with the default imageSmoothingQuality=low, a setting the template never sets, so any target that is not 1:1 passes through one resampling. How blurry that gets is the subject of another site article on why a shrunk screenshot goes soft, and I do not pick that thread up here.

Two Rounding Schemes Disagree, and the Quality Slider Does Nothing for BMP

The width and height boxes go through the parseInt on lines 211 and 212, which truncates: typing 1.5 gives 1, 2.9 gives 2, 1e3 gives 1 rather than 1000, and 0.9 gives 0 plus the warning 请输入有效的宽高尺寸. Linked scaling and percentages go through Math.round, where 0.5 rounds up, so on that 120x80 image (aspectRatio=1.5) a width of 100 returns a height of 67 and the ratio ends up rounded to 1.4925; on the 2000x100 image a width of 9 computes a height of 0, which line 213 stops. Lock ON only performs linked scaling: editing the height box never back-solves the width.

The quality row is display=none for a BMP input, yet line 229 still passes its value into toBlob: at 0.1, 0.5 and 1 the output is 281 bytes every time, while the same slider on a JPEG source spreads results from 2038 to 54687 bytes. Only two ceilings exist. The file side counts bytes, and 图片大小不能超过 20MB fires at 20971521 but 20971520 is let through, and those two samples were all-zero Blobs, so whether a legitimate BMP of exactly 20 MiB decodes was never tested on this machine. The size side, line 216, checks only one edge at a time, so 10000x10000 is let through too.

Every Failure Is Silent, and the Previous Image Can Still Ship

The template attaches only the onload on line 141; nowhere in the file is there an onerror. A 72-byte broken PNG, a 0-byte file, a 16-byte truncation and the biSize=12 sample at 3096 bytes all end with toast=[], raising no error and never clearing the previous picture. Line 137 also writes originalFile before decoding happens: resize first and get a 134-byte output, then upload that broken file, and pressing download gives the success-toned 下载已开始, the new file's name, and the old image's 134 bytes.

There is no batch mode either: line 120 and line 124 read only the first entry of files, and the picker has no multiple. After resetAll() the leftover values in the width box, the height box and the quality field were still there in the measurement. Of the 12 claims in the site copy, four are marked conflict and three partial conflict, and one sentence still stands: a BMP goes in, a PNG comes out, and the name keeps .bmp.

Questions

Q: What exactly is that .bmp that came back?

It is a PNG. The first 12 bytes are the PNG magic number followed by IHDR, blob.type=image/png, and the container header is still 8-bit RGBA. The .bmp in the name is only the extension that line 246 carried over; no line of code anywhere goes back and fixes it.

Q: Can I ask for a specific output container?

No. There is no format selector in the whole template: MIME can only be inherited from the input on line 227 and then rewritten on line 228, and a same-size re-export is re-encoded just the same into a 281-byte PNG. If you really want a bitmap container, that is a different road.

Q: Does the file always come out smaller?

All 12 cases came out smaller, by wildly different amounts: the 24bpp one keeps only 2.28%, while the already compact RLE4 one still keeps 80.4%. In the opposite direction, JPEG on a .bmp name inflates to 116541 bytes, and a fully transparent 1x1 PNG re-exported at its own size goes from 70 to 88 bytes.

📎 Related articles

Factorial page says 0 to 1000 cannot overflow: Number loses exactness at 23! and only overflows at 171! 2026-10-27 A Single CJK Character Is Worth 5 bits: When Crack Time Turns Into Age of the Universe 2026-10-27 The ASCII in text-to-ASCII is really UTF-8: one Han character prints three numbers, and 100% cannot be reversed 2026-10-27 The GCD page prints the zero-remainder row too: step counts, fold order, and the 29-step Fibonacci worst case 2026-10-27 Resizing a GIF Keeps Only the First Frame: a PNG File Named .gif 2026-10-27