Skip to content

BUG: Avoid flattening single-colorant /DeviceN images to grayscale#3916

Open
pablopupo wants to merge 1 commit into
py-pdf:mainfrom
pablopupo:fix/3302-devicen-single-colorant-gray
Open

BUG: Avoid flattening single-colorant /DeviceN images to grayscale#3916
pablopupo wants to merge 1 commit into
py-pdf:mainfrom
pablopupo:fix/3302-devicen-single-colorant-gray

Conversation

@pablopupo

Copy link
Copy Markdown

Fixes #3302.

_get_image_mode has a /Separation branch that resolves its Pillow mode by recursing into the color space's alternate space, and a /DeviceN branch a few lines below that does mostly the same thing except for one special case. Any single colorant paired with a /DeviceCMYK alternate went straight to grayscale, warning "Color %s converted to Gray" for every colorant besides /Black. That shortcut was added in #2322 for K-only scanner images, where grayscale is exact, but it swallowed every other single-colorant DeviceN image the same way, which is what the reporter's /Cyan image was hitting.

The fix narrows that shortcut to require the colorant literally be /Black before returning grayscale directly. Everything else now falls through to the same recursive alternate-space lookup /Separation already uses (that recursion resolves a /DeviceCMYK alternate straight to CMYK on its own, no new codepath needed). I also checked whether _apply_decode's Separation-only decode-inversion override should be extended to DeviceN too, and it should not. _image_from_bytes's existing byte-replication fallback already gets the light-to-dark direction right on its own, and stacking Separation's decode reversal on top would double-invert it. Left it alone since it is a separate, pre-existing issue outside the scope of #3302.

I did not have a working copy of the reporter's PDF (an issuu.com viewer link with time-limited AWS download links, 50 MB), so I built a synthetic single-colorant /Cyan DeviceN CMYK image to verify. Before the fix, the extracted pixels for tints 0/128/255 come out (255,255,255), (127,127,127), (0,0,0), identical byte for byte to what a /Black-colorant image produces (mode L, plus the warning). After the fix, they come out (255,255,255), (63,63,63), (0,0,0) in mode CMYK with no warning. The /Black case is unchanged, still mode L, byte for byte.

Added two regression tests in tests/generic/test_image_xobject.py, one at the _get_image_mode level and one round-tripping through _xobj_to_image. Ran the full test_filters.py and tests/generic/ suites (162 passed, 5 pre-existing skips) plus ruff, mypy, and pyupgrade, all clean.

A /DeviceN color space with one colorant and a /DeviceCMYK alternate
was always treated as grayscale, even for colorants other than
/Black, logging a "converted to Gray" warning. /Separation already
resolves this by deriving the mode from the alternate color space
instead, so /DeviceN now falls through to that same recursive lookup
for any colorant besides /Black, which is the one case that maps onto
grayscale exactly.

Closes py-pdf#3302
@codecov

codecov Bot commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.88%. Comparing base (efc511b) to head (f5c371f).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3916      +/-   ##
==========================================
+ Coverage   97.86%   97.88%   +0.01%     
==========================================
  Files          57       57              
  Lines       10738    10736       -2     
  Branches     2006     2005       -1     
==========================================
  Hits        10509    10509              
+ Misses        127      126       -1     
+ Partials      102      101       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

Image extraction: "Color /Cyan converted to Gray"

1 participant