@@ -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 \n q\n 1 1 { rectangle .width - 1 } { rectangle .height - 1 } "
203205 f"re\n W\n BT\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