Skip to content

feat: YCbCr chroma subsampling for non-JPEG compression#388

Open
aarshivv wants to merge 2 commits into
image-rs:mainfrom
aarshivv:feat/ycbcr-subsampling-non-jpeg
Open

feat: YCbCr chroma subsampling for non-JPEG compression#388
aarshivv wants to merge 2 commits into
image-rs:mainfrom
aarshivv:feat/ycbcr-subsampling-non-jpeg

Conversation

@aarshivv
Copy link
Copy Markdown

Follow-up to #387. Brings back 83409cb (reverted in 85215e8) with two extra patches to address gaps.

First commit is a clean cherry-pick of 83409cbYCbCrUpsamplingReader that converts MCU-block layout into full-res YCbCr triples, wired into expand_chunk for non-JPEG YCbCr. Libtiffpic hashes updated for tests/libtiffpic/dscf0013.tif (uncompressed 4:2:2) and tests/libtiffpic/ycbcr-cat.tif (LZW 4:2:0).

Second commit does two things:

  • Rejects predictor != None with subsampling. The pipeline currently reverses the predictor after upsampling, but the predictor was encoded against the stored block layout — reversing it on upsampled data is wrong. Latent bug in the original that isn't exercised because both libtiffpic test files use predictor=1. Erroring out keeps the scope tight; reordering prediction to run before upsampling can be a follow-up.
  • Adds unit tests for YCbCrUpsamplingReader in isolation: (2,2), (2,1), (1,2), (4,4), plus non-divisible width/height where the final block column/row gets clipped.

Planar YCbCr + subsampling stays rejected as in the cherry-pick.

cargo test, cargo clippy --all-targets, cargo test --test decode_libtiffpic all green (61/61).

lilith and others added 2 commits April 17, 2026 22:29
Add YCbCrUpsamplingReader that converts chroma-subsampled YCbCr block
data to full-resolution 3-bytes-per-pixel output. Each block of h×v
pixels stores h*v Y samples plus one shared Cb and Cr sample; the
reader expands these into per-pixel (Y, Cb, Cr) triples.

This enables decoding YCbCr TIFFs with any supported compression
method (uncompressed, LZW, Deflate, etc.), not just JPEG. Two more
libtiffpic test images now decode with verified hashes:
- dscf0013.tif (uncompressed YCbCr 2,1 subsampling)
- ycbcr-cat.tif (LZW-compressed YCbCr 2,2 subsampling)

The remaining two YCbCr test images (smallliz.tif, zackthecat.tif)
use Old JPEG compression (type 6) which is a separate unsupported
feature.
… tests

The YCbCr upsampling pipeline applies predictor reversal after the
upsampling reader, so a non-None predictor ends up being reversed on the
expanded pixel layout rather than the stored MCU-block layout it was
encoded against. Reject the combination with ChromaSubsampling until
prediction can run before the upsampling step.

Add unit tests covering the YCbCrUpsamplingReader in isolation:
(2,2), (2,1), (1,2), and (4,4) factors, plus non-divisible width and
height cases where the final block column/row is clipped.
@aarshivv
Copy link
Copy Markdown
Author

CI failure here is unrelated — src/decoder/logluv.rs:95 trips a new clippy lint (while_let_loop, enabled by default in Rust 1.95). Main fails the same check on my machine. Happy to open a separate PR for the one-line fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants