Skip to content

Commit 48b0be2

Browse files
authored
Merge pull request #6879 from Yay295/eps_plugin_perf
2 parents 3cfdef3 + 61d0c8f commit 48b0be2

File tree

6 files changed

+262
-100
lines changed

6 files changed

+262
-100
lines changed

Tests/test_deprecate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def test_version(version, expected):
2929

3030

3131
def test_unknown_version():
32-
expected = r"Unknown removal version, update PIL\._deprecate\?"
32+
expected = r"Unknown removal version: 12345. Update PIL\._deprecate\?"
3333
with pytest.raises(ValueError, match=expected):
3434
_deprecate.deprecate("Old thing", 12345, "new thing")
3535

Tests/test_file_eps.py

Lines changed: 145 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -28,34 +28,65 @@
2828
# EPS test files with binary preview
2929
FILE3 = "Tests/images/binary_preview_map.eps"
3030

31+
# Three unsigned 32bit little-endian values:
32+
# 0xC6D3D0C5 magic number
33+
# byte position of start of postscript section (12)
34+
# byte length of postscript section (0)
35+
# this byte length isn't valid, but we don't read it
36+
simple_binary_header = b"\xc5\xd0\xd3\xc6\x0c\x00\x00\x00\x00\x00\x00\x00"
37+
38+
# taken from page 8 of the specification
39+
# https://web.archive.org/web/20220120164601/https://www.adobe.com/content/dam/acom/en/devnet/actionscript/articles/5002.EPSF_Spec.pdf
40+
simple_eps_file = (
41+
b"%!PS-Adobe-3.0 EPSF-3.0",
42+
b"%%BoundingBox: 5 5 105 105",
43+
b"10 setlinewidth",
44+
b"10 10 moveto",
45+
b"0 90 rlineto 90 0 rlineto 0 -90 rlineto closepath",
46+
b"stroke",
47+
)
48+
simple_eps_file_with_comments = (
49+
simple_eps_file[:1]
50+
+ (
51+
b"%%Comment1: Some Value",
52+
b"%%SecondComment: Another Value",
53+
)
54+
+ simple_eps_file[1:]
55+
)
56+
simple_eps_file_without_version = simple_eps_file[1:]
57+
simple_eps_file_without_boundingbox = simple_eps_file[:1] + simple_eps_file[2:]
58+
simple_eps_file_with_invalid_boundingbox = (
59+
simple_eps_file[:1] + (b"%%BoundingBox: a b c d",) + simple_eps_file[2:]
60+
)
61+
simple_eps_file_with_invalid_boundingbox_valid_imagedata = (
62+
simple_eps_file_with_invalid_boundingbox + (b"%ImageData: 100 100 8 3",)
63+
)
64+
simple_eps_file_with_long_ascii_comment = (
65+
simple_eps_file[:2] + (b"%%Comment: " + b"X" * 300,) + simple_eps_file[2:]
66+
)
67+
simple_eps_file_with_long_binary_data = (
68+
simple_eps_file[:2]
69+
+ (
70+
b"%%BeginBinary: 300",
71+
b"\0" * 300,
72+
b"%%EndBinary",
73+
)
74+
+ simple_eps_file[2:]
75+
)
3176

32-
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
33-
def test_sanity():
34-
# Regular scale
35-
with Image.open(FILE1) as image1:
36-
image1.load()
37-
assert image1.mode == "RGB"
38-
assert image1.size == (460, 352)
39-
assert image1.format == "EPS"
40-
41-
with Image.open(FILE2) as image2:
42-
image2.load()
43-
assert image2.mode == "RGB"
44-
assert image2.size == (360, 252)
45-
assert image2.format == "EPS"
46-
47-
# Double scale
48-
with Image.open(FILE1) as image1_scale2:
49-
image1_scale2.load(scale=2)
50-
assert image1_scale2.mode == "RGB"
51-
assert image1_scale2.size == (920, 704)
52-
assert image1_scale2.format == "EPS"
5377

54-
with Image.open(FILE2) as image2_scale2:
55-
image2_scale2.load(scale=2)
56-
assert image2_scale2.mode == "RGB"
57-
assert image2_scale2.size == (720, 504)
58-
assert image2_scale2.format == "EPS"
78+
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
79+
@pytest.mark.parametrize(
80+
("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252)))
81+
)
82+
@pytest.mark.parametrize("scale", (1, 2))
83+
def test_sanity(filename, size, scale):
84+
expected_size = tuple(s * scale for s in size)
85+
with Image.open(filename) as image:
86+
image.load(scale=scale)
87+
assert image.mode == "RGB"
88+
assert image.size == expected_size
89+
assert image.format == "EPS"
5990

6091

6192
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@@ -69,11 +100,72 @@ def test_load():
69100

70101
def test_invalid_file():
71102
invalid_file = "Tests/images/flower.jpg"
72-
73103
with pytest.raises(SyntaxError):
74104
EpsImagePlugin.EpsImageFile(invalid_file)
75105

76106

107+
def test_binary_header_only():
108+
data = io.BytesIO(simple_binary_header)
109+
with pytest.raises(SyntaxError, match='EPS header missing "%!PS-Adobe" comment'):
110+
EpsImagePlugin.EpsImageFile(data)
111+
112+
113+
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
114+
def test_missing_version_comment(prefix):
115+
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version))
116+
with pytest.raises(SyntaxError):
117+
EpsImagePlugin.EpsImageFile(data)
118+
119+
120+
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
121+
def test_missing_boundingbox_comment(prefix):
122+
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox))
123+
with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'):
124+
EpsImagePlugin.EpsImageFile(data)
125+
126+
127+
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
128+
def test_invalid_boundingbox_comment(prefix):
129+
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox))
130+
with pytest.raises(OSError, match="cannot determine EPS bounding box"):
131+
EpsImagePlugin.EpsImageFile(data)
132+
133+
134+
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
135+
def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix):
136+
data = io.BytesIO(
137+
prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata)
138+
)
139+
with Image.open(data) as img:
140+
assert img.mode == "RGB"
141+
assert img.size == (100, 100)
142+
assert img.format == "EPS"
143+
144+
145+
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
146+
def test_ascii_comment_too_long(prefix):
147+
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment))
148+
with pytest.raises(SyntaxError, match="not an EPS file"):
149+
EpsImagePlugin.EpsImageFile(data)
150+
151+
152+
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
153+
def test_long_binary_data(prefix):
154+
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
155+
EpsImagePlugin.EpsImageFile(data)
156+
157+
158+
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
159+
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
160+
def test_load_long_binary_data(prefix):
161+
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
162+
with Image.open(data) as img:
163+
img.load()
164+
assert img.mode == "RGB"
165+
assert img.size == (100, 100)
166+
assert img.format == "EPS"
167+
168+
77169
@mark_if_feature_version(
78170
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
79171
)
@@ -100,7 +192,7 @@ def test_showpage():
100192
with Image.open("Tests/images/reqd_showpage.png") as target:
101193
# should not crash/hang
102194
plot_image.load()
103-
# fonts could be slightly different
195+
# fonts could be slightly different
104196
assert_image_similar(plot_image, target, 6)
105197

106198

@@ -111,7 +203,7 @@ def test_transparency():
111203
assert plot_image.mode == "RGBA"
112204

113205
with Image.open("Tests/images/reqd_showpage_transparency.png") as target:
114-
# fonts could be slightly different
206+
# fonts could be slightly different
115207
assert_image_similar(plot_image, target, 6)
116208

117209

@@ -206,7 +298,6 @@ def test_resize(filename):
206298
@pytest.mark.parametrize("filename", (FILE1, FILE2))
207299
def test_thumbnail(filename):
208300
# Issue #619
209-
# Arrange
210301
with Image.open(filename) as im:
211302
new_size = (100, 100)
212303
im.thumbnail(new_size)
@@ -220,7 +311,7 @@ def test_read_binary_preview():
220311
pass
221312

222313

223-
def test_readline(tmp_path):
314+
def test_readline_psfile(tmp_path):
224315
# check all the freaking line endings possible from the spec
225316
# test_string = u'something\r\nelse\n\rbaz\rbif\n'
226317
line_endings = ["\r\n", "\n", "\n\r", "\r"]
@@ -237,7 +328,8 @@ def _test_readline(t, ending):
237328

238329
def _test_readline_io_psfile(test_string, ending):
239330
f = io.BytesIO(test_string.encode("latin-1"))
240-
t = EpsImagePlugin.PSFile(f)
331+
with pytest.warns(DeprecationWarning):
332+
t = EpsImagePlugin.PSFile(f)
241333
_test_readline(t, ending)
242334

243335
def _test_readline_file_psfile(test_string, ending):
@@ -246,7 +338,8 @@ def _test_readline_file_psfile(test_string, ending):
246338
w.write(test_string.encode("latin-1"))
247339

248340
with open(f, "rb") as r:
249-
t = EpsImagePlugin.PSFile(r)
341+
with pytest.warns(DeprecationWarning):
342+
t = EpsImagePlugin.PSFile(r)
250343
_test_readline(t, ending)
251344

252345
for ending in line_endings:
@@ -255,6 +348,25 @@ def _test_readline_file_psfile(test_string, ending):
255348
_test_readline_file_psfile(s, ending)
256349

257350

351+
def test_psfile_deprecation():
352+
with pytest.warns(DeprecationWarning):
353+
EpsImagePlugin.PSFile(None)
354+
355+
356+
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
357+
@pytest.mark.parametrize(
358+
"line_ending",
359+
(b"\r\n", b"\n", b"\n\r", b"\r"),
360+
)
361+
def test_readline(prefix, line_ending):
362+
simple_file = prefix + line_ending.join(simple_eps_file_with_comments)
363+
data = io.BytesIO(simple_file)
364+
test_file = EpsImagePlugin.EpsImageFile(data)
365+
assert test_file.info["Comment1"] == "Some Value"
366+
assert test_file.info["SecondComment"] == "Another Value"
367+
assert test_file.size == (100, 100)
368+
369+
258370
@pytest.mark.parametrize(
259371
"filename",
260372
(

docs/deprecations.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,16 @@ Use instead::
207207
left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld")
208208
width, height = right - left, bottom - top
209209

210+
PSFile
211+
~~~~~~
212+
213+
.. deprecated:: 9.5.0
214+
215+
The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will
216+
be removed in Pillow 11 (2024-10-15). This class was only made as a helper to
217+
be used internally, so there is no replacement. If you need this functionality
218+
though, it is a very short class that can easily be recreated in your own code.
219+
210220
Removed features
211221
----------------
212222

docs/releasenotes/9.5.0.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ TODO
1212
Deprecations
1313
============
1414

15-
TODO
16-
^^^^
15+
PSFile
16+
^^^^^^
1717

18-
TODO
18+
The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will
19+
be removed in Pillow 11 (2024-10-15). This class was only made as a helper to
20+
be used internally, so there is no replacement. If you need this functionality
21+
though, it is a very short class that can easily be recreated in your own code.
1922

2023
API Changes
2124
===========

0 commit comments

Comments
 (0)