Skip to content

Commit 5548f0e

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 2fea98c commit 5548f0e

File tree

1 file changed

+38
-9
lines changed

1 file changed

+38
-9
lines changed

pypdf/generic/_appearance_stream.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ def _generate_appearance_stream_data(
138138
font_name: str = "/Helv",
139139
font_size: float = 0.0,
140140
font_color: str = "0 g",
141-
multiline: bool = False
141+
multiline: bool = False,
142+
alignment: int = 0
142143
) -> bytes:
143144
"""
144145
Generates the raw bytes of the PDF appearance stream for a text field.
@@ -160,6 +161,7 @@ def _generate_appearance_stream_data(
160161
font_color: The color to apply to the font, represented as a PDF
161162
graphics state string (e.g., "0 g" for black).
162163
multiline: A boolean indicating if the text field is multiline.
164+
alignment: Left-aligned (0), centered (1) or right-aligned (2) text.
163165
164166
Returns:
165167
A byte string containing the PDF content stream data.
@@ -181,7 +183,7 @@ def _generate_appearance_stream_data(
181183
font_descriptor,
182184
font_size,
183185
rectangle.width - 3, # One point margin left and right, and an additional point because the first
184-
# offset takes one extra point (see below, under "line_number == 0:")
186+
# offset takes one extra point (see below, "desired_abs_x_start")
185187
rectangle.height - 3, # One point margin for top and bottom, one point extra for the first line
186188
# (see y_offset)
187189
text,
@@ -201,19 +203,41 @@ def _generate_appearance_stream_data(
201203
f"q\n/Tx BMC \nq\n1 1 {rectangle.width - 1} {rectangle.height - 1} "
202204
f"re\nW\nBT\n{default_appearance}\n".encode()
203205
)
204-
206+
current_x_pos: float = 0 # Initial virtual position within the text object.
205207
for line_number, (line_width, line) in enumerate(lines):
206208
if selection and line in selection:
207209
# Might be improved, but cannot find how to get fill working => replaced with lined box
208210
ap_stream += (
209211
f"1 {y_offset - (line_number * font_size * 1.4) - 1} {rectangle.width - 2} {font_size + 2} re\n"
210212
f"0.5 0.5 0.5 rg s\n{default_appearance}\n"
211213
).encode()
214+
215+
# Calculate the desired absolute starting X for the current line
216+
desired_abs_x_start: float = 0
217+
if alignment == 2: # Right aligned
218+
desired_abs_x_start = rectangle.width - 2 - line_width
219+
elif alignment == 1: # Centered
220+
desired_abs_x_start = (rectangle.width - line_width) / 2
221+
else: # Left aligned; default
222+
desired_abs_x_start = 2
223+
# Calculate x_rel_offset: how much to move from the current_x_pos
224+
# to reach the desired_abs_x_start.
225+
x_rel_offset = desired_abs_x_start - current_x_pos
226+
227+
# Y-offset:
228+
y_rel_offset: float = 0
212229
if line_number == 0:
213-
ap_stream += f"2 {y_offset} Td\n".encode()
230+
y_rel_offset = y_offset # Initial vertical position
214231
else:
215-
# Td is a relative translation
216-
ap_stream += f"0 {-font_size * 1.4} Td\n".encode()
232+
y_rel_offset = - font_size * 1.4 # Move down by line height
233+
234+
# Td is a relative translation (Tx and Ty).
235+
# It updates the current text position.
236+
ap_stream += f"{x_rel_offset} {y_rel_offset} Td\n".encode()
237+
# Update current_x_pos based on the Td operation for the next iteration.
238+
# This is the X position where the *current line* will start.
239+
current_x_pos = desired_abs_x_start
240+
217241
encoded_line: list[bytes] = [
218242
font_glyph_byte_map.get(c, c.encode("utf-16-be")) for c in line
219243
]
@@ -233,7 +257,8 @@ def __init__(
233257
font_name: str = "/Helv",
234258
font_size: float = 0.0,
235259
font_color: str = "0 g",
236-
multiline: bool = False
260+
multiline: bool = False,
261+
alignment: int = 0
237262
) -> None:
238263
"""
239264
Initializes a TextStreamAppearance object.
@@ -252,6 +277,7 @@ def __init__(
252277
font_size: The font size. If 0, it's auto-calculated.
253278
font_color: The font color string.
254279
multiline: A boolean indicating if the text field is multiline.
280+
alignment: Left-aligned (0), centered (1) or right-aligned (2) text.
255281
256282
"""
257283
super().__init__()
@@ -299,7 +325,8 @@ def __init__(
299325
font_name,
300326
font_size,
301327
font_color,
302-
multiline
328+
multiline,
329+
alignment
303330
)
304331

305332
self[NameObject("/Type")] = NameObject("/XObject")
@@ -406,6 +433,7 @@ def from_text_annotation(
406433
# Retrieve field text, selected values and formatting information
407434
multiline = False
408435
field_flags = field.get(FieldDictionaryAttributes.Ff, 0)
436+
alignment = field.get("/Q", 0)
409437
if field_flags & FieldDictionaryAttributes.FfBits.Multiline:
410438
multiline = True
411439
if (
@@ -432,7 +460,8 @@ def from_text_annotation(
432460
font_name,
433461
font_size,
434462
font_color,
435-
multiline
463+
multiline,
464+
alignment
436465
)
437466
if AnnotationDictionaryAttributes.AP in annotation:
438467
for key, value in (

0 commit comments

Comments
 (0)