55
55
from ._cmap import _default_fonts_space_width , build_char_map_from_dict
56
56
from ._doc_common import DocumentInformation , PdfDocCommon
57
57
from ._encryption import EncryptAlgorithm , Encryption
58
- from ._page import PageObject
58
+ from ._page import PageObject , Transformation
59
59
from ._page_labels import nums_clear_range , nums_insert , nums_next
60
60
from ._reader import PdfReader
61
61
from ._utils import (
@@ -865,12 +865,102 @@ def append_pages_from_reader(
865
865
if callable (after_page_append ):
866
866
after_page_append (writer_page )
867
867
868
+ def _merge_content_stream_to_page (
869
+ self ,
870
+ page : PageObject ,
871
+ new_content_data : bytes ,
872
+ ) -> None :
873
+ """
874
+ Combines existing content stream(s) with new content (as bytes),
875
+ and returns a new single StreamObject.
876
+
877
+ Args:
878
+ page: The page to which the new content data will be added.
879
+ new_content_data: A binary-encoded new content stream, for
880
+ instance the commands to draw an XObject.
881
+ """
882
+ # First resolve the existing page content. This always is an IndirectObject:
883
+ # PDF Explained by John Whitington
884
+ # https://www.oreilly.com/library/view/pdf-explained/9781449321581/ch04.html
885
+ if NameObject ("/Contents" ) in page :
886
+ existing_content_ref = page [NameObject ("/Contents" )]
887
+ existing_content = existing_content_ref .get_object ()
888
+
889
+ if isinstance (existing_content , ArrayObject ):
890
+ # Create a new StreamObject for the new_content_data
891
+ new_stream_obj = StreamObject ()
892
+ new_stream_obj .set_data (new_content_data )
893
+ existing_content .append (self ._add_object (new_stream_obj ))
894
+ page [NameObject ("/Contents" )] = self ._add_object (existing_content )
895
+ if isinstance (existing_content , StreamObject ):
896
+ # Merge new content to existing StreamObject
897
+ merged_data = existing_content .get_data () + b"\n " + new_content_data
898
+ new_stream = StreamObject ()
899
+ new_stream .set_data (merged_data )
900
+ page [NameObject ("/Contents" )] = self ._add_object (new_stream )
901
+ else :
902
+ # If no existing content, then we have an empty page.
903
+ # Create a new StreamObject in a new /Contents entry.
904
+ new_stream = StreamObject ()
905
+ new_stream .set_data (new_content_data )
906
+ page [NameObject ("/Contents" )] = self ._add_object (new_stream )
907
+
908
+ def _add_apstream_object (
909
+ self ,
910
+ page : PageObject ,
911
+ appearance_stream_obj : StreamObject ,
912
+ object_name : str ,
913
+ x_offset : float ,
914
+ y_offset : float ,
915
+ font_res : Optional [DictionaryObject ] = None
916
+ ) -> None :
917
+ """
918
+ Adds an appearance stream to the page content in the form of
919
+ an XObject.
920
+
921
+ Args:
922
+ page: The page to which to add the appearance stream.
923
+ appearance_stream_obj: The appearance stream.
924
+ object_name: The name of the appearance stream.
925
+ x_offset: The horizontal offset for the appearance stream.
926
+ y_offset: The vertical offset for the appearance stream.
927
+ font_res: The appearance stream's font resource (if given).
928
+ """
929
+ # Prepare XObject resource dictionary on the page
930
+ pg_res = cast (DictionaryObject , page [PG .RESOURCES ])
931
+ if font_res is not None :
932
+ font_name = font_res ["/BaseFont" ] # [/"Name"] often also exists, but is deprecated
933
+ if "/Font" not in pg_res :
934
+ pg_res [NameObject ("/Font" )] = DictionaryObject ()
935
+ pg_ft_res = cast (DictionaryObject , pg_res [NameObject ("/Font" )])
936
+ if font_name not in pg_ft_res :
937
+ pg_ft_res [NameObject (font_name )] = font_res
938
+ # Always add the resolved stream object to the writer to get a new IndirectObject.
939
+ # This ensures we have a valid IndirectObject managed by *this* writer.
940
+ xobject_ref = self ._add_object (appearance_stream_obj )
941
+ xobject_name = NameObject (f"/Fm_{ object_name } " )._sanitize ()
942
+ if "/XObject" not in pg_res :
943
+ pg_res [NameObject ("/XObject" )] = DictionaryObject ()
944
+ pg_xo_res = cast (DictionaryObject , pg_res ["/XObject" ])
945
+ if xobject_name not in pg_xo_res :
946
+ pg_xo_res [xobject_name ] = xobject_ref
947
+ else :
948
+ logger_warning (
949
+ f"XObject { xobject_name !r} already added to page resources. This might be an issue." ,
950
+ __name__
951
+ )
952
+ xobject_cm = Transformation ().translate (x_offset , y_offset )
953
+ xobject_drawing_commands = f"q\n { xobject_cm ._to_cm ()} \n { xobject_name } Do\n Q" .encode ()
954
+ self ._merge_content_stream_to_page (page , xobject_drawing_commands )
955
+
868
956
def _update_field_annotation (
869
957
self ,
958
+ page : PageObject ,
870
959
field : DictionaryObject ,
871
960
annotation : DictionaryObject ,
872
961
font_name : str = "" ,
873
962
font_size : float = - 1 ,
963
+ flatten : bool = False ,
874
964
) -> None :
875
965
# Calculate rectangle dimensions
876
966
_rct = cast (RectangleObject , annotation [AA .Rect ])
@@ -1013,6 +1103,10 @@ def _update_field_annotation(
1013
1103
self ._objects [n - 1 ] = dct
1014
1104
dct .indirect_reference = IndirectObject (n , 0 , self )
1015
1105
1106
+ if flatten :
1107
+ field_name = self ._get_qualified_field_name (annotation )
1108
+ self ._add_apstream_object (page , dct , field_name , _rct [0 ], _rct [1 ], font_res )
1109
+
1016
1110
FFBITS_NUL = FA .FfBits (0 )
1017
1111
1018
1112
def update_page_form_field_values (
@@ -1021,6 +1115,7 @@ def update_page_form_field_values(
1021
1115
fields : Dict [str , Union [str , List [str ], Tuple [str , str , float ]]],
1022
1116
flags : FA .FfBits = FFBITS_NUL ,
1023
1117
auto_regenerate : Optional [bool ] = True ,
1118
+ flatten : bool = False ,
1024
1119
) -> None :
1025
1120
"""
1026
1121
Update the form field values for a given page from a fields dictionary.
@@ -1047,6 +1142,10 @@ def update_page_form_field_values(
1047
1142
auto_regenerate: Set/unset the need_appearances flag;
1048
1143
the flag is unchanged if auto_regenerate is None.
1049
1144
1145
+ flatten: Whether or not to flatten the annotation. If True, this adds the annotation's
1146
+ appearance stream to the page contents. Note that this option does not remove the
1147
+ annotation itself.
1148
+
1050
1149
"""
1051
1150
if CatalogDictionary .ACRO_FORM not in self ._root_object :
1052
1151
raise PyPdfError ("No /AcroForm dictionary in PDF of PdfWriter Object" )
@@ -1061,7 +1160,7 @@ def update_page_form_field_values(
1061
1160
if isinstance (page , list ):
1062
1161
for p in page :
1063
1162
if PG .ANNOTS in p : # just to prevent warnings
1064
- self .update_page_form_field_values (p , fields , flags , None )
1163
+ self .update_page_form_field_values (p , fields , flags , None , flatten = flatten )
1065
1164
return
1066
1165
if PG .ANNOTS not in page :
1067
1166
logger_warning ("No fields to update on this page" , __name__ )
@@ -1090,35 +1189,43 @@ def update_page_form_field_values(
1090
1189
del parent_annotation ["/I" ]
1091
1190
if flags :
1092
1191
annotation [NameObject (FA .Ff )] = NumberObject (flags )
1093
- if isinstance (value , list ):
1094
- lst = ArrayObject (TextStringObject (v ) for v in value )
1095
- parent_annotation [NameObject (FA .V )] = lst
1096
- elif isinstance (value , tuple ):
1097
- annotation [NameObject (FA .V )] = TextStringObject (
1098
- value [0 ],
1099
- )
1100
- else :
1101
- parent_annotation [NameObject (FA .V )] = TextStringObject (value )
1192
+ if not (value is None and flatten ): # Only change values if given by user and not flattening.
1193
+ if isinstance (value , list ):
1194
+ lst = ArrayObject (TextStringObject (v ) for v in value )
1195
+ parent_annotation [NameObject (FA .V )] = lst
1196
+ elif isinstance (value , tuple ):
1197
+ annotation [NameObject (FA .V )] = TextStringObject (
1198
+ value [0 ],
1199
+ )
1200
+ else :
1201
+ parent_annotation [NameObject (FA .V )] = TextStringObject (value )
1102
1202
if parent_annotation .get (FA .FT ) == "/Btn" :
1103
1203
# Checkbox button (no /FT found in Radio widgets)
1104
1204
v = NameObject (value )
1105
1205
ap = cast (DictionaryObject , annotation [NameObject (AA .AP )])
1106
- if v not in cast (ArrayObject , ap [NameObject ("/N" )]):
1206
+ normal_ap = cast (DictionaryObject , ap ["/N" ])
1207
+ if v not in normal_ap :
1107
1208
v = NameObject ("/Off" )
1209
+ appearance_stream_obj = normal_ap .get (v )
1108
1210
# other cases will be updated through the for loop
1109
1211
annotation [NameObject (AA .AS )] = v
1110
1212
annotation [NameObject (FA .V )] = v
1213
+ if flatten and appearance_stream_obj is not None :
1214
+ # We basically copy the entire appearance stream, which should be an XObject that
1215
+ # is already registered. No need to add font resources.
1216
+ rct = cast (RectangleObject , annotation [AA .Rect ])
1217
+ self ._add_apstream_object (page , appearance_stream_obj , field , rct [0 ], rct [1 ])
1111
1218
elif (
1112
1219
parent_annotation .get (FA .FT ) == "/Tx"
1113
1220
or parent_annotation .get (FA .FT ) == "/Ch"
1114
1221
):
1115
1222
# textbox
1116
1223
if isinstance (value , tuple ):
1117
1224
self ._update_field_annotation (
1118
- parent_annotation , annotation , value [1 ], value [2 ]
1225
+ page , parent_annotation , annotation , value [1 ], value [2 ], flatten = flatten
1119
1226
)
1120
1227
else :
1121
- self ._update_field_annotation (parent_annotation , annotation )
1228
+ self ._update_field_annotation (page , parent_annotation , annotation , flatten = flatten )
1122
1229
elif (
1123
1230
annotation .get (FA .FT ) == "/Sig"
1124
1231
): # deprecated # not implemented yet
0 commit comments