diff --git a/docs/widget.rst b/docs/widget.rst index b3d042320..b9b4be108 100644 --- a/docs/widget.rst +++ b/docs/widget.rst @@ -53,9 +53,11 @@ Like annotations, widgets live on PDF pages. Similar to annotations, the first w True - .. method:: update + .. method:: update(sync_flags=False) - After any changes to a widget, this method **must be used** to store them in the PDF [#f1]_. + After any changes to a widget, this **method must be used** to reflect changes in the PDF [#f1]_. + + :arg bool sync_flags: if ``True``, the widget's :attr:`Widget.field_flags` are copied to the ``Parent`` object (if present) and all widgets named in its ``Kids`` array. This provides a convenient way to -- for example -- set all instances of the widget to read-only, no matter on which page they may occur [#f2]_. .. method:: reset @@ -247,11 +249,13 @@ PyMuPDF supports the creation and update of many, but not all widget types. * check box (`PDF_WIDGET_TYPE_CHECKBOX`) * combo box (`PDF_WIDGET_TYPE_COMBOBOX`) * list box (`PDF_WIDGET_TYPE_LISTBOX`) -* radio button (`PDF_WIDGET_TYPE_RADIOBUTTON`): PyMuPDF does not currently support the **creation** of groups of (interconnected) radio buttons, where setting one automatically unsets the other buttons in the group. The widget object also does not reflect the presence of a button group. However: consistently selecting (or unselecting) a radio button is supported. This includes correctly setting the value maintained in the owning button group. Selecting a radio button may be done by either assigning `True` or `field.on_state()` to the field value. **De-selecting** the button should be done assigning `False`. -* signature (`PDF_WIDGET_TYPE_SIGNATURE`) **read only**. +* radio button (`PDF_WIDGET_TYPE_RADIOBUTTON`): PyMuPDF does not currently support the **creation** of groups of (interconnected) radio buttons, where setting one button automatically unsets the other buttons in the group. The widget object also does not reflect the presence of a button group. However: consistently selecting (or unselecting) a radio button is supported. This includes correctly setting the value maintained in the owning button group. Selecting a radio button may be done by either assigning `True` or `field.on_state()` to the field value. **De-selecting** the button should be done assigning `False`. +* signature (`PDF_WIDGET_TYPE_SIGNATURE`) **read only** -- no update or creation of signatures. .. rubric:: Footnotes .. [#f1] If you intend to re-access a new or updated field (e.g. for making a pixmap), make sure to reload the page first. Either close and re-open the document, or load another page first, or simply do `page = doc.reload_page(page)`. +.. [#f2] Among other purposes, ``Parent`` objects are also used to facilitate multiple occurrences of a field (on the same or on different pages). The ``Kids`` array in this ``Parent`` object contains the cross references of all widgets that are "copies" of the same field. Whenever the field value of any "kid" widget is changed, all the other kids are immediately updated too. This is a very efficient way to handle multiple copies of the same field, e.g. for filling out forms. This simultaneous update only happens for :attr:`Widget.field value`. The new parameter ``sync_flags`` extends this to :attr:`Widget.field_flags`. This cannot be automated in the same way as for the field value to allow for more flexibility. + .. include:: footer.rst diff --git a/src/__init__.py b/src/__init__.py index 442456886..0c729e620 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -7173,6 +7173,51 @@ def _validate(self): self._checker() # any field_type specific checks + def _sync_flags(self): + """Propagate the field flags. + + If this widget has a "/Parent", set its field flags and that of all + its /Kids widgets to the value of the current widget. + Only possible for widgets existing in the PDF. + + Returns True or False. + """ + if not self.xref: + return False # no xref: widget not in the PDF + doc = self.parent.parent # the owning document + assert doc + pdf = _as_pdf_document(doc) + # load underlying PDF object + pdf_widget = mupdf.pdf_load_object(pdf, self.xref) + Parent = mupdf.pdf_dict_get(pdf_widget, PDF_NAME("Parent")) + if not Parent.pdf_is_dict(): + return False # no /Parent: nothing to do + + # put the field flags value into the parent field flags: + Parent.pdf_dict_put_int(PDF_NAME("Ff"), self.field_flags) + + # also put that value into all kids of the Parent + kids = Parent.pdf_dict_get(PDF_NAME("Kids")) + if not kids.pdf_is_array(): + message("warning: malformed PDF, Parent has no Kids array") + return False # no /Kids: should never happen! + + for i in range(kids.pdf_array_len()): # walk through all kids + # access kid widget, and do some precautionary checks + kid = kids.pdf_array_get(i) + if not kid.pdf_is_dict(): + continue + xref = kid.pdf_to_num() # get xref of the kid + if xref == self.xref: # skip self widget + continue + subtype = kid.pdf_dict_get(PDF_NAME("Subtype")) + if not subtype.pdf_to_name() == "Widget": + continue + # put the field flags value into the kid field flags: + kid.pdf_dict_put_int(PDF_NAME("Ff"), self.field_flags) + + return True # all done + def button_states(self): """Return the on/off state names for button widgets. @@ -7252,9 +7297,8 @@ def reset(self): """ TOOLS._reset_widget(self._annot) - def update(self): - """Reflect Python object in the PDF. - """ + def update(self, sync_flags=False): + """Reflect Python object in the PDF.""" self._validate() self._adjust_font() # ensure valid text_font name @@ -7280,6 +7324,8 @@ def update(self): # finally update the widget TOOLS._save_widget(self._annot, self) self._text_da = "" + if sync_flags: + self._sync_flags() # propagate field flags to parent and kids from . import _extra diff --git a/tests/resources/test_4505.pdf b/tests/resources/test_4505.pdf new file mode 100644 index 000000000..038b34d8e Binary files /dev/null and b/tests/resources/test_4505.pdf differ diff --git a/tests/test_4505.py b/tests/test_4505.py new file mode 100644 index 000000000..9c1fdc9f2 --- /dev/null +++ b/tests/test_4505.py @@ -0,0 +1,27 @@ +import pymupdf +import os.path + + +def test_4505(): + """Copy field flags to Parent widget and all of its kids.""" + path = os.path.abspath(f"{__file__}/../../tests/resources/test_4505.pdf") + doc = pymupdf.open(path) + page = doc[0] + text1_flags_before = {} + text1_flags_after = {} + # extract all widgets having the same field name + for w in page.widgets(): + if w.field_name != "text_1": + continue + text1_flags_before[w.xref] = w.field_flags + # expected exiting field flags + assert text1_flags_before == {8: 1, 10: 0, 33: 0} + w = page.load_widget(8) # first of these widgets + # give all connected widgets that field flags value + w.update(sync_flags=True) + # confirm that all connected widgets have the same field flags + for w in page.widgets(): + if w.field_name != "text_1": + continue + text1_flags_after[w.xref] = w.field_flags + assert text1_flags_after == {8: 1, 10: 1, 33: 1}