Commit 35ca7efe103fb65d2c00eb9b6deb29d3d95bfc0c

Authored by m-holger
Committed by GitHub
2 parents 31c7b097 2ac099bd

Merge pull request #1625 from m-holger/ffoh

Refactor FormNode
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
... ...