Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions docs/widget.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
52 changes: 49 additions & 3 deletions src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Binary file added tests/resources/test_4505.pdf
Binary file not shown.
27 changes: 27 additions & 0 deletions tests/test_4505.py
Original file line number Diff line number Diff line change
@@ -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}