Commit 35ca7efe103fb65d2c00eb9b6deb29d3d95bfc0c
Committed by
GitHub
Merge pull request #1625 from m-holger/ffoh
Refactor FormNode
Showing
4 changed files
with
113 additions
and
51 deletions
libqpdf/QPDFAcroFormDocumentHelper.cc
| ... | ... | @@ -412,10 +412,9 @@ AcroForm::traverseField(QPDFObjectHandle field, QPDFObjectHandle const& parent, |
| 412 | 412 | // fields can be merged with terminal field dictionaries. Otherwise, the annotation fields might |
| 413 | 413 | // be there to be inherited by annotations below it. |
| 414 | 414 | |
| 415 | - Array Kids = field["/Kids"]; | |
| 416 | - const bool is_field = depth == 0 || Kids || field.hasKey("/Parent"); | |
| 417 | - const bool is_annotation = | |
| 418 | - !Kids && (field.hasKey("/Subtype") || field.hasKey("/Rect") || field.hasKey("/AP")); | |
| 415 | + FormNode node = field; | |
| 416 | + const bool is_field = node.field(); | |
| 417 | + const bool is_annotation = node.widget(); | |
| 419 | 418 | |
| 420 | 419 | QTC::TC("qpdf", "QPDFAcroFormDocumentHelper field found", (depth == 0) ? 0 : 1); |
| 421 | 420 | QTC::TC("qpdf", "QPDFAcroFormDocumentHelper annotation found", (is_field ? 0 : 1)); |
| ... | ... | @@ -428,12 +427,16 @@ AcroForm::traverseField(QPDFObjectHandle field, QPDFObjectHandle const& parent, |
| 428 | 427 | } |
| 429 | 428 | |
| 430 | 429 | if (is_annotation) { |
| 431 | - QPDFObjectHandle our_field = (is_field ? field : parent); | |
| 432 | - fields_[our_field.getObjGen()].annotations.emplace_back(field); | |
| 430 | + auto our_field = (is_field ? field : parent); | |
| 431 | + fields_[our_field].annotations.emplace_back(field); | |
| 433 | 432 | annotation_to_field_[og] = QPDFFormFieldObjectHelper(our_field); |
| 434 | 433 | } |
| 435 | 434 | |
| 436 | - if (is_field && depth != 0 && field["/Parent"] != parent) { | |
| 435 | + if (!is_field) { | |
| 436 | + return true; | |
| 437 | + } | |
| 438 | + | |
| 439 | + if (depth != 0 && field["/Parent"] != parent) { | |
| 437 | 440 | for (auto const& kid: Array(field["/Parent"]["/Kids"])) { |
| 438 | 441 | if (kid == field) { |
| 439 | 442 | field.warn("while traversing /AcroForm found field with two parents"); |
| ... | ... | @@ -450,19 +453,18 @@ AcroForm::traverseField(QPDFObjectHandle field, QPDFObjectHandle const& parent, |
| 450 | 453 | field.replaceKey("/Parent", parent); |
| 451 | 454 | } |
| 452 | 455 | |
| 453 | - if (is_field && field.hasKey("/T")) { | |
| 454 | - QPDFFormFieldObjectHelper foh(field); | |
| 455 | - std::string name = foh.getFullyQualifiedName(); | |
| 456 | + if (node.T()) { | |
| 456 | 457 | auto old = fields_.find(og); |
| 457 | 458 | if (old != fields_.end() && !old->second.name.empty()) { |
| 458 | 459 | // We might be updating after a name change, so remove any old information |
| 459 | 460 | name_to_fields_[old->second.name].erase(og); |
| 460 | 461 | } |
| 462 | + std::string name = node.fully_qualified_name(); | |
| 461 | 463 | fields_[og].name = name; |
| 462 | 464 | name_to_fields_[name].insert(og); |
| 463 | 465 | } |
| 464 | 466 | |
| 465 | - for (auto const& kid: Kids) { | |
| 467 | + for (auto const& kid: node.Kids()) { | |
| 466 | 468 | if (bad_fields_.contains(kid)) { |
| 467 | 469 | continue; |
| 468 | 470 | } | ... | ... |
libqpdf/QPDFFormFieldObjectHelper.cc
| ... | ... | @@ -415,7 +415,13 @@ QPDFFormFieldObjectHelper::setFieldAttribute(std::string const& key, QPDFObjectH |
| 415 | 415 | void |
| 416 | 416 | FormNode::setFieldAttribute(std::string const& key, QPDFObjectHandle value) |
| 417 | 417 | { |
| 418 | - oh().replaceKey(key, value); | |
| 418 | + replace(key, value); | |
| 419 | +} | |
| 420 | + | |
| 421 | +void | |
| 422 | +FormNode::setFieldAttribute(std::string const& key, Name const& value) | |
| 423 | +{ | |
| 424 | + replace(key, value); | |
| 419 | 425 | } |
| 420 | 426 | |
| 421 | 427 | void |
| ... | ... | @@ -427,7 +433,7 @@ QPDFFormFieldObjectHelper::setFieldAttribute(std::string const& key, std::string |
| 427 | 433 | void |
| 428 | 434 | FormNode::setFieldAttribute(std::string const& key, std::string const& utf8_value) |
| 429 | 435 | { |
| 430 | - oh().replaceKey(key, QPDFObjectHandle::newUnicodeString(utf8_value)); | |
| 436 | + replace(key, String::utf16(utf8_value)); | |
| 431 | 437 | } |
| 432 | 438 | |
| 433 | 439 | void |
| ... | ... | @@ -510,21 +516,20 @@ FormNode::setRadioButtonValue(QPDFObjectHandle name) |
| 510 | 516 | return; |
| 511 | 517 | } |
| 512 | 518 | } |
| 513 | - | |
| 514 | - QPDFObjectHandle kids = oh().getKey("/Kids"); | |
| 515 | - if (!(isRadioButton() && parent.null() && kids.isArray())) { | |
| 519 | + auto kids = Kids(); | |
| 520 | + if (!(isRadioButton() && parent.null() && kids)) { | |
| 516 | 521 | warn("don't know how to set the value of this field as a radio button"); |
| 517 | 522 | return; |
| 518 | 523 | } |
| 519 | 524 | setFieldAttribute("/V", name); |
| 520 | - for (auto const& kid: kids.as_array()) { | |
| 521 | - QPDFObjectHandle AP = kid.getKey("/AP"); | |
| 525 | + for (FormNode kid: kids) { | |
| 526 | + auto ap = kid.AP(); | |
| 522 | 527 | QPDFObjectHandle annot; |
| 523 | - if (AP.null()) { | |
| 528 | + if (!ap) { | |
| 524 | 529 | // The widget may be below. If there is more than one, just find the first one. |
| 525 | - for (auto const& grandkid: kid.getKey("/Kids").as_array()) { | |
| 526 | - AP = grandkid.getKey("/AP"); | |
| 527 | - if (!AP.null()) { | |
| 530 | + for (FormNode grandkid: kid.Kids()) { | |
| 531 | + ap = grandkid.AP(); | |
| 532 | + if (ap) { | |
| 528 | 533 | annot = grandkid; |
| 529 | 534 | break; |
| 530 | 535 | } |
| ... | ... | @@ -536,11 +541,10 @@ FormNode::setRadioButtonValue(QPDFObjectHandle name) |
| 536 | 541 | warn("unable to set the value of this radio button"); |
| 537 | 542 | continue; |
| 538 | 543 | } |
| 539 | - if (AP.isDictionary() && AP.getKey("/N").isDictionary() && | |
| 540 | - AP.getKey("/N").hasKey(name.getName())) { | |
| 541 | - annot.replaceKey("/AS", name); | |
| 544 | + if (ap["/N"].contains(name.getName())) { | |
| 545 | + annot.replace("/AS", name); | |
| 542 | 546 | } else { |
| 543 | - annot.replaceKey("/AS", QPDFObjectHandle::newName("/Off")); | |
| 547 | + annot.replace("/AS", Name("/Off")); | |
| 544 | 548 | } |
| 545 | 549 | } |
| 546 | 550 | } |
| ... | ... | @@ -548,29 +552,26 @@ FormNode::setRadioButtonValue(QPDFObjectHandle name) |
| 548 | 552 | void |
| 549 | 553 | FormNode::setCheckBoxValue(bool value) |
| 550 | 554 | { |
| 551 | - QPDFObjectHandle AP = oh().getKey("/AP"); | |
| 555 | + auto ap = AP(); | |
| 552 | 556 | QPDFObjectHandle annot; |
| 553 | - if (AP.null()) { | |
| 554 | - // The widget may be below. If there is more than one, just | |
| 555 | - // find the first one. | |
| 556 | - QPDFObjectHandle kids = oh().getKey("/Kids"); | |
| 557 | - for (auto const& kid: oh().getKey("/Kids").as_array(qpdf::strict)) { | |
| 558 | - AP = kid.getKey("/AP"); | |
| 559 | - if (!AP.null()) { | |
| 560 | - QTC::TC("qpdf", "QPDFFormFieldObjectHelper checkbox kid widget"); | |
| 557 | + if (ap) { | |
| 558 | + annot = oh(); | |
| 559 | + } else { | |
| 560 | + // The widget may be below. If there is more than one, just find the first one. | |
| 561 | + for (FormNode kid: Kids()) { | |
| 562 | + ap = kid.AP(); | |
| 563 | + if (ap) { | |
| 561 | 564 | annot = kid; |
| 562 | 565 | break; |
| 563 | 566 | } |
| 564 | 567 | } |
| 565 | - } else { | |
| 566 | - annot = oh(); | |
| 567 | 568 | } |
| 568 | 569 | std::string on_value; |
| 569 | 570 | if (value) { |
| 570 | 571 | // Set the "on" value to the first value in the appearance stream's normal state dictionary |
| 571 | 572 | // that isn't /Off. If not found, fall back to /Yes. |
| 572 | - if (AP.isDictionary()) { | |
| 573 | - for (auto const& item: AP.getKey("/N").as_dictionary()) { | |
| 573 | + if (ap) { | |
| 574 | + for (auto const& item: Dictionary(ap["/N"])) { | |
| 574 | 575 | if (item.first != "/Off") { |
| 575 | 576 | on_value = item.first; |
| 576 | 577 | break; |
| ... | ... | @@ -583,15 +584,13 @@ FormNode::setCheckBoxValue(bool value) |
| 583 | 584 | } |
| 584 | 585 | |
| 585 | 586 | // Set /AS to the on value or /Off in addition to setting /V. |
| 586 | - QPDFObjectHandle name = QPDFObjectHandle::newName(value ? on_value : "/Off"); | |
| 587 | + auto name = Name(value ? on_value : "/Off"); | |
| 587 | 588 | setFieldAttribute("/V", name); |
| 588 | 589 | if (!annot) { |
| 589 | - QTC::TC("qpdf", "QPDFObjectHandle broken checkbox"); | |
| 590 | 590 | warn("unable to set the value of this checkbox"); |
| 591 | 591 | return; |
| 592 | 592 | } |
| 593 | - QTC::TC("qpdf", "QPDFFormFieldObjectHelper set checkbox AS"); | |
| 594 | - annot.replaceKey("/AS", name); | |
| 593 | + annot.replace("/AS", name); | |
| 595 | 594 | } |
| 596 | 595 | |
| 597 | 596 | void |
| ... | ... | @@ -902,12 +901,11 @@ FormNode::generateTextAppearance(QPDFAnnotationObjectHelper& aoh) |
| 902 | 901 | {"/Subtype", Name("/Form")}}); |
| 903 | 902 | AS = QPDFObjectHandle::newStream(oh().getOwningQPDF(), "/Tx BMC\nEMC\n"); |
| 904 | 903 | AS.replaceDict(dict); |
| 905 | - Dictionary AP = aoh.getAppearanceDictionary(); | |
| 906 | - if (!AP) { | |
| 907 | - aoh.getObjectHandle().replaceKey("/AP", Dictionary::empty()); | |
| 908 | - AP = aoh.getAppearanceDictionary(); | |
| 904 | + if (auto ap = AP()) { | |
| 905 | + ap.replace("/N", AS); | |
| 906 | + } else { | |
| 907 | + aoh.replace("/AP", Dictionary({{"/N", AS}})); | |
| 909 | 908 | } |
| 910 | - AP.replace("/N", AS); | |
| 911 | 909 | } |
| 912 | 910 | if (!AS.isStream()) { |
| 913 | 911 | aoh.warn("unable to get normal appearance stream for update"); | ... | ... |
libqpdf/qpdf/AcroForm.hh
| ... | ... | @@ -356,6 +356,21 @@ namespace qpdf::impl |
| 356 | 356 | { |
| 357 | 357 | } |
| 358 | 358 | |
| 359 | + // Widget and annotation attributes | |
| 360 | + | |
| 361 | + /// @brief Retrieves the /AP attribute of the form node as a Dictionary. | |
| 362 | + /// | |
| 363 | + /// The /AP attribute, short for "appearance dictionary," defines how an annotation is | |
| 364 | + /// presented visually on a page. See section 12.5.5 of the PDF specification for more | |
| 365 | + /// details. | |
| 366 | + /// | |
| 367 | + /// @return A Dictionary containing the /AP attribute of the form node. | |
| 368 | + Dictionary | |
| 369 | + AP() const | |
| 370 | + { | |
| 371 | + return {get("/AP")}; | |
| 372 | + } | |
| 373 | + | |
| 359 | 374 | /// Retrieves the /Parent form field of the current field. |
| 360 | 375 | /// |
| 361 | 376 | /// This function accesses the parent field in the hierarchical structure of form fields, if |
| ... | ... | @@ -370,6 +385,20 @@ namespace qpdf::impl |
| 370 | 385 | return {get("/Parent")}; |
| 371 | 386 | } |
| 372 | 387 | |
| 388 | + /// @brief Retrieves the /Kids array. | |
| 389 | + /// | |
| 390 | + /// This method returns the /Kids entry, which is an array of the immediate descendants of | |
| 391 | + /// this node. It is only present if the node is a form field rather than a pure widget | |
| 392 | + /// annotation. | |
| 393 | + /// | |
| 394 | + /// @return An `Array` object containing the /Kids elements. If the /Kids entry | |
| 395 | + /// does not exist or is not a valid array, the returned `Array` will be invalid. | |
| 396 | + Array | |
| 397 | + Kids() const | |
| 398 | + { | |
| 399 | + return {get("/Kids")}; | |
| 400 | + } | |
| 401 | + | |
| 373 | 402 | /// @brief Returns the top-level field associated with the current field. |
| 374 | 403 | /// |
| 375 | 404 | /// The function traverses the hierarchy of parent fields to identify the highest-level |
| ... | ... | @@ -615,6 +644,41 @@ namespace qpdf::impl |
| 615 | 644 | /// no value is found. |
| 616 | 645 | std::string default_appearance() const; |
| 617 | 646 | |
| 647 | + /// @brief Determines if the current node represents a valid form field node. | |
| 648 | + /// | |
| 649 | + /// This function evaluates whether the current node is valid by combining | |
| 650 | + /// checks for the node's partial name (/T attribute), its immediate descendants | |
| 651 | + /// (/Kids array), and its field type (/FT attribute). It ensures that at least | |
| 652 | + /// one of these properties exists or returns a valid value. | |
| 653 | + /// | |
| 654 | + /// @return true if the form node contains a valid /T attribute, /Kids array, | |
| 655 | + /// or /FT attribute; otherwise, false. | |
| 656 | + bool | |
| 657 | + field() const | |
| 658 | + { | |
| 659 | + return T() || Kids() || FT(); | |
| 660 | + } | |
| 661 | + | |
| 662 | + /// @brief Determines if the form node represents a widget annotation. | |
| 663 | + /// | |
| 664 | + /// This method checks whether the current form node is a widget annotation | |
| 665 | + /// by verifying the following conditions: | |
| 666 | + /// | |
| 667 | + /// - The node does not have any /Kids entries (i.e., it is not a parent node with | |
| 668 | + /// descendants). | |
| 669 | + /// - The node contains any of the following attributes commonly associated with widget | |
| 670 | + /// annotations: | |
| 671 | + /// - `/Subtype` | |
| 672 | + /// - `/Rect` | |
| 673 | + /// - `/AP` | |
| 674 | + /// | |
| 675 | + /// @return `true` if the form node is a widget annotation; otherwise, `false`. | |
| 676 | + bool | |
| 677 | + widget() const | |
| 678 | + { | |
| 679 | + return !Kids() && (contains("/Subtype") || contains("/Rect") || contains("/AP")); | |
| 680 | + } | |
| 681 | + | |
| 618 | 682 | // Return the default resource dictionary for the field. This comes not from the field but |
| 619 | 683 | // from the document-level /AcroForm dictionary. While several PDF generators put a /DR key |
| 620 | 684 | // in the form field's dictionary, experimentation suggests that many popular readers, |
| ... | ... | @@ -656,6 +720,7 @@ namespace qpdf::impl |
| 656 | 720 | // want to set the name of a field, use QPDFAcroFormDocumentHelper::setFormFieldName |
| 657 | 721 | // instead. |
| 658 | 722 | void setFieldAttribute(std::string const& key, QPDFObjectHandle value); |
| 723 | + void setFieldAttribute(std::string const& key, Name const& value); | |
| 659 | 724 | |
| 660 | 725 | // Set an attribute to the given value as a Unicode string (UTF-16 BE encoded). The input |
| 661 | 726 | // string should be UTF-8 encoded. If you have a QPDFAcroFormDocumentHelper and you want to | ... | ... |
qpdf/qpdf.testcov
| ... | ... | @@ -193,9 +193,6 @@ QPDFPageDocumentHelper non-widget annotation 0 |
| 193 | 193 | QPDFObjectHandle replace with copy 0 |
| 194 | 194 | QPDFAnnotationObjectHelper forbidden flags 0 |
| 195 | 195 | QPDFAnnotationObjectHelper missing required flags 0 |
| 196 | -QPDFFormFieldObjectHelper checkbox kid widget 0 | |
| 197 | -QPDFFormFieldObjectHelper set checkbox AS 0 | |
| 198 | -QPDFObjectHandle broken checkbox 0 | |
| 199 | 196 | QPDFFormFieldObjectHelper list not found 0 |
| 200 | 197 | QPDFFormFieldObjectHelper list found 0 |
| 201 | 198 | QPDFFormFieldObjectHelper list first too low 0 | ... | ... |