From c3134dc04994fa3125f127c6c5a54f03e294fa5e Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 10 Jan 2023 00:03:07 -0600 Subject: [PATCH 01/13] refactor EpsImagePlugin Merge the PSFile class into the EpsImageFile class to hopefully improve performance. Also added a check for the required "%!PS-Adobe" and "%%BoundingBox" header comments. --- Tests/test_file_eps.py | 27 ++++++++- src/PIL/EpsImagePlugin.py | 113 +++++++++++++++++++++++--------------- 2 files changed, 94 insertions(+), 46 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 015dda992c6..9558d149fd7 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -221,7 +221,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"] @@ -256,6 +256,31 @@ def _test_readline_file_psfile(test_string, ending): _test_readline_file_psfile(s, ending) +@pytest.mark.parametrize( + "line_ending", + (b"\r\n", b"\n", b"\n\r", b"\r"), +) +def test_readline(line_ending): + simple_file = line_ending.join( + ( + b"%!PS-Adobe-3.0 EPSF-3.0", + b"%%Comment1: Some Value", + b"%%SecondComment: Another Value", + 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", + ) + ) + + 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", ( diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 016e3c1353c..6b3e353f2a4 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -162,6 +162,7 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False): class PSFile: """ Wrapper for bytesio object that treats either CR or LF as end of line. + This class is no longer used internally, but kept for backwards-compatibility. """ def __init__(self, fp): @@ -194,7 +195,7 @@ def _accept(prefix): ## -# Image plugin for Encapsulated PostScript. This plugin supports only +# Image plugin for Encapsulated PostScript. This plugin supports only # a few variants of this format. @@ -209,29 +210,69 @@ class EpsImageFile(ImageFile.ImageFile): def _open(self): (length, offset) = self._find_offset(self.fp) - # Rewrap the open file pointer in something that will - # convert line endings and decode to latin-1. - fp = PSFile(self.fp) - # go to offset - start of "%!PS" - fp.seek(offset) - - box = None + self.fp.seek(offset) self.mode = "RGB" - self._size = 1, 1 # FIXME: huh? + self._size = None - # - # Load EPS header + byte_arr = bytearray(255) + bytes_mv = memoryview(byte_arr) + bytes_read = 0 + reading_comments = True - s_raw = fp.readline() - s = s_raw.strip("\r\n") + def check_required_header_comments(): + if "PS-Adobe" not in self.info: + msg = 'EPS header missing "%!PS-Adobe" comment' + raise SyntaxError(msg) + if "BoundingBox" not in self.info: + msg = 'EPS header missing "%%BoundingBox" comment' + raise SyntaxError(msg) - while s_raw: - if s: - if len(s) > 255: - msg = "not an EPS file" - raise SyntaxError(msg) + while True: + byte = self.fp.read(1) + if byte == b"": + # if we didn't read a byte we must be at the end of the file + if bytes_read == 0: + break + elif byte in b"\r\n": + # if we read a line ending character, ignore it and parse what + # we have already read. if we haven't read any other characters, + # continue reading + if bytes_read == 0: + continue + else: + # ASCII/hexadecimal lines in an EPS file must not exceed + # 255 characters, not including line ending characters + if bytes_read >= 255: + # only enforce this for lines starting with a "%", + # otherwise assume it's binary data + if byte_arr[0] == ord("%"): + msg = "not an EPS file" + raise SyntaxError(msg) + else: + if reading_comments: + check_required_header_comments() + reading_comments = False + # reset bytes_read so we can keep reading + # data until the end of the line + bytes_read = 0 + byte_arr[bytes_read] = byte[0] + bytes_read += 1 + continue + + if reading_comments: + # Load EPS header + + # if this line doesn't start with a "%", + # or does start with "%%EndComments", + # then we've reached the end of the header/comments + if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments": + check_required_header_comments() + reading_comments = False + continue + + s = str(bytes_mv[:bytes_read], "latin-1") try: m = split.match(s) @@ -254,16 +295,12 @@ def _open(self): ] except Exception: pass - else: m = field.match(s) if m: k = m.group(1) - - if k == "EndComments": - break if k[:8] == "PS-Adobe": - self.info[k[:8]] = k[9:] + self.info["PS-Adobe"] = k[9:] else: self.info[k] = "" elif s[0] == "%": @@ -273,25 +310,11 @@ def _open(self): else: msg = "bad EPS header" raise OSError(msg) + elif bytes_mv[:11] == b"%ImageData:": + # Check for an "ImageData" descriptor - s_raw = fp.readline() - s = s_raw.strip("\r\n") - - if s and s[:1] != "%": - break - - # - # Scan for an "ImageData" descriptor - - while s[:1] == "%": - - if len(s) > 255: - msg = "not an EPS file" - raise SyntaxError(msg) - - if s[:11] == "%ImageData:": # Encoded bitmapped image. - x, y, bi, mo = s[11:].split(None, 7)[:4] + x, y, bi, mo = byte_arr[11:].split(None, 7)[:4] if int(bi) == 1: self.mode = "1" @@ -306,16 +329,16 @@ def _open(self): self._size = int(x), int(y) return - s = fp.readline().strip("\r\n") - if not s: - break + bytes_read = 0 - if not box: + check_required_header_comments() + + if not self._size: + self._size = 1, 1 # errors if this isn't set. why (1,1)? msg = "cannot determine EPS bounding box" raise OSError(msg) def _find_offset(self, fp): - s = fp.read(160) if s[:4] == b"%!PS": From 0334e68f956a18350fc2cba872aef54e57c51e14 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 12 Jan 2023 08:36:17 -0600 Subject: [PATCH 02/13] add more eps file tests --- Tests/test_file_eps.py | 109 ++++++++++++++++++++++++----------------- 1 file changed, 64 insertions(+), 45 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 9558d149fd7..bded99cf13b 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -28,34 +28,47 @@ # 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:] -@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") @@ -69,18 +82,36 @@ 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) + + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_cmyk(): with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: - assert cmyk_image.mode == "CMYK" assert cmyk_image.size == (100, 100) assert cmyk_image.format == "EPS" @@ -101,7 +132,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) @@ -112,7 +143,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) @@ -207,7 +238,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) @@ -256,24 +286,13 @@ def _test_readline_file_psfile(test_string, ending): _test_readline_file_psfile(s, ending) +@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(line_ending): - simple_file = line_ending.join( - ( - b"%!PS-Adobe-3.0 EPSF-3.0", - b"%%Comment1: Some Value", - b"%%SecondComment: Another Value", - 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", - ) - ) - +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" From 4c2550db423523efedd773a3042754f2ad627477 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 15 Jan 2023 15:29:23 -0600 Subject: [PATCH 03/13] add test for invalid bounding box --- Tests/test_file_eps.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index bded99cf13b..7abed6f42f7 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -55,6 +55,7 @@ ) 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",) + simple_eps_file[2:] @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @@ -106,6 +107,13 @@ def test_missing_boundingbox_comment(prefix): 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) + + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) From 3d6770d0f33fdfc18a6833a384fbccb9ef878dfa Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 15 Jan 2023 15:56:25 -0600 Subject: [PATCH 04/13] add tests for long lines --- Tests/test_file_eps.py | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 7abed6f42f7..26ac2e5a191 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -55,7 +55,21 @@ ) 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",) + simple_eps_file[2:] +simple_eps_file_with_invalid_boundingbox = ( + simple_eps_file[:1] + (b"%%BoundingBox",) + simple_eps_file[2:] +) +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") @@ -114,6 +128,30 @@ def test_invalid_boundingbox_comment(prefix): EpsImagePlugin.EpsImageFile(data) +@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" ) From be9aea35a892b5e551e17057252403ea5a4e0929 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 28 Jan 2023 14:22:05 -0600 Subject: [PATCH 05/13] add eps test for bad BoundingBox, good ImageData --- Tests/test_file_eps.py | 16 +++++++++++++++- src/PIL/EpsImagePlugin.py | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 26ac2e5a191..5d63df4a618 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -56,7 +56,10 @@ 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",) + simple_eps_file[2:] + 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:] @@ -128,6 +131,17 @@ def test_invalid_boundingbox_comment(prefix): 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)) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 4c0ab0e127b..2a4e804ce21 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -314,7 +314,7 @@ def check_required_header_comments(): # Check for an "ImageData" descriptor # Encoded bitmapped image. - x, y, bi, mo = byte_arr[11:].split(None, 7)[:4] + x, y, bi, mo = byte_arr[11:bytes_read].split(None, 7)[:4] if int(bi) == 1: self.mode = "1" From bd0fac80c4f439d6fdb706889beb9b8c627c1ee8 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 6 Feb 2023 17:23:57 -0600 Subject: [PATCH 06/13] deprecate EpsImagePlugin.PSFile --- Tests/test_file_eps.py | 6 ++++++ docs/deprecations.rst | 10 ++++++++++ docs/releasenotes/9.4.0.rst | 11 +++++++++++ src/PIL/EpsImagePlugin.py | 6 ++++++ src/PIL/_deprecate.py | 4 +++- 5 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 5d63df4a618..e4c1000e26a 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -311,6 +311,7 @@ def test_read_binary_preview(): pass +@pytest.mark.filterwarnings("ignore::DeprecationWarning") 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' @@ -346,6 +347,11 @@ 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", diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 4d48b822a85..c31b0dac949 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -80,6 +80,16 @@ A number of constants have been deprecated and will be removed in Pillow 10.0.0 was reversed in Pillow 9.4.0 and those constants will now remain available. See :ref:`restored-image-constants` +PSFile +~~~~~~ + +.. deprecated:: 9.4.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. + ===================================================== ============================================================ Deprecated Use instead ===================================================== ============================================================ diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index 0af5bc8ca11..b7a63dd61e7 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -1,6 +1,17 @@ 9.4.0 ----- +Deprecations +============ + +PSFile +^^^^^^ + +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 Additions ============= diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 2a4e804ce21..6c63ef08a38 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -29,6 +29,7 @@ from . import Image, ImageFile from ._binary import i32le as i32 +from ._deprecate import deprecate # # -------------------------------------------------------------------- @@ -166,6 +167,11 @@ class PSFile: """ def __init__(self, fp): + deprecate( + "PSFile", + 11, + action="If you need the functionality of this class you will need to implement it yourself.", + ) self.fp = fp self.char = None diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index 7c4b1623d26..81f2189dcfc 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -47,8 +47,10 @@ def deprecate( raise RuntimeError(msg) elif when == 10: removed = "Pillow 10 (2023-07-01)" + elif when == 11: + removed = "Pillow 11 (2024-10-15)" else: - msg = f"Unknown removal version, update {__name__}?" + msg = f"Unknown removal version: {when}. Update {__name__}?" raise ValueError(msg) if replacement and action: From 62ab8bf80c79e1c7d8c16cf32174421f64cc8c0c Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 6 Feb 2023 18:00:31 -0600 Subject: [PATCH 07/13] update "unknown version" deprecation test --- Tests/test_deprecate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index 3375eb6b282..c7a7a9ff50f 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -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") From 99b153c9cadbcb1c39c6747c3d97057aef054517 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 7 Feb 2023 13:49:00 -0600 Subject: [PATCH 08/13] hyphenate "backwards-compatibility" Co-authored-by: Hugo van Kemenade --- src/PIL/EpsImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 6c63ef08a38..48d32498f2b 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -163,7 +163,7 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False): class PSFile: """ Wrapper for bytesio object that treats either CR or LF as end of line. - This class is no longer used internally, but kept for backwards-compatibility. + This class is no longer used internally, but kept for backwards compatibility. """ def __init__(self, fp): From 0f27ddafb710e104d84605a26a0e58f2a6495019 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 7 Feb 2023 13:56:38 -0600 Subject: [PATCH 09/13] split long line --- src/PIL/EpsImagePlugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 48d32498f2b..9da6e946bee 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -170,7 +170,8 @@ def __init__(self, fp): deprecate( "PSFile", 11, - action="If you need the functionality of this class you will need to implement it yourself.", + action="If you need the functionality of this class " + "you will need to implement it yourself.", ) self.fp = fp self.char = None From dd985b2a5e265118c36683b20afa4153a75cf69f Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 7 Feb 2023 13:58:05 -0600 Subject: [PATCH 10/13] make deprecation check more specific --- Tests/test_file_eps.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index e4c1000e26a..26adfff8786 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -311,7 +311,6 @@ def test_read_binary_preview(): pass -@pytest.mark.filterwarnings("ignore::DeprecationWarning") 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' @@ -329,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): @@ -338,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: From 4f9c3847e85a210e5b57269b04ef7ad6179ad698 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 2 Mar 2023 14:21:17 -0600 Subject: [PATCH 11/13] notes about %ImageData, and use better var names --- src/PIL/EpsImagePlugin.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 9da6e946bee..52036b0ac53 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -320,20 +320,32 @@ def check_required_header_comments(): elif bytes_mv[:11] == b"%ImageData:": # Check for an "ImageData" descriptor - # Encoded bitmapped image. - x, y, bi, mo = byte_arr[11:bytes_read].split(None, 7)[:4] - - if int(bi) == 1: + # Values: + # columns + # rows + # bit depth + # mode (1: L, 2: LAB, 3: RGB, 4: CMYK) + # number of padding channels + # block size (number of bytes per row per channel) + # binary/ascii (1: binary, 2: ascii) + # data start identifier (the image data follows after a single line + # consisting only of this quoted value) + image_data_values = byte_arr[11:bytes_read].split(None, 7) + columns, rows, bit_depth, mode_id = [ + int(value) for value in image_data_values[:4] + ] + + if bit_depth == 1: self.mode = "1" - elif int(bi) == 8: + elif bit_depth == 8: try: - self.mode = self.mode_map[int(mo)] + self.mode = self.mode_map[mode_id] except ValueError: break else: break - self._size = int(x), int(y) + self._size = columns, rows return bytes_read = 0 From 60b717a94b98d4d56fb21b758330e613e5d3ae24 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 2 Mar 2023 15:26:06 -0600 Subject: [PATCH 12/13] add link to %ImageData definition and remove empty comment lines --- src/PIL/EpsImagePlugin.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 52036b0ac53..1c88d22c749 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -31,9 +31,9 @@ from ._binary import i32le as i32 from ._deprecate import deprecate -# # -------------------------------------------------------------------- + split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") @@ -319,11 +319,12 @@ def check_required_header_comments(): raise OSError(msg) elif bytes_mv[:11] == b"%ImageData:": # Check for an "ImageData" descriptor + # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096 # Values: # columns # rows - # bit depth + # bit depth (1 or 8) # mode (1: L, 2: LAB, 3: RGB, 4: CMYK) # number of padding channels # block size (number of bytes per row per channel) @@ -395,18 +396,15 @@ def load_seek(self, *args, **kwargs): pass -# # -------------------------------------------------------------------- def _save(im, fp, filename, eps=1): """EPS Writer for the Python Imaging Library.""" - # # make sure image data is available im.load() - # # determine PostScript image mode if im.mode == "L": operator = (8, 1, b"image") @@ -419,7 +417,6 @@ def _save(im, fp, filename, eps=1): raise ValueError(msg) if eps: - # # write EPS header fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n") fp.write(b"%%Creator: PIL 0.1 EpsEncode\n") @@ -431,7 +428,6 @@ def _save(im, fp, filename, eps=1): fp.write(b"%%ImageData: %d %d " % im.size) fp.write(b'%d %d 0 1 1 "%s"\n' % operator) - # # image header fp.write(b"gsave\n") fp.write(b"10 dict begin\n") @@ -452,7 +448,6 @@ def _save(im, fp, filename, eps=1): fp.flush() -# # -------------------------------------------------------------------- From 61d0c8f5230a3fdde4c601a73217acd386d1b3be Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 29 Mar 2023 10:30:20 -0500 Subject: [PATCH 13/13] change PSFile deprecation from 9.4.0 to 9.5.0 --- docs/deprecations.rst | 20 ++++++++++---------- docs/releasenotes/9.4.0.rst | 11 ----------- docs/releasenotes/9.5.0.rst | 9 ++++++--- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 4a03890a327..5669d2827f8 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -80,16 +80,6 @@ A number of constants have been deprecated and will be removed in Pillow 10.0.0 was reversed in Pillow 9.4.0 and those constants will now remain available. See :ref:`restored-image-constants` -PSFile -~~~~~~ - -.. deprecated:: 9.4.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. - ===================================================== ============================================================ Deprecated Use instead ===================================================== ============================================================ @@ -217,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 ---------------- diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index b7a63dd61e7..0af5bc8ca11 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -1,17 +1,6 @@ 9.4.0 ----- -Deprecations -============ - -PSFile -^^^^^^ - -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 Additions ============= diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index 0b0e0dd2f10..585e790eaf2 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -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 ===========