Skip to content

Commit adced32

Browse files
committed
Fix testing errors
1 parent 20b7b96 commit adced32

2 files changed

Lines changed: 241 additions & 2 deletions

File tree

‎src/__init__.py‎

Lines changed: 192 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10064,6 +10064,193 @@ def _addWidget(self, field_type, field_name):
1006410064
JM_add_annot_id(annot, "W")
1006510065
return Annot(annot)
1006610066

10067+
def _ensure_field_hierarchy(self, field_name):
10068+
"""Ensures that the complete Form field hierarchy exists.
10069+
10070+
If the field_name shows a hierarchy like "A.B.C" establish a chain of
10071+
bidirectional links between the fields A, B, and C. The last field
10072+
("C") in the hierarchy will point to the actual Widget on the page
10073+
(via /Kids). "A" is a root field and thus lives in the AcroForm/Fields
10074+
array. All fields in the chain will be created when missing.
10075+
10076+
Args:
10077+
field_name: (raw) field name of the Widget() object.
10078+
10079+
Returns:
10080+
The xref of the last field_name segment ("C"). To be used for the
10081+
/Parent value of the widget.
10082+
None if field_name has no segments, and hence is a root field.
10083+
"""
10084+
parts = field_name.split(".")
10085+
if any(x == "" for x in parts):
10086+
raise ValueError(f"Invalid field name '{field_name}'.")
10087+
10088+
# Helper: Find name in /Kids of parent_xref
10089+
def name_in_array(array, name):
10090+
"""Check if a name exists in an array of XREFs.
10091+
10092+
Returns the xref of the item if found, else False.
10093+
"""
10094+
for i in range(array.pdf_array_len()):
10095+
item = array.pdf_array_get(i)
10096+
T = item.pdf_dict_get(PDF_NAME("T"))
10097+
T_name = T.pdf_to_string()
10098+
if T_name == (name, len(name)):
10099+
return item.pdf_to_num(), item
10100+
return False, None
10101+
10102+
doc = self.parent
10103+
10104+
# Access or create /AcroForm/Fields array
10105+
pdfdoc = _as_pdf_document(doc)
10106+
root = mupdf.pdf_dict_get(mupdf.pdf_trailer(pdfdoc), PDF_NAME("Root"))
10107+
acro = root.pdf_dict_get(PDF_NAME("AcroForm"))
10108+
if not acro.pdf_is_dict():
10109+
acro = root.pdf_dict_put_dict(PDF_NAME("AcroForm"), 5)
10110+
root_array = acro.pdf_dict_get(PDF_NAME("Fields"))
10111+
if not root_array.pdf_is_array():
10112+
root_array = acro.pdf_dict_put_array(PDF_NAME("Fields"), 1)
10113+
10114+
# ------------------------------------------------------------
10115+
# 3) Create missing fields in hierarchy
10116+
# ------------------------------------------------------------
10117+
parent_xref = None
10118+
10119+
for i, part in enumerate(parts):
10120+
if i == 0:
10121+
kids_array = acro.pdf_dict_get(PDF_NAME("Fields"))
10122+
parent = None
10123+
else:
10124+
parent = mupdf.pdf_load_object(pdfdoc, parent_xref)
10125+
kids_array = parent.pdf_dict_get(PDF_NAME("Kids"))
10126+
if kids_array.pdf_array_len() == 0:
10127+
kids_array = parent.pdf_dict_put_array(PDF_NAME("Kids"), 1)
10128+
10129+
item_xref, item_obj = name_in_array(kids_array, part)
10130+
if parent is None and item_obj and not item_obj.pdf_dict_get(PDF_NAME("Kids")).pdf_is_array():
10131+
new_xref = doc.get_new_xref()
10132+
doc.update_object(new_xref, f"<</T ({part})>>")
10133+
new_obj = mupdf.pdf_new_indirect(pdfdoc, new_xref, 0)
10134+
kids_array.pdf_array_push(new_obj)
10135+
item_xref = new_xref
10136+
if item_xref:
10137+
parent_xref = item_xref
10138+
continue # Field in parent Kids, move to next 'part'
10139+
10140+
# need to create a new field for this part
10141+
part_xref = doc.get_new_xref()
10142+
if parent_xref is not None:
10143+
doc.update_object(part_xref, f"<</Parent {parent_xref} 0 R /T ({part})>>")
10144+
else:
10145+
doc.update_object(part_xref, f"<</T ({part})>>")
10146+
10147+
part_obj = mupdf.pdf_new_indirect(pdfdoc, part_xref, 0)
10148+
kids_array.pdf_array_push(part_obj)
10149+
parent_xref = part_xref
10150+
10151+
return parent_xref
10152+
10153+
def _adjust_widget(self, widget):
10154+
"""Adjust a widget whose field_name exhibits a field hierarchy."""
10155+
doc = self.parent
10156+
parent_xref = self._ensure_field_hierarchy(widget.field_name)
10157+
annot = widget._annot
10158+
if parent_xref is None: # just a safeguard
10159+
return annot
10160+
10161+
annot_xref = annot.xref
10162+
pdfdoc = _as_pdf_document(doc)
10163+
10164+
# remove the widget from the root Fields array
10165+
fields = (
10166+
mupdf.pdf_dict_get(mupdf.pdf_trailer(pdfdoc), PDF_NAME("Root"))
10167+
.pdf_dict_get(PDF_NAME("AcroForm"))
10168+
.pdf_dict_get(PDF_NAME("Fields"))
10169+
)
10170+
found = False
10171+
for i in range(fields.pdf_array_len()):
10172+
if fields.pdf_array_get(i).pdf_to_num() == annot_xref:
10173+
fields.pdf_array_delete(i)
10174+
found = True
10175+
break
10176+
10177+
if not found:
10178+
raise ValueError(
10179+
f"Unexpected: Widget {annot_xref} not found in root Fields array."
10180+
)
10181+
10182+
# ----------------------------------------------------------------
10183+
# Our widget will now get a /Parent. This requires moving
10184+
# multiple values of its object over to the parent object.
10185+
# Note: the name '/T' is in the parent already.
10186+
# ----------------------------------------------------------------
10187+
parent_obj = mupdf.pdf_new_indirect(pdfdoc, parent_xref, 0)
10188+
annot_obj = mupdf.pdf_load_object(pdfdoc, annot_xref)
10189+
10190+
kids_array = parent_obj.pdf_dict_get(PDF_NAME("Kids"))
10191+
if not kids_array.pdf_is_array():
10192+
kids_array = mupdf.pdf_dict_put_array(parent_obj, PDF_NAME("Kids"), 1)
10193+
kids_array.pdf_array_push(mupdf.pdf_new_indirect(pdfdoc, annot.xref, 0))
10194+
10195+
# store parent in /Parent key
10196+
annot_obj.pdf_dict_put(PDF_NAME("Parent"), parent_obj)
10197+
# store page reference
10198+
annot_obj.pdf_dict_put(PDF_NAME("P"), mupdf.pdf_new_indirect(pdfdoc, self.xref, 0))
10199+
10200+
annot_obj.pdf_dict_del(PDF_NAME("T")) # widget has no own name!
10201+
field_value = widget.field_value
10202+
if widget.field_type == PDF_WIDGET_TYPE_CHECKBOX: # noqa: F821
10203+
if field_value is True:
10204+
field_value = "Yes"
10205+
else:
10206+
field_value = "Off"
10207+
elif field_value is None:
10208+
field_value = ""
10209+
# move field value to parent
10210+
parent_obj.pdf_dict_put_string(PDF_NAME("V"), field_value, len(field_value))
10211+
annot_obj.pdf_dict_del(PDF_NAME("V"))
10212+
10213+
# move field type to parent
10214+
old = annot_obj.pdf_dict_get(PDF_NAME("FT"))
10215+
if not old.pdf_is_null():
10216+
parent_obj.pdf_dict_put(PDF_NAME("FT"), old)
10217+
annot_obj.pdf_dict_del(PDF_NAME("FT"))
10218+
10219+
# move field flags to parent
10220+
old = annot_obj.pdf_dict_get(PDF_NAME("Ff"))
10221+
if not old.pdf_is_null():
10222+
parent_obj.pdf_dict_put(PDF_NAME("Ff"), old)
10223+
annot_obj.pdf_dict_del(PDF_NAME("Ff"))
10224+
10225+
# move default appearance to parent
10226+
old = annot_obj.pdf_dict_get(PDF_NAME("DA"))
10227+
if not old.pdf_is_null():
10228+
parent_obj.pdf_dict_put(PDF_NAME("DA"), old)
10229+
annot_obj.pdf_dict_del(PDF_NAME("DA"))
10230+
10231+
# move choices list to parent
10232+
old = annot_obj.pdf_dict_get(PDF_NAME("Opt"))
10233+
if not old.pdf_is_null():
10234+
parent_obj.pdf_dict_put(PDF_NAME("Opt"), old)
10235+
annot_obj.pdf_dict_del(PDF_NAME("Opt"))
10236+
10237+
# move choices list index to parent
10238+
old = annot_obj.pdf_dict_get(PDF_NAME("I"))
10239+
if not old.pdf_is_null():
10240+
parent_obj.pdf_dict_put(PDF_NAME("I"), old)
10241+
annot_obj.pdf_dict_del(PDF_NAME("I"))
10242+
10243+
# Specials for RadioButtons
10244+
if widget.field_type == PDF_WIDGET_TYPE_RADIOBUTTON: # noqa: F821
10245+
doc.xref_set_key(annot_xref,"AS", f"/{field_value}")
10246+
apn=doc.xref_get_key(annot_xref,"AP/N")[1]
10247+
if apn and "/Yes" in apn:
10248+
apn = apn.replace("/Yes", f"/{field_value}")
10249+
doc.xref_set_key(annot_xref, "AP/N", apn)
10250+
10251+
self.parent.need_appearances(False)
10252+
return annot
10253+
1006710254
def _apply_redactions(self, text, images, graphics):
1006810255
page = self._pdf_page()
1006910256
opts = mupdf.PdfRedactOptions()
@@ -10799,8 +10986,11 @@ def add_widget(self, widget: Widget) -> Annot:
1079910986
self._annot_refs[id(annot)] = annot
1080010987
widget.parent = annot.parent
1080110988
widget._annot = annot
10989+
widget.xref = annot.xref
1080210990
widget.update()
10803-
return annot
10991+
# if "." not in widget.field_name: # done if no field hierarchy
10992+
# return annot
10993+
return self._adjust_widget(widget)
1080410994

1080510995
def annot_names(self):
1080610996
'''
@@ -24909,7 +25099,7 @@ def pdfobj_string(o, prefix=''):
2490925099
ret += f'jpx_image:\n'
2491025100
elif mupdf.pdf_is_name(o):
2491125101
ret += f'name: {mupdf.pdf_to_name(o)}\n'
24912-
elif o.pdf_is_null:
25102+
elif o.pdf_is_null():
2491325103
ret += f'null\n'
2491425104
#elif o.pdf_is_number:
2491525105
# ret += f'number\n'

‎tests/test_widgets.py‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55
import pymupdf
66
import os
7+
from pymupdf import mupdf
78

89
scriptdir = os.path.abspath(os.path.dirname(__file__))
910
filename = os.path.join(scriptdir, "resources", "widgettest.pdf")
@@ -486,3 +487,51 @@ def test_4950():
486487

487488
page.remove_rotation()
488489
assert page.rotation == 0
490+
491+
492+
def test_hierarchy():
493+
# Create a new PDF document
494+
doc = pymupdf.open()
495+
pdfdoc = pymupdf._as_pdf_document(doc)
496+
497+
def chain_upwards(w):
498+
"""Check the names climbing up the hierarchy."""
499+
xref = w.xref
500+
parts = [""] + w.field_name.split(".")
501+
obj = mupdf.pdf_load_object(pdfdoc, xref) # the widget object
502+
for part in reversed(parts):
503+
obj = obj.pdf_dict_get(pymupdf.PDF_NAME("Parent"))
504+
if obj is None or obj.pdf_is_null():
505+
break
506+
pdf_name = obj.pdf_dict_get(pymupdf.PDF_NAME("T")).pdf_to_string()
507+
assert pdf_name[0] == part
508+
509+
# Add a page to the document
510+
page = doc.new_page()
511+
r = pymupdf.Rect(100, 100, 200, 120)
512+
w = pymupdf.Widget()
513+
w.field_name = "Person.Name.First"
514+
w.field_type = pymupdf.PDF_WIDGET_TYPE_TEXT
515+
w.rect = r
516+
w.field_value = "John"
517+
w.border_width = 1
518+
page.add_widget(w)
519+
r += (0, 30, 0, 30)
520+
521+
w = pymupdf.Widget()
522+
w.field_name = "Person.Name.Last"
523+
w.field_type = pymupdf.PDF_WIDGET_TYPE_TEXT
524+
w.rect = r
525+
w.field_value = "Doe"
526+
w.border_width = 1
527+
page.add_widget(w)
528+
529+
field_names = ["Person.Name.First", "Person.Name.Last"]
530+
root = mupdf.pdf_dict_get(mupdf.pdf_trailer(pdfdoc), pymupdf.PDF_NAME("Root"))
531+
acro = root.pdf_dict_get(pymupdf.PDF_NAME("AcroForm"))
532+
fields = acro.pdf_dict_get(pymupdf.PDF_NAME("Fields"))
533+
assert fields.pdf_array_len() == 1, "must have exactly 1 root field"
534+
for i, w in enumerate(page.widgets()):
535+
field_name = w.field_name # the field name is a concatenation
536+
assert field_name == field_names[i]
537+
chain_upwards(w) # confirm the hierarchy is correct

0 commit comments

Comments
 (0)