Skip to content

Fix convert() from I;16N to I or F clipping 16-bit values - #9931

Merged
hugovk merged 2 commits into
python-pillow:mainfrom
Kayvan-Zahiri:fix-i16n-convert-i-f
Aug 31, 2026
Merged

Fix convert() from I;16N to I or F clipping 16-bit values#9931
hugovk merged 2 commits into
python-pillow:mainfrom
Kayvan-Zahiri:fix-i16n-convert-i-f

Conversation

@Kayvan-Zahiri

Copy link
Copy Markdown
Contributor

Changes proposed in this pull request:

  • Add I;16N rows for the I and F conversions in the Convert.c table, dispatching to the little or big endian handler the same way the existing L and RGB rows do.
  • Add a regression test covering I, F and the reverse direction for all four 16-bit modes.

I;16N is native endian, so on a little endian machine it stores the same bytes as I;16L. The table has had L rows for it since #6834 and an RGB row since #7920, but never I or F, so convert() fell back to normalizing through L and clipped anything above 255:

>>> Image.new("I;16N", (1, 1), 4660).convert("I").getpixel((0, 0))
255
>>> Image.new("I;16L", (1, 1), 4660).convert("I").getpixel((0, 0))
4660

Same for convert("F"), and for I to I;16N. This is the problem #274 reported for I;16, still present for I;16N.

The existing test_convert already calls convert("I;16N").convert("I"), but its source image only holds values up to 255, so it cannot catch this.

I;16N is native endian, so it should convert like I;16L on little endian
machines and I;16B on big endian ones, as the L and RGB rows in the
converter table already do. There were no I;16N rows for I or F, so
convert() fell back to normalizing through L and clipped values above 255.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BCtY1NAjuJ1jRvq5P6vSED
@radarhere radarhere added the 🤖-assisted AI-assisted label Aug 31, 2026
@radarhere

Copy link
Copy Markdown
Member

Hi. Would you mind letting us know a bit of the backstory for why you created this PR? I just want to check this was motivated by a need, rather than purely by ensuring all modes can be converted to all other modes.

@Kayvan-Zahiri

Copy link
Copy Markdown
Contributor Author

Fair question. No user report behind it, I found it comparing the four 16-bit modes against each other.

The reason I opened it anyway is that it isn't a missing conversion, it's a silent wrong answer. Same bytes, different result:

from PIL import Image
raw = (4660).to_bytes(2, "little")
for m in ("I;16L", "I;16N"):
    im = Image.frombytes(m, (1, 1), raw)
    print(m, im.tobytes().hex(), im.convert("I").getpixel((0, 0)))
# I;16L 3412 4660
# I;16N 3412 255

No exception, no warning, and I;16N is the only 16-bit mode that does it. Same shape as #274.

Thanks.

Comment thread Tests/test_mode_i16.py Outdated
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
@hugovk
hugovk enabled auto-merge (squash) August 31, 2026 11:27
@hugovk
hugovk merged commit c9e4cff into python-pillow:main Aug 31, 2026
57 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants