Added support for arbitrary maxval

This commit is contained in:
Andrew Murray 2022-03-09 22:29:45 +11:00
parent 397a940995
commit 4283a604c0
2 changed files with 101 additions and 28 deletions

View File

@ -13,16 +13,64 @@ TEST_FILE = "Tests/images/hopper.ppm"
def test_sanity():
with Image.open(TEST_FILE) as im:
im.load()
assert im.mode == "RGB"
assert im.size == (128, 128)
assert im.format, "PPM"
assert im.format == "PPM"
assert im.get_format_mimetype() == "image/x-portable-pixmap"
def test_arbitrary_maxval():
# P5 L mode
fp = BytesIO(b"P5 3 1 4 \x00\x02\x04")
with Image.open(fp) as im:
assert im.size == (3, 1)
assert im.mode == "L"
px = im.load()
assert tuple(px[x, 0] for x in range(3)) == (0, 128, 255)
# P5 I mode
fp = BytesIO(b"P5 3 1 257 \x00\x00\x00\x80\x01\x01")
with Image.open(fp) as im:
assert im.size == (3, 1)
assert im.mode == "I"
px = im.load()
assert tuple(px[x, 0] for x in range(3)) == (0, 32640, 65535)
# P6 with maxval < 255
fp = BytesIO(b"P6 3 1 17 \x00\x01\x02\x08\x09\x0A\x0F\x10\x11")
with Image.open(fp) as im:
assert im.size == (3, 1)
assert im.mode == "RGB"
px = im.load()
assert tuple(px[x, 0] for x in range(3)) == (
(0, 15, 30),
(120, 135, 150),
(225, 240, 255),
)
# P6 with maxval > 255
# Scale down to 255, since there is no RGB mode with more than 8-bit
fp = BytesIO(
b"P6 3 1 257 \x00\x00\x00\x01\x00\x02"
b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF"
)
with Image.open(fp) as im:
assert im.size == (3, 1)
assert im.mode == "RGB"
px = im.load()
assert tuple(px[x, 0] for x in range(3)) == (
(0, 1, 2),
(127, 128, 129),
(254, 255, 255),
)
def test_16bit_pgm():
with Image.open("Tests/images/16_bit_binary.pgm") as im:
im.load()
assert im.mode == "I"
assert im.size == (20, 100)
assert im.get_format_mimetype() == "image/x-portable-graymap"
@ -32,8 +80,6 @@ def test_16bit_pgm():
def test_16bit_pgm_write(tmp_path):
with Image.open("Tests/images/16_bit_binary.pgm") as im:
im.load()
f = str(tmp_path / "temp.pgm")
im.save(f, "PPM")
@ -91,19 +137,8 @@ def test_token_too_long(tmp_path):
assert str(e.value) == "Token too long in file header: b'01234567890'"
def test_too_many_colors(tmp_path):
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"P6\n1 1\n1000\n")
with pytest.raises(ValueError) as e:
with Image.open(path):
pass
assert str(e.value) == "Too many colors for band: 1000"
def test_truncated_file(tmp_path):
# Test EOF in header
path = str(tmp_path / "temp.pgm")
with open(path, "w") as f:
f.write("P6")
@ -114,6 +149,12 @@ def test_truncated_file(tmp_path):
assert str(e.value) == "Reached EOF while reading header"
# Test EOF for PyDecoder
fp = BytesIO(b"P5 3 1 4")
with Image.open(fp) as im:
with pytest.raises(ValueError):
im.load()
def test_neg_ppm():
# Storage.c accepted negative values for xsize, ysize. the

View File

@ -16,6 +16,9 @@
from . import Image, ImageFile
from ._binary import i16be as i16
from ._binary import o8
from ._binary import o32le as o32
#
# --------------------------------------------------------------------
@ -102,6 +105,7 @@ class PpmImageFile(ImageFile.ImageFile):
else:
self.mode = rawmode = mode
decoder_name = "raw"
for ix in range(3):
token = int(self._read_token())
if ix == 0: # token is the x size
@ -112,18 +116,44 @@ class PpmImageFile(ImageFile.ImageFile):
break
elif ix == 2: # token is maxval
maxval = token
if maxval > 255:
if not mode == "L":
raise ValueError(f"Too many colors for band: {token}")
if maxval < 2**16:
self.mode = "I"
rawmode = "I;16B"
else:
self.mode = "I"
rawmode = "I;32B"
if maxval > 255 and mode == "L":
self.mode = "I"
# If maxval matches a bit depth,
# use the raw decoder directly
if maxval == 65535 and mode == "L":
rawmode = "I;16B"
elif maxval != 255:
decoder_name = "ppm"
args = (rawmode, 0, 1) if decoder_name == "raw" else (rawmode, maxval)
self._size = xsize, ysize
self.tile = [("raw", (0, 0, xsize, ysize), self.fp.tell(), (rawmode, 0, 1))]
self.tile = [(decoder_name, (0, 0, xsize, ysize), self.fp.tell(), args)]
class PpmDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer):
data = bytearray()
maxval = min(self.args[-1], 65535)
in_byte_count = 1 if maxval < 256 else 2
out_byte_count = 4 if self.mode == "I" else 1
out_max = 65535 if self.mode == "I" else 255
bands = Image.getmodebands(self.mode)
while len(data) < self.state.xsize * self.state.ysize * bands * out_byte_count:
pixels = self.fd.read(in_byte_count * bands)
if len(pixels) < in_byte_count * bands:
# eof
break
for b in range(bands):
value = (
pixels[b] if in_byte_count == 1 else i16(pixels, b * in_byte_count)
)
value = min(out_max, round(value / maxval * out_max))
data += o32(value) if self.mode == "I" else o8(value)
self.set_as_raw(bytes(data), (self.mode, 0, 1))
return -1, 0
#
@ -149,7 +179,7 @@ def _save(im, fp, filename):
fp.write(head + ("\n%d %d\n" % im.size).encode("ascii"))
if head == b"P6":
fp.write(b"255\n")
if head == b"P5":
elif head == b"P5":
if rawmode == "L":
fp.write(b"255\n")
elif rawmode == "I;16B":
@ -169,6 +199,8 @@ def _save(im, fp, filename):
Image.register_open(PpmImageFile.format, PpmImageFile, _accept)
Image.register_save(PpmImageFile.format, _save)
Image.register_decoder("ppm", PpmDecoder)
Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm"])
Image.register_mime(PpmImageFile.format, "image/x-portable-anymap")