Commit 8767429a4f8f598ef65a447618150dcdc2417b35

Authored by m-holger
Committed by GitHub
2 parents c8f431de 7ac9a086

Merge pull request #1633 from m-holger/fngta

Refactor FormNode::generateTextAppearance
libqpdf/QPDFFormFieldObjectHelper.cc
@@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
2 2
3 #include <qpdf/AcroForm.hh> 3 #include <qpdf/AcroForm.hh>
4 4
  5 +#include <qpdf/Pipeline_private.hh>
5 #include <qpdf/Pl_QPDFTokenizer.hh> 6 #include <qpdf/Pl_QPDFTokenizer.hh>
6 #include <qpdf/QIntC.hh> 7 #include <qpdf/QIntC.hh>
7 #include <qpdf/QPDFAcroFormDocumentHelper.hh> 8 #include <qpdf/QPDFAcroFormDocumentHelper.hh>
@@ -876,58 +877,55 @@ namespace @@ -876,58 +877,55 @@ namespace
876 }; 877 };
877 } // namespace 878 } // namespace
878 879
879 -QPDFObjectHandle  
880 -FormNode::getFontFromResource(QPDFObjectHandle resources, std::string const& name)  
881 -{  
882 - QPDFObjectHandle result;  
883 - if (resources.isDictionary() && resources.getKey("/Font").isDictionary() &&  
884 - resources.getKey("/Font").hasKey(name)) {  
885 - result = resources.getKey("/Font").getKey(name);  
886 - }  
887 - return result;  
888 -}  
889 -  
890 void 880 void
891 FormNode::generateTextAppearance(QPDFAnnotationObjectHelper& aoh) 881 FormNode::generateTextAppearance(QPDFAnnotationObjectHelper& aoh)
892 { 882 {
893 - QPDFObjectHandle AS = aoh.getAppearanceStream("/N");  
894 - if (AS.null()) {  
895 - QPDFObjectHandle::Rectangle rect = aoh.getRect(); 883 + no_ci_warn_if(
  884 + !Dictionary(aoh), // There is no guarantee that aoh is a dictionary
  885 + "cannot generate appearance for non-dictionary annotation" //
  886 + );
  887 + Stream AS = aoh.getAppearanceStream("/N"); // getAppearanceStream returns a stream or null.
  888 + if (!AS) {
  889 + QPDFObjectHandle::Rectangle rect = aoh.getRect(); // may silently be invalid / all zeros
896 QPDFObjectHandle::Rectangle bbox(0, 0, rect.urx - rect.llx, rect.ury - rect.lly); 890 QPDFObjectHandle::Rectangle bbox(0, 0, rect.urx - rect.llx, rect.ury - rect.lly);
897 - auto dict = Dictionary( 891 + auto* pdf = qpdf();
  892 + no_ci_stop_damaged_if(!pdf, "unable to get owning QPDF for appearance generation");
  893 + AS = pdf->newStream("/Tx BMC\nEMC\n");
  894 + AS.replaceDict(Dictionary(
898 {{"/BBox", QPDFObjectHandle::newFromRectangle(bbox)}, 895 {{"/BBox", QPDFObjectHandle::newFromRectangle(bbox)},
899 {"/Resources", Dictionary({{"/ProcSet", Array({Name("/PDF"), Name("/Text")})}})}, 896 {"/Resources", Dictionary({{"/ProcSet", Array({Name("/PDF"), Name("/Text")})}})},
900 {"/Type", Name("/XObject")}, 897 {"/Type", Name("/XObject")},
901 - {"/Subtype", Name("/Form")}});  
902 - AS = QPDFObjectHandle::newStream(oh().getOwningQPDF(), "/Tx BMC\nEMC\n");  
903 - AS.replaceDict(dict); 898 + {"/Subtype", Name("/Form")}}));
904 if (auto ap = AP()) { 899 if (auto ap = AP()) {
905 ap.replace("/N", AS); 900 ap.replace("/N", AS);
906 } else { 901 } else {
907 aoh.replace("/AP", Dictionary({{"/N", AS}})); 902 aoh.replace("/AP", Dictionary({{"/N", AS}}));
908 } 903 }
909 } 904 }
910 - if (!AS.isStream()) {  
911 - aoh.warn("unable to get normal appearance stream for update");  
912 - return;  
913 - }  
914 905
915 if (AS.obj_sp().use_count() > 3) { 906 if (AS.obj_sp().use_count() > 3) {
916 - // The following check ensures that we only update the appearance stream if it is not  
917 - // shared. The threshold of 3 is based on the current implementation details: 907 + // Ensures that the appearance stream is not shared by copying it if the threshold of 3 is
  908 + // exceeded. The threshold is based on the current implementation details:
918 // - One reference from the local variable AS 909 // - One reference from the local variable AS
919 // - One reference from the appearance dictionary (/AP) 910 // - One reference from the appearance dictionary (/AP)
920 // - One reference from the object table 911 // - One reference from the object table
921 // If use_count() is greater than 3, it means the appearance stream is shared elsewhere, 912 // If use_count() is greater than 3, it means the appearance stream is shared elsewhere,
922 // and updating it could have unintended side effects. This threshold may need to be updated 913 // and updating it could have unintended side effects. This threshold may need to be updated
923 // if the internal reference counting changes in the future. 914 // if the internal reference counting changes in the future.
924 - // The long-term solution will we to replace appearance streams at the point of flattening  
925 - // annotations rather than attaching token filters that modify the streams at time of  
926 - // writing.  
927 - aoh.warn("unable to generate text appearance from shared appearance stream for update");  
928 - return; 915 + //
  916 + // There is currently no explicit CI test for this code. It has been manually tested by
  917 + // running it through CI with a threshold of 0, unconditionally copying streams.
  918 + auto data = AS.getStreamData(qpdf_dl_all);
  919 + AS = AS.copy();
  920 + AS.replaceStreamData(std::move(data), Null::temp(), Null::temp());
  921 + if (Dictionary AP = aoh.getAppearanceDictionary()) {
  922 + AP.replace("/N", AS);
  923 + } else {
  924 + aoh.replace("/AP", Dictionary({{"/N", AS}}));
  925 + // aoh is a dictionary, so insertion will succeed. No need to check by retrieving it.
  926 + }
929 } 927 }
930 - QPDFObjectHandle bbox_obj = AS.getDict().getKey("/BBox"); 928 + QPDFObjectHandle bbox_obj = AS.getDict()["/BBox"];
931 if (!bbox_obj.isRectangle()) { 929 if (!bbox_obj.isRectangle()) {
932 aoh.warn("unable to get appearance stream bounding box"); 930 aoh.warn("unable to get appearance stream bounding box");
933 return; 931 return;
@@ -935,10 +933,6 @@ FormNode::generateTextAppearance(QPDFAnnotationObjectHelper&amp; aoh) @@ -935,10 +933,6 @@ FormNode::generateTextAppearance(QPDFAnnotationObjectHelper&amp; aoh)
935 QPDFObjectHandle::Rectangle bbox = bbox_obj.getArrayAsRectangle(); 933 QPDFObjectHandle::Rectangle bbox = bbox_obj.getArrayAsRectangle();
936 std::string DA = default_appearance(); 934 std::string DA = default_appearance();
937 std::string V = value(); 935 std::string V = value();
938 - std::vector<std::string> opt;  
939 - if (isChoice() && (getFlags() & ff_ch_combo) == 0) {  
940 - opt = getChoices();  
941 - }  
942 936
943 TfFinder tff; 937 TfFinder tff;
944 Pl_QPDFTokenizer tok("tf", &tff); 938 Pl_QPDFTokenizer tok("tf", &tff);
@@ -952,18 +946,18 @@ FormNode::generateTextAppearance(QPDFAnnotationObjectHelper&amp; aoh) @@ -952,18 +946,18 @@ FormNode::generateTextAppearance(QPDFAnnotationObjectHelper&amp; aoh)
952 if (!font_name.empty()) { 946 if (!font_name.empty()) {
953 // See if the font is encoded with something we know about. 947 // See if the font is encoded with something we know about.
954 Dictionary resources = AS.getDict()["/Resources"]; 948 Dictionary resources = AS.getDict()["/Resources"];
955 - Dictionary font = getFontFromResource(resources, font_name); 949 + Dictionary font = resources["/Font"][font_name];
956 if (!font) { 950 if (!font) {
957 - font = getFontFromResource(getDefaultResources(), font_name); 951 + font = getDefaultResources()["/Font"][font_name];
958 if (resources) { 952 if (resources) {
959 if (resources.indirect()) { 953 if (resources.indirect()) {
960 resources = resources.qpdf()->makeIndirectObject(resources.copy()); 954 resources = resources.qpdf()->makeIndirectObject(resources.copy());
961 - AS.getDict().replaceKey("/Resources", resources); 955 + AS.getDict().replace("/Resources", resources);
962 } 956 }
963 // Use mergeResources to force /Font to be local 957 // Use mergeResources to force /Font to be local
964 QPDFObjectHandle res = resources; 958 QPDFObjectHandle res = resources;
965 res.mergeResources(Dictionary({{"/Font", Dictionary::empty()}})); 959 res.mergeResources(Dictionary({{"/Font", Dictionary::empty()}}));
966 - res.getKey("/Font").replaceKey(font_name, font); 960 + res.getKey("/Font").replace(font_name, font);
967 } 961 }
968 } 962 }
969 963
@@ -977,9 +971,20 @@ FormNode::generateTextAppearance(QPDFAnnotationObjectHelper&amp; aoh) @@ -977,9 +971,20 @@ FormNode::generateTextAppearance(QPDFAnnotationObjectHelper&amp; aoh)
977 } 971 }
978 972
979 V = (*encoder)(V, '?'); 973 V = (*encoder)(V, '?');
980 - for (size_t i = 0; i < opt.size(); ++i) {  
981 - opt.at(i) = (*encoder)(opt.at(i), '?'); 974 +
  975 + std::vector<std::string> opt;
  976 + if (isChoice() && (getFlags() & ff_ch_combo) == 0) {
  977 + opt = getChoices();
  978 + for (auto& o: opt) {
  979 + o = (*encoder)(o, '?');
  980 + }
982 } 981 }
983 - AS.addTokenFilter(  
984 - std::shared_ptr<QPDFObjectHandle::TokenFilter>(new ValueSetter(DA, V, opt, tf, bbox))); 982 +
  983 + std::string result;
  984 + pl::String pl(result);
  985 + ValueSetter vs(DA, V, opt, tf, bbox);
  986 + Pl_QPDFTokenizer vs_tok("", &vs, &pl);
  987 + vs_tok.writeString(AS.getStreamData(qpdf_dl_all));
  988 + vs_tok.finish();
  989 + AS.replaceStreamData(std::move(result), Null::temp(), Null::temp());
985 } 990 }
libqpdf/qpdf/AcroForm.hh
@@ -769,8 +769,6 @@ namespace qpdf::impl @@ -769,8 +769,6 @@ namespace qpdf::impl
769 void setRadioButtonValue(QPDFObjectHandle name); 769 void setRadioButtonValue(QPDFObjectHandle name);
770 void setCheckBoxValue(bool value); 770 void setCheckBoxValue(bool value);
771 void generateTextAppearance(QPDFAnnotationObjectHelper&); 771 void generateTextAppearance(QPDFAnnotationObjectHelper&);
772 - QPDFObjectHandle  
773 - getFontFromResource(QPDFObjectHandle resources, std::string const& font_name);  
774 772
775 static const QPDFObjectHandle null_oh; 773 static const QPDFObjectHandle null_oh;
776 }; // class FormNode 774 }; // class FormNode
manual/release-notes.rst
@@ -115,10 +115,6 @@ more detail. @@ -115,10 +115,6 @@ more detail.
115 - There has been significant internal refactoring affecting most parts of 115 - There has been significant internal refactoring affecting most parts of
116 qpdf's code base. 116 qpdf's code base.
117 117
118 - - When flattening widget annotations further checks have been added to detect  
119 - when qpdf cannot reliably generate the necessary appearance streams. As in  
120 - other such cases a warning is issued and the annotation remains unflattened.  
121 -  
122 - By default, streams with more than 25 filters are now treated as unfilterable. 118 - By default, streams with more than 25 filters are now treated as unfilterable.
123 A large number of filters typically occur in damaged or specially constructed 119 A large number of filters typically occur in damaged or specially constructed
124 files and can cause excessive use of resources and/or stack overflows. The 120 files and can cause excessive use of resources and/or stack overflows. The