Skip to content

Commit 6d432f2

Browse files
authored
Merge pull request #5121 from Piolie/PPMheaders
Improve handling of PPM headers
2 parents 92c26a7 + f7504b1 commit 6d432f2

File tree

2 files changed

+101
-36
lines changed

2 files changed

+101
-36
lines changed

Tests/test_file_ppm.py

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

44
import pytest
55

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

88
from .helper import assert_image_equal_tofile, assert_image_similar, hopper
99

@@ -50,15 +50,70 @@ def test_pnm(tmp_path):
5050
assert_image_equal_tofile(im, f)
5151

5252

53+
def test_magic(tmp_path):
54+
path = str(tmp_path / "temp.ppm")
55+
with open(path, "wb") as f:
56+
f.write(b"PyInvalid")
57+
58+
with pytest.raises(UnidentifiedImageError):
59+
with Image.open(path):
60+
pass
61+
62+
63+
def test_header_with_comments(tmp_path):
64+
path = str(tmp_path / "temp.ppm")
65+
with open(path, "wb") as f:
66+
f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n")
67+
68+
with Image.open(path) as im:
69+
assert im.size == (128, 128)
70+
71+
72+
def test_non_integer_token(tmp_path):
73+
path = str(tmp_path / "temp.ppm")
74+
with open(path, "wb") as f:
75+
f.write(b"P6\nTEST")
76+
77+
with pytest.raises(ValueError):
78+
with Image.open(path):
79+
pass
80+
81+
82+
def test_token_too_long(tmp_path):
83+
path = str(tmp_path / "temp.ppm")
84+
with open(path, "wb") as f:
85+
f.write(b"P6\n 01234567890")
86+
87+
with pytest.raises(ValueError) as e:
88+
with Image.open(path):
89+
pass
90+
91+
assert str(e.value) == "Token too long in file header: b'01234567890'"
92+
93+
94+
def test_too_many_colors(tmp_path):
95+
path = str(tmp_path / "temp.ppm")
96+
with open(path, "wb") as f:
97+
f.write(b"P6\n1 1\n1000\n")
98+
99+
with pytest.raises(ValueError) as e:
100+
with Image.open(path):
101+
pass
102+
103+
assert str(e.value) == "Too many colors for band: 1000"
104+
105+
53106
def test_truncated_file(tmp_path):
54107
path = str(tmp_path / "temp.pgm")
55108
with open(path, "w") as f:
56109
f.write("P6")
57110

58-
with pytest.raises(ValueError):
111+
with pytest.raises(ValueError) as e:
59112
with Image.open(path):
60113
pass
61114

115+
assert str(e.value) == "Reached EOF while reading header"
116+
62117

63118
def test_neg_ppm():
64119
# Storage.c accepted negative values for xsize, ysize. the

src/PIL/PpmImagePlugin.py

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -49,26 +49,46 @@ class PpmImageFile(ImageFile.ImageFile):
4949
format = "PPM"
5050
format_description = "Pbmplus image"
5151

52-
def _token(self, s=b""):
53-
while True: # read until next whitespace
52+
def _read_magic(self):
53+
magic = b""
54+
# read until whitespace or longest available magic number
55+
for _ in range(6):
5456
c = self.fp.read(1)
5557
if not c or c in b_whitespace:
5658
break
57-
if c > b"\x79":
58-
raise ValueError("Expected ASCII value, found binary")
59-
s = s + c
60-
if len(s) > 9:
61-
raise ValueError("Expected int, got > 9 digits")
62-
return s
59+
magic += c
60+
return magic
6361

64-
def _open(self):
62+
def _read_token(self):
63+
token = b""
64+
while len(token) <= 10: # read until next whitespace or limit of 10 characters
65+
c = self.fp.read(1)
66+
if not c:
67+
break
68+
elif c in b_whitespace: # token ended
69+
if not token:
70+
# skip whitespace at start
71+
continue
72+
break
73+
elif c == b"#":
74+
# ignores rest of the line; stops at CR, LF or EOF
75+
while self.fp.read(1) not in b"\r\n":
76+
pass
77+
continue
78+
token += c
79+
if not token:
80+
# Token was not even 1 byte
81+
raise ValueError("Reached EOF while reading header")
82+
elif len(token) > 10:
83+
raise ValueError(f"Token too long in file header: {token}")
84+
return token
6585

66-
# check magic
67-
s = self.fp.read(1)
68-
if s != b"P":
86+
def _open(self):
87+
magic_number = self._read_magic()
88+
try:
89+
mode = MODES[magic_number]
90+
except KeyError:
6991
raise SyntaxError("not a PPM file")
70-
magic_number = self._token(s)
71-
mode = MODES[magic_number]
7292

7393
self.custom_mimetype = {
7494
b"P4": "image/x-portable-bitmap",
@@ -83,29 +103,19 @@ def _open(self):
83103
self.mode = rawmode = mode
84104

85105
for ix in range(3):
86-
while True:
87-
while True:
88-
s = self.fp.read(1)
89-
if s not in b_whitespace:
90-
break
91-
if s == b"":
92-
raise ValueError("File does not extend beyond magic number")
93-
if s != b"#":
94-
break
95-
s = self.fp.readline()
96-
s = int(self._token(s))
97-
if ix == 0:
98-
xsize = s
99-
elif ix == 1:
100-
ysize = s
106+
token = int(self._read_token())
107+
if ix == 0: # token is the x size
108+
xsize = token
109+
elif ix == 1: # token is the y size
110+
ysize = token
101111
if mode == "1":
102112
break
103-
elif ix == 2:
104-
# maxgrey
105-
if s > 255:
113+
elif ix == 2: # token is maxval
114+
maxval = token
115+
if maxval > 255:
106116
if not mode == "L":
107-
raise ValueError(f"Too many colors for band: {s}")
108-
if s < 2 ** 16:
117+
raise ValueError(f"Too many colors for band: {token}")
118+
if maxval < 2 ** 16:
109119
self.mode = "I"
110120
rawmode = "I;16B"
111121
else:

0 commit comments

Comments
 (0)