From 32addd8adc78d2b1d14b275f1b24f8509a6c1a55 Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Thu, 9 May 2024 23:43:31 +0200 Subject: [PATCH 01/10] ENH: add capability to set font and size in fields closes #2633 --- pypdf/_writer.py | 52 ++++++++++++++++++++++++++++++++++---------- pypdf/constants.py | 25 +++++++++++++++++++++ tests/test_writer.py | 23 ++++++++++++++++++++ 3 files changed, 88 insertions(+), 12 deletions(-) diff --git a/pypdf/_writer.py b/pypdf/_writer.py index 6d42cdfaa..2ea6fa3cd 100644 --- a/pypdf/_writer.py +++ b/pypdf/_writer.py @@ -780,7 +780,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 +803,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("/Ff", 0) & InteractiveFormDictEntries.Ff_Multiline: + font_height = 12 + 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 ... @@ -944,11 +958,16 @@ 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. + 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: An integer. You can build it with InteractiveFormDictEntries: + ex: InteractiveFormDictEntries.Ff_ReadOnly ^ InteractiveFormDictEntries.Ff_Multiline auto_regenerate: set/unset the need_appearances flag ; the flag is unchanged if auto_regenerate is None. """ @@ -997,6 +1016,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 +1034,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 c9a57ba0e..b4d781490 100644 --- a/pypdf/constants.py +++ b/pypdf/constants.py @@ -433,6 +433,31 @@ class InteractiveFormDictEntries: Q = "/Q" XFA = "/XFA" + # Common Field Flags + Ff_ReadOnly = 1 + Ff_Required = 2 + Ff_NoExport = 4 + # Text Field Flags + Ff_Multiline = 1 << (13 - 1) + Ff_Password = 1 << (14 - 1) + Ff_FileSelect = 1 << (21 - 1) + Ff_DoNotSpellCheck = 1 << (23 - 1) + Ff_DoNotScroll = 1 << (24 - 1) + Ff_Comb = 1 << (25 - 1) + Ff_RichText = 1 << (26 - 1) + # Button Field Flags + Ff_NoToggleToOff = 1 << (15 - 1) + Ff_Radio = 1 << (16 - 1) + Ff_Pushbutton = 1 << (17 - 1) + Ff_RadiosInUnison = 1 << (26 - 1) + # Choice Field FlagsZ + Ff_Combo = 1 << (18 - 1) + Ff_Edit = 1 << (19 - 1) + Ff_Sort = 1 << (20 - 1) + Ff_MultiSelect = 1 << (22 - 1) + # Ff_DoNotSpellCheck + Ff_CommitOnSelChange = 1 << (27 - 1) + class FieldDictionaryAttributes: """Table 8.69 Entries common to all field dictionaries (PDF 1.7 reference).""" 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() + ) From ebb60c26356935658c18c5a26cb4b2a903c4fe63 Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Fri, 10 May 2024 00:12:11 +0200 Subject: [PATCH 02/10] doc --- pypdf/_writer.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pypdf/_writer.py b/pypdf/_writer.py index 2ea6fa3cd..6e32b6373 100644 --- a/pypdf/_writer.py +++ b/pypdf/_writer.py @@ -959,15 +959,16 @@ def update_page_form_field_values( `List[Pageobject]` - provides list of pages to be processed. `None` - all pages. 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 : + + * 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: An integer. You can build it with InteractiveFormDictEntries: - ex: InteractiveFormDictEntries.Ff_ReadOnly ^ InteractiveFormDictEntries.Ff_Multiline + ex: InteractiveFormDictEntries.Ff_ReadOnly ^ InteractiveFormDictEntries.Ff_Multiline + auto_regenerate: set/unset the need_appearances flag ; the flag is unchanged if auto_regenerate is None. """ From 4e4fc37c8958526e2d5e4920150f04181180f4e8 Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Fri, 10 May 2024 09:18:05 +0200 Subject: [PATCH 03/10] Update pypdf/_writer.py Co-authored-by: Stefan <96178532+stefan6419846@users.noreply.github.com> --- pypdf/_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pypdf/_writer.py b/pypdf/_writer.py index 6e32b6373..06c0c06c4 100644 --- a/pypdf/_writer.py +++ b/pypdf/_writer.py @@ -960,7 +960,7 @@ def update_page_form_field_values( `None` - all pages. fields: a Python dictionary of: - * field names (/T) as keys and text values (/V) as value + * 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) From aff57f750d2d575ab5fa5a379bcf2e59c6ccb411 Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Fri, 10 May 2024 09:32:08 +0200 Subject: [PATCH 04/10] Update pypdf/_writer.py Co-authored-by: Stefan <96178532+stefan6419846@users.noreply.github.com> --- pypdf/_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pypdf/_writer.py b/pypdf/_writer.py index 06c0c06c4..7d5701741 100644 --- a/pypdf/_writer.py +++ b/pypdf/_writer.py @@ -961,7 +961,7 @@ def update_page_form_field_values( 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 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) From d7b05e78cf0305aebef631cd5af1816a952edcff Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Fri, 10 May 2024 09:32:30 +0200 Subject: [PATCH 05/10] Update pypdf/_writer.py Co-authored-by: Stefan <96178532+stefan6419846@users.noreply.github.com> --- pypdf/_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pypdf/_writer.py b/pypdf/_writer.py index 7d5701741..39a2a6c0d 100644 --- a/pypdf/_writer.py +++ b/pypdf/_writer.py @@ -962,7 +962,7 @@ def update_page_form_field_values( * 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 : + * 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) From d62e51360d5956360dd853c7a1bd552b5ed84122 Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Fri, 10 May 2024 12:39:23 +0200 Subject: [PATCH 06/10] Update pypdf/_writer.py Co-authored-by: Stefan <96178532+stefan6419846@users.noreply.github.com> --- pypdf/_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pypdf/_writer.py b/pypdf/_writer.py index 39a2a6c0d..98cb387e0 100644 --- a/pypdf/_writer.py +++ b/pypdf/_writer.py @@ -969,7 +969,7 @@ def update_page_form_field_values( flags: An integer. You can build it with InteractiveFormDictEntries: ex: InteractiveFormDictEntries.Ff_ReadOnly ^ InteractiveFormDictEntries.Ff_Multiline - auto_regenerate: set/unset the need_appearances flag ; + 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: From 3b3bcee4ccd3c68ff8ecc20d6373b6876a110712 Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Fri, 10 May 2024 12:45:23 +0200 Subject: [PATCH 07/10] doc --- docs/modules/constants.rst | 6 ++ pypdf/_writer.py | 10 ++-- pypdf/constants.py | 109 ++++++++++++++++++++----------------- 3 files changed, 71 insertions(+), 54 deletions(-) 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 6e32b6373..9302e169e 100644 --- a/pypdf/_writer.py +++ b/pypdf/_writer.py @@ -125,6 +125,7 @@ OPTIONAL_READ_WRITE_FIELD = FieldFlag(0) ALL_DOCUMENT_PERMISSIONS = UserAccessPermissions.all() +DEFAULT_FONT_HEIGHT_IN_MULTILINE = 12 class ObjectDeletionFlag(enum.IntFlag): @@ -813,8 +814,8 @@ def _update_field_annotation( else float(font_properties[font_properties.index("Tf") - 1]) ) if font_height == 0: - if field.get("/Ff", 0) & InteractiveFormDictEntries.Ff_Multiline: - font_height = 12 + 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) @@ -966,8 +967,9 @@ def update_page_form_field_values( * text values (/V) * font id (e.g. /F1, the font id must exist) * font size (0 for autosize) - flags: An integer. You can build it with InteractiveFormDictEntries: - ex: InteractiveFormDictEntries.Ff_ReadOnly ^ InteractiveFormDictEntries.Ff_Multiline + + flags: An integer. You can build it using FieldDictionaryAttributes.FfBits + see :doc:`FfBits in constants` auto_regenerate: set/unset the need_appearances flag ; the flag is unchanged if auto_regenerate is None. diff --git a/pypdf/constants.py b/pypdf/constants.py index b4d781490..df7212190 100644 --- a/pypdf/constants.py +++ b/pypdf/constants.py @@ -433,34 +433,14 @@ class InteractiveFormDictEntries: Q = "/Q" XFA = "/XFA" - # Common Field Flags - Ff_ReadOnly = 1 - Ff_Required = 2 - Ff_NoExport = 4 - # Text Field Flags - Ff_Multiline = 1 << (13 - 1) - Ff_Password = 1 << (14 - 1) - Ff_FileSelect = 1 << (21 - 1) - Ff_DoNotSpellCheck = 1 << (23 - 1) - Ff_DoNotScroll = 1 << (24 - 1) - Ff_Comb = 1 << (25 - 1) - Ff_RichText = 1 << (26 - 1) - # Button Field Flags - Ff_NoToggleToOff = 1 << (15 - 1) - Ff_Radio = 1 << (16 - 1) - Ff_Pushbutton = 1 << (17 - 1) - Ff_RadiosInUnison = 1 << (26 - 1) - # Choice Field FlagsZ - Ff_Combo = 1 << (18 - 1) - Ff_Edit = 1 << (19 - 1) - Ff_Sort = 1 << (20 - 1) - Ff_MultiSelect = 1 << (22 - 1) - # Ff_DoNotSpellCheck - Ff_CommitOnSelChange = 1 << (27 - 1) - 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 @@ -475,33 +455,62 @@ class FieldDictionaryAttributes: Opt = "/Opt" class FfBits: + """ + 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, ...]: From 6654d5d3bbf272f87aa0a59e51a70e202b978d1e Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Fri, 10 May 2024 14:57:49 +0200 Subject: [PATCH 08/10] FfBits --- pypdf/_writer.py | 4 +--- pypdf/constants.py | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pypdf/_writer.py b/pypdf/_writer.py index 91ca65c20..6b78f5d3e 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,7 +122,6 @@ ) from .xmp import XmpInformation -OPTIONAL_READ_WRITE_FIELD = FieldFlag(0) ALL_DOCUMENT_PERMISSIONS = UserAccessPermissions.all() DEFAULT_FONT_HEIGHT_IN_MULTILINE = 12 @@ -945,7 +943,7 @@ 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 = FA.FfBits.Nul, auto_regenerate: Optional[bool] = True, ) -> None: """ diff --git a/pypdf/constants.py b/pypdf/constants.py index df7212190..c0c74953e 100644 --- a/pypdf/constants.py +++ b/pypdf/constants.py @@ -454,7 +454,7 @@ class FieldDictionaryAttributes: AA = "/AA" # dictionary, optional Opt = "/Opt" - class FfBits: + class FfBits(IntFlag): """ Ease building /Ff flags Some entries may be specific to: @@ -464,6 +464,7 @@ class FfBits: * List(Ch) (Table 8.79 PDF 1.7 reference) """ + Nul = 0 ReadOnly = 1 << 0 """common to Tx/Btn/Ch in Table 8.70""" Required = 1 << 1 From 940f8a4d09fb0747cdb8e3298de5b57ce18bd040 Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Fri, 10 May 2024 15:16:05 +0200 Subject: [PATCH 09/10] comments --- pypdf/_writer.py | 4 +++- pypdf/constants.py | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pypdf/_writer.py b/pypdf/_writer.py index 6b78f5d3e..5c493cf00 100644 --- a/pypdf/_writer.py +++ b/pypdf/_writer.py @@ -939,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: FA.FfBits = FA.FfBits.Nul, + flags: FA.FfBits = FFBITS_NUL, auto_regenerate: Optional[bool] = True, ) -> None: """ diff --git a/pypdf/constants.py b/pypdf/constants.py index c0c74953e..708d6cc57 100644 --- a/pypdf/constants.py +++ b/pypdf/constants.py @@ -464,7 +464,6 @@ class FfBits(IntFlag): * List(Ch) (Table 8.79 PDF 1.7 reference) """ - Nul = 0 ReadOnly = 1 << 0 """common to Tx/Btn/Ch in Table 8.70""" Required = 1 << 1 From 7858da243bd3db08b82812f1da6c80c20337c2ae Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Mon, 20 May 2024 12:21:25 +0200 Subject: [PATCH 10/10] doc for Ff flags --- pypdf/_writer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pypdf/_writer.py b/pypdf/_writer.py index 5c493cf00..f32e74041 100644 --- a/pypdf/_writer.py +++ b/pypdf/_writer.py @@ -968,8 +968,7 @@ def update_page_form_field_values( * font id (e.g. /F1, the font id must exist) * font size (0 for autosize) - flags: An integer. You can build it using FieldDictionaryAttributes.FfBits - see :doc:`FfBits in constants` + 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.