Skip to content

Commit 683f479

Browse files
committed
ENH: TextAppearanceStream: Scale and wrap text
This patch scales and/or wrap text that does not fit into a text field unaltered, under the condition that font size was set to 0 in the default appearance stream. We only wrap text if the multiline bit was set in the corresponding annotation's field flags, otherwise we just scale the font until it fits. We move the escaping of parentheses below, so that it does not interfere with calculating the width of a text string.
1 parent 053e67e commit 683f479

File tree

1 file changed

+23
-1
lines changed

1 file changed

+23
-1
lines changed

pypdf/generic/_appearance_stream.py

Lines changed: 23 additions & 1 deletion
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 .._codecs.core_fontmetrics import CORE_FONT_METRICS
56
from .._font import FontDescriptor
67
from .._utils import logger_warning
78
from ..constants import AnnotationDictionaryAttributes, FieldDictionaryAttributes
@@ -133,6 +134,7 @@ def _generate_appearance_stream_data(
133134
text: str = "",
134135
selection: Optional[list[str]] = None,
135136
rectangle: Union[RectangleObject, tuple[float, float, float, float]] = (0.0, 0.0, 0.0, 0.0),
137+
font_descriptor: FontDescriptor = CORE_FONT_METRICS["Helvetica"],
136138
font_glyph_byte_map: Optional[dict[str, bytes]] = None,
137139
font_name: str = "/Helv",
138140
font_size: float = 0.0,
@@ -170,10 +172,27 @@ def _generate_appearance_stream_data(
170172

171173
# If font_size is 0, apply the logic for multiline or large-as-possible font
172174
if font_size == 0:
175+
if selection: # Don't wrap text when dealing with a /Ch field, in order to prevent problems
176+
is_multiline = False # with matching "selection" with "line" later on.
173177
if is_multiline:
174178
font_size = DEFAULT_FONT_SIZE_IN_MULTILINE
175179
else:
176180
font_size = rectangle.height - 2
181+
lines, font_size = self._scale_text(
182+
font_descriptor,
183+
font_size,
184+
rectangle.width - 3, # One point margin left and right, and an additional point because the first
185+
# offset takes one extra point (see below, under "line_number == 0:")
186+
rectangle.height - 3, # One point margin for top and bottom, one point extra for the first line
187+
# (see y_offset)
188+
text,
189+
is_multiline,
190+
)
191+
else:
192+
lines = [(
193+
font_descriptor.text_width(line) * font_size / 1000,
194+
line
195+
) for line in text.replace("\n", "\r").split("\r")]
177196

178197
# Set the vertical offset
179198
y_offset = rectangle.height - 1 - font_size
@@ -184,7 +203,7 @@ def _generate_appearance_stream_data(
184203
f"re\nW\nBT\n{default_appearance}\n"
185204
).encode()
186205

187-
for line_number, line in enumerate(text.replace("\n", "\r").split("\r")):
206+
for line_number, (line_width, line) in enumerate(lines):
188207
if selection and line in selection:
189208
# Might be improved, but cannot find how to get fill working => replaced with lined box
190209
ap_stream += (
@@ -241,6 +260,7 @@ def __init__(
241260
# If a font resource was added, get the font character map
242261
if font_resource:
243262
font_resource = cast(DictionaryObject, font_resource.get_object())
263+
font_descriptor = FontDescriptor.from_font_resource(font_resource)
244264
_font_subtype, _, font_encoding, font_map = build_char_map_from_dict(
245265
200, font_resource
246266
)
@@ -261,11 +281,13 @@ def __init__(
261281
else:
262282
logger_warning(f"Font dictionary for {font_name} not found.", __name__)
263283
font_glyph_byte_map = {}
284+
font_descriptor = FontDescriptor()
264285

265286
ap_stream_data = self._generate_appearance_stream_data(
266287
text,
267288
selection,
268289
rectangle,
290+
font_descriptor,
269291
font_glyph_byte_map,
270292
font_name,
271293
font_size,

0 commit comments

Comments
 (0)