diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index e017bc54e6a..e06557c5107 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -4,14 +4,14 @@ import sys from io import BytesIO +import pytest + from PIL import Image, PSDraw TYPE_CHECKING = False if TYPE_CHECKING: from pathlib import Path - import pytest - def _create_document(ps: PSDraw.PSDraw) -> None: title = "hopper" @@ -65,3 +65,34 @@ class MyStdOut: _create_document(ps) assert mystdout.buffer.getvalue() != b"" + + +@pytest.mark.parametrize("escape", (None, False, True)) +@pytest.mark.parametrize( + "text, expected, escaped", + ( + ("plain text", b"plain text", b"plain text"), + ("a(b)c", rb"a\(b\)c", rb"a\(b\)c"), + (r"C:\temp\new", rb"C:\temp\new", rb"C:\\temp\\new"), + (r"a\(b)\c", rb"a\\(b\)\c", rb"a\\\(b\)\\c"), + (r"\n\t\101", rb"\n\t\101", rb"\\n\\t\\101"), + ), +) +def test_text(text: str, expected: bytes, escaped: bytes, escape: bool | None) -> None: + with BytesIO() as buffer: + ps = PSDraw.PSDraw(buffer) + if escape is None: + ps.text((10, 20), text) + else: + ps.text((10, 20), text, escape=escape) + if escape: + expected = escaped + assert buffer.getvalue() == b"10 20 M (" + expected + b") S\n" + + +def test_text_encoding_error() -> None: + with BytesIO() as buffer: + ps = PSDraw.PSDraw(buffer) + with pytest.raises(UnicodeEncodeError): + ps.text((10, 20), "\u0100") + assert buffer.getvalue() == b"" diff --git a/docs/releasenotes/13.0.0.rst b/docs/releasenotes/13.0.0.rst index 46dbed3b58b..e59941927c9 100644 --- a/docs/releasenotes/13.0.0.rst +++ b/docs/releasenotes/13.0.0.rst @@ -123,6 +123,14 @@ Two new filters are available for :py:meth:`~PIL.Image.Image.resize` and :py:meth:`~PIL.Image.Image.thumbnail`: ``Image.Resampling.MKS2013`` and ``Image.Resampling.MKS2021``. These are versions of the Magic Kernel Sharp filter. +Added ``escape`` argument to ``PSDraw.text()`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.PSDraw.PSDraw.text` now accepts a keyword-only boolean argument, +``escape``. Set it to ``True`` to escape backslashes, for example in Windows paths. +The default, ``False``, preserves the existing handling of backslashes and PostScript +escape sequences. + Other changes ============= diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index d0e4d561a2a..dd43c2445ff 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -96,15 +96,25 @@ def rectangle(self, box: tuple[int, int, int, int]) -> None: """ self.fp.write(b"%d %d M 0 %d %d Vr\n" % box) - def text(self, xy: tuple[int, int], text: str) -> None: + def text(self, xy: tuple[int, int], text: str, *, escape: bool = False) -> None: """ Draws text at the given position. You must use :py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method. + + :param xy: The position, in PostScript point coordinates. + :param text: The text to draw, encoded as Latin-1. + :param escape: Whether to escape backslashes in the text. This keyword-only + boolean defaults to ``False``, leaving backslashes unchanged for backwards + compatibility. Parentheses are always escaped. + + .. versionadded:: 13.0.0 """ # The font is loaded as ISOLatin1Encoding, so use latin-1 here. text_bytes = bytes(text, "latin-1") - text_bytes = b"\\(".join(text_bytes.split(b"(")) - text_bytes = b"\\)".join(text_bytes.split(b")")) + if escape: + text_bytes = text_bytes.replace(b"\\", b"\\\\") + for char in (b"(", b")"): + text_bytes = text_bytes.replace(char, b"\\" + char) self.fp.write(b"%d %d M (%s) S\n" % (xy + (text_bytes,))) if TYPE_CHECKING: