Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: add capability to set font and size in fields #2636

Merged
merged 15 commits into from
May 20, 2024
6 changes: 6 additions & 0 deletions docs/modules/constants.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:
57 changes: 44 additions & 13 deletions pypdf/_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@

OPTIONAL_READ_WRITE_FIELD = FieldFlag(0)
pubpub-zz marked this conversation as resolved.
Show resolved Hide resolved
ALL_DOCUMENT_PERMISSIONS = UserAccessPermissions.all()
DEFAULT_FONT_HEIGHT_IN_MULTILINE = 12


class ObjectDeletionFlag(enum.IntFlag):
Expand Down Expand Up @@ -780,7 +781,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])
Expand All @@ -799,12 +804,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 ...
Expand Down Expand Up @@ -944,12 +959,19 @@ 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: An integer. You can build it using FieldDictionaryAttributes.FfBits
see :doc:`FfBits in constants</modules/constants>`
stefan6419846 marked this conversation as resolved.
Show resolved Hide resolved

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:
Expand Down Expand Up @@ -997,6 +1019,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"):
Expand All @@ -1011,7 +1037,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
Expand Down
84 changes: 59 additions & 25 deletions pypdf/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,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
Expand All @@ -450,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, ...]:
Expand Down
23 changes: 23 additions & 0 deletions tests/test_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
)
Loading