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 | 2 | |
| 3 | 3 | #include <qpdf/AcroForm.hh> |
| 4 | 4 | |
| 5 | +#include <qpdf/Pipeline_private.hh> | |
| 5 | 6 | #include <qpdf/Pl_QPDFTokenizer.hh> |
| 6 | 7 | #include <qpdf/QIntC.hh> |
| 7 | 8 | #include <qpdf/QPDFAcroFormDocumentHelper.hh> |
| ... | ... | @@ -876,58 +877,55 @@ namespace |
| 876 | 877 | }; |
| 877 | 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 | 880 | void |
| 891 | 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 | 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 | 895 | {{"/BBox", QPDFObjectHandle::newFromRectangle(bbox)}, |
| 899 | 896 | {"/Resources", Dictionary({{"/ProcSet", Array({Name("/PDF"), Name("/Text")})}})}, |
| 900 | 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 | 899 | if (auto ap = AP()) { |
| 905 | 900 | ap.replace("/N", AS); |
| 906 | 901 | } else { |
| 907 | 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 | 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 | 909 | // - One reference from the local variable AS |
| 919 | 910 | // - One reference from the appearance dictionary (/AP) |
| 920 | 911 | // - One reference from the object table |
| 921 | 912 | // If use_count() is greater than 3, it means the appearance stream is shared elsewhere, |
| 922 | 913 | // and updating it could have unintended side effects. This threshold may need to be updated |
| 923 | 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 | 929 | if (!bbox_obj.isRectangle()) { |
| 932 | 930 | aoh.warn("unable to get appearance stream bounding box"); |
| 933 | 931 | return; |
| ... | ... | @@ -935,10 +933,6 @@ FormNode::generateTextAppearance(QPDFAnnotationObjectHelper& aoh) |
| 935 | 933 | QPDFObjectHandle::Rectangle bbox = bbox_obj.getArrayAsRectangle(); |
| 936 | 934 | std::string DA = default_appearance(); |
| 937 | 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 | 937 | TfFinder tff; |
| 944 | 938 | Pl_QPDFTokenizer tok("tf", &tff); |
| ... | ... | @@ -952,18 +946,18 @@ FormNode::generateTextAppearance(QPDFAnnotationObjectHelper& aoh) |
| 952 | 946 | if (!font_name.empty()) { |
| 953 | 947 | // See if the font is encoded with something we know about. |
| 954 | 948 | Dictionary resources = AS.getDict()["/Resources"]; |
| 955 | - Dictionary font = getFontFromResource(resources, font_name); | |
| 949 | + Dictionary font = resources["/Font"][font_name]; | |
| 956 | 950 | if (!font) { |
| 957 | - font = getFontFromResource(getDefaultResources(), font_name); | |
| 951 | + font = getDefaultResources()["/Font"][font_name]; | |
| 958 | 952 | if (resources) { |
| 959 | 953 | if (resources.indirect()) { |
| 960 | 954 | resources = resources.qpdf()->makeIndirectObject(resources.copy()); |
| 961 | - AS.getDict().replaceKey("/Resources", resources); | |
| 955 | + AS.getDict().replace("/Resources", resources); | |
| 962 | 956 | } |
| 963 | 957 | // Use mergeResources to force /Font to be local |
| 964 | 958 | QPDFObjectHandle res = resources; |
| 965 | 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 | 971 | } |
| 978 | 972 | |
| 979 | 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 | 769 | void setRadioButtonValue(QPDFObjectHandle name); |
| 770 | 770 | void setCheckBoxValue(bool value); |
| 771 | 771 | void generateTextAppearance(QPDFAnnotationObjectHelper&); |
| 772 | - QPDFObjectHandle | |
| 773 | - getFontFromResource(QPDFObjectHandle resources, std::string const& font_name); | |
| 774 | 772 | |
| 775 | 773 | static const QPDFObjectHandle null_oh; |
| 776 | 774 | }; // class FormNode | ... | ... |
manual/release-notes.rst
| ... | ... | @@ -115,10 +115,6 @@ more detail. |
| 115 | 115 | - There has been significant internal refactoring affecting most parts of |
| 116 | 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 | 118 | - By default, streams with more than 25 filters are now treated as unfilterable. |
| 123 | 119 | A large number of filters typically occur in damaged or specially constructed |
| 124 | 120 | files and can cause excessive use of resources and/or stack overflows. The | ... | ... |