Skip to content

Commit aad3af4

Browse files
authored
Merge pull request #5242 from Piolie/plainPPM
Add support for decoding plain PPM formats
2 parents c083ead + 1bac1cf commit aad3af4

File tree

10 files changed

+304
-28
lines changed

10 files changed

+304
-28
lines changed

Tests/images/hopper_16bit.pgm

32 KB
Binary file not shown.

Tests/images/hopper_16bit_plain.pgm

Lines changed: 4 additions & 0 deletions
Large diffs are not rendered by default.

Tests/images/hopper_1bit.pbm

2.01 KB
Binary file not shown.

Tests/images/hopper_1bit_plain.pbm

Lines changed: 14 additions & 0 deletions
Large diffs are not rendered by default.

Tests/images/hopper_8bit.pgm

16 KB
Binary file not shown.

Tests/images/hopper_8bit.ppm

48 KB
Binary file not shown.

Tests/images/hopper_8bit_plain.pgm

Lines changed: 4 additions & 0 deletions
Large diffs are not rendered by default.

Tests/images/hopper_8bit_plain.ppm

Lines changed: 4 additions & 0 deletions
Large diffs are not rendered by default.

Tests/test_file_ppm.py

Lines changed: 119 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import pytest
55

6-
from PIL import Image, UnidentifiedImageError
6+
from PIL import Image, PpmImagePlugin
77

88
from .helper import assert_image_equal_tofile, assert_image_similar, hopper
99

@@ -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",
@@ -85,14 +99,111 @@ def test_pnm(tmp_path):
8599
assert_image_equal_tofile(im, f)
86100

87101

88-
def test_magic(tmp_path):
102+
@pytest.mark.parametrize(
103+
"plain_path, raw_path",
104+
(
105+
(
106+
"Tests/images/hopper_1bit_plain.pbm", # P1
107+
"Tests/images/hopper_1bit.pbm", # P4
108+
),
109+
(
110+
"Tests/images/hopper_8bit_plain.pgm", # P2
111+
"Tests/images/hopper_8bit.pgm", # P5
112+
),
113+
(
114+
"Tests/images/hopper_8bit_plain.ppm", # P3
115+
"Tests/images/hopper_8bit.ppm", # P6
116+
),
117+
),
118+
)
119+
def test_plain(plain_path, raw_path):
120+
with Image.open(plain_path) as im:
121+
assert_image_equal_tofile(im, raw_path)
122+
123+
124+
def test_16bit_plain_pgm():
125+
# P2 with maxval 2 ** 16 - 1
126+
with Image.open("Tests/images/hopper_16bit_plain.pgm") as im:
127+
assert im.mode == "I"
128+
assert im.size == (128, 128)
129+
assert im.get_format_mimetype() == "image/x-portable-graymap"
130+
131+
# P5 with maxval 2 ** 16 - 1
132+
assert_image_equal_tofile(im, "Tests/images/hopper_16bit.pgm")
133+
134+
135+
@pytest.mark.parametrize(
136+
"header, data, comment_count",
137+
(
138+
(b"P1\n2 2", b"1010", 10**6),
139+
(b"P2\n3 1\n4", b"0 2 4", 1),
140+
(b"P3\n2 2\n255", b"0 0 0 001 1 1 2 2 2 255 255 255", 10**6),
141+
),
142+
)
143+
def test_plain_data_with_comment(tmp_path, header, data, comment_count):
144+
path1 = str(tmp_path / "temp1.ppm")
145+
path2 = str(tmp_path / "temp2.ppm")
146+
comment = b"# comment" * comment_count
147+
with open(path1, "wb") as f1, open(path2, "wb") as f2:
148+
f1.write(header + b"\n\n" + data)
149+
f2.write(header + b"\n" + comment + b"\n" + data + comment)
150+
151+
with Image.open(path1) as im:
152+
assert_image_equal_tofile(im, path2)
153+
154+
155+
@pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n"))
156+
def test_plain_truncated_data(tmp_path, data):
89157
path = str(tmp_path / "temp.ppm")
90158
with open(path, "wb") as f:
91-
f.write(b"PyInvalid")
159+
f.write(data)
92160

93-
with pytest.raises(UnidentifiedImageError):
94-
with Image.open(path):
95-
pass
161+
with Image.open(path) as im:
162+
with pytest.raises(ValueError):
163+
im.load()
164+
165+
166+
@pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A"))
167+
def test_plain_invalid_data(tmp_path, data):
168+
path = str(tmp_path / "temp.ppm")
169+
with open(path, "wb") as f:
170+
f.write(data)
171+
172+
with Image.open(path) as im:
173+
with pytest.raises(ValueError):
174+
im.load()
175+
176+
177+
@pytest.mark.parametrize(
178+
"data",
179+
(
180+
b"P3\n128 128\n255\n012345678910", # half token too long
181+
b"P3\n128 128\n255\n012345678910 0", # token too long
182+
),
183+
)
184+
def test_plain_ppm_token_too_long(tmp_path, data):
185+
path = str(tmp_path / "temp.ppm")
186+
with open(path, "wb") as f:
187+
f.write(data)
188+
189+
with Image.open(path) as im:
190+
with pytest.raises(ValueError):
191+
im.load()
192+
193+
194+
def test_plain_ppm_value_too_large(tmp_path):
195+
path = str(tmp_path / "temp.ppm")
196+
with open(path, "wb") as f:
197+
f.write(b"P3\n128 128\n255\n256")
198+
199+
with Image.open(path) as im:
200+
with pytest.raises(ValueError):
201+
im.load()
202+
203+
204+
def test_magic():
205+
with pytest.raises(SyntaxError):
206+
PpmImagePlugin.PpmImageFile(fp=BytesIO(b"PyInvalid"))
96207

97208

98209
def test_header_with_comments(tmp_path):
@@ -114,7 +225,7 @@ def test_non_integer_token(tmp_path):
114225
pass
115226

116227

117-
def test_token_too_long(tmp_path):
228+
def test_header_token_too_long(tmp_path):
118229
path = str(tmp_path / "temp.ppm")
119230
with open(path, "wb") as f:
120231
f.write(b"P6\n 01234567890")

0 commit comments

Comments
 (0)