Skip to content

Commit a5fc2ef

Browse files
committed
ENH: TextAppearanceStream: Add method to scale and wrap text
This patch adds a method to scale and wrap text, depending on whether or not text is allowed to be wrapped. It takes a couple of arguments, including the text string itself, field width and height, font size, a FontDescriptor with character widths, and a bool specifying whether or not text is allowed to wrap. Returns the text in in the form of list of tuples, each tuple containing the length of a line and its contents, and the font size for these lines and lengths.
1 parent f69c182 commit a5fc2ef

File tree

1 file changed

+103
-0
lines changed

1 file changed

+103
-0
lines changed

pypdf/generic/_appearance_stream.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import Any, Optional, Union, cast
33

44
from .._cmap import _default_fonts_space_width, build_char_map_from_dict
5+
from .._font import FontDescriptor
56
from .._utils import logger_warning
67
from ..constants import AnnotationDictionaryAttributes, FieldDictionaryAttributes
78
from ..generic import (
@@ -25,6 +26,108 @@ class TextStreamAppearance(DecodedStreamObject):
2526
like font, font size, color, multiline text, and text selection highlighting.
2627
"""
2728

29+
def _scale_text(
30+
self,
31+
font_descriptor: FontDescriptor,
32+
font_size: float,
33+
field_width: float,
34+
field_height: float,
35+
txt: str,
36+
is_multiline: bool,
37+
min_font_size: float = 4.0, # Minimum font size to attempt
38+
font_size_step: float = 0.2 # How much to decrease font size by each step
39+
) -> tuple[list[tuple[float, str]], float]:
40+
"""
41+
Takes a piece of text and scales it to field_width or field_height, given font_name
42+
and font_size. For multiline fields, adds newlines to wrap the text.
43+
44+
Args:
45+
font_descriptor: A FontDescriptor for the font to be used.
46+
font_size: The font size in points.
47+
field_width: The width of the field in which to fit the text.
48+
field_height: The height of the field in which to fit the text.
49+
txt: The text to fit with the field.
50+
is_multiline: Whether to scale and wrap the text, or only to scale.
51+
min_font_size: The minimum font size at which to scale the text.
52+
font_size_step: The amount by which to decrement font size per step while scaling.
53+
54+
Returns:
55+
The text in the form of list of tuples, each tuple containing the length of a line
56+
and its contents, and the font_size for these lines and lengths.
57+
"""
58+
# Single line:
59+
if not is_multiline:
60+
test_width = font_descriptor.text_width(txt) * font_size / 1000
61+
if test_width > field_width or font_size > field_height:
62+
new_font_size = font_size - font_size_step
63+
if new_font_size >= min_font_size:
64+
# Text overflows height; Retry with smaller font size.
65+
return self._scale_text(
66+
font_descriptor,
67+
round(new_font_size, 1),
68+
field_width,
69+
field_height,
70+
txt,
71+
is_multiline,
72+
min_font_size,
73+
font_size_step
74+
)
75+
# Font size lower than set minimum font size, give up.
76+
return [(test_width, txt)], font_size
77+
return [(test_width, txt)], font_size
78+
# Multiline:
79+
orig_txt = txt
80+
paragraphs = re.sub(r"\n", "\r", txt).split("\r")
81+
wrapped_lines = []
82+
current_line_words: list[str] = []
83+
current_line_width: float = 0
84+
space_width = font_descriptor.text_width(" ") * font_size / 1000
85+
for paragraph in paragraphs:
86+
if not paragraph.strip():
87+
wrapped_lines.append((0.0, ""))
88+
continue
89+
words = paragraph.split(" ")
90+
for i, word in enumerate(words):
91+
word_width = font_descriptor.text_width(word) * font_size / 1000
92+
test_width = current_line_width + word_width + (space_width if i else 0)
93+
if test_width > field_width and current_line_words:
94+
wrapped_lines.append((current_line_width, " ".join(current_line_words)))
95+
current_line_words = [word]
96+
current_line_width = word_width
97+
elif not current_line_words and word_width > field_width:
98+
wrapped_lines.append((word_width, word))
99+
current_line_words = []
100+
current_line_width = 0
101+
else:
102+
if current_line_words:
103+
current_line_width += space_width
104+
current_line_words.append(word)
105+
current_line_width += word_width
106+
if current_line_words:
107+
wrapped_lines.append((current_line_width, " ".join(current_line_words)))
108+
current_line_words = []
109+
current_line_width = 0
110+
# Estimate total height.
111+
# Assumed line spacing of 1.4
112+
estimated_total_height = font_size + (len(wrapped_lines) - 1) * 1.4 * font_size
113+
if estimated_total_height > field_height:
114+
new_font_size = font_size - font_size_step
115+
if new_font_size >= min_font_size:
116+
# Text overflows height; Retry with smaller font size.
117+
return self._scale_text(
118+
font_descriptor,
119+
round(new_font_size, 1),
120+
field_width,
121+
field_height,
122+
orig_txt,
123+
is_multiline,
124+
min_font_size,
125+
font_size_step
126+
)
127+
# Font size lower than set minimum font size, give up.
128+
return (wrapped_lines, font_size)
129+
return (wrapped_lines, font_size)
130+
28131
def _generate_appearance_stream_data(
29132
self,
30133
text: str = "",

0 commit comments

Comments
 (0)