Commit 8767429a4f8f598ef65a447618150dcdc2417b35
Committed by
GitHub
Merge pull request #1633 from m-holger/fngta
Refactor FormNode::generateTextAppearance
Showing
3 changed files
with
47 additions
and
48 deletions
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& aoh) | @@ -935,10 +933,6 @@ FormNode::generateTextAppearance(QPDFAnnotationObjectHelper& 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& aoh) | @@ -952,18 +946,18 @@ FormNode::generateTextAppearance(QPDFAnnotationObjectHelper& 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& aoh) | @@ -977,9 +971,20 @@ FormNode::generateTextAppearance(QPDFAnnotationObjectHelper& 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 |