Say what the last large peak is: a four-component JPEG's chroma - #43
Merged
Merged
Conversation
§14 left two large peaks in the converted bucket. §16 answered the 110. This is the 33, and it is the last one in this corpus that is ours. gh-openpdf/objectXref.pdf's 258x258 DeviceCMYK JPEG is YCCK -- Adobe APP14 transform 2 -- with luma and black at 2x2 and the two chroma planes at 1x1. Separating the decode from the conversion settles it. pdfimages -tiff writes a DeviceCMYK picture as a four-sample separated TIFF, which is poppler's own CMYK before any colour conversion. Against what render produces for the same stream, 91.2% of samples agree within one level and the worst per plane is C 36, M 18, Y 21 -- and K 1. K is the control and it is the whole argument: it is carried at full resolution, never upsampled, and it is right. Sorting every pixel by how fast the chroma changes across neighbouring blocks sorts the error with it, 5 to 36 on the C plane, while K stays at 0 or 1 in every band. A decode that were simply different would move K too. chroma.go replicates libjpeg's fancy upsampling term for term and §14 measured it at peak 4 for three-component pictures. It never runs here: Go's image/jpeg applyBlack merges the four planes itself through imageutil.DrawYCbCr, which samples chroma by replication, and hands back an image.CMYK. The planes are gone before render is given anything. Whether render could put it back is measured rather than guessed. Inverting the conversion recovers the per-block chroma exactly where nothing clipped: over 14244 unclipped blocks, 14215 show a spread of 0.0 and the worst is 0.5. But 7.8% of blocks have every pixel clipped on some channel and there the chroma is gone. Re-upsampling what can be recovered takes the worst CMY error from 36 to 26, and the 26 sits entirely in the blocks that cannot be recovered. So it is a hundred lines for a partial improvement on one picture, bounded by information another library discarded, and it is named rather than done -- as §15 does for the JPEG 2000 defect. §5 also gains a witness. It says the two-minute bound is unexercised by images on this corpus, and that its value is keeping the next document from stopping a sweep dead with nothing to show. A throwaway census run without a bound on 2026-09-20 stalled twelve minutes on one 2496-byte document -- qpdf_qtest_qpdf_shared-unnamed-field.pdf, the very file §5 names.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
§14 left two large peaks in the
convertedbucket. §16 answered the 110. This is the 33, and it is the last one in this corpus that is ours.The picture
gh-openpdf/objectXref.pdf’s 258×258DeviceCMYKJPEG is YCCK — Adobe APP14 transform 2 — with luma and the black plate at 2×2 and the two chroma planes at 1×1, so the chroma is subsampled by two in each direction.Separating the decode from the conversion
pdfimages -tiffwrites aDeviceCMYKpicture as a four-sample separated TIFF: poppler’s own CMYK, before any colour conversion. Against the samplesrenderproduces for the same stream, 91.2% agree within one level and the worst per plane is:K is the control and it is the whole argument. It is carried at full resolution, never upsampled, and it is right. Sorting every pixel by how fast the chroma changes across neighbouring blocks sorts the error with it:
A decode that were simply different would move K too. This one does not.
Why
chroma.goreplicates libjpeg’s fancy upsampling term for term, and §14 measured the result: three-component pictures peak at 4 whether their chroma is subsampled or not. It never runs here. Go’simage/jpegapplyBlack(reader.go:675) merges the four planes itself throughimageutil.DrawYCbCr, which samples chroma by replication, and hands back an*image.CMYK. The planes are gone beforerenderis given anything.Whether it could be put back — measured, not guessed
Inverting the conversion recovers the per-block chroma exactly where nothing clipped: over 14 244 unclipped blocks, 14 215 show a spread of 0.0 within the block and the worst is 0.5, which is the rounding of Go’s integer conversion. But 7.8% of blocks have every pixel clipped on some channel, and there the chroma is genuinely gone.
Re-upsampling what can be recovered, with libjpeg’s own filter, takes the worst CMY error from 36 to 26 — and the 26 that remains sits entirely in the blocks that cannot be recovered.
So a reconstruction is around a hundred lines for a partial improvement on one picture in this corpus, bounded by information another library discarded. It is named rather than done, as §15 does for the JPEG 2000 defect, and the honest fix is the same shape: a four-component decode that hands back its planes.
§5 gains a witness
§5 says the two-minute bound is unexercised by
imageson this corpus, and that its value is keeping the next document from stopping a sweep dead with nothing to show for it. A throwaway census run without a bound stalled twelve minutes on one 2496-byte document —qpdf_qtest_qpdf_shared-unnamed-field.pdf, the very file §5 names.No records change; this is the reading of the run already landed.
🤖 Generated with Claude Code