Skip to content

Commit

Permalink
added support for radio buttons py-pdf#2824
Browse files Browse the repository at this point in the history
  • Loading branch information
ljbergmann committed Sep 4, 2024
1 parent 7a65af9 commit 5db62a9
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 60 deletions.
169 changes: 109 additions & 60 deletions pypdf/_doc_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
)
from .types import OutlineType, PagemodeType
from .xmp import XmpInformation
import xml.etree.ElementTree as ET


def convert_to_int(d: bytes, size: int) -> Union[int, Tuple[Any, ...]]:
Expand Down Expand Up @@ -266,7 +267,7 @@ def pdf_header(self) -> str:

@abstractmethod
def get_object(
self, indirect_reference: Union[int, IndirectObject]
self, indirect_reference: Union[int, IndirectObject]
) -> Optional[PdfObject]:
... # pragma: no cover

Expand Down Expand Up @@ -300,9 +301,9 @@ def xmp_metadata(self) -> Optional[XmpInformation]:

@abstractmethod
def _repr_mimebundle_(
self,
include: Union[None, Iterable[str]] = None,
exclude: Union[None, Iterable[str]] = None,
self,
include: Union[None, Iterable[str]] = None,
exclude: Union[None, Iterable[str]] = None,
) -> Dict[str, Any]:
"""
Integration into Jupyter Notebooks.
Expand Down Expand Up @@ -381,7 +382,7 @@ def named_destinations(self) -> Dict[str, Any]:
def get_named_dest_root(self) -> ArrayObject:
named_dest = ArrayObject()
if CA.NAMES in self.root_object and isinstance(
self.root_object[CA.NAMES], DictionaryObject
self.root_object[CA.NAMES], DictionaryObject
):
names = cast(DictionaryObject, self.root_object[CA.NAMES])
names_ref = names.indirect_reference
Expand Down Expand Up @@ -414,9 +415,9 @@ def get_named_dest_root(self) -> ArrayObject:

## common
def _get_named_destinations(
self,
tree: Union[TreeObject, None] = None,
retval: Optional[Any] = None,
self,
tree: Union[TreeObject, None] = None,
retval: Optional[Any] = None,
) -> Dict[str, Any]:
"""
Retrieve the named destinations present in the document.
Expand Down Expand Up @@ -487,11 +488,11 @@ def _get_named_destinations(
# See §12.3.2 of the PDF 1.7 or PDF 2.0 specification.

def get_fields(
self,
tree: Optional[TreeObject] = None,
retval: Optional[Dict[Any, Any]] = None,
fileobj: Optional[Any] = None,
stack: Optional[List[PdfObject]] = None,
self,
tree: Optional[TreeObject] = None,
retval: Optional[Dict[Any, Any]] = None,
fileobj: Optional[Any] = None,
stack: Optional[List[PdfObject]] = None,
) -> Optional[Dict[str, Any]]:
"""
Extract field data if this PDF contains interactive form fields.
Expand Down Expand Up @@ -540,22 +541,22 @@ def _get_qualified_field_name(self, parent: DictionaryObject) -> str:
return cast(str, parent["/TM"])
elif "/Parent" in parent:
return (
self._get_qualified_field_name(
cast(DictionaryObject, parent["/Parent"])
)
+ "."
+ cast(str, parent.get("/T", ""))
self._get_qualified_field_name(
cast(DictionaryObject, parent["/Parent"])
)
+ "."
+ cast(str, parent.get("/T", ""))
)
else:
return cast(str, parent.get("/T", ""))

def _build_field(
self,
field: Union[TreeObject, DictionaryObject],
retval: Dict[Any, Any],
fileobj: Any,
field_attributes: Any,
stack: List[PdfObject],
self,
field: Union[TreeObject, DictionaryObject],
retval: Dict[Any, Any],
fileobj: Any,
field_attributes: Any,
stack: List[PdfObject],
) -> None:
if all(attr not in field for attr in ("/T", "/TM")):
return
Expand Down Expand Up @@ -584,19 +585,19 @@ def _build_field(
states.append(s)
retval[key][NameObject("/_States_")] = ArrayObject(states)
if (
obj.get(FA.Ff, 0) & FA.FfBits.NoToggleToOff != 0
and "/Off" in retval[key]["/_States_"]
obj.get(FA.Ff, 0) & FA.FfBits.NoToggleToOff != 0
and "/Off" in retval[key]["/_States_"]
):
del retval[key]["/_States_"][retval[key]["/_States_"].index("/Off")]
# at last for order
self._check_kids(field, retval, fileobj, stack)

def _check_kids(
self,
tree: Union[TreeObject, DictionaryObject],
retval: Any,
fileobj: Any,
stack: List[PdfObject],
self,
tree: Union[TreeObject, DictionaryObject],
retval: Any,
fileobj: Any,
stack: List[PdfObject],
) -> None:
if tree in stack:
logger_warning(
Expand All @@ -613,13 +614,13 @@ def _check_kids(
def _write_field(self, fileobj: Any, field: Any, field_attributes: Any) -> None:
field_attributes_tuple = FA.attributes()
field_attributes_tuple = (
field_attributes_tuple + CheckboxRadioButtonAttributes.attributes()
field_attributes_tuple + CheckboxRadioButtonAttributes.attributes()
)

for attr in field_attributes_tuple:
if attr in (
FA.Kids,
FA.AA,
FA.Kids,
FA.AA,
):
continue
attr_name = field_attributes[attr]
Expand Down Expand Up @@ -667,9 +668,9 @@ def indexed_key(k: str, fields: Dict[Any, Any]) -> str:
return k
else:
return (
k
+ "."
+ str(sum([1 for kk in fields if kk.startswith(k + ".")]) + 2)
k
+ "."
+ str(sum([1 for kk in fields if kk.startswith(k + ".")]) + 2)
)

# Retrieve document form fields
Expand All @@ -686,7 +687,7 @@ def indexed_key(k: str, fields: Dict[Any, Any]) -> str:
return ff

def get_pages_showing_field(
self, field: Union[Field, PdfObject, IndirectObject]
self, field: Union[Field, PdfObject, IndirectObject]
) -> List[PageObject]:
"""
Provides list of pages where the field is called.
Expand Down Expand Up @@ -757,7 +758,7 @@ def _get_inherited(obj: DictionaryObject, key: str) -> Any:

@property
def open_destination(
self,
self,
) -> Union[None, Destination, TextStringObject, ByteStringObject]:
"""
Property to access the opening destination (``/OpenAction`` entry in
Expand Down Expand Up @@ -799,7 +800,7 @@ def outline(self) -> OutlineType:
return self._get_outline()

def _get_outline(
self, node: Optional[DictionaryObject] = None, outline: Optional[Any] = None
self, node: Optional[DictionaryObject] = None, outline: Optional[Any] = None
) -> OutlineType:
if outline is None:
outline = []
Expand Down Expand Up @@ -863,7 +864,7 @@ def threads(self) -> Optional[ArrayObject]:

@abstractmethod
def _get_page_number_by_indirect(
self, indirect_reference: Union[None, int, NullObject, IndirectObject]
self, indirect_reference: Union[None, int, NullObject, IndirectObject]
) -> Optional[int]:
... # pragma: no cover

Expand Down Expand Up @@ -893,20 +894,20 @@ def get_destination_page_number(self, destination: Destination) -> Optional[int]
return self._get_page_number_by_indirect(destination.page)

def _build_destination(
self,
title: str,
array: Optional[
List[
Union[NumberObject, IndirectObject, None, NullObject, DictionaryObject]
]
],
self,
title: str,
array: Optional[
List[
Union[NumberObject, IndirectObject, None, NullObject, DictionaryObject]
]
],
) -> Destination:
page, typ = None, None
# handle outline items with missing or invalid destination
if (
isinstance(array, (NullObject, str))
or (isinstance(array, ArrayObject) and len(array) == 0)
or array is None
isinstance(array, (NullObject, str))
or (isinstance(array, ArrayObject) and len(array) == 0)
or array is None
):
page = NullObject()
return Destination(title, page, Fit.fit())
Expand Down Expand Up @@ -1081,10 +1082,10 @@ def page_mode(self) -> Optional[PagemodeType]:
return None

def _flatten(
self,
pages: Union[None, DictionaryObject, PageObject] = None,
inherit: Optional[Dict[str, Any]] = None,
indirect_reference: Optional[IndirectObject] = None,
self,
pages: Union[None, DictionaryObject, PageObject] = None,
inherit: Optional[Dict[str, Any]] = None,
indirect_reference: Optional[IndirectObject] = None,
) -> None:
inheritable_page_attributes = (
NameObject(PG.RESOURCES),
Expand Down Expand Up @@ -1140,9 +1141,9 @@ def _flatten(
self.flattened_pages.append(page_obj) # type: ignore

def remove_page(
self,
page: Union[int, PageObject, IndirectObject],
clean: bool = False,
self,
page: Union[int, PageObject, IndirectObject],
clean: bool = False,
) -> None:
"""
Remove page from pages list.
Expand Down Expand Up @@ -1198,7 +1199,7 @@ def _get_indirect_object(self, num: int, gen: int) -> Optional[PdfObject]:
return IndirectObject(num, gen, self).get_object()

def decode_permissions(
self, permissions_code: int
self, permissions_code: int
) -> Dict[str, bool]: # pragma: no cover
"""Take the permissions as an integer, return the allowed access."""
deprecate_with_replacement(
Expand Down Expand Up @@ -1266,6 +1267,54 @@ def xfa(self) -> Optional[Dict[str, Any]]:
retval[tag] = es
return retval

@property
def xfa_dataset(self) -> ET:
if self._xfa_dataset is None:
self._parse_xfa_dataset()
return self._xfa_dataset

def _update_xfa_field(self, key, value, type):

if type == '/Btn':
value = value[1:]

if self._xfa_dataset is None and self.xfa is not None:
self._parse_xfa_dataset()

field = self._xfa_dataset.find(".//" + key)
if field is not None:
field.text = value
self._update_xfa_dataset_string()
else:
raise PdfReadError("{} not found, update not possible".format(key))

def _parse_xfa_dataset(self) -> ET:
# check if xfa exists for this PDF
if self.xfa is not None:
xfa = self.xfa
# check if xfa is an ArrayObject
if isinstance(xfa, Dict):
if 'datasets' in xfa:
# convert ByteString to str to create xfa dataset
if isinstance(xfa['datasets'], bytes):
xfa_str = xfa['datasets'].decode("utf-8")
xfa_ds_elm = ET.fromstring(xfa_str)
self._xfa_dataset = xfa_ds_elm

def _update_xfa_dataset_string(self):
if self._xfa_dataset is None:
raise PdfReadError("xfadataset not found")

namespace = self._xfa_dataset.tag.split('}')[0].strip('{')
ET.register_namespace('xfa',namespace)
xml_str = ET.tostring(self._xfa_dataset)

xfa_fields = cast(ArrayObject, self.root_object["/AcroForm"]['/XFA'])
if 'datasets' in xfa_fields:
index = xfa_fields.index('datasets') + 1
xfa_ds_object = xfa_fields[index].get_object()
xfa_ds_object.set_data(xml_str)

@property
def attachments(self) -> Mapping[str, List[bytes]]:
return LazyDict(
Expand Down Expand Up @@ -1304,7 +1353,7 @@ def _get_attachment_list(self, name: str) -> List[bytes]:
return [out]

def _get_attachments(
self, filename: Optional[str] = None
self, filename: Optional[str] = None
) -> Dict[str, Union[bytes, List[bytes]]]:
"""
Retrieves all or selected file attachments of the PDF as a dictionary of file names
Expand Down
5 changes: 5 additions & 0 deletions pypdf/_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
from itertools import compress
from pathlib import Path
from types import TracebackType
import xml.etree.ElementTree as ET

from typing import (
IO,
Any,
Expand Down Expand Up @@ -235,6 +237,7 @@ def _get_clone_from(
self._encryption: Optional[Encryption] = None
self._encrypt_entry: Optional[DictionaryObject] = None
self._ID: Union[ArrayObject, None] = None
self._xfa_dataset: Union[ET,None] = None

# for commonality
@property
Expand Down Expand Up @@ -1032,6 +1035,7 @@ def update_page_form_field_values(
)
else:
writer_parent_annot[NameObject(FA.V)] = TextStringObject(value)

if writer_parent_annot.get(FA.FT) in ("/Btn"):
# case of Checkbox button (no /FT found in Radio widgets
v = NameObject(value)
Expand All @@ -1056,6 +1060,7 @@ def update_page_form_field_values(
# signature
logger_warning("Signature forms not implemented yet", __name__)

self._update_xfa_field(field.replace('[0]', ''), value, writer_annot.get(FA.FT))
def reattach_fields(
self, page: Optional[PageObject] = None
) -> List[DictionaryObject]:
Expand Down
Binary file added resources/xfa_form.pdf
Binary file not shown.

0 comments on commit 5db62a9

Please sign in to comment.