diff --git a/Tests/test_imagetext.py b/Tests/test_imagetext.py index 2b424629d..e9d6d7886 100644 --- a/Tests/test_imagetext.py +++ b/Tests/test_imagetext.py @@ -108,3 +108,20 @@ def test_stroke() -> None: assert_image_similar_tofile( im, "Tests/images/imagedraw_stroke_" + suffix + ".png", 3.1 ) + + +def test_wrap() -> None: + # No wrap required + text = ImageText.Text("Hello World!") + text.wrap(100) + assert text.text == "Hello World!" + + # Wrap word to a new line + text = ImageText.Text("Hello World!") + text.wrap(50) + assert text.text == "Hello\nWorld!" + + # Split word across lines + text = ImageText.Text("Hello World!") + text.wrap(25) + assert text.text == "Hello\nWorl\nd!" diff --git a/src/PIL/ImageText.py b/src/PIL/ImageText.py index e6ccd8243..34c0336c8 100644 --- a/src/PIL/ImageText.py +++ b/src/PIL/ImageText.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import cast + from . import ImageFont from ._typing import _Ink @@ -88,6 +90,70 @@ class Text: else: return "L" + def wrap(self, width: int) -> None: + str_type = isinstance(self.text, str) + wrapped_lines = [] + emptystring = "" if str_type else b"" + fontmode = self._get_fontmode() + for line in self.text.splitlines(): + wrapped_line = emptystring + words = line.split() + while words: + word = words[0] + + new_wrapped_line: str | bytes + if wrapped_line: + if str_type: + new_wrapped_line = ( + cast(str, wrapped_line) + " " + cast(str, word) + ) + else: + new_wrapped_line = ( + cast(bytes, wrapped_line) + b" " + cast(bytes, word) + ) + else: + new_wrapped_line = word + + def get_width(text) -> float: + left, _, right, _ = self.font.getbbox( + text, + fontmode, + self.direction, + self.features, + self.language, + self.stroke_width, + ) + return right - left + + if get_width(new_wrapped_line) > width: + if wrapped_line: + wrapped_lines.append(wrapped_line) + wrapped_line = emptystring + else: + # This word is too long for a single line, so split the word + characters = word + i = len(characters) + while i > 1 and get_width(characters[:i]) > width: + i -= 1 + wrapped_line = characters[:i] + if str_type: + cast(list[str], words)[0] = cast(str, characters[i:]) + else: + cast(list[bytes], words)[0] = cast(bytes, characters[i:]) + else: + words.pop(0) + wrapped_line = new_wrapped_line + if wrapped_line: + wrapped_lines.append(wrapped_line) + if str_type: + self.text = "\n".join( + [line for line in wrapped_lines if isinstance(line, str)] + ) + else: + self.text = b"\n".join( + [line for line in wrapped_lines if isinstance(line, bytes)] + ) + def get_length(self) -> float: """ Returns length (in pixels with 1/64 precision) of text.