Skip to content

Commit 3a6fdac

Browse files
committed
ENH: PdfWriter: Scale and wrap text in text annotations
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 fe016da commit 3a6fdac

File tree

1 file changed

+43
-8
lines changed

1 file changed

+43
-8
lines changed

pypdf/_writer.py

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
from ._cmap import _default_fonts_space_width, build_char_map_from_dict
5252
from ._doc_common import DocumentInformation, PdfDocCommon
5353
from ._encryption import EncryptAlgorithm, Encryption
54+
from ._font import FontDescriptor
5455
from ._page import PageObject, Transformation
5556
from ._page_labels import nums_clear_range, nums_insert, nums_next
5657
from ._reader import PdfReader
@@ -929,23 +930,35 @@ def _update_field_annotation(
929930
da = da.get_object()
930931
font_properties = da.replace("\n", " ").replace("\r", " ").split(" ")
931932
font_properties = [x for x in font_properties if x != ""]
933+
# If font name was given when calling this method, then add it to
934+
# the font properties, otherwise read it from the default appearance
932935
if font_name:
933936
font_properties[font_properties.index("Tf") - 2] = font_name
934937
else:
935938
font_name = font_properties[font_properties.index("Tf") - 2]
939+
# If font size was given when calling this method, then add it to
940+
# the font properties, otherwise read it from the default appearance
936941
font_height = (
937942
font_size
938943
if font_size >= 0
939944
else float(font_properties[font_properties.index("Tf") - 1])
940945
)
946+
formatting = {
947+
"wrap" : False,
948+
"scale" : False,
949+
}
950+
# Only when font height is 0 in the default appearance (or when there is no default
951+
# appearance, but this should not be the case for a text annotation) set the text
952+
# wrapping and/or font scaling options for generating the appearance stream.
941953
if font_height == 0:
942954
if field.get(FA.Ff, 0) & FA.FfBits.Multiline:
943955
font_height = DEFAULT_FONT_HEIGHT_IN_MULTILINE
956+
formatting["wrap"] = True
957+
formatting["scale"] = True
944958
else:
945959
font_height = rct.height - 2
960+
formatting["scale"] = True
946961
font_properties[font_properties.index("Tf") - 1] = str(font_height)
947-
da = " ".join(font_properties)
948-
y_offset = rct.height - 1 - font_height
949962

950963
# Retrieve font information from local DR ...
951964
dr: Any = cast(
@@ -977,6 +990,7 @@ def _update_field_annotation(
977990
_font_subtype, _, font_encoding, font_map = build_char_map_from_dict(
978991
200, font_res
979992
)
993+
font_descriptor = FontDescriptor.from_font_resource(font_res)
980994
try: # remove width stored in -1 key
981995
del font_map[-1]
982996
except KeyError:
@@ -994,6 +1008,7 @@ def _update_field_annotation(
9941008
else:
9951009
logger_warning(f"Font dictionary for {font_name} not found.", __name__)
9961010
font_full_rev = {}
1011+
font_descriptor = FontDescriptor()
9971012

9981013
# Retrieve field text and selected values
9991014
field_flags = field.get(FA.Ff, 0)
@@ -1005,11 +1020,9 @@ def _update_field_annotation(
10051020
else: # /Tx
10061021
txt = field.get("/V", "")
10071022
sel = []
1008-
# Escape parentheses (PDF 1.7 reference, table 3.2, Literal Strings)
1009-
txt = txt.replace("\\", "\\\\").replace("(", r"\(").replace(")", r"\)")
10101023
# Generate appearance stream
10111024
ap_stream = generate_appearance_stream(
1012-
txt, sel, da, font_full_rev, rct, font_height, y_offset
1025+
txt, sel, font_properties, font_full_rev, font_descriptor, rct, font_height, formatting
10131026
)
10141027

10151028
# Create appearance dictionary
@@ -3536,14 +3549,36 @@ def scale_text(
35363549
def generate_appearance_stream(
35373550
txt: str,
35383551
sel: list[str],
3539-
da: str,
3552+
font_properties: list[str],
35403553
font_full_rev: dict[str, bytes],
3554+
font_descriptor: FontDescriptor,
35413555
rct: RectangleObject,
35423556
font_height: float,
3543-
y_offset: float,
3557+
formatting: dict[str, bool],
35443558
) -> bytes:
3559+
# Only wrap text for non-choice fields, otherwise we break matching sel and line later on.
3560+
if sel:
3561+
formatting["wrap"] = False
3562+
if formatting["scale"]:
3563+
lines, font_height = scale_text(
3564+
font_descriptor,
3565+
font_height,
3566+
rct.width - 3, # One point margin left and right, and an additional point because the first offset
3567+
# takes one extra point (see below, under "line_number == 0:")
3568+
rct.height - 3, # One point margin for top and bottom, one point extra for the first line (see y_offset)
3569+
txt,
3570+
formatting["wrap"],
3571+
)
3572+
font_properties[1] = str(font_height)
3573+
else:
3574+
lines = txt.replace("\n", "\r").split("\r")
3575+
3576+
y_offset = rct.height - 1 - font_height
3577+
da = " ".join(font_properties)
35453578
ap_stream = f"q\n/Tx BMC \nq\n1 1 {rct.width - 1} {rct.height - 1} re\nW\nBT\n{da}\n".encode()
3546-
for line_number, line in enumerate(txt.replace("\n", "\r").split("\r")):
3579+
for line_number, line in enumerate(lines):
3580+
# Escape parentheses (PDF 1.7 reference, table 3.2, Literal Strings)
3581+
line = line.replace("\\", "\\\\").replace("(", r"\(").replace(")", r"\)")
35473582
if line in sel:
35483583
# may be improved but cannot find how to get fill working => replaced with lined box
35493584
ap_stream += (

0 commit comments

Comments
 (0)