Skip to content

Commit f788818

Browse files
committed
Address 4505
Offer updating field flags of the Parent and its Kids.
1 parent 8d69fe8 commit f788818

File tree

4 files changed

+84
-7
lines changed

4 files changed

+84
-7
lines changed

docs/widget.rst

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,11 @@ Like annotations, widgets live on PDF pages. Similar to annotations, the first w
5353
True
5454

5555

56-
.. method:: update
56+
.. method:: update(sync_flags=False)
5757

58-
After any changes to a widget, this method **must be used** to store them in the PDF [#f1]_.
58+
After any changes to a widget, this **method must be used** to reflect changes in the PDF [#f1]_.
59+
60+
: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]_.
5961

6062
.. method:: reset
6163

@@ -247,11 +249,13 @@ PyMuPDF supports the creation and update of many, but not all widget types.
247249
* check box (`PDF_WIDGET_TYPE_CHECKBOX`)
248250
* combo box (`PDF_WIDGET_TYPE_COMBOBOX`)
249251
* list box (`PDF_WIDGET_TYPE_LISTBOX`)
250-
* 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`.
251-
* signature (`PDF_WIDGET_TYPE_SIGNATURE`) **read only**.
252+
* 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`.
253+
* signature (`PDF_WIDGET_TYPE_SIGNATURE`) **read only** -- no update or creation of signatures.
252254

253255
.. rubric:: Footnotes
254256

255257
.. [#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)`.
256258
259+
.. [#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.
260+
257261
.. include:: footer.rst

src/__init__.py

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7173,6 +7173,51 @@ def _validate(self):
71737173

71747174
self._checker() # any field_type specific checks
71757175

7176+
def _sync_flags(self):
7177+
"""Propagate the field flags.
7178+
7179+
If this widget has a "/Parent", set its field flags and that of all
7180+
its /Kids widgets to the value of the current widget.
7181+
Only possible for widgets existing in the PDF.
7182+
7183+
Returns True or False.
7184+
"""
7185+
if not self.xref:
7186+
return False # no xref: widget not in the PDF
7187+
doc = self.parent.parent # the owning document
7188+
assert doc
7189+
pdf = _as_pdf_document(doc)
7190+
# load underlying PDF object
7191+
pdf_widget = mupdf.pdf_load_object(pdf, self.xref)
7192+
Parent = mupdf.pdf_dict_get(pdf_widget, PDF_NAME("Parent"))
7193+
if not Parent.pdf_is_dict():
7194+
return False # no /Parent: nothing to do
7195+
7196+
# put the field flags value into the parent field flags:
7197+
Parent.pdf_dict_put_int(PDF_NAME("Ff"), self.field_flags)
7198+
7199+
# also put that value into all kids of the Parent
7200+
kids = Parent.pdf_dict_get(PDF_NAME("Kids"))
7201+
if not kids.pdf_is_array():
7202+
message("warning: malformed PDF, Parent has no Kids array")
7203+
return False # no /Kids: should never happen!
7204+
7205+
for i in range(kids.pdf_array_len()): # walk through all kids
7206+
# access kid widget, and do some precautionary checks
7207+
kid = kids.pdf_array_get(i)
7208+
if not kid.pdf_is_dict():
7209+
continue
7210+
xref = kid.pdf_to_num() # get xref of the kid
7211+
if xref == self.xref: # skip self widget
7212+
continue
7213+
subtype = kid.pdf_dict_get(PDF_NAME("Subtype"))
7214+
if not subtype.pdf_to_name() == "Widget":
7215+
continue
7216+
# put the field flags value into the kid field flags:
7217+
kid.pdf_dict_put_int(PDF_NAME("Ff"), self.field_flags)
7218+
7219+
return True # all done
7220+
71767221
def button_states(self):
71777222
"""Return the on/off state names for button widgets.
71787223

@@ -7252,9 +7297,8 @@ def reset(self):
72527297
"""
72537298
TOOLS._reset_widget(self._annot)
72547299

7255-
def update(self):
7256-
"""Reflect Python object in the PDF.
7257-
"""
7300+
def update(self, sync_flags=False):
7301+
"""Reflect Python object in the PDF."""
72587302
self._validate()
72597303

72607304
self._adjust_font() # ensure valid text_font name
@@ -7280,6 +7324,8 @@ def update(self):
72807324
# finally update the widget
72817325
TOOLS._save_widget(self._annot, self)
72827326
self._text_da = ""
7327+
if sync_flags:
7328+
self._sync_flags() # propagate field flags to parent and kids
72837329

72847330

72857331
from . import _extra

tests/resources/test_4505.pdf

11.3 KB
Binary file not shown.

tests/test_4505.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import pymupdf
2+
import os.path
3+
4+
5+
def test_4505():
6+
"""Copy field flags to Parent widget and all of its kids."""
7+
path = os.path.abspath(f"{__file__}/../../tests/resources/test_4505.pdf")
8+
doc = pymupdf.open(path)
9+
page = doc[0]
10+
text1_flags_before = {}
11+
text1_flags_after = {}
12+
# extract all widgets having the same field name
13+
for w in page.widgets():
14+
if w.field_name != "text_1":
15+
continue
16+
text1_flags_before[w.xref] = w.field_flags
17+
# expected exiting field flags
18+
assert text1_flags_before == {8: 1, 10: 0, 33: 0}
19+
w = page.load_widget(8) # first of these widgets
20+
# give all connected widgets that field flags value
21+
w.update(sync_flags=True)
22+
# confirm that all connected widgets have the same field flags
23+
for w in page.widgets():
24+
if w.field_name != "text_1":
25+
continue
26+
text1_flags_after[w.xref] = w.field_flags
27+
assert text1_flags_after == {8: 1, 10: 1, 33: 1}

0 commit comments

Comments
 (0)