Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions Tests/test_psdraw.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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""
8 changes: 8 additions & 0 deletions docs/releasenotes/13.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
=============

Expand Down
16 changes: 13 additions & 3 deletions src/PIL/PSDraw.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down