Skip to content

Commit c4d51fb

Browse files
committed
Added support for PPM arbitrary maxval in plain formats
1 parent 5051a29 commit c4d51fb

File tree

2 files changed

+55
-44
lines changed

2 files changed

+55
-44
lines changed

Tests/test_file_ppm.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,21 @@ def test_sanity():
2222
@pytest.mark.parametrize(
2323
"data, mode, pixels",
2424
(
25+
(b"P2 3 1 4 0 2 4", "L", (0, 128, 255)),
26+
(b"P2 3 1 257 0 128 257", "I", (0, 32640, 65535)),
27+
# P3 with maxval < 255
28+
(
29+
b"P3 3 1 17 0 1 2 8 9 10 15 16 17",
30+
"RGB",
31+
((0, 15, 30), (120, 135, 150), (225, 240, 255)),
32+
),
33+
# P3 with maxval > 255
34+
# Scale down to 255, since there is no RGB mode with more than 8-bit
35+
(
36+
b"P3 3 1 257 0 1 2 128 129 130 256 257 257",
37+
"RGB",
38+
((0, 1, 2), (127, 128, 129), (254, 255, 255)),
39+
),
2540
(b"P5 3 1 4 \x00\x02\x04", "L", (0, 128, 255)),
2641
(b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)),
2742
# P6 with maxval < 255
@@ -35,7 +50,6 @@ def test_sanity():
3550
),
3651
),
3752
# P6 with maxval > 255
38-
# Scale down to 255, since there is no RGB mode with more than 8-bit
3953
(
4054
b"P6 3 1 257 \x00\x00\x00\x01\x00\x02"
4155
b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF",

src/PIL/PpmImagePlugin.py

Lines changed: 40 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ def _open(self):
105105

106106
maxval = None
107107
decoder_name = "raw"
108+
if magic_number in (b"P1", b"P2", b"P3"):
109+
decoder_name = "ppm_plain"
108110
for ix in range(3):
109111
token = int(self._read_token())
110112
if ix == 0: # token is the x size
@@ -126,14 +128,13 @@ def _open(self):
126128
if maxval > 255 and mode == "L":
127129
self.mode = "I"
128130

129-
# If maxval matches a bit depth, use the raw decoder directly
130-
if maxval == 65535 and mode == "L":
131-
rawmode = "I;16B"
132-
elif maxval != 255:
133-
decoder_name = "ppm"
131+
if decoder_name != "ppm_plain":
132+
# If maxval matches a bit depth, use the raw decoder directly
133+
if maxval == 65535 and mode == "L":
134+
rawmode = "I;16B"
135+
elif maxval != 255:
136+
decoder_name = "ppm"
134137

135-
if magic_number in (b"P1", b"P2", b"P3"):
136-
decoder_name = "ppm_plain"
137138
args = (rawmode, 0, 1) if decoder_name == "raw" else (rawmode, maxval)
138139
self._size = xsize, ysize
139140
self.tile = [(decoder_name, (0, 0, xsize, ysize), self.fp.tell(), args)]
@@ -156,7 +157,7 @@ def _find_comment_end(self, block, start=0):
156157

157158
def _ignore_comments(self, block):
158159
"""
159-
Deletes comments from block.
160+
Delete comments from block.
160161
If comment does not end in this block, raises a flag.
161162
"""
162163
comment_spans = False
@@ -176,14 +177,14 @@ def _ignore_comments(self, block):
176177

177178
def _decode_bitonal(self):
178179
"""
179-
This is a separate method because the plain PBM format all data tokens
180-
are exactly one byte, and so the inter-token whitespace is optional.
180+
This is a separate method because in the plain PBM format, all data tokens are
181+
exactly one byte, so the inter-token whitespace is optional.
181182
"""
182-
decoded_data = bytearray()
183+
data = bytearray()
183184
total_bytes = self.state.xsize * self.state.ysize
184185

185186
comment_spans = False
186-
while len(decoded_data) != total_bytes:
187+
while len(data) != total_bytes:
187188
block = self._read_block() # read next block
188189
if not block:
189190
# eof
@@ -193,7 +194,7 @@ def _decode_bitonal(self):
193194
comment_end = self._find_comment_end(block)
194195
if comment_end != -1: # comment ends in this block
195196
block = block[comment_end + 1 :] # delete tail of previous comment
196-
comment_spans = False
197+
break
197198
else: # comment spans whole block
198199
block = self._read_block()
199200

@@ -203,19 +204,21 @@ def _decode_bitonal(self):
203204
for token in tokens:
204205
if token not in (48, 49):
205206
raise ValueError(f"Invalid token for this mode: {bytes([token])}")
206-
decoded_data = (decoded_data + tokens)[:total_bytes]
207+
data = (data + tokens)[:total_bytes]
207208
invert = bytes.maketrans(b"01", b"\xFF\x00")
208-
return decoded_data.translate(invert)
209+
return data.translate(invert)
209210

210-
def _decode_blocks(self, channels, depth, maxval):
211-
decoded_data = bytearray()
211+
def _decode_blocks(self, maxval):
212+
data = bytearray()
212213
max_len = 10
213-
bytes_per_sample = depth // 8
214-
total_bytes = self.state.xsize * self.state.ysize * channels * bytes_per_sample
214+
out_byte_count = 4 if self.mode == "I" else 1
215+
out_max = 65535 if self.mode == "I" else 255
216+
bands = Image.getmodebands(self.mode)
217+
total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count
215218

216219
comment_spans = False
217220
half_token = False
218-
while len(decoded_data) != total_bytes:
221+
while len(data) != total_bytes:
219222
block = self._read_block() # read next block
220223
if not block:
221224
if half_token:
@@ -251,31 +254,24 @@ def _decode_blocks(self, channels, depth, maxval):
251254
raise ValueError(
252255
f"Token too long found in data: {token[:max_len + 1]}"
253256
)
254-
token = int(token)
255-
if token > maxval:
256-
raise ValueError(f"Channel value too large for this mode: {token}")
257-
decoded_data += token.to_bytes(bytes_per_sample, "big")
258-
if len(decoded_data) == total_bytes: # finished!
257+
value = int(token)
258+
if value > maxval:
259+
raise ValueError(f"Channel value too large for this mode: {value}")
260+
value = round(value / maxval * out_max)
261+
data += o32(value) if self.mode == "I" else o8(value)
262+
if len(data) == total_bytes: # finished!
259263
break
260-
return decoded_data
264+
return data
261265

262266
def decode(self, buffer):
263-
rawmode, maxval = self.args
264-
265267
if self.mode == "1":
266-
decoded_data = self._decode_bitonal()
268+
data = self._decode_bitonal()
267269
rawmode = "1;8"
268-
elif self.mode == "L":
269-
decoded_data = self._decode_blocks(1, 8, maxval)
270-
elif self.mode == "I":
271-
if rawmode == "I;16B":
272-
decoded_data = self._decode_blocks(1, 16, maxval)
273-
elif rawmode == "I;32B":
274-
decoded_data = self._decode_blocks(1, 32, maxval)
275-
elif self.mode == "RGB":
276-
decoded_data = self._decode_blocks(3, 8, maxval)
277-
278-
self.set_as_raw(bytes(decoded_data), rawmode)
270+
else:
271+
maxval = self.args[-1]
272+
data = self._decode_blocks(maxval)
273+
rawmode = "I;32" if self.mode == "I" else self.mode
274+
self.set_as_raw(bytes(data), rawmode)
279275
return -1, 0
280276

281277

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

285281
def decode(self, buffer):
286282
data = bytearray()
287-
maxval = min(self.args[-1], 65535)
283+
maxval = self.args[-1]
288284
in_byte_count = 1 if maxval < 256 else 2
289285
out_byte_count = 4 if self.mode == "I" else 1
290286
out_max = 65535 if self.mode == "I" else 255
@@ -301,7 +297,7 @@ def decode(self, buffer):
301297
value = min(out_max, round(value / maxval * out_max))
302298
data += o32(value) if self.mode == "I" else o8(value)
303299
rawmode = "I;32" if self.mode == "I" else self.mode
304-
self.set_as_raw(bytes(data), (rawmode, 0, 1))
300+
self.set_as_raw(bytes(data), rawmode)
305301
return -1, 0
306302

307303

@@ -337,11 +333,12 @@ def _save(im, fp, filename):
337333
#
338334
# --------------------------------------------------------------------
339335

340-
Image.register_decoder("ppm_plain", PpmPlainDecoder)
336+
341337
Image.register_open(PpmImageFile.format, PpmImageFile, _accept)
342338
Image.register_save(PpmImageFile.format, _save)
343339

344340
Image.register_decoder("ppm", PpmDecoder)
341+
Image.register_decoder("ppm_plain", PpmPlainDecoder)
345342

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

0 commit comments

Comments
 (0)