Skip to content
Open
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
61 changes: 61 additions & 0 deletions Tests/test_imagetext.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from typing import AnyStr

import pytest

from PIL import Image, ImageDraw, ImageFont, ImageText, features
Expand Down Expand Up @@ -170,6 +172,29 @@ def test_wrap_height() -> None:
assert text.text == "Text does\nnot fit"


@skip_unless_feature("freetype2")
@pytest.mark.parametrize(
"data",
(
"A\n\nB",
b"A\n\nB",
"A\n\n\nB",
b"A\n\n\nB",
"A\n \nB",
b"A\n \nB",
),
)
def test_wrap_height_blank_line(layout_engine: ImageFont.Layout, data: AnyStr) -> None:
font = ImageFont.truetype(FONT_PATH, 10, layout_engine=layout_engine)
text = ImageText.Text(data, font)

wrapped = text.wrap(100, int(font.getbbox("A")[3]))

assert text.text == data[:1]
assert wrapped is not None
assert wrapped.text == data[2:]


def test_wrap_scaling_unsupported() -> None:
font = ImageFont.load_default_imagefont()
text = ImageText.Text("Hello World!", font)
Expand Down Expand Up @@ -231,3 +256,39 @@ def test_wrap_grow() -> None:
assert text.wrap(50, 50, ("grow", 18)) is None
assert isinstance(text.font, ImageFont.FreeTypeFont)
assert text.font.size == 16


@skip_unless_feature("freetype2")
@pytest.mark.parametrize("scaling, size", (("grow", 10), ("shrink", 60)))
@pytest.mark.parametrize(
"data, width, spacing, stroke_width",
(
("A\nB", 400, 4, 0),
("A\n\nB", 400, 6, 2),
("A B C D", 40, 4, 0),
),
)
def test_wrap_scaling_height(
layout_engine: ImageFont.Layout,
scaling: str,
size: int,
data: str,
width: int,
spacing: int,
stroke_width: int,
) -> None:
font = ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine)
text = ImageText.Text(data, font, spacing=spacing)
text.stroke(stroke_width)

assert text.wrap(width, 50, scaling) is None
assert text.get_bbox()[2] <= width
assert text.get_bbox()[3] <= 50

assert isinstance(text.font, ImageFont.FreeTypeFont)
larger = ImageText.Text(
data, text.font.font_variant(size=text.font.size + 1), spacing=spacing
)
larger.stroke(stroke_width)
assert larger.wrap(width) is None
assert larger.get_bbox()[3] > 50
11 changes: 11 additions & 0 deletions docs/releasenotes/13.1.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ TODO
Other changes
=============

Scaling multiline text to a height limit
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

:py:meth:`~PIL.ImageText.Text.wrap` now measures line spacing using the scaled
font when growing or shrinking text. Previously, growing multiline text could
exceed the height limit, and shrinking could select a smaller font than needed
or fail to find a font size that fits.

Wrapping also stops at a blank line that exceeds the height limit, preserving the
remaining text. This avoids ``RuntimeError: raqm_layout() failed`` when using RAQM.

Resizing I;16B and I;16N images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
15 changes: 9 additions & 6 deletions src/PIL/ImageText.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ def __init__(
break
for i, line in enumerate(newlines):
if i != 0 and not self.add_line(emptystring):
break
self.remaining_text: AnyStr = input_text[self.position :]
return
self.position += len(line)
word = word[len(line) :]
line = emptystring
Expand Down Expand Up @@ -80,12 +81,12 @@ def __init__(
else:
if line:
self.add_line(line)
self.remaining_text: AnyStr = input_text[self.position :]
self.remaining_text = input_text[self.position :]

def add_line(self, line: AnyStr) -> bool:
lines = self.lines + [line]
if self.height is not None:
last_line_y = self.text._split(lines=lines)[-1].y
last_line_y = self.text._split(lines=lines, font=self.font)[-1].y
last_line_height = self.text._get_bbox(line, self.font)[3]
if last_line_y + last_line_height > self.height:
return False
Expand Down Expand Up @@ -331,6 +332,7 @@ def _split(
anchor: str | None = None,
align: str = "left",
lines: list[str] | list[bytes] | None = None,
font: ImageFont.BaseImageFont | None = None,
) -> list[_Line]:
if anchor is None:
anchor = "lt" if self.direction == "ttb" else "la"
Expand All @@ -351,9 +353,10 @@ def _split(
msg = "anchor not supported for multiline text"
raise ValueError(msg)

font = font or self.font
fontmode = self._get_fontmode()
line_spacing = (
self.font.getbbox(
font.getbbox(
"A",
fontmode,
None,
Expand All @@ -376,7 +379,7 @@ def _split(
widths = []
max_width: float = 0
for line in lines:
line_width = self.font.getlength(
line_width = font.getlength(
line, fontmode, self.direction, self.features, self.language
)
widths.append(line_width)
Expand Down Expand Up @@ -420,7 +423,7 @@ def _split(
left -= max_width

word_widths = [
self.font.getlength(
font.getlength(
word,
fontmode,
self.direction,
Expand Down
Loading