Skip to content

Commit 3b6a69b

Browse files
committed
MAINT: _appearance_writer: Don't pass default_appearance
Instead of passing around default appearance, construct it from given font name, size and color. Also, having a default appearance as an argument for a text stream appearance seems less "natural" than just passing font name, size and color. This patch also represents a small number of simplifications that improve test coverage.
1 parent c5f6e51 commit 3b6a69b

File tree

2 files changed

+28
-32
lines changed

2 files changed

+28
-32
lines changed

pypdf/generic/_appearance_stream.py

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ def _appearance_stream_data(
2525
self,
2626
text: str = "",
2727
selection: Optional[list[str]] = None,
28-
default_appearance: str = "",
2928
font_glyph_byte_map: Optional[dict[str, bytes]] = None,
3029
rect: Union[RectangleObject, tuple[float, float, float, float]] = (0.0, 0.0, 0.0, 0.0),
31-
font_size: float = 0,
30+
font_name: str = "/Helv",
31+
font_size: float = 0.0,
32+
font_color: str = "0 g",
3233
multiline: bool = False
3334
) -> bytes:
3435
font_glyph_byte_map = font_glyph_byte_map or {}
@@ -41,11 +42,10 @@ def _appearance_stream_data(
4142
font_size = DEFAULT_FONT_SIZE_IN_MULTILINE
4243
else:
4344
font_size = rect.height - 2
44-
default_appearance = re.sub(r"0.0 Tf", str(font_size) + r" Tf", default_appearance)
4545

4646
# Set the vertical offset
4747
y_offset = rect.height - 1 - font_size
48-
48+
default_appearance = f"{font_name} {font_size} Tf {font_color}"
4949
ap_stream = f"q\n/Tx BMC \nq\n1 1 {rect.width - 1} {rect.height - 1} re\nW\nBT\n{default_appearance}\n".encode()
5050
for line_number, line in enumerate(text.replace("\n", "\r").split("\r")):
5151
if selection and line in selection:
@@ -73,24 +73,23 @@ def __init__(
7373
self,
7474
text: str = "",
7575
selection: Optional[list[str]] = None,
76-
default_appearance: str = "",
7776
font_glyph_byte_map: Optional[dict[str, bytes]] = None,
7877
rect: Union[RectangleObject, tuple[float, float, float, float]] = (0.0, 0.0, 0.0, 0.0),
79-
font_size: float = 0,
78+
font_name: str = "/Helv",
79+
font_size: float = 0.0,
80+
font_color: str = "0 g",
8081
multiline: bool = False
8182
) -> None:
8283
font_glyph_byte_map = font_glyph_byte_map or {}
83-
if isinstance(rect, tuple):
84-
rect = RectangleObject(rect)
8584

8685
ap_stream_data = self._appearance_stream_data(
87-
text, selection, default_appearance, font_glyph_byte_map, rect, font_size, multiline
86+
text, selection, font_glyph_byte_map, rect, font_name, font_size, font_color, multiline
8887
)
8988

9089
super().__init__()
9190
self[NameObject("/Type")] = NameObject("/XObject")
9291
self[NameObject("/Subtype")] = NameObject("/Form")
93-
self[NameObject("/BBox")] = rect
92+
self[NameObject("/BBox")] = RectangleObject(rect)
9493
self.set_data(ByteStringObject(ap_stream_data))
9594
self[NameObject("/Length")] = NumberObject(len(ap_stream_data))
9695

@@ -113,33 +112,30 @@ def from_text_annotation(
113112
AnnotationDictionaryAttributes.DA,
114113
acro_form.get(AnnotationDictionaryAttributes.DA, None),
115114
)
116-
if default_appearance is None:
115+
if not default_appearance:
117116
# Create a default appearance if none was found in the annotation
118117
default_appearance = TextStringObject("/Helv 0 Tf 0 g")
119118
else:
120119
default_appearance = default_appearance.get_object()
121120

122-
# Embed user-provided font name and font size in the default appearance, also
123-
# taking into account whether the field flags indicate a multiline field.
124-
# Uses the variable font_properties as an intermediate.
125-
font_properties = default_appearance.replace("\n", " ").replace("\r", " ").split(" ")
126-
font_properties = [x for x in font_properties if x != ""]
127-
# Override default appearance font name with user provided font name, if given.
121+
# Derive font name, size and color from the default appearance. Also set
122+
# user-provided font name and font size in the default appearance, if given.
123+
# For a font name, this presumes that we can find an associated font resource
124+
# dictionary. Uses the variable font_properties as an intermediate.
125+
# As per the PDF spec:
126+
# "At a minimum, the string [that is, default_appearance] shall include a Tf (text
127+
# font) operator along with its two operands, font and size" (p. 519 of Version 2.0).
128+
font_properties = [prop for prop in re.split(r"\s", default_appearance) if prop]
129+
font_name = font_properties.pop(font_properties.index("Tf") - 2)
130+
font_size = float(font_properties.pop(font_properties.index("Tf") - 1))
131+
font_properties.remove("Tf")
132+
font_color = " ".join(font_properties)
133+
# Determine the font name to use, prioritizing the user's input
128134
if user_font_name:
129135
font_name = user_font_name
130-
font_properties[font_properties.index("Tf") - 2] = user_font_name
131-
else:
132-
# Indirectly this just reads font_name from default appearance.
133-
font_name = font_properties[font_properties.index("Tf") - 2]
134-
# Override default appearance font size with user provided font size, if given.
135-
font_size = (
136-
user_font_size
137-
if user_font_size >= 0
138-
else float(font_properties[font_properties.index("Tf") - 1])
139-
)
140-
font_properties[font_properties.index("Tf") - 1] = str(font_size)
141-
# Reconstruct default appearance with user info and flags information
142-
default_appearance = " ".join(font_properties)
136+
# Determine the font size to use, prioritizing the user's input
137+
if user_font_size > 0:
138+
font_size = user_font_size
143139

144140
# Try to find a resource dictionary for the font
145141
document_resources: Any = cast(
@@ -207,7 +203,7 @@ def from_text_annotation(
207203

208204
# Create the TextStreamAppearance instance
209205
new_appearance_stream = cls(
210-
text, selection, default_appearance, font_glyph_byte_map, rect, font_size, multiline
206+
text, selection, font_glyph_byte_map, rect, font_name, font_size, font_color, multiline
211207
)
212208

213209
if AnnotationDictionaryAttributes.AP in annotation:

tests/test_writer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2406,7 +2406,7 @@ def test_selfont():
24062406
b"Text_1" in writer.pages[0]["/Annots"][1].get_object()["/AP"]["/N"].get_data()
24072407
)
24082408
assert (
2409-
b"/F3 12 Tf"
2409+
b"/F3 12.0 Tf"
24102410
in writer.pages[0]["/Annots"][2].get_object()["/AP"]["/N"].get_data()
24112411
)
24122412
assert (

0 commit comments

Comments
 (0)