Skip to content

Commit 370c7e9

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 ad26892 commit 370c7e9

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__()
@@ -300,7 +327,8 @@ def __init__(
300327
font_name,
301328
font_size,
302329
font_color,
303-
is_multiline
330+
is_multiline,
331+
alignment
304332
)
305333

306334
self[NameObject("/Type")] = NameObject("/XObject")
@@ -407,6 +435,7 @@ def from_text_annotation(
407435
# Retrieve field text, selected values and formatting information
408436
is_multiline = False
409437
field_flags = field.get(FieldDictionaryAttributes.Ff, 0)
438+
alignment = field.get("/Q", 0)
410439
if field_flags & FieldDictionaryAttributes.FfBits.Multiline:
411440
is_multiline = True
412441
if (
@@ -433,7 +462,8 @@ def from_text_annotation(
433462
font_name,
434463
font_size,
435464
font_color,
436-
is_multiline
465+
is_multiline,
466+
alignment
437467
)
438468
if AnnotationDictionaryAttributes.AP in annotation:
439469
for key, value in (

0 commit comments

Comments
 (0)