diff --git a/libqpdf/QPDFAcroFormDocumentHelper.cc b/libqpdf/QPDFAcroFormDocumentHelper.cc index 8eb496a..c0d0207 100644 --- a/libqpdf/QPDFAcroFormDocumentHelper.cc +++ b/libqpdf/QPDFAcroFormDocumentHelper.cc @@ -412,10 +412,9 @@ AcroForm::traverseField(QPDFObjectHandle field, QPDFObjectHandle const& parent, // fields can be merged with terminal field dictionaries. Otherwise, the annotation fields might // be there to be inherited by annotations below it. - Array Kids = field["/Kids"]; - const bool is_field = depth == 0 || Kids || field.hasKey("/Parent"); - const bool is_annotation = - !Kids && (field.hasKey("/Subtype") || field.hasKey("/Rect") || field.hasKey("/AP")); + FormNode node = field; + const bool is_field = node.field(); + const bool is_annotation = node.widget(); QTC::TC("qpdf", "QPDFAcroFormDocumentHelper field found", (depth == 0) ? 0 : 1); QTC::TC("qpdf", "QPDFAcroFormDocumentHelper annotation found", (is_field ? 0 : 1)); @@ -428,12 +427,16 @@ AcroForm::traverseField(QPDFObjectHandle field, QPDFObjectHandle const& parent, } if (is_annotation) { - QPDFObjectHandle our_field = (is_field ? field : parent); - fields_[our_field.getObjGen()].annotations.emplace_back(field); + auto our_field = (is_field ? field : parent); + fields_[our_field].annotations.emplace_back(field); annotation_to_field_[og] = QPDFFormFieldObjectHelper(our_field); } - if (is_field && depth != 0 && field["/Parent"] != parent) { + if (!is_field) { + return true; + } + + if (depth != 0 && field["/Parent"] != parent) { for (auto const& kid: Array(field["/Parent"]["/Kids"])) { if (kid == field) { field.warn("while traversing /AcroForm found field with two parents"); @@ -450,19 +453,18 @@ AcroForm::traverseField(QPDFObjectHandle field, QPDFObjectHandle const& parent, field.replaceKey("/Parent", parent); } - if (is_field && field.hasKey("/T")) { - QPDFFormFieldObjectHelper foh(field); - std::string name = foh.getFullyQualifiedName(); + if (node.T()) { auto old = fields_.find(og); if (old != fields_.end() && !old->second.name.empty()) { // We might be updating after a name change, so remove any old information name_to_fields_[old->second.name].erase(og); } + std::string name = node.fully_qualified_name(); fields_[og].name = name; name_to_fields_[name].insert(og); } - for (auto const& kid: Kids) { + for (auto const& kid: node.Kids()) { if (bad_fields_.contains(kid)) { continue; } diff --git a/libqpdf/QPDFFormFieldObjectHelper.cc b/libqpdf/QPDFFormFieldObjectHelper.cc index eb8fe1a..96ad0b7 100644 --- a/libqpdf/QPDFFormFieldObjectHelper.cc +++ b/libqpdf/QPDFFormFieldObjectHelper.cc @@ -415,7 +415,13 @@ QPDFFormFieldObjectHelper::setFieldAttribute(std::string const& key, QPDFObjectH void FormNode::setFieldAttribute(std::string const& key, QPDFObjectHandle value) { - oh().replaceKey(key, value); + replace(key, value); +} + +void +FormNode::setFieldAttribute(std::string const& key, Name const& value) +{ + replace(key, value); } void @@ -427,7 +433,7 @@ QPDFFormFieldObjectHelper::setFieldAttribute(std::string const& key, std::string void FormNode::setFieldAttribute(std::string const& key, std::string const& utf8_value) { - oh().replaceKey(key, QPDFObjectHandle::newUnicodeString(utf8_value)); + replace(key, String::utf16(utf8_value)); } void @@ -510,21 +516,20 @@ FormNode::setRadioButtonValue(QPDFObjectHandle name) return; } } - - QPDFObjectHandle kids = oh().getKey("/Kids"); - if (!(isRadioButton() && parent.null() && kids.isArray())) { + auto kids = Kids(); + if (!(isRadioButton() && parent.null() && kids)) { warn("don't know how to set the value of this field as a radio button"); return; } setFieldAttribute("/V", name); - for (auto const& kid: kids.as_array()) { - QPDFObjectHandle AP = kid.getKey("/AP"); + for (FormNode kid: kids) { + auto ap = kid.AP(); QPDFObjectHandle annot; - if (AP.null()) { + if (!ap) { // The widget may be below. If there is more than one, just find the first one. - for (auto const& grandkid: kid.getKey("/Kids").as_array()) { - AP = grandkid.getKey("/AP"); - if (!AP.null()) { + for (FormNode grandkid: kid.Kids()) { + ap = grandkid.AP(); + if (ap) { annot = grandkid; break; } @@ -536,11 +541,10 @@ FormNode::setRadioButtonValue(QPDFObjectHandle name) warn("unable to set the value of this radio button"); continue; } - if (AP.isDictionary() && AP.getKey("/N").isDictionary() && - AP.getKey("/N").hasKey(name.getName())) { - annot.replaceKey("/AS", name); + if (ap["/N"].contains(name.getName())) { + annot.replace("/AS", name); } else { - annot.replaceKey("/AS", QPDFObjectHandle::newName("/Off")); + annot.replace("/AS", Name("/Off")); } } } @@ -548,29 +552,26 @@ FormNode::setRadioButtonValue(QPDFObjectHandle name) void FormNode::setCheckBoxValue(bool value) { - QPDFObjectHandle AP = oh().getKey("/AP"); + auto ap = AP(); QPDFObjectHandle annot; - if (AP.null()) { - // The widget may be below. If there is more than one, just - // find the first one. - QPDFObjectHandle kids = oh().getKey("/Kids"); - for (auto const& kid: oh().getKey("/Kids").as_array(qpdf::strict)) { - AP = kid.getKey("/AP"); - if (!AP.null()) { - QTC::TC("qpdf", "QPDFFormFieldObjectHelper checkbox kid widget"); + if (ap) { + annot = oh(); + } else { + // The widget may be below. If there is more than one, just find the first one. + for (FormNode kid: Kids()) { + ap = kid.AP(); + if (ap) { annot = kid; break; } } - } else { - annot = oh(); } std::string on_value; if (value) { // Set the "on" value to the first value in the appearance stream's normal state dictionary // that isn't /Off. If not found, fall back to /Yes. - if (AP.isDictionary()) { - for (auto const& item: AP.getKey("/N").as_dictionary()) { + if (ap) { + for (auto const& item: Dictionary(ap["/N"])) { if (item.first != "/Off") { on_value = item.first; break; @@ -583,15 +584,13 @@ FormNode::setCheckBoxValue(bool value) } // Set /AS to the on value or /Off in addition to setting /V. - QPDFObjectHandle name = QPDFObjectHandle::newName(value ? on_value : "/Off"); + auto name = Name(value ? on_value : "/Off"); setFieldAttribute("/V", name); if (!annot) { - QTC::TC("qpdf", "QPDFObjectHandle broken checkbox"); warn("unable to set the value of this checkbox"); return; } - QTC::TC("qpdf", "QPDFFormFieldObjectHelper set checkbox AS"); - annot.replaceKey("/AS", name); + annot.replace("/AS", name); } void @@ -902,12 +901,11 @@ FormNode::generateTextAppearance(QPDFAnnotationObjectHelper& aoh) {"/Subtype", Name("/Form")}}); AS = QPDFObjectHandle::newStream(oh().getOwningQPDF(), "/Tx BMC\nEMC\n"); AS.replaceDict(dict); - Dictionary AP = aoh.getAppearanceDictionary(); - if (!AP) { - aoh.getObjectHandle().replaceKey("/AP", Dictionary::empty()); - AP = aoh.getAppearanceDictionary(); + if (auto ap = AP()) { + ap.replace("/N", AS); + } else { + aoh.replace("/AP", Dictionary({{"/N", AS}})); } - AP.replace("/N", AS); } if (!AS.isStream()) { aoh.warn("unable to get normal appearance stream for update"); diff --git a/libqpdf/qpdf/AcroForm.hh b/libqpdf/qpdf/AcroForm.hh index 98728cd..ecfc301 100644 --- a/libqpdf/qpdf/AcroForm.hh +++ b/libqpdf/qpdf/AcroForm.hh @@ -356,6 +356,21 @@ namespace qpdf::impl { } + // Widget and annotation attributes + + /// @brief Retrieves the /AP attribute of the form node as a Dictionary. + /// + /// The /AP attribute, short for "appearance dictionary," defines how an annotation is + /// presented visually on a page. See section 12.5.5 of the PDF specification for more + /// details. + /// + /// @return A Dictionary containing the /AP attribute of the form node. + Dictionary + AP() const + { + return {get("/AP")}; + } + /// Retrieves the /Parent form field of the current field. /// /// This function accesses the parent field in the hierarchical structure of form fields, if @@ -370,6 +385,20 @@ namespace qpdf::impl return {get("/Parent")}; } + /// @brief Retrieves the /Kids array. + /// + /// This method returns the /Kids entry, which is an array of the immediate descendants of + /// this node. It is only present if the node is a form field rather than a pure widget + /// annotation. + /// + /// @return An `Array` object containing the /Kids elements. If the /Kids entry + /// does not exist or is not a valid array, the returned `Array` will be invalid. + Array + Kids() const + { + return {get("/Kids")}; + } + /// @brief Returns the top-level field associated with the current field. /// /// The function traverses the hierarchy of parent fields to identify the highest-level @@ -615,6 +644,41 @@ namespace qpdf::impl /// no value is found. std::string default_appearance() const; + /// @brief Determines if the current node represents a valid form field node. + /// + /// This function evaluates whether the current node is valid by combining + /// checks for the node's partial name (/T attribute), its immediate descendants + /// (/Kids array), and its field type (/FT attribute). It ensures that at least + /// one of these properties exists or returns a valid value. + /// + /// @return true if the form node contains a valid /T attribute, /Kids array, + /// or /FT attribute; otherwise, false. + bool + field() const + { + return T() || Kids() || FT(); + } + + /// @brief Determines if the form node represents a widget annotation. + /// + /// This method checks whether the current form node is a widget annotation + /// by verifying the following conditions: + /// + /// - The node does not have any /Kids entries (i.e., it is not a parent node with + /// descendants). + /// - The node contains any of the following attributes commonly associated with widget + /// annotations: + /// - `/Subtype` + /// - `/Rect` + /// - `/AP` + /// + /// @return `true` if the form node is a widget annotation; otherwise, `false`. + bool + widget() const + { + return !Kids() && (contains("/Subtype") || contains("/Rect") || contains("/AP")); + } + // Return the default resource dictionary for the field. This comes not from the field but // from the document-level /AcroForm dictionary. While several PDF generators put a /DR key // in the form field's dictionary, experimentation suggests that many popular readers, @@ -656,6 +720,7 @@ namespace qpdf::impl // want to set the name of a field, use QPDFAcroFormDocumentHelper::setFormFieldName // instead. void setFieldAttribute(std::string const& key, QPDFObjectHandle value); + void setFieldAttribute(std::string const& key, Name const& value); // Set an attribute to the given value as a Unicode string (UTF-16 BE encoded). The input // string should be UTF-8 encoded. If you have a QPDFAcroFormDocumentHelper and you want to diff --git a/qpdf/qpdf.testcov b/qpdf/qpdf.testcov index 6c58745..02b41e3 100644 --- a/qpdf/qpdf.testcov +++ b/qpdf/qpdf.testcov @@ -193,9 +193,6 @@ QPDFPageDocumentHelper non-widget annotation 0 QPDFObjectHandle replace with copy 0 QPDFAnnotationObjectHelper forbidden flags 0 QPDFAnnotationObjectHelper missing required flags 0 -QPDFFormFieldObjectHelper checkbox kid widget 0 -QPDFFormFieldObjectHelper set checkbox AS 0 -QPDFObjectHandle broken checkbox 0 QPDFFormFieldObjectHelper list not found 0 QPDFFormFieldObjectHelper list found 0 QPDFFormFieldObjectHelper list first too low 0