diff --git a/docs/modules/constants.rst b/docs/modules/constants.rst index 37f991303..b5de3ba52 100644 --- a/docs/modules/constants.rst +++ b/docs/modules/constants.rst @@ -20,3 +20,9 @@ Constants :members: :undoc-members: :show-inheritance: + +.. autoclass:: pypdf.constants.FieldDictionaryAttributes + :members: + :undoc-members: + :exclude-members: FT, Parent, Kids, T, TU, TM, V, DV, AA, Opt, attributes, attributes_dict + :show-inheritance: diff --git a/pypdf/_writer.py b/pypdf/_writer.py index 11ead48ff..62fdf0e86 100644 --- a/pypdf/_writer.py +++ b/pypdf/_writer.py @@ -70,7 +70,6 @@ from .constants import CatalogAttributes as CA from .constants import ( CatalogDictionary, - FieldFlag, FileSpecificationDictionaryEntries, GoToActionArguments, ImageType, @@ -123,8 +122,8 @@ ) from .xmp import XmpInformation -OPTIONAL_READ_WRITE_FIELD = FieldFlag(0) ALL_DOCUMENT_PERMISSIONS = UserAccessPermissions.all() +DEFAULT_FONT_HEIGHT_IN_MULTILINE = 12 class ObjectDeletionFlag(enum.IntFlag): @@ -780,7 +779,11 @@ def append_pages_from_reader( after_page_append(writer_page) def _update_field_annotation( - self, field: DictionaryObject, anno: DictionaryObject + self, + field: DictionaryObject, + anno: DictionaryObject, + font_name: str = "", + font_size: float = -1, ) -> None: # Calculate rectangle dimensions _rct = cast(RectangleObject, anno[AA.Rect]) @@ -799,12 +802,22 @@ def _update_field_annotation( da = da.get_object() font_properties = da.replace("\n", " ").replace("\r", " ").split(" ") font_properties = [x for x in font_properties if x != ""] - font_name = font_properties[font_properties.index("Tf") - 2] - font_height = float(font_properties[font_properties.index("Tf") - 1]) + if font_name: + font_properties[font_properties.index("Tf") - 2] = font_name + else: + font_name = font_properties[font_properties.index("Tf") - 2] + font_height = ( + font_size + if font_size >= 0 + else float(font_properties[font_properties.index("Tf") - 1]) + ) if font_height == 0: - font_height = rct.height - 2 - font_properties[font_properties.index("Tf") - 1] = str(font_height) - da = " ".join(font_properties) + if field.get(FA.Ff, 0) & FA.FfBits.Multiline: + font_height = DEFAULT_FONT_HEIGHT_IN_MULTILINE + else: + font_height = rct.height - 2 + font_properties[font_properties.index("Tf") - 1] = str(font_height) + da = " ".join(font_properties) y_offset = rct.height - 1 - font_height # Retrieve font information from local DR ... @@ -926,11 +939,13 @@ def _update_field_annotation( self._objects[n - 1] = dct dct.indirect_reference = IndirectObject(n, 0, self) + FFBITS_NUL = FA.FfBits(0) + def update_page_form_field_values( self, page: Union[PageObject, List[PageObject], None], fields: Dict[str, Any], - flags: FieldFlag = OPTIONAL_READ_WRITE_FIELD, + flags: FA.FfBits = FFBITS_NUL, auto_regenerate: Optional[bool] = True, ) -> None: """ @@ -944,12 +959,18 @@ def update_page_form_field_values( annotations and field data will be updated. `List[Pageobject]` - provides list of pages to be processed. `None` - all pages. - fields: a Python dictionary of field names (/T) and text - values (/V). - flags: An integer (0 to 7). The first bit sets ReadOnly, the - second bit sets Required, the third bit sets NoExport. See - PDF Reference Table 8.70 for details. - auto_regenerate: set/unset the need_appearances flag ; + fields: a Python dictionary of: + + * field names (/T) as keys and text values (/V) as value + * field names (/T) as keys and list of text values (/V) for multiple choice list + * field names (/T) as keys and tuple of: + * text values (/V) + * font id (e.g. /F1, the font id must exist) + * font size (0 for autosize) + + flags: A set of flags from :class:`~pypdf.constants.FieldDictionaryAttributes.FfBits`. + + auto_regenerate: Set/unset the need_appearances flag; the flag is unchanged if auto_regenerate is None. """ if CatalogDictionary.ACRO_FORM not in self._root_object: @@ -997,6 +1018,10 @@ def update_page_form_field_values( if isinstance(value, list): lst = ArrayObject(TextStringObject(v) for v in value) writer_parent_annot[NameObject(FA.V)] = lst + elif isinstance(value, tuple): + writer_annot[NameObject(FA.V)] = TextStringObject( + value[0], + ) else: writer_parent_annot[NameObject(FA.V)] = TextStringObject(value) if writer_parent_annot.get(FA.FT) in ("/Btn"): @@ -1011,7 +1036,12 @@ def update_page_form_field_values( or writer_parent_annot.get(FA.FT) == "/Ch" ): # textbox - self._update_field_annotation(writer_parent_annot, writer_annot) + if isinstance(value, tuple): + self._update_field_annotation( + writer_parent_annot, writer_annot, value[1], value[2] + ) + else: + self._update_field_annotation(writer_parent_annot, writer_annot) elif ( writer_annot.get(FA.FT) == "/Sig" ): # deprecated # not implemented yet diff --git a/pypdf/constants.py b/pypdf/constants.py index 6cb8e14d7..a14e0168f 100644 --- a/pypdf/constants.py +++ b/pypdf/constants.py @@ -427,7 +427,12 @@ class InteractiveFormDictEntries: class FieldDictionaryAttributes: - """Table 8.69 Entries common to all field dictionaries (PDF 1.7 reference).""" + """ + Entries common to all field dictionaries (Table 8.69 PDF 1.7 reference) + (*very partially documented here*). + + FFBits provides the constants used for `/Ff` from Table 8.70/8.75/8.77/8.79 + """ FT = "/FT" # name, required for terminal fields Parent = "/Parent" # dictionary, required for children @@ -441,34 +446,63 @@ class FieldDictionaryAttributes: AA = "/AA" # dictionary, optional Opt = "/Opt" - class FfBits: + class FfBits(IntFlag): + """ + Ease building /Ff flags + Some entries may be specific to: + + * Text(Tx) (Table 8.75 PDF 1.7 reference) + * Buttons(Btn) (Table 8.77 PDF 1.7 reference) + * List(Ch) (Table 8.79 PDF 1.7 reference) + """ + ReadOnly = 1 << 0 + """common to Tx/Btn/Ch in Table 8.70""" Required = 1 << 1 + """common to Tx/Btn/Ch in Table 8.70""" NoExport = 1 << 2 - Multiline = 1 << 12 # Tx Table 8.77 - Password = 1 << 13 # Tx - - NoToggleToOff = 1 << 14 # Btn table 8.75 - Radio = 1 << 15 # Btn - Pushbutton = 1 << 16 # Btn - - Combo = 1 << 17 # Ch table 8.79 - Edit = 1 << 18 # Ch - Sort = 1 << 19 # Ch - - FileSelect = 1 << 20 # Tx - - MultiSelect = 1 << 21 # Ch - - DoNotSpellCheck = 1 << 22 # Tx / Ch - DoNotScroll = 1 << 23 # Tx - Comb = 1 << 24 # Tx - - RadiosInUnison = 1 << 25 # Btn - - RichText = 1 << 25 # Tx - - CommitOnSelChange = 1 << 26 # Ch + """common to Tx/Btn/Ch in Table 8.70""" + + Multiline = 1 << 12 + """Tx""" + Password = 1 << 13 + """Tx""" + + NoToggleToOff = 1 << 14 + """Btn""" + Radio = 1 << 15 + """Btn""" + Pushbutton = 1 << 16 + """Btn""" + + Combo = 1 << 17 + """Ch""" + Edit = 1 << 18 + """Ch""" + Sort = 1 << 19 + """Ch""" + + FileSelect = 1 << 20 + """Tx""" + + MultiSelect = 1 << 21 + """Tx""" + + DoNotSpellCheck = 1 << 22 + """Tx/Ch""" + DoNotScroll = 1 << 23 + """Tx""" + Comb = 1 << 24 + """Tx""" + + RadiosInUnison = 1 << 25 + """Btn""" + + RichText = 1 << 25 + """Tx""" + + CommitOnSelChange = 1 << 26 + """Ch""" @classmethod def attributes(cls) -> Tuple[str, ...]: diff --git a/tests/test_writer.py b/tests/test_writer.py index 3460a3a48..7a1bad60f 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -2230,3 +2230,26 @@ def test_i_in_choice_fields(): writer.pages[0], {"State": "NY"}, auto_regenerate=False ) assert "/I" not in writer.get_fields()["State"].indirect_reference.get_object() + + +def test_selfont(): + writer = PdfWriter(clone_from=RESOURCE_ROOT / "FormTestFromOo.pdf") + writer.update_page_form_field_values( + writer.pages[0], + {"Text1": ("Text_1", "", 5), "Text2": ("Text_2", "/F3", 0)}, + auto_regenerate=False, + ) + assert ( + b"/F3 5 Tf" + in writer.pages[0]["/Annots"][1].get_object()["/AP"]["/N"].get_data() + ) + assert ( + b"Text_1" in writer.pages[0]["/Annots"][1].get_object()["/AP"]["/N"].get_data() + ) + assert ( + b"/F3 12 Tf" + in writer.pages[0]["/Annots"][2].get_object()["/AP"]["/N"].get_data() + ) + assert ( + b"Text_2" in writer.pages[0]["/Annots"][2].get_object()["/AP"]["/N"].get_data() + )