Skip to content

Commit

Permalink
Added support for PPM arbitrary maxval in plain formats
Browse files Browse the repository at this point in the history
  • Loading branch information
radarhere committed Jun 13, 2022
1 parent 5051a29 commit c6646f7
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 40 deletions.
16 changes: 15 additions & 1 deletion Tests/test_file_ppm.py
Expand Up @@ -22,6 +22,21 @@ def test_sanity():
@pytest.mark.parametrize(
"data, mode, pixels",
(
(b"P2 3 1 4 0 2 4", "L", (0, 128, 255)),
(b"P2 3 1 257 0 128 257", "I", (0, 32640, 65535)),
# P3 with maxval < 255
(
b"P3 3 1 17 0 1 2 8 9 10 15 16 17",
"RGB",
((0, 15, 30), (120, 135, 150), (225, 240, 255)),
),
# P3 with maxval > 255
# Scale down to 255, since there is no RGB mode with more than 8-bit
(
b"P3 3 1 257 0 1 2 128 129 130 256 257 257",
"RGB",
((0, 1, 2), (127, 128, 129), (254, 255, 255)),
),
(b"P5 3 1 4 \x00\x02\x04", "L", (0, 128, 255)),
(b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)),
# P6 with maxval < 255
Expand All @@ -35,7 +50,6 @@ def test_sanity():
),
),
# P6 with maxval > 255
# Scale down to 255, since there is no RGB mode with more than 8-bit
(
b"P6 3 1 257 \x00\x00\x00\x01\x00\x02"
b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF",
Expand Down
74 changes: 35 additions & 39 deletions src/PIL/PpmImagePlugin.py
Expand Up @@ -105,6 +105,8 @@ def _open(self):

maxval = None
decoder_name = "raw"
if magic_number in (b"P1", b"P2", b"P3"):
decoder_name = "ppm_plain"
for ix in range(3):
token = int(self._read_token())
if ix == 0: # token is the x size
Expand All @@ -126,14 +128,13 @@ def _open(self):
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"
if decoder_name != "ppm_plain":
# 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"

if magic_number in (b"P1", b"P2", b"P3"):
decoder_name = "ppm_plain"
args = (rawmode, 0, 1) if decoder_name == "raw" else (rawmode, maxval)
self._size = xsize, ysize
self.tile = [(decoder_name, (0, 0, xsize, ysize), self.fp.tell(), args)]
Expand Down Expand Up @@ -179,11 +180,11 @@ def _decode_bitonal(self):
This is a separate method because the plain PBM format all data tokens
are exactly one byte, and so the inter-token whitespace is optional.
"""
decoded_data = bytearray()
data = bytearray()
total_bytes = self.state.xsize * self.state.ysize

comment_spans = False
while len(decoded_data) != total_bytes:
while len(data) != total_bytes:
block = self._read_block() # read next block
if not block:
# eof
Expand All @@ -203,19 +204,21 @@ def _decode_bitonal(self):
for token in tokens:
if token not in (48, 49):
raise ValueError(f"Invalid token for this mode: {bytes([token])}")
decoded_data = (decoded_data + tokens)[:total_bytes]
data = (data + tokens)[:total_bytes]
invert = bytes.maketrans(b"01", b"\xFF\x00")
return decoded_data.translate(invert)
return data.translate(invert)

def _decode_blocks(self, channels, depth, maxval):
decoded_data = bytearray()
def _decode_blocks(self, maxval):
data = bytearray()
max_len = 10
bytes_per_sample = depth // 8
total_bytes = self.state.xsize * self.state.ysize * channels * bytes_per_sample
out_byte_count = 4 if self.mode == "I" else 1
out_max = 65535 if self.mode == "I" else 255
bands = Image.getmodebands(self.mode)
total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count

comment_spans = False
half_token = False
while len(decoded_data) != total_bytes:
while len(data) != total_bytes:
block = self._read_block() # read next block
if not block:
if half_token:
Expand Down Expand Up @@ -251,31 +254,24 @@ def _decode_blocks(self, channels, depth, maxval):
raise ValueError(
f"Token too long found in data: {token[:max_len + 1]}"
)
token = int(token)
if token > maxval:
raise ValueError(f"Channel value too large for this mode: {token}")
decoded_data += token.to_bytes(bytes_per_sample, "big")
if len(decoded_data) == total_bytes: # finished!
value = int(token)
if value > maxval:
raise ValueError(f"Channel value too large for this mode: {value}")
value = round(value / maxval * out_max)
data += o32(value) if self.mode == "I" else o8(value)
if len(data) == total_bytes: # finished!
break
return decoded_data
return data

def decode(self, buffer):
rawmode, maxval = self.args

if self.mode == "1":
decoded_data = self._decode_bitonal()
data = self._decode_bitonal()
rawmode = "1;8"
elif self.mode == "L":
decoded_data = self._decode_blocks(1, 8, maxval)
elif self.mode == "I":
if rawmode == "I;16B":
decoded_data = self._decode_blocks(1, 16, maxval)
elif rawmode == "I;32B":
decoded_data = self._decode_blocks(1, 32, maxval)
elif self.mode == "RGB":
decoded_data = self._decode_blocks(3, 8, maxval)

self.set_as_raw(bytes(decoded_data), rawmode)
else:
maxval = self.args[-1]
data = self._decode_blocks(maxval)
rawmode = "I;32" if self.mode == "I" else self.mode
self.set_as_raw(bytes(data), rawmode)
return -1, 0


Expand All @@ -284,7 +280,7 @@ class PpmDecoder(ImageFile.PyDecoder):

def decode(self, buffer):
data = bytearray()
maxval = min(self.args[-1], 65535)
maxval = self.args[-1]
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
Expand All @@ -301,7 +297,7 @@ def decode(self, buffer):
value = min(out_max, round(value / maxval * out_max))
data += o32(value) if self.mode == "I" else o8(value)
rawmode = "I;32" if self.mode == "I" else self.mode
self.set_as_raw(bytes(data), (rawmode, 0, 1))
self.set_as_raw(bytes(data), rawmode)
return -1, 0


Expand Down Expand Up @@ -337,11 +333,11 @@ def _save(im, fp, filename):
#
# --------------------------------------------------------------------

Image.register_decoder("ppm_plain", PpmPlainDecoder)
Image.register_open(PpmImageFile.format, PpmImageFile, _accept)
Image.register_save(PpmImageFile.format, _save)

Image.register_decoder("ppm", PpmDecoder)
Image.register_decoder("ppm_plain", PpmPlainDecoder)

Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm"])

Expand Down

0 comments on commit c6646f7

Please sign in to comment.