What is actually inside the JPG this converter writes?

Every one of the 37 JPGs I made here today has the same eleven markers in the same order: baseline sequential, three-component YCbCr at 4:2:0, a single scan, no restart markers, a JFIF header that carries no resolution, and a 456-byte sRGB profile. The quality setting changes exactly one thing — the quantization table — and it changes it by the formula printed in the JPEG specification, to the byte.

37 JPGs taken apart 0 progressive, 0 restart markers Chrome 154, Windows

Short answer: you get the plainest, most widely readable JPEG there is. It is baseline sequential — FFC0, not the progressive FFC2 — with three components in YCbCr, luma sampled at 2×2 and both chroma planes at 1×1, which is what 4:2:0 means. One scan covers the whole image. There are no restart markers, no EXIF block, no Adobe marker, no comment, no embedded thumbnail, and no print resolution. The only thing the quality menu changes is the two quantization tables.

Those tables are not invented. They are the example tables printed in Annex K of the JPEG specification, scaled by the reference implementation's own formula: scale is 200 − 2 × quality, and each of the 64 entries becomes floor((entry × scale + 50) / 100), clamped to 1–255. I predicted all 64 entries of both tables at all three quality settings and compared them against what the site actually produced: 0 mismatches out of 64, every time, for luma and chroma alike. So the "95%" on the menu is a real JPEG quality of 95.

How I measured it: today, in Chrome 154.0.8037.99 (headless, Windows, window 1,280 × 900) against the live page. I ran 37 real conversions — 12 source files across the three quality settings — pulled every output back over the DevTools protocol as raw bytes, and parsed the markers locally. Zero conversions failed. The live page's HTML today is 13,951 bytes and was usable 1,985 ms after navigation.

The eleven markers, in the same order every time

This is the complete skeleton of a 38,078-byte output — a 700 × 476 photo at the default 0.85 setting. Sizes are the bytes the segment occupies in the file, marker and length field included.

MarkerBytesWhat it holds
FFD8 SOI2start of image
FFE0 APP018JFIF 1.1, units 0, density 1 × 1, thumbnail 0 × 0
FFE2 APP2474ICC profile, 456 bytes, chunk 1 of 1
FFDB DQT69luma quantization table (64 entries, 8-bit)
FFDB DQT69chroma quantization table
FFC0 SOF019baseline, 8-bit, 700 × 476, 3 components
FFC4 DHT30DC table 0 — 9 symbols
FFC4 DHT78AC table 0 — 57 symbols
FFC4 DHT28DC table 1 — 7 symbols
FFC4 DHT46AC table 1 — 25 symbols
FFDA SOS14one scan, 3 components, Ss 0, Se 63, Ah/Al 0
FFD9 EOI2end of image — the last two bytes of the file

All 37 files I produced had this exact sequence. Not one gained a segment, dropped one, or reordered one — not the 16 × 16 image, not the 4,032 × 3,024 one, not the monochrome ones, not the ones with an alpha channel. That uniformity is the finding: there is no "small file mode" or "big photo mode" hiding in here.

Baseline, and only baseline

The frame header is FFC0. I looked for FFC2 — the progressive marker — in all 37 files and found it 0 times. The scan header says it plainly too: each file has exactly one FFDA, and its parameters are Ss = 0, Se = 63, Ah = 0, Al = 0 — one pass over all 64 coefficients, no successive approximation. That is the definition of baseline sequential.

Practically: the file decodes top to bottom in one pass and shows nothing until it is whole. Progressive JPEG would paint a rough version first, then sharpen it. Since the file never travels a network here, that costs you nothing.

4:2:0, even when there is no colour to subsample

The three components in the frame header read: component 1 (luma) sampling 2 × 2 against quant table 0, component 2 sampling 1 × 1 against table 1, component 3 sampling 1 × 1 against table 1. Two chroma planes at half the resolution of luma in both directions — 4:2:0. A minimum coded unit therefore covers a 16 × 16 block of pixels; the 700 × 476 photo is 1,320 MCUs, the 12-megapixel one is 47,628.

The converter never writes anything else. I fed it a genuinely monochrome HEIC — a real photo converted to a single channel before encoding — and it still came back as three components at 4:2:0, at 35,179 bytes. The same photo with its colour intact came back at 38,078 bytes. Stripping all the colour out saved 7.6%. At 12 megapixels the same comparison was 2,896,377 bytes against 3,177,680 — 8.9%.

There is a neat piece of evidence inside the file itself. Because the Huffman tables are rebuilt per image, a monochrome photo makes the chroma tables collapse: for that file, DC table 1 and AC table 1 each carry exactly 1 symbol. The chroma planes are so flat that one codeword describes them entirely — and the file still pays for them.

Two things I did not establish, so I am not going to claim them: I did not measure how much a single-component encode of the same pixels would have saved, and I could not isolate how much visible softening on hairline coloured edges comes from this JPEG's 4:2:0 rather than from the source HEIC, which is itself chroma-subsampled. Both would need an encoder path this site does not expose.

The quantization table is the textbook one, scaled

This was the part that surprised me by how exact it is. Take the luminance table in Annex K of the JPEG spec — the one that begins 16, 11, 10, 16, 24, 40, 51, 61 — apply the reference implementation's quality scaling, and you get precisely what the site wrote:

QualityScaleLuma, first eight entriesRangeChroma, first eight entriesEntries at the floor of 1
0.75508 6 5 8 12 20 26 315 – 619 9 12 24 50 50 50 500 of 64
0.85305 3 3 5 7 12 15 183 – 365 5 7 14 30 30 30 300 of 64
0.95102 1 1 2 2 4 5 61 – 122 2 2 5 10 10 10 108 of 64

I ran the comparison over all 64 entries of both tables at all three settings: 0 mismatches. Every file at a given quality carried byte-identical tables, regardless of the image — all 37 outputs shared one table pair per setting.

At 0.95 eight of the luma entries bottom out at 1, the smallest step the format allows. Those coefficients are effectively being kept unquantized, which is why 0.95 costs so much more space for so little visible gain.

The Huffman tables are rebuilt for every photo

Each file carries four tables: DC and AC for luma, DC and AC for chroma. None of them is the standard table from the JPEG spec — I compared all four against the Annex K originals in all 37 files and the count of matches was 0. They are optimized tables, computed from the actual symbol frequencies of that particular image, which is the usual thing for a modern encoder to do and shaves a few percent off the size.

The symbol counts show the tables tracking the content, which is the clearest proof they are per-image:

SourceDC lumaAC lumaDC chromaAC chroma
Flat colour, no detail2131
16 × 16 thumbnail31828
Monochrome photo96111
Colour photo (0.75)957618
Colour photo (0.85)957725
Colour photo (0.95)1162839
12-megapixel photo747820

A flat image needs one AC codeword. A detailed photo at 0.95 needs 62. Nothing here is fixed boilerplate.

What is not in there

The absences are as consistent as the presences. Across all 37 files:

AbsentWhere it would appearWhat it means for you
EXIFFFE1 — 0 occurrencesno camera data, no GPS, no capture date, no orientation flag
Adobe markerFFEE — 0 occurrencesno colour-transform hint; the file is plain YCbCr
Restart markersFFDD — 0 occurrencesno error-resilience segmentation; a corrupt byte can take the rest of the scan with it
CommentFFFE — 0 occurrencesno software tag, no provenance string
Print resolutionAPP0 units 0, density 1 × 1the file declares a 1:1 pixel aspect ratio and no physical size at all
JFIF thumbnailAPP0 thumbnail 0 × 0nothing embedded for file managers to show

The missing EXIF is the one people notice; the EXIF, GPS and orientation measurement covers that ground separately. The missing resolution is the one that quietly bites: drop this file into a page-layout app and it will be placed at whatever default the app assumes, because the file never states a DPI.

The colour profile is present, and it is a small one. The APP2 segment holds a 456-byte profile: device class mntr, colour space RGB, nine tags — desc, rXYZ, gXYZ, bXYZ, wtpt, rTRC, gTRC, bTRC, cprt. Its description string reads sRGB and its copyright reads Google Inc. 2016 — the same compact sRGB profile the browser attaches to its own output.

What the wrapper costs you

Subtracting the entropy-coded image data from the file size leaves everything that is not pixels — the markers, the two quantization tables, the four Huffman tables and the colour profile:

OutputFileImage dataEverything else
12-megapixel photo, 0.853,177,680 B3,176,847 B833 B (0.03%)
Colour photo, 0.8538,078 B37,229 B849 B (2.23%)
Monochrome photo, 0.8535,179 B34,356 B823 B (2.34%)
Flat colour, 0.852,741 B1,983 B758 B (27.65%)
16 × 16 image, 0.85910 B128 B782 B (85.93%)

The colour photo paid 849 bytes, the monochrome one 823, and the 12-megapixel one 833 — essentially the same number however big the picture is, because the profile and the quantization tables are fixed and only the Huffman tables flex with content. On a 16 × 16 image the wrapper is 85.93% of the file.

The quality dial only does something when there is detail

Because quality changes nothing but the quantization table, it can only matter where there are coefficients to throw away. I fed the site three images of the same 700 × 476 size at all three settings:

Content0.750.850.950.95 vs 0.75
Flat colour, no detail2,741 B2,741 B2,741 B1.00×
Real photo30,231 B38,078 B57,617 B1.91×
Random noise190,228 B239,219 B341,608 B1.80×

The flat image came out the same size at every setting — although not the same file: all three were different byte sequences with distinct SHA-256 hashes. The tables changed; there was simply nothing in the image for finer tables to preserve. A photo and a noisy image roughly doubled between the lowest and highest setting.

What this means when you use the file

It will open anywhere. Baseline sequential, 4:2:0, optimized Huffman, no arithmetic coding, no 12-bit precision, no restart markers, no unusual APP segments. This is the intersection of everything every JPEG reader has supported since the 1990s.

It carries no metadata. No camera data, no location, no date, no orientation flag, no DPI. Anything downstream that expects those will find nothing — see the EXIF measurement for why.

Colour detail is stored at half resolution. That is what 4:2:0 means, and it is right there in the frame header. For photographs it is invisible. For screenshots, scans of documents, or anything with saturated hairlines and thin coloured text, colour edges are stored coarsely than brightness edges.

Black-and-white photos are stored in colour. Three components, always. A monochrome image came out only 7.6% smaller than its colour original because two chroma planes are still being written, even though their Huffman tables reduced to a single symbol each.

The quality number is honest. The 95 on the menu produces quantization tables that a JPEG reference implementation at quality 95 would produce, entry for entry. Whatever you think of canvas JPEG encoding, the dial is not inflated.

Questions

Is the output progressive JPEG?

No. All 37 files used FFC0 (baseline) and none used FFC2 (progressive). Each had exactly one scan covering all 64 coefficients.

What chroma subsampling does it use?

4:2:0, in every file: luma at 2 × 2 sampling, both chroma planes at 1 × 1, giving a 16 × 16-pixel minimum coded unit. A 12-megapixel photo is 47,628 MCUs.

Does the JPG keep EXIF or GPS?

No. There is no FFE1 segment in any output. The EXIF, GPS and orientation measurement covers this in detail.

Does the quality setting do anything besides file size?

Nothing I could find in the file structure. Only the two quantization tables change; the frame header, sampling, profile, segment order and component count are identical across all three settings. Speed is unaffected too — the size and speed measurement has those numbers.

What colour profile is embedded?

A 456-byte sRGB profile in a single APP2 chunk, device class mntr, colour space RGB, nine tags, described as sRGB and credited to Google Inc. 2016.

Why it comes out this way

The live page's own HTML explains it in one line. It calls HeicTo({blob:file,type:'image/jpeg',quality:quality}) — the quality value is read straight off the menu and handed to the decoder, and the type requested is plain image/jpeg. Inside the decoder script the final step is canvas.toBlob(...), and the page's own copy says the quality is "passed directly to the browser's native canvas encoder". Everything above follows from that: the browser's encoder, called with a type and a number, produces baseline 4:2:0 with optimized Huffman tables and the standard quantization tables scaled to the number it was given. The page's HTML also names libheif 1.22.2 as the decoder underneath.

Try it: convert a photo, then open the JPG in a hex editor — the first markers you will see are FFD8 FF E0 for JFIF and FF E2 for the colour profile, and the last two bytes will be FF D9.

First published: 9 October 2026