Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor EpsImagePlugin and deprecate PSFile #6879

Merged
merged 18 commits into from
Apr 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion Tests/test_deprecate.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def test_version(version, expected):


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

Expand Down
178 changes: 145 additions & 33 deletions Tests/test_file_eps.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,34 +28,65 @@
# EPS test files with binary preview
FILE3 = "Tests/images/binary_preview_map.eps"

# Three unsigned 32bit little-endian values:
# 0xC6D3D0C5 magic number
# byte position of start of postscript section (12)
# byte length of postscript section (0)
# this byte length isn't valid, but we don't read it
simple_binary_header = b"\xc5\xd0\xd3\xc6\x0c\x00\x00\x00\x00\x00\x00\x00"

# taken from page 8 of the specification
# https://web.archive.org/web/20220120164601/https://www.adobe.com/content/dam/acom/en/devnet/actionscript/articles/5002.EPSF_Spec.pdf
simple_eps_file = (
b"%!PS-Adobe-3.0 EPSF-3.0",
b"%%BoundingBox: 5 5 105 105",
b"10 setlinewidth",
b"10 10 moveto",
b"0 90 rlineto 90 0 rlineto 0 -90 rlineto closepath",
b"stroke",
)
simple_eps_file_with_comments = (
simple_eps_file[:1]
+ (
b"%%Comment1: Some Value",
b"%%SecondComment: Another Value",
)
+ simple_eps_file[1:]
)
simple_eps_file_without_version = simple_eps_file[1:]
simple_eps_file_without_boundingbox = simple_eps_file[:1] + simple_eps_file[2:]
simple_eps_file_with_invalid_boundingbox = (
simple_eps_file[:1] + (b"%%BoundingBox: a b c d",) + simple_eps_file[2:]
)
simple_eps_file_with_invalid_boundingbox_valid_imagedata = (
simple_eps_file_with_invalid_boundingbox + (b"%ImageData: 100 100 8 3",)
)
simple_eps_file_with_long_ascii_comment = (
simple_eps_file[:2] + (b"%%Comment: " + b"X" * 300,) + simple_eps_file[2:]
)
simple_eps_file_with_long_binary_data = (
simple_eps_file[:2]
+ (
b"%%BeginBinary: 300",
b"\0" * 300,
b"%%EndBinary",
)
+ simple_eps_file[2:]
)

@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_sanity():
# Regular scale
with Image.open(FILE1) as image1:
image1.load()
assert image1.mode == "RGB"
assert image1.size == (460, 352)
assert image1.format == "EPS"

with Image.open(FILE2) as image2:
image2.load()
assert image2.mode == "RGB"
assert image2.size == (360, 252)
assert image2.format == "EPS"

# Double scale
with Image.open(FILE1) as image1_scale2:
image1_scale2.load(scale=2)
assert image1_scale2.mode == "RGB"
assert image1_scale2.size == (920, 704)
assert image1_scale2.format == "EPS"

with Image.open(FILE2) as image2_scale2:
image2_scale2.load(scale=2)
assert image2_scale2.mode == "RGB"
assert image2_scale2.size == (720, 504)
assert image2_scale2.format == "EPS"
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@pytest.mark.parametrize(
("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252)))
)
@pytest.mark.parametrize("scale", (1, 2))
def test_sanity(filename, size, scale):
expected_size = tuple(s * scale for s in size)
with Image.open(filename) as image:
image.load(scale=scale)
assert image.mode == "RGB"
assert image.size == expected_size
assert image.format == "EPS"


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

def test_invalid_file():
invalid_file = "Tests/images/flower.jpg"

with pytest.raises(SyntaxError):
EpsImagePlugin.EpsImageFile(invalid_file)


def test_binary_header_only():
data = io.BytesIO(simple_binary_header)
with pytest.raises(SyntaxError, match='EPS header missing "%!PS-Adobe" comment'):
EpsImagePlugin.EpsImageFile(data)


@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_missing_version_comment(prefix):
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version))
with pytest.raises(SyntaxError):
EpsImagePlugin.EpsImageFile(data)


@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_missing_boundingbox_comment(prefix):
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox))
with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'):
EpsImagePlugin.EpsImageFile(data)


@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_invalid_boundingbox_comment(prefix):
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox))
with pytest.raises(OSError, match="cannot determine EPS bounding box"):
EpsImagePlugin.EpsImageFile(data)


@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix):
data = io.BytesIO(
prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata)
)
with Image.open(data) as img:
assert img.mode == "RGB"
assert img.size == (100, 100)
assert img.format == "EPS"


@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_ascii_comment_too_long(prefix):
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment))
with pytest.raises(SyntaxError, match="not an EPS file"):
EpsImagePlugin.EpsImageFile(data)


@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_long_binary_data(prefix):
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
EpsImagePlugin.EpsImageFile(data)


@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_load_long_binary_data(prefix):
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
with Image.open(data) as img:
img.load()
assert img.mode == "RGB"
assert img.size == (100, 100)
assert img.format == "EPS"


@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
Expand All @@ -100,7 +192,7 @@ def test_showpage():
with Image.open("Tests/images/reqd_showpage.png") as target:
# should not crash/hang
plot_image.load()
# fonts could be slightly different
# fonts could be slightly different
assert_image_similar(plot_image, target, 6)


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

with Image.open("Tests/images/reqd_showpage_transparency.png") as target:
# fonts could be slightly different
# fonts could be slightly different
assert_image_similar(plot_image, target, 6)


Expand Down Expand Up @@ -206,7 +298,6 @@ def test_resize(filename):
@pytest.mark.parametrize("filename", (FILE1, FILE2))
def test_thumbnail(filename):
# Issue #619
# Arrange
with Image.open(filename) as im:
new_size = (100, 100)
im.thumbnail(new_size)
Expand All @@ -220,7 +311,7 @@ def test_read_binary_preview():
pass


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

def _test_readline_io_psfile(test_string, ending):
f = io.BytesIO(test_string.encode("latin-1"))
t = EpsImagePlugin.PSFile(f)
with pytest.warns(DeprecationWarning):
t = EpsImagePlugin.PSFile(f)
_test_readline(t, ending)

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

with open(f, "rb") as r:
t = EpsImagePlugin.PSFile(r)
with pytest.warns(DeprecationWarning):
t = EpsImagePlugin.PSFile(r)
_test_readline(t, ending)

for ending in line_endings:
Expand All @@ -255,6 +348,25 @@ def _test_readline_file_psfile(test_string, ending):
_test_readline_file_psfile(s, ending)


def test_psfile_deprecation():
with pytest.warns(DeprecationWarning):
EpsImagePlugin.PSFile(None)


@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
@pytest.mark.parametrize(
"line_ending",
(b"\r\n", b"\n", b"\n\r", b"\r"),
)
def test_readline(prefix, line_ending):
simple_file = prefix + line_ending.join(simple_eps_file_with_comments)
data = io.BytesIO(simple_file)
test_file = EpsImagePlugin.EpsImageFile(data)
assert test_file.info["Comment1"] == "Some Value"
assert test_file.info["SecondComment"] == "Another Value"
assert test_file.size == (100, 100)


@pytest.mark.parametrize(
"filename",
(
Expand Down
10 changes: 10 additions & 0 deletions docs/deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,16 @@ Use instead::
left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld")
width, height = right - left, bottom - top

PSFile
~~~~~~

.. deprecated:: 9.5.0

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

Removed features
----------------

Expand Down
9 changes: 6 additions & 3 deletions docs/releasenotes/9.5.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ TODO
Deprecations
============

TODO
^^^^
PSFile
^^^^^^

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

API Changes
===========
Expand Down