Skip to content

Commit 06c430e

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 dd832f9 commit 06c430e

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

0 commit comments

Comments
 (0)