mirror of
https://github.com/python-pillow/Pillow.git
synced 2026-02-04 22:39:33 +03:00
Added height argument to wrap()
This commit is contained in:
parent
9ac4edc54b
commit
16691657cc
|
|
@ -110,18 +110,28 @@ def test_stroke() -> None:
|
|||
)
|
||||
|
||||
|
||||
def test_wrap() -> None:
|
||||
# No wrap required
|
||||
text = ImageText.Text("Hello World!")
|
||||
text.wrap(100)
|
||||
assert text.text == "Hello World!"
|
||||
@pytest.mark.parametrize(
|
||||
"text, width, expected",
|
||||
(
|
||||
("Hello World!", 100, "Hello World!"), # No wrap required
|
||||
("Hello World!", 50, "Hello\nWorld!"), # Wrap word to a new line
|
||||
("Hello World!", 25, "Hello\nWorl\nd!"), # Split word across lines
|
||||
# Keep multiple spaces within a line
|
||||
("Keep multiple spaces", 75, "Keep multiple\nspaces"),
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("string", (True, False))
|
||||
def test_wrap(text: str, width: int, expected: str, string: bool) -> None:
|
||||
text = ImageText.Text(text if string else text.encode())
|
||||
assert text.wrap(width) is None
|
||||
assert text.text == expected if string else expected.encode()
|
||||
|
||||
# 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!"
|
||||
def test_wrap_height() -> None:
|
||||
text = ImageText.Text("Text does not fit within height")
|
||||
assert text.wrap(50, 25).text == " within height"
|
||||
assert text.text == "Text does\nnot fit"
|
||||
|
||||
text = ImageText.Text("Text does not fit singlelongword")
|
||||
assert text.wrap(50, 25).text == " singlelongword"
|
||||
assert text.text == "Text does\nnot fit"
|
||||
|
|
|
|||
|
|
@ -591,49 +591,49 @@ class ImageDraw:
|
|||
else ink
|
||||
)
|
||||
|
||||
for xy, anchor, line in image_text._split(xy, anchor, align):
|
||||
for line in image_text._split(xy, anchor, align):
|
||||
|
||||
def draw_text(ink: int, stroke_width: float = 0) -> None:
|
||||
mode = self.fontmode
|
||||
if stroke_width == 0 and embedded_color:
|
||||
mode = "RGBA"
|
||||
coord = []
|
||||
for i in range(2):
|
||||
coord.append(int(xy[i]))
|
||||
start = (math.modf(xy[0])[0], math.modf(xy[1])[0])
|
||||
x = int(line.x)
|
||||
y = int(line.y)
|
||||
start = (math.modf(line.x)[0], math.modf(line.y)[0])
|
||||
try:
|
||||
mask, offset = image_text.font.getmask2( # type: ignore[union-attr,misc]
|
||||
line,
|
||||
line.text,
|
||||
mode,
|
||||
direction=direction,
|
||||
features=features,
|
||||
language=language,
|
||||
stroke_width=stroke_width,
|
||||
stroke_filled=True,
|
||||
anchor=anchor,
|
||||
anchor=line.anchor,
|
||||
ink=ink,
|
||||
start=start,
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
coord = [coord[0] + offset[0], coord[1] + offset[1]]
|
||||
x += offset[0]
|
||||
y += offset[1]
|
||||
except AttributeError:
|
||||
try:
|
||||
mask = image_text.font.getmask( # type: ignore[misc]
|
||||
line,
|
||||
line.text,
|
||||
mode,
|
||||
direction,
|
||||
features,
|
||||
language,
|
||||
stroke_width,
|
||||
anchor,
|
||||
line.anchor,
|
||||
ink,
|
||||
start=start,
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
except TypeError:
|
||||
mask = image_text.font.getmask(line)
|
||||
mask = image_text.font.getmask(line.text)
|
||||
if mode == "RGBA":
|
||||
# image_text.font.getmask2(mode="RGBA")
|
||||
# returns color in RGB bands and mask in A
|
||||
|
|
@ -641,13 +641,12 @@ class ImageDraw:
|
|||
color, mask = mask, mask.getband(3)
|
||||
ink_alpha = struct.pack("i", ink)[3]
|
||||
color.fillband(3, ink_alpha)
|
||||
x, y = coord
|
||||
if self.im is not None:
|
||||
self.im.paste(
|
||||
color, (x, y, x + mask.size[0], y + mask.size[1]), mask
|
||||
)
|
||||
else:
|
||||
self.draw.draw_bitmap(coord, mask, ink)
|
||||
self.draw.draw_bitmap((x, y), mask, ink)
|
||||
|
||||
if stroke_ink is not None:
|
||||
# Draw stroked text
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import cast
|
||||
from typing import NamedTuple, cast
|
||||
|
||||
from . import ImageFont
|
||||
from ._typing import _Ink
|
||||
|
||||
|
||||
class _Line(NamedTuple):
|
||||
x: float
|
||||
y: float
|
||||
anchor: str
|
||||
text: str | bytes
|
||||
|
||||
|
||||
class Text:
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -90,69 +97,140 @@ 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""
|
||||
def wrap(
|
||||
self,
|
||||
width: int,
|
||||
height: int | None = None,
|
||||
) -> Text | None:
|
||||
wrapped_lines: list[str] | list[bytes] = []
|
||||
emptystring = "" if isinstance(self.text, str) else b""
|
||||
newline = "\n" if isinstance(self.text, str) else b"\n"
|
||||
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)
|
||||
)
|
||||
def getbbox(text) -> tuple[float, float]:
|
||||
_, _, right, bottom = self.font.getbbox(
|
||||
text,
|
||||
fontmode,
|
||||
self.direction,
|
||||
self.features,
|
||||
self.language,
|
||||
self.stroke_width,
|
||||
)
|
||||
return right, bottom
|
||||
|
||||
wrapped_line = emptystring
|
||||
word = emptystring
|
||||
reached_end = False
|
||||
remaining_position = 0
|
||||
|
||||
def join_text(a: str | bytes, b: str | bytes) -> str | bytes:
|
||||
if isinstance(a, str):
|
||||
return a + cast(str, b)
|
||||
else:
|
||||
return a + cast(bytes, b)
|
||||
|
||||
for i in range(len(self.text)):
|
||||
last_character = i == len(self.text) - 1
|
||||
|
||||
def add_line() -> bool:
|
||||
nonlocal wrapped_lines, remaining_position
|
||||
lines = cast(
|
||||
list[str] | list[bytes], wrapped_lines + [wrapped_line.rstrip()]
|
||||
)
|
||||
if height is not None:
|
||||
last_line_y = self._split(lines=lines)[-1].y
|
||||
last_line_height = getbbox(wrapped_line)[1]
|
||||
if last_line_y + last_line_height > height:
|
||||
return False
|
||||
|
||||
wrapped_lines = lines
|
||||
remaining_position = i - len(word)
|
||||
if last_character:
|
||||
remaining_position += 1
|
||||
return True
|
||||
|
||||
character = self.text[i : i + 1]
|
||||
if last_character:
|
||||
word = join_text(word, character)
|
||||
character = newline
|
||||
if character.isspace():
|
||||
if not word or word.isspace():
|
||||
# Do not use whitespace until a non-whitespace character is reached
|
||||
# Trimming whitespace from the end of the line
|
||||
word = join_text(word, character)
|
||||
else:
|
||||
new_wrapped_line = word
|
||||
# Append the word to the current line
|
||||
if not wrapped_line:
|
||||
word = word.lstrip()
|
||||
new_wrapped_line = join_text(wrapped_line, word)
|
||||
if getbbox(new_wrapped_line)[0] > width:
|
||||
|
||||
def get_width(text) -> float:
|
||||
left, _, right, _ = self.font.getbbox(
|
||||
text,
|
||||
fontmode,
|
||||
self.direction,
|
||||
self.features,
|
||||
self.language,
|
||||
self.stroke_width,
|
||||
)
|
||||
return right - left
|
||||
def split_word():
|
||||
nonlocal wrapped_line, word, reached_end
|
||||
# This word is too long for a single line, so split the word
|
||||
j = len(word)
|
||||
while j > 1 and getbbox(word[:j])[0] > width:
|
||||
j -= 1
|
||||
wrapped_line = word[:j]
|
||||
if not add_line():
|
||||
reached_end = True
|
||||
return
|
||||
word = word[j:]
|
||||
wrapped_line = word
|
||||
if getbbox(wrapped_line)[0] > width:
|
||||
split_word()
|
||||
|
||||
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:])
|
||||
if wrapped_line:
|
||||
# This word does not fit on the line
|
||||
if not add_line():
|
||||
reached_end = True
|
||||
break
|
||||
word = word.lstrip()
|
||||
if getbbox(word)[0] > width:
|
||||
split_word()
|
||||
else:
|
||||
wrapped_line = word
|
||||
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)]
|
||||
split_word()
|
||||
if reached_end:
|
||||
break
|
||||
else:
|
||||
# This word fits on the line
|
||||
wrapped_line = new_wrapped_line
|
||||
word = emptystring
|
||||
|
||||
word = emptystring if character == newline else character
|
||||
|
||||
if character == newline:
|
||||
if not add_line():
|
||||
break
|
||||
wrapped_line = emptystring
|
||||
elif not character.isspace():
|
||||
# Word is not finished yet
|
||||
word = join_text(word, character)
|
||||
|
||||
remaining_text = self.text[remaining_position:]
|
||||
if remaining_text:
|
||||
text = Text(
|
||||
text=remaining_text,
|
||||
font=self.font,
|
||||
mode=self.mode,
|
||||
spacing=self.spacing,
|
||||
direction=self.direction,
|
||||
features=self.features,
|
||||
language=self.language,
|
||||
)
|
||||
text.embedded_color = self.embedded_color
|
||||
text.stroke_width = self.stroke_width
|
||||
text.stroke_fill = self.stroke_fill
|
||||
else:
|
||||
self.text = b"\n".join(
|
||||
[line for line in wrapped_lines if isinstance(line, bytes)]
|
||||
)
|
||||
text = None
|
||||
|
||||
if isinstance(self.text, str):
|
||||
self.text = "\n".join(cast(list[str], wrapped_lines))
|
||||
else:
|
||||
self.text = b"\n".join(cast(list[bytes], wrapped_lines))
|
||||
return text
|
||||
|
||||
def get_length(self) -> float:
|
||||
"""
|
||||
|
|
@ -212,21 +290,26 @@ class Text:
|
|||
)
|
||||
|
||||
def _split(
|
||||
self, xy: tuple[float, float], anchor: str | None, align: str
|
||||
) -> list[tuple[tuple[float, float], str, str | bytes]]:
|
||||
self,
|
||||
xy: tuple[float, float] = (0, 0),
|
||||
anchor: str | None = None,
|
||||
align: str = "left",
|
||||
lines: list[str] | list[bytes] | None = None,
|
||||
) -> list[_Line]:
|
||||
if anchor is None:
|
||||
anchor = "lt" if self.direction == "ttb" else "la"
|
||||
elif len(anchor) != 2:
|
||||
msg = "anchor must be a 2 character string"
|
||||
raise ValueError(msg)
|
||||
|
||||
lines = (
|
||||
self.text.split("\n")
|
||||
if isinstance(self.text, str)
|
||||
else self.text.split(b"\n")
|
||||
)
|
||||
if lines is None:
|
||||
lines = (
|
||||
self.text.split("\n")
|
||||
if isinstance(self.text, str)
|
||||
else self.text.split(b"\n")
|
||||
)
|
||||
if len(lines) == 1:
|
||||
return [(xy, anchor, self.text)]
|
||||
return [_Line(xy[0], xy[1], anchor, lines[0])]
|
||||
|
||||
if anchor[1] in "tb" and self.direction != "ttb":
|
||||
msg = "anchor not supported for multiline text"
|
||||
|
|
@ -251,7 +334,7 @@ class Text:
|
|||
if self.direction == "ttb":
|
||||
left = xy[0]
|
||||
for line in lines:
|
||||
parts.append(((left, top), anchor, line))
|
||||
parts.append(_Line(left, top, anchor, line))
|
||||
left += line_spacing
|
||||
else:
|
||||
widths = []
|
||||
|
|
@ -314,7 +397,7 @@ class Text:
|
|||
width_difference = max_width - sum(word_widths)
|
||||
i = 0
|
||||
for word in words:
|
||||
parts.append(((left, top), word_anchor, word))
|
||||
parts.append(_Line(left, top, word_anchor, word))
|
||||
left += word_widths[i] + width_difference / (len(words) - 1)
|
||||
i += 1
|
||||
top += line_spacing
|
||||
|
|
@ -325,7 +408,7 @@ class Text:
|
|||
left -= width_difference / 2.0
|
||||
elif anchor[0] == "r":
|
||||
left -= width_difference
|
||||
parts.append(((left, top), anchor, line))
|
||||
parts.append(_Line(left, top, anchor, line))
|
||||
top += line_spacing
|
||||
|
||||
return parts
|
||||
|
|
@ -356,9 +439,9 @@ class Text:
|
|||
"""
|
||||
bbox: tuple[float, float, float, float] | None = None
|
||||
fontmode = self._get_fontmode()
|
||||
for xy, anchor, line in self._split(xy, anchor, align):
|
||||
for x, y, anchor, text in self._split(xy, anchor, align):
|
||||
bbox_line = self.font.getbbox(
|
||||
line,
|
||||
text,
|
||||
fontmode,
|
||||
self.direction,
|
||||
self.features,
|
||||
|
|
@ -367,10 +450,10 @@ class Text:
|
|||
anchor,
|
||||
)
|
||||
bbox_line = (
|
||||
bbox_line[0] + xy[0],
|
||||
bbox_line[1] + xy[1],
|
||||
bbox_line[2] + xy[0],
|
||||
bbox_line[3] + xy[1],
|
||||
bbox_line[0] + x,
|
||||
bbox_line[1] + y,
|
||||
bbox_line[2] + x,
|
||||
bbox_line[3] + y,
|
||||
)
|
||||
if bbox is None:
|
||||
bbox = bbox_line
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user