Skip to content

test(codecs): JPEG fixtures and baseline read acceptance suite, disabled until IC-6 - #594

Merged
github-actions[bot] merged 1 commit into
devfrom
lead/image-codecs-IC-2b
Oct 19, 2026
Merged

github-actions[bot] merged 1 commit into
devfrom
lead/image-codecs-IC-2b

Conversation

@prakashUXtech

@prakashUXtech prakashUXtech commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Third part of IC-2 (after #590 and #592). It adds the JPEG fixtures and the baseline JPEGReadTests. Every test is disabled until the task that implements it, following lead.md rule 13. No Sources file changes.

What

Tests/CoreRasterTests/Fixtures/JPEGFixtures.swift builds JPEG streams byte by byte from the specs, and builds their reference pixels from the spec maths:

  • Tables. The zigzag order (zigzag) and the K.1/K.2 quantization tables (lumaQ, chromaQ). The K.3 to K.6 Huffman tables (dcLuma, dcChroma, acLuma, acChroma) as DHT carries them, with codes generated by C.1 and C.2 (codes, huffman).
  • Entropy coding. Bits writes most-significant bit first, stuffs 0xFF00 and pads with 1-bits. magnitude gives the F.1.2.1 category and extra bits. encodeAC handles run/size, ZRL and EOB.
  • Marker segments. segment, jfif (APP0 1.02), exif(orientation, littleEndian:) (APP1 with TIFF IFD0 and tag 0x0112 SHORT, in MM or II form), dqt, dht, sos and header.
  • Frame. Size, sampling factors, tables and a quantized block for each (component, block x, block y). mcus, blocks and units give the coding order: MCU-interleaved per A.2.3, or a one-component scan over the component's own block grid per A.2.2.
  • Builders.
    • baseline(f, sof:precision:restart:orientation:littleEndian:): a full sequential stream, with DRI and RSTm wrapping 0 to 7 and the DC predictions reset.
    • stub(f, sof:): headers plus a 2-byte scan.
    • split / join: segments before SOS, and the scan, for building damaged cases.
  • Expected pixels. seeded(seed) gives deterministic SplitMix64 blocks. idct is A.3.3 in Double, plus 128, clamped. reference(f) applies box chroma upsampling and then JFIF YCbCr to RGB. error(data, f) returns the mean and max error in sRGB levels; alpha must be 1.

JPEGReadTests (all .disabled("waits on IC-6")):

Test What it checks
infoReadsFrame JPEGInfo for 4:2:0, a SOF2 frame reports progressive, a side of exactly maxSide is accepted
greyDCOnly samples are 128 + 2q exactly (within 0.01 level)
zigzag 64 blocks, block k holding only zigzag coefficient k under a flat table; any swapped entry is about 11 levels off. Covers the mutation .zigzag
ycbcrSampling 4:4:4, 4:2:2 and 4:2:0 at 13x11 and 37x21, grey, and SOF1 8-bit decoding the same as SOF0; all within mean 1/255 and max 4/255
restartResetsDC intervals 1 and 3 on 20 one-block MCUs (RSTm wraps twice), intervals 1, 2 and 4 on 4:2:0, and output equal to the same image with no restarts. Covers the mutation .restartResetsDC
exifOrientation orientations 6 (MM), 8 (II), 3 and 1 are reported; pixels are not rotated
unsupportedThrows SOF9, SOF10, SOF3, SOF5 throw unsupported from both info and read; 12-bit and 4-component throw from read
refusesHuge maxSide+1 on either side and 20,000² throw tooLarge from read before the 2-byte scan matters; info refuses only a side
damagedThrows no DHT, no DQT, missing DQT 1, no scan, a cut scan, a 16-ones code, sampling 0, an unknown scan component, no SOF, and a segment running past the end throw damaged; no SOI throws notThisFormat

Which task enables what

  • IC-6: every test in this PR.
  • Next PR: JPEGFuzzTests (IC-6), the progressive tests in JPEGReadTests (IC-7) and JPEGWriteTests (IC-8). It is split out to stay under 400 lines; this PR is 394.

Spec sources and readings

Sources are ITU-T T.81 (A.2, A.3.3, Annex B, C, F.1.2, K.1 to K.6), JFIF 1.02 and EXIF 2.3 (TIFF IFD0, Orientation).

Places where the specs leave a choice, and the reading I picked:

  • Upsampling. T.81 does not define chroma upsampling. The references use box replication (sample x·H/Hmax), as the packet's pseudocode says ("box for 4:2:x"). A smoothing upsampler will fail the tolerance on 4:2:x.
  • Rounding. The IDCT reference is unrounded Double. The tolerance absorbs a decoder that rounds samples to 8 bits.
  • Where 12-bit and CMYK throw. They are asserted on read only. The doc says info throws for frame types it does not read, and these two are not frame types.
  • Segment order. EXIF APP1 follows JFIF APP0.

Validation

  • I checked the K.5/K.6 value lists for 162 distinct values with counts matching BITS.
  • As a syntax check only, an independent JPEG decoder (Pillow 12.3) opened every generated stream, with restarts, EXIF in MM and II, and 4:4:4, 4:2:2 and 4:2:0. All expected values come from the spec maths, not from that decoder. Against the references:
    • Grey and 4:4:4: mean at most 0.46 levels, max at most 1.75.
    • 4:2:x: luma matches within 0.94 levels, so the block order is right. Chroma differs only by the smoothing upsampler that decoder uses.
  • scripts/ci: CI: PASS, before and after the rebase on origin/dev.

IC-1 was squash-merged to dev as 13f1006 (#575).

Refs #562

Belt-Lead: act-1a1511a1174-tlch
Feature: image-codecs
Slice: IC-a
Fast-Check: pass
Head: 8723542


@prakashUXtech prakashUXtech added the fast-lane Small green PR, auto-merge when eligible label Oct 19, 2026 — with Claude
@github-actions
github-actions Bot merged commit 22ac021 into dev Oct 19, 2026
1 check passed
@github-actions github-actions Bot added the fast-lane-merged Merged by the fast lane; review after the fact label Oct 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Fast lane: merged into dev (394 lines, 2 files, Fast-Check marker).

@github-actions github-actions Bot mentioned this pull request Oct 19, 2026
github-actions Bot pushed a commit that referenced this pull request Oct 19, 2026
…, disabled until IC-6 to IC-8 (#599)

Last part of IC-2, after #590, #592 and #594. It adds the rest of the JPEG checks, each disabled until the task that implements it (lead.md rule 13). No Sources file changes.

## What

**`JPEGFixtures`** gains two functions:
- `progressive(f)`: a SOF2 stream (T.81 Annex G) carrying the same coefficients as `baseline(f)`. The scans are:
  - DC first at Al 1, then the DC refinement, both interleaved.
  - Chroma AC by spectral selection, bands 1–5 and 6–63.
  - Luma AC 1–63 at Al 1, then refined to Al 0.

  Each AC scan holds one component over that component's own block grid (A.2.2).
- `refineAC`: the G.1.2.3 refinement coding.
  - A newly nonzero coefficient is coded as (R, 1) plus a sign bit. R counts only zero-history zeros.
  - A coefficient with history sends a correction bit, held back until the next symbol.
  - A ZRL goes out at the next nonzero once 16 zeros have passed, and never after the last new coefficient.

**`JPEGFuzzTests`** (IC-6):
- `thousandMutationsThrowOrDecode`: 1000 mutated reads and 200 mutated `info` calls.
- `scanMutations`: damages only the entropy-coded bytes, behind valid headers.
- `seededValidStreamsDecode`: 40 seeded random sizes, samplings and restart intervals, each within tolerance.

The seeds cover grey, 4:2:0 and 4:2:2, restarts, and EXIF in both byte orders. Each mutated case must throw a `CodecError` or decode.

**`JPEGReadTests`, progressive half** (IC-7):
- `progressiveMatchesBaseline`: grey and the three samplings. The progressive coding must read within 1/255 of the baseline coding and within the reference tolerance, and `info` must report progressive.
- `acRefinement`: AC values large enough that refinement carries every low bit and every ±1. It covers mutation `.acRefinement`. Zigzag positions 2–17 are 16 zero-history zeros in front of a coefficient with history, so the stream needs a ZRL with a held correction bit.
- `damagedThrows` now wraps `dropFirst()` in `Data(...)` so the slice starts at index 0.

**`JPEGWriteTests`** (IC-8):

| Test | What it checks |
|---|---|
| `qualityNinetyReopens` | seeded gradients reopen within a mean of 3/255; alpha is 1 |
| `qualityRange` | qualities 1 and 100 are accepted; 0, 101, -1 and Int.max throw `unsupported`; a side over `maxSide` throws `tooLarge` |
| `startsWithSOIAndJFIF` | SOI, then APP0 "JFIF\0", then EOI. SOF0 only, 8-bit, Y at 2x2 and Cb/Cr at 1x1. `info` agrees |
| `annexKHuffmanTables` | the DHT tables equal K.3 to K.6 |
| `qualityScale` | covers mutation `.qualityScale`. Below 50 the scale is 5000/q, otherwise 200 − 2q, and each entry is clamp((K·scale + 50)/100, 1, 255). Quality 50 gives K.1 exactly and 100 gives all ones |
| `lumaTables` | covers mutation `.lumaTables`. The Y component's Tq names scaled K.1, and Cb/Cr name scaled K.2 |

## Spec sources and readings

Sources are ITU-T T.81 (A.2.2, A.4, Annex G (G.1.1.1.1, G.1.2.1 to G.1.2.3), B.2.4.1, B.2.4.2, Annex K) and JFIF 1.02.

Places where the spec leaves a choice, and the reading I picked:
- **AC point transform.** The AC point transform divides toward zero. That is what makes the refinement's correction bit equal bit Al of |coefficient|.
- **DC point transform.** The DC point transform is an arithmetic shift right, and its refinement sends bit Al of the two's-complement value (G.1.2.1).
- **Where correction bits go around a ZRL.** In refinement scans, each ZRL is followed only by the correction bits of history coefficients before the 16th zero it skips, which is how the G.1.2.3 decoder reads them. My encoder sends a ZRL at any nonzero once 16 zeros have passed, so the bits it holds back always fall inside that span.
- **Quality scaling.** The packet asks for "the common 50-centred formula" without spelling it out. I used the one above.
- **Write errors.** For `write`, a quality outside 1…100 is `unsupported`, as the IC-1 doc comment says.

## Validation

All expected values come from the spec or the formula above, never from a decoder. As a syntax check only, an independent decoder (Pillow 12.3) was used three ways:
- It decoded every progressive fixture, including the ZRL refinement case, to exactly the same pixels as its baseline twin (difference 0).
- The quality scaling above matched that library's own tables at qualities 1, 10, 25, 50, 75, 90 and 100.
- At quality 90 with 4:2:0, gradients like these round-trip with a mean error of about 0.8/255, against the 3/255 limit.

`scripts/ci`: CI: PASS before and after the rebase on origin/dev (1808edc). This PR is 247 lines in 4 files.

Refs #562

Belt-Lead: act-1a1511a1174-tlch
Feature: image-codecs
Slice: IC-a
Fast-Check: pass
Head: a0d47bd

---
prakashUXtech added a commit to qbtrix/creative-apps that referenced this pull request Oct 19, 2026
…rallel (#3126)

Moves the creative-core pin from bf5426e to 0de5687, the head of core
`main` (green, promoted from dev). The range has the resident-rendering
speedups from qbtrix/creative-core#620: the batched composite commits
without waiting, the resident dirty check is storage identity instead of
a pixel compare, and the Float16 tile upload vectorizes. It also has
qbtrix/creative-core#626, which repoints the mutation anchor #620 moved.

Core range (58 commits):
qbtrix/creative-core@bf5426e...0de5687

Core PRs in the range: qbtrix/creative-core#567 qbtrix/creative-core#568
qbtrix/creative-core#569 qbtrix/creative-core#570
qbtrix/creative-core#571 qbtrix/creative-core#573
qbtrix/creative-core#574 qbtrix/creative-core#575
qbtrix/creative-core#576 qbtrix/creative-core#577
qbtrix/creative-core#578 qbtrix/creative-core#579
qbtrix/creative-core#580 qbtrix/creative-core#581
qbtrix/creative-core#582 qbtrix/creative-core#583
qbtrix/creative-core#584 qbtrix/creative-core#585
qbtrix/creative-core#586 qbtrix/creative-core#587
qbtrix/creative-core#588 qbtrix/creative-core#589
qbtrix/creative-core#590 qbtrix/creative-core#591
qbtrix/creative-core#592 qbtrix/creative-core#593
qbtrix/creative-core#594 qbtrix/creative-core#595
qbtrix/creative-core#596 qbtrix/creative-core#597
qbtrix/creative-core#598 qbtrix/creative-core#599
qbtrix/creative-core#600 qbtrix/creative-core#601
qbtrix/creative-core#602 qbtrix/creative-core#603
qbtrix/creative-core#605 qbtrix/creative-core#606
qbtrix/creative-core#607 qbtrix/creative-core#608
qbtrix/creative-core#609 qbtrix/creative-core#612
qbtrix/creative-core#613 qbtrix/creative-core#614
qbtrix/creative-core#615 qbtrix/creative-core#616
qbtrix/creative-core#617 qbtrix/creative-core#618
qbtrix/creative-core#619 qbtrix/creative-core#620
qbtrix/creative-core#621 qbtrix/creative-core#622
qbtrix/creative-core#623 qbtrix/creative-core#625
qbtrix/creative-core#626

Besides the pin:
- `project.yml` gets the same revision, as in #3023.
- `PhotoWorkspace.residentComposited` syncs a region's layer tiles
across the cores (through `PhotoLayerComposite.inParallel`), as the CPU
route renders its tiles. Steps keep their order. On a brush frame that
crosses two tiles the sync went from about 0.73 ms to 0.39 ms.
- `Tests/render-budgets.md` has the new 8K table and says where a
resident brush frame's time goes.

8K brush frame, release on the M2 Pro (median of three runs; the Mac was
loaded, with another Photo running): CPU 1.0 ms, GPU-resident 1.4 ms
(was 1.7). The GPU route is still behind the CPU. Most of what's left is
the display into the presenter's texture, which waits about 0.55 ms for
the GPU because the presenter draws from its own queue. Full redraw on
the resident route is 31 ms (was 35) and the first picture 166 ms (was
192).

Checked locally on macOS: `scripts/bump-core`'s steps at 0de5687. `swift
build --build-tests` passed. Every `*ModelTests` target passed except
four Library wall-clock budget tests
(`oneEditOnSixtyPhotosStaysInBudget`, `cullOneWithinBudget`,
`beginImportWithinBudget`, `orderRebuildWithinBudget`), which went over
in the parallel run on the loaded Mac and pass when run alone. The brush
perf suites (PhotoBrushDisplayTests, PhotoBrushLatencyTests,
PhotoBrushInkLatencyTests) passed. With the parallel sync on top,
`PhotoModelTests` and `PhotoRenderBackendTests` passed (647 tests), and
`PhotoRenderBudgetTests` ran in release with
`Tests/SuiteUITests/SegmentedTabsTests.swift` set aside, since it
doesn't link in release on SDK 27.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fast-lane Small green PR, auto-merge when eligible fast-lane-merged Merged by the fast lane; review after the fact

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant