Skip to content

Commit 9ebdbe4

Browse files
committed
ENH: TextAppearanceStream: Add right alignment and centering
This patch changes the TextAppearanceStream code so that it can deal with right alignment and centered text. Note that both require correct font metrics in order to work.
1 parent baefcb7 commit 9ebdbe4

File tree

1 file changed

+38
-8
lines changed

1 file changed

+38
-8
lines changed

pypdf/generic/_appearance_stream.py

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ def _generate_appearance_stream_data(
139139
font_name: str = "/Helv",
140140
font_size: float = 0.0,
141141
font_color: str = "0 g",
142-
is_multiline: bool = False
142+
is_multiline: bool = False,
143+
alignment: int = 0
143144
) -> bytes:
144145
"""
145146
Generates the raw bytes of the PDF appearance stream for a text field.
@@ -161,6 +162,7 @@ def _generate_appearance_stream_data(
161162
font_color: The color to apply to the font, represented as a PDF
162163
graphics state string (e.g., "0 g" for black).
163164
is_multiline: A boolean indicating if the text field is multiline.
165+
alignment: Left-aligned (0), centered (1) or right-aligned (2) text.
164166
165167
Returns:
166168
A byte string containing the PDF content stream data.
@@ -182,7 +184,7 @@ def _generate_appearance_stream_data(
182184
font_descriptor,
183185
font_size,
184186
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:")
187+
# offset takes one extra point (see below, "desired_abs_x_start")
186188
rectangle.height - 3, # One point margin for top and bottom, one point extra for the first line
187189
# (see y_offset)
188190
text,
@@ -202,6 +204,7 @@ def _generate_appearance_stream_data(
202204
f"q\n/Tx BMC \nq\n1 1 {rectangle.width - 1} {rectangle.height - 1} "
203205
f"re\nW\nBT\n{default_appearance}\n"
204206
).encode()
207+
current_x_pos: float = 0 # Initial virtual position within the text object.
205208

206209
for line_number, (line_width, line) in enumerate(lines):
207210
if selection and line in selection:
@@ -210,11 +213,33 @@ def _generate_appearance_stream_data(
210213
f"1 {y_offset - (line_number * font_size * 1.4) - 1} {rectangle.width - 2} {font_size + 2} re\n"
211214
f"0.5 0.5 0.5 rg s\n{default_appearance}\n"
212215
).encode()
216+
217+
# Calculate the desired absolute starting X for the current line
218+
desired_abs_x_start: float = 0
219+
if alignment == 2: # Right aligned
220+
desired_abs_x_start = rectangle.width - 2 - line_width
221+
elif alignment == 1: # Centered
222+
desired_abs_x_start = (rectangle.width - line_width) / 2
223+
else: # Left aligned; default
224+
desired_abs_x_start = 2
225+
# Calculate x_rel_offset: how much to move from the current_x_pos
226+
# to reach the desired_abs_x_start.
227+
x_rel_offset = desired_abs_x_start - current_x_pos
228+
229+
# Y-offset:
230+
y_rel_offset: float = 0
213231
if line_number == 0:
214-
ap_stream += f"2 {y_offset} Td\n".encode()
232+
y_rel_offset = y_offset # Initial vertical position
215233
else:
216-
# Td is a relative translation
217-
ap_stream += f"0 {-font_size * 1.4} Td\n".encode()
234+
y_rel_offset = - font_size * 1.4 # Move down by line height
235+
236+
# Td is a relative translation (Tx and Ty).
237+
# It updates the current text position.
238+
ap_stream += f"{x_rel_offset} {y_rel_offset} Td\n".encode()
239+
# Update current_x_pos based on the Td operation for the next iteration.
240+
# This is the X position where the *current line* will start.
241+
current_x_pos = desired_abs_x_start
242+
218243
encoded_line: list[bytes] = [
219244
font_glyph_byte_map.get(c, c.encode("utf-16-be")) for c in line
220245
]
@@ -234,7 +259,8 @@ def __init__(
234259
font_name: str = "/Helv",
235260
font_size: float = 0.0,
236261
font_color: str = "0 g",
237-
is_multiline: bool = False
262+
is_multiline: bool = False,
263+
alignment: int = 0
238264
) -> None:
239265
"""
240266
Initializes a TextStreamAppearance object.
@@ -253,6 +279,7 @@ def __init__(
253279
font_size: The font size. If 0, it's auto-calculated.
254280
font_color: The font color string.
255281
is_multiline: A boolean indicating if the text field is multiline.
282+
alignment: Left-aligned (0), centered (1) or right-aligned (2) text.
256283
257284
"""
258285
super().__init__()
@@ -301,7 +328,8 @@ def __init__(
301328
font_name,
302329
font_size,
303330
font_color,
304-
is_multiline
331+
is_multiline,
332+
alignment
305333
)
306334

307335
self[NameObject("/Type")] = NameObject("/XObject")
@@ -408,6 +436,7 @@ def from_text_annotation(
408436
# Retrieve field text, selected values and formatting information
409437
is_multiline = False
410438
field_flags = field.get(FieldDictionaryAttributes.Ff, 0)
439+
alignment = field.get("/Q", 0)
411440
if field_flags & FieldDictionaryAttributes.FfBits.Multiline:
412441
is_multiline = True
413442
if (
@@ -434,7 +463,8 @@ def from_text_annotation(
434463
font_name,
435464
font_size,
436465
font_color,
437-
is_multiline
466+
is_multiline,
467+
alignment
438468
)
439469
if AnnotationDictionaryAttributes.AP in annotation:
440470
for key, value in (

0 commit comments

Comments
 (0)