← Blog

How we detect a JPEG's existing quality before re-compressing

JPEG file → DQT marker → quantization table → compare against reference Q=50 → quality estimate DQT marker scan scan bytes for FF DB pattern read length: (b+2 << 8) | b+3 read 1 byte: precision | tableId read 64 bytes: table values stop at FF DA (start of scan) tables[0] = luma quantization tables[1] = chroma quantization Reverse the IJG mapping for each entry i in 64: ratio[i] = ref_table[i] / found[i] median = sort(ratios)[middle] if median ≥ 1: Q = 50 + 50 × (1 − 1/median) else: Q = 50 × median

When you upload a JPEG and ask us to compress it at quality 80, the obvious behaviour — re-encode at quality 80, return the result — is often wrong. If the input is already at quality 72, re-encoding at 80 produces a file that's bigger than the input and a generation worse in fidelity. To do the right thing we need to know what quality the input actually is, before we touch it. There's no field in the JPEG file that stores "I was saved at quality 80". But there's a way to back-compute it from what's there.

Quantization tables are visible

JPEG files carry their quantization tables in the file itself, in DQT (Define Quantization Table) markers. The decoder needs them to reverse the encoder's per-block rounding — without them, the file is unreadable. They're not hidden, encrypted, or proprietary. Every JPEG has them right there in the header.

Each table is 64 bytes (an 8 × 8 grid of values). A typical photo has two: one for luma (brightness) and one for chroma (the two colour channels share a table). The values are how aggressively each frequency coefficient gets rounded.

Parsing the DQT marker

To find the tables we walk the JPEG byte stream looking for the marker pattern FF DB. When we find one we read its length, then loop through the table entries until the marker's region ends or we hit the start-of-scan marker FF DA:

for i in [0 .. data.length - 70]:
    if data[i] == 0xFF and data[i+1] == 0xDB:
        length = (data[i+2] << 8) | data[i+3]
        offset = i + 4
        end    = i + 2 + length
        while offset < end:
            precision = (data[offset] >> 4) & 0x0F
            tableId   = data[offset] & 0x0F
            if precision == 0:                       # 8-bit values
                table = data[offset+1 .. offset+65]  # 64 bytes
                tables[tableId] = table
                offset += 65
    if data[i] == 0xFF and data[i+1] == 0xDA: break

After this loop runs, tables[0] is the luma quantization table and tables[1] is the chroma table (if present).

Reversing the quality multiplier

The IJG quality mapping is well known: for a quality setting Q, the encoder scales a fixed reference table by a factor that depends on Q. To recover Q we do the reverse. Divide each entry of the reference table by the corresponding entry of the found table, take the median ratio, and invert the scaling formula:

median = median ratio of (ref_table[i] / found_table[i])

if median ≥ 1:                              # higher than Q=50 reference
    Q = round(50 + 50 × (1 − 1/median))
else:                                       # lower than Q=50
    Q = round(50 × median)

Median, not mean: the quantization tables can have a few outlier entries (especially in the corners) that throw a mean estimate off. Median is robust against those. We also skip entries where the reference value is exactly 99, which is a sentinel for "round everything to zero anyway" and doesn't carry quality information.

Why it mostly works (and when it doesn't)

Almost every JPEG encoder uses the standard reference tables, with the standard IJG quality mapping. For those — phone cameras, web export tools, every common image editor — the back-computation is accurate to within a quality unit or two.

The detector returns null in cases where the math doesn't apply:

The skip-if-already-good guard

With a measured input quality in hand, the compress path becomes:

detected = extractJpegQuality(input.bytes)
if detected != null and detected ≤ slider:
    return input unchanged                # nothing to gain
else:
    re-encode at slider quality

This guard saves a generation of damage on photos that were already smaller than the requested target. It also speeds up the page — there's no need to spin up the encoder for a no-op. About 30% of real-world inputs hit the skip path, depending on what people are uploading.

Why we average luma and chroma

If both quantization tables are present, we compute Q from each separately and combine: (2 × Q_luma + Q_chroma) / 3. The luma table is weighted heavier because it dominates the perceived quality of a photo — chroma quantization with its all-99 corners is mostly noise even at high Q, and reading too much into its variations leads to less stable estimates. Two-thirds luma, one-third chroma is a heuristic that holds up across a wide range of real-world JPEGs.

For the size-search and the content-aware re-encode decisions downstream, this single Q estimate is what gets passed forward.