Commit 9ca80c23fa72ced55e15b2b3ebf21810240b34cf
Committed by
GitHub
Merge pull request #1538 from m-holger/afdh5
Refactor QPDFAcroFormDocumentHelper::transformAnnotations
Showing
2 changed files
with
154 additions
and
156 deletions
libqpdf/QPDFAcroFormDocumentHelper.cc
| @@ -8,6 +8,7 @@ | @@ -8,6 +8,7 @@ | ||
| 8 | #include <qpdf/QUtil.hh> | 8 | #include <qpdf/QUtil.hh> |
| 9 | #include <qpdf/ResourceFinder.hh> | 9 | #include <qpdf/ResourceFinder.hh> |
| 10 | 10 | ||
| 11 | +#include <deque> | ||
| 11 | #include <utility> | 12 | #include <utility> |
| 12 | 13 | ||
| 13 | using namespace qpdf; | 14 | using namespace qpdf; |
| @@ -480,6 +481,9 @@ QPDFAcroFormDocumentHelper::adjustInheritedFields( | @@ -480,6 +481,9 @@ QPDFAcroFormDocumentHelper::adjustInheritedFields( | ||
| 480 | bool override_q, | 481 | bool override_q, |
| 481 | int from_default_q) | 482 | int from_default_q) |
| 482 | { | 483 | { |
| 484 | + if (!(override_da || override_q)) { | ||
| 485 | + return; | ||
| 486 | + } | ||
| 483 | // Override /Q or /DA if needed. If this object has a field type, directly or inherited, it is a | 487 | // Override /Q or /DA if needed. If this object has a field type, directly or inherited, it is a |
| 484 | // field and not just an annotation. In that case, we need to override if we are getting a value | 488 | // field and not just an annotation. In that case, we need to override if we are getting a value |
| 485 | // from the document that is different from the value we would have gotten from the old | 489 | // from the document that is different from the value we would have gotten from the old |
| @@ -719,7 +723,7 @@ QPDFAcroFormDocumentHelper::adjustAppearanceStream( | @@ -719,7 +723,7 @@ QPDFAcroFormDocumentHelper::adjustAppearanceStream( | ||
| 719 | 723 | ||
| 720 | void | 724 | void |
| 721 | QPDFAcroFormDocumentHelper::transformAnnotations( | 725 | QPDFAcroFormDocumentHelper::transformAnnotations( |
| 722 | - QPDFObjectHandle old_annots, | 726 | + QPDFObjectHandle a_old_annots, |
| 723 | std::vector<QPDFObjectHandle>& new_annots, | 727 | std::vector<QPDFObjectHandle>& new_annots, |
| 724 | std::vector<QPDFObjectHandle>& new_fields, | 728 | std::vector<QPDFObjectHandle>& new_fields, |
| 725 | std::set<QPDFObjGen>& old_fields, | 729 | std::set<QPDFObjGen>& old_fields, |
| @@ -727,94 +731,87 @@ QPDFAcroFormDocumentHelper::transformAnnotations( | @@ -727,94 +731,87 @@ QPDFAcroFormDocumentHelper::transformAnnotations( | ||
| 727 | QPDF* from_qpdf, | 731 | QPDF* from_qpdf, |
| 728 | QPDFAcroFormDocumentHelper* from_afdh) | 732 | QPDFAcroFormDocumentHelper* from_afdh) |
| 729 | { | 733 | { |
| 730 | - std::shared_ptr<QPDFAcroFormDocumentHelper> afdhph; | 734 | + Array old_annots = std::move(a_old_annots); |
| 731 | if (!from_qpdf) { | 735 | if (!from_qpdf) { |
| 732 | // Assume these are from the same QPDF. | 736 | // Assume these are from the same QPDF. |
| 733 | from_qpdf = &qpdf; | 737 | from_qpdf = &qpdf; |
| 734 | from_afdh = this; | 738 | from_afdh = this; |
| 735 | - } else if ((from_qpdf != &qpdf) && (!from_afdh)) { | ||
| 736 | - afdhph = std::make_shared<QPDFAcroFormDocumentHelper>(*from_qpdf); | ||
| 737 | - from_afdh = afdhph.get(); | 739 | + } else if (from_qpdf != &qpdf && !from_afdh) { |
| 740 | + from_afdh = &QPDFAcroFormDocumentHelper::get(*from_qpdf); | ||
| 738 | } | 741 | } |
| 739 | - bool foreign = (from_qpdf != &qpdf); | 742 | + const bool foreign = from_qpdf != &qpdf; |
| 740 | 743 | ||
| 741 | // It's possible that we will transform annotations that don't include any form fields. This | 744 | // It's possible that we will transform annotations that don't include any form fields. This |
| 742 | // code takes care not to muck around with /AcroForm unless we have to. | 745 | // code takes care not to muck around with /AcroForm unless we have to. |
| 743 | 746 | ||
| 744 | - QPDFObjectHandle acroform = qpdf.getRoot().getKey("/AcroForm"); | ||
| 745 | - QPDFObjectHandle from_acroform = from_qpdf->getRoot().getKey("/AcroForm"); | 747 | + Dictionary acroform = qpdf.getRoot()["/AcroForm"]; |
| 748 | + Dictionary from_acroform = from_qpdf->getRoot()["/AcroForm"]; | ||
| 746 | 749 | ||
| 747 | // /DA and /Q may be inherited from the document-level /AcroForm dictionary. If we are copying a | 750 | // /DA and /Q may be inherited from the document-level /AcroForm dictionary. If we are copying a |
| 748 | // foreign stream and the stream is getting one of these values from its document's /AcroForm, | 751 | // foreign stream and the stream is getting one of these values from its document's /AcroForm, |
| 749 | // we will need to copy the value explicitly so that it doesn't start getting its default from | 752 | // we will need to copy the value explicitly so that it doesn't start getting its default from |
| 750 | // the destination document. | 753 | // the destination document. |
| 751 | - bool override_da = false; | ||
| 752 | - bool override_q = false; | ||
| 753 | std::string from_default_da; | 754 | std::string from_default_da; |
| 754 | int from_default_q = 0; | 755 | int from_default_q = 0; |
| 755 | // If we copy any form fields, we will need to merge the source document's /DR into this | 756 | // If we copy any form fields, we will need to merge the source document's /DR into this |
| 756 | // document's /DR. | 757 | // document's /DR. |
| 757 | - QPDFObjectHandle from_dr = QPDFObjectHandle::newNull(); | 758 | + Dictionary from_dr; |
| 759 | + std::string default_da; | ||
| 760 | + int default_q = 0; | ||
| 758 | if (foreign) { | 761 | if (foreign) { |
| 759 | - std::string default_da; | ||
| 760 | - int default_q = 0; | ||
| 761 | - if (acroform.isDictionary()) { | ||
| 762 | - if (acroform.getKey("/DA").isString()) { | ||
| 763 | - default_da = acroform.getKey("/DA").getUTF8Value(); | 762 | + if (acroform) { |
| 763 | + if (acroform["/DA"].isString()) { | ||
| 764 | + default_da = acroform["/DA"].getUTF8Value(); | ||
| 764 | } | 765 | } |
| 765 | - if (acroform.getKey("/Q").isInteger()) { | ||
| 766 | - default_q = acroform.getKey("/Q").getIntValueAsInt(); | 766 | + if (Integer Q = acroform["/Q"]) { |
| 767 | + default_q = Q; | ||
| 767 | } | 768 | } |
| 768 | } | 769 | } |
| 769 | - if (from_acroform.isDictionary()) { | ||
| 770 | - if (from_acroform.getKey("/DR").isDictionary()) { | ||
| 771 | - from_dr = from_acroform.getKey("/DR"); | ||
| 772 | - if (!from_dr.isIndirect()) { | 770 | + if (from_acroform) { |
| 771 | + from_dr = from_acroform["/DR"]; | ||
| 772 | + if (from_dr) { | ||
| 773 | + if (!from_dr.indirect()) { | ||
| 773 | from_dr = from_qpdf->makeIndirectObject(from_dr); | 774 | from_dr = from_qpdf->makeIndirectObject(from_dr); |
| 774 | } | 775 | } |
| 775 | from_dr = qpdf.copyForeignObject(from_dr); | 776 | from_dr = qpdf.copyForeignObject(from_dr); |
| 776 | } | 777 | } |
| 777 | - if (from_acroform.getKey("/DA").isString()) { | ||
| 778 | - from_default_da = from_acroform.getKey("/DA").getUTF8Value(); | 778 | + if (from_acroform["/DA"].isString()) { |
| 779 | + from_default_da = from_acroform["/DA"].getUTF8Value(); | ||
| 779 | } | 780 | } |
| 780 | - if (from_acroform.getKey("/Q").isInteger()) { | ||
| 781 | - from_default_q = from_acroform.getKey("/Q").getIntValueAsInt(); | 781 | + if (Integer Q = from_acroform["/Q"]) { |
| 782 | + from_default_q = Q; | ||
| 782 | } | 783 | } |
| 783 | } | 784 | } |
| 784 | - if (from_default_da != default_da) { | ||
| 785 | - override_da = true; | ||
| 786 | - } | ||
| 787 | - if (from_default_q != default_q) { | ||
| 788 | - override_q = true; | ||
| 789 | - } | ||
| 790 | } | 785 | } |
| 786 | + const bool override_da = from_acroform ? from_default_da != default_da : false; | ||
| 787 | + const bool override_q = from_acroform ? from_default_q != default_q : false; | ||
| 791 | 788 | ||
| 792 | // If we have to merge /DR, we will need a mapping of conflicting keys for rewriting /DA. Set | 789 | // If we have to merge /DR, we will need a mapping of conflicting keys for rewriting /DA. Set |
| 793 | // this up for lazy initialization in case we encounter any form fields. | 790 | // this up for lazy initialization in case we encounter any form fields. |
| 794 | std::map<std::string, std::map<std::string, std::string>> dr_map; | 791 | std::map<std::string, std::map<std::string, std::string>> dr_map; |
| 795 | - bool initialized_dr_map = false; | ||
| 796 | - QPDFObjectHandle dr = QPDFObjectHandle::newNull(); | 792 | + Dictionary dr; |
| 793 | + | ||
| 797 | auto init_dr_map = [&]() { | 794 | auto init_dr_map = [&]() { |
| 798 | - if (!initialized_dr_map) { | ||
| 799 | - initialized_dr_map = true; | 795 | + if (!dr) { |
| 800 | // Ensure that we have a /DR that is an indirect | 796 | // Ensure that we have a /DR that is an indirect |
| 801 | // dictionary object. | 797 | // dictionary object. |
| 802 | - if (!acroform.isDictionary()) { | 798 | + if (!acroform) { |
| 803 | acroform = getOrCreateAcroForm(); | 799 | acroform = getOrCreateAcroForm(); |
| 804 | } | 800 | } |
| 805 | - dr = acroform.getKey("/DR"); | ||
| 806 | - if (!dr.isDictionary()) { | ||
| 807 | - dr = QPDFObjectHandle::newDictionary(); | 801 | + dr = acroform["/DR"]; |
| 802 | + if (!dr) { | ||
| 803 | + dr = Dictionary::empty(); | ||
| 808 | } | 804 | } |
| 809 | - dr.makeResourcesIndirect(qpdf); | ||
| 810 | - if (!dr.isIndirect()) { | ||
| 811 | - dr = acroform.replaceKeyAndGetNew("/DR", qpdf.makeIndirectObject(dr)); | 805 | + QPDFObjectHandle(dr).makeResourcesIndirect(qpdf); |
| 806 | + if (!dr.indirect()) { | ||
| 807 | + acroform.replaceKey("/DR", qpdf.makeIndirectObject(dr)); | ||
| 808 | + dr = acroform["/DR"]; | ||
| 812 | } | 809 | } |
| 813 | // Merge the other document's /DR, creating a conflict map. mergeResources checks to | 810 | // Merge the other document's /DR, creating a conflict map. mergeResources checks to |
| 814 | // make sure both objects are dictionaries. By this point, if this is foreign, from_dr | 811 | // make sure both objects are dictionaries. By this point, if this is foreign, from_dr |
| 815 | // has been copied, so we use the target qpdf as the owning qpdf. | 812 | // has been copied, so we use the target qpdf as the owning qpdf. |
| 816 | - from_dr.makeResourcesIndirect(qpdf); | ||
| 817 | - dr.mergeResources(from_dr, &dr_map); | 813 | + QPDFObjectHandle(from_dr).makeResourcesIndirect(qpdf); |
| 814 | + QPDFObjectHandle(dr).mergeResources(from_dr, &dr_map); | ||
| 818 | 815 | ||
| 819 | if (from_afdh->getNeedAppearances()) { | 816 | if (from_afdh->getNeedAppearances()) { |
| 820 | setNeedAppearances(true); | 817 | setNeedAppearances(true); |
| @@ -836,15 +833,59 @@ QPDFAcroFormDocumentHelper::transformAnnotations( | @@ -836,15 +833,59 @@ QPDFAcroFormDocumentHelper::transformAnnotations( | ||
| 836 | } | 833 | } |
| 837 | }; | 834 | }; |
| 838 | 835 | ||
| 839 | - // Now do the actual copies. | ||
| 840 | - | ||
| 841 | - QPDFObjGen::set added_new_fields; | ||
| 842 | - for (auto annot: old_annots.aitems()) { | ||
| 843 | - if (annot.isStream()) { | ||
| 844 | - annot.warn("ignoring annotation that's a stream"); | ||
| 845 | - continue; | 836 | + // Traverse the field, copying kids, and preserving integrity. |
| 837 | + auto traverse_field = [&](QPDFObjectHandle& top_field) -> void { | ||
| 838 | + std::deque<Dictionary> queue({top_field}); | ||
| 839 | + QPDFObjGen::set seen; | ||
| 840 | + for (auto it = queue.begin(); it != queue.end(); ++it) { | ||
| 841 | + auto& obj = *it; | ||
| 842 | + if (seen.add(obj)) { | ||
| 843 | + Dictionary parent = obj["/Parent"]; | ||
| 844 | + if (parent.indirect()) { | ||
| 845 | + auto parent_og = parent.id_gen(); | ||
| 846 | + if (orig_to_copy.contains(parent_og)) { | ||
| 847 | + obj.replaceKey("/Parent", orig_to_copy[parent_og]); | ||
| 848 | + } else { | ||
| 849 | + parent.warn( | ||
| 850 | + "while traversing field " + obj.id_gen().unparse(',') + | ||
| 851 | + ", found parent (" + parent_og.unparse(',') + | ||
| 852 | + ") that had not been seen, indicating likely invalid field structure"); | ||
| 853 | + } | ||
| 854 | + } | ||
| 855 | + size_t i = 0; | ||
| 856 | + Array Kids = obj["/Kids"]; | ||
| 857 | + for (auto& kid: Kids) { | ||
| 858 | + if (maybe_copy_object(kid)) { | ||
| 859 | + Kids.set(i, kid); | ||
| 860 | + queue.emplace_back(kid); | ||
| 861 | + } | ||
| 862 | + ++i; | ||
| 863 | + } | ||
| 864 | + adjustInheritedFields( | ||
| 865 | + obj, override_da, from_default_da, override_q, from_default_q); | ||
| 866 | + if (foreign) { | ||
| 867 | + // Lazily initialize our /DR and the conflict map. | ||
| 868 | + init_dr_map(); | ||
| 869 | + // The spec doesn't say anything about /DR on the field, but lots of writers | ||
| 870 | + // put one there, and it is frequently the same as the document-level /DR. | ||
| 871 | + // To avoid having the field's /DR point to information that we are not | ||
| 872 | + // maintaining, just reset it to that if it exists. Empirical evidence | ||
| 873 | + // suggests that many readers, including Acrobat, Adobe Acrobat Reader, | ||
| 874 | + // chrome, firefox, the mac Preview application, and several of the free | ||
| 875 | + // readers on Linux all ignore /DR at the field level. | ||
| 876 | + if (obj.contains("/DR")) { | ||
| 877 | + obj.replaceKey("/DR", dr); | ||
| 878 | + } | ||
| 879 | + if (obj["/DA"].isString() && !dr_map.empty()) { | ||
| 880 | + adjustDefaultAppearances(obj, dr_map); | ||
| 881 | + } | ||
| 882 | + } | ||
| 883 | + } | ||
| 846 | } | 884 | } |
| 885 | + }; | ||
| 847 | 886 | ||
| 887 | + auto transform_annotation = | ||
| 888 | + [&](QPDFObjectHandle& annot) -> std::tuple<QPDFObjectHandle, bool, bool> { | ||
| 848 | // Make copies of annotations and fields down to the appearance streams, preserving all | 889 | // Make copies of annotations and fields down to the appearance streams, preserving all |
| 849 | // internal referential integrity. When the incoming annotations are from a different file, | 890 | // internal referential integrity. When the incoming annotations are from a different file, |
| 850 | // we first copy them locally. Then, whether local or foreign, we copy them again so that if | 891 | // we first copy them locally. Then, whether local or foreign, we copy them again so that if |
| @@ -870,98 +911,57 @@ QPDFAcroFormDocumentHelper::transformAnnotations( | @@ -870,98 +911,57 @@ QPDFAcroFormDocumentHelper::transformAnnotations( | ||
| 870 | 911 | ||
| 871 | auto ffield = from_afdh->getFieldForAnnotation(annot); | 912 | auto ffield = from_afdh->getFieldForAnnotation(annot); |
| 872 | auto ffield_oh = ffield.getObjectHandle(); | 913 | auto ffield_oh = ffield.getObjectHandle(); |
| 873 | - QPDFObjectHandle top_field; | ||
| 874 | - bool have_field = false; | ||
| 875 | - bool have_parent = false; | 914 | + if (ffield.null()) { |
| 915 | + return {{}, false, false}; | ||
| 916 | + } | ||
| 876 | if (ffield_oh.isStream()) { | 917 | if (ffield_oh.isStream()) { |
| 877 | ffield.warn("ignoring form field that's a stream"); | 918 | ffield.warn("ignoring form field that's a stream"); |
| 878 | - } else if (!ffield_oh.null() && !ffield_oh.isIndirect()) { | 919 | + return {{}, false, false}; |
| 920 | + } | ||
| 921 | + if (!ffield_oh.isIndirect()) { | ||
| 879 | ffield.warn("ignoring form field not indirect"); | 922 | ffield.warn("ignoring form field not indirect"); |
| 880 | - } else if (!ffield.null()) { | ||
| 881 | - // A field and its associated annotation can be the same object. This matters because we | ||
| 882 | - // don't want to clone the annotation and field separately in this case. | ||
| 883 | - have_field = true; | ||
| 884 | - // Find the top-level field. It may be the field itself. | ||
| 885 | - top_field = ffield.getTopLevelField(&have_parent).getObjectHandle(); | ||
| 886 | - if (foreign) { | ||
| 887 | - // copyForeignObject returns the same value if called multiple times with the same | ||
| 888 | - // field. Create/retrieve the local copy of the original field. This pulls over | ||
| 889 | - // everything the field references including annotations and appearance streams, but | ||
| 890 | - // it's harmless to call copyForeignObject on them too. They will already be copied, | ||
| 891 | - // so we'll get the right object back. | ||
| 892 | - | ||
| 893 | - // top_field and ffield_oh are known to be indirect. | ||
| 894 | - top_field = qpdf.copyForeignObject(top_field); | ||
| 895 | - ffield_oh = qpdf.copyForeignObject(ffield_oh); | ||
| 896 | - } else { | ||
| 897 | - // We don't need to add top_field to old_fields if it's foreign because the new copy | ||
| 898 | - // of the foreign field won't be referenced anywhere. It's just the starting point | ||
| 899 | - // for us to make an additional local copy of. | ||
| 900 | - old_fields.insert(top_field.getObjGen()); | ||
| 901 | - } | 923 | + return {{}, false, false}; |
| 924 | + } | ||
| 925 | + // A field and its associated annotation can be the same object. This matters because we | ||
| 926 | + // don't want to clone the annotation and field separately in this case. | ||
| 927 | + // Find the top-level field. It may be the field itself. | ||
| 928 | + bool have_parent = false; | ||
| 929 | + QPDFObjectHandle top_field = ffield.getTopLevelField(&have_parent).getObjectHandle(); | ||
| 930 | + if (foreign) { | ||
| 931 | + // copyForeignObject returns the same value if called multiple times with the same | ||
| 932 | + // field. Create/retrieve the local copy of the original field. This pulls over | ||
| 933 | + // everything the field references including annotations and appearance streams, but | ||
| 934 | + // it's harmless to call copyForeignObject on them too. They will already be copied, | ||
| 935 | + // so we'll get the right object back. | ||
| 936 | + | ||
| 937 | + // top_field and ffield_oh are known to be indirect. | ||
| 938 | + top_field = qpdf.copyForeignObject(top_field); | ||
| 939 | + ffield_oh = qpdf.copyForeignObject(ffield_oh); | ||
| 940 | + } else { | ||
| 941 | + // We don't need to add top_field to old_fields if it's foreign because the new copy | ||
| 942 | + // of the foreign field won't be referenced anywhere. It's just the starting point | ||
| 943 | + // for us to make an additional local copy of. | ||
| 944 | + old_fields.insert(top_field.getObjGen()); | ||
| 945 | + } | ||
| 902 | 946 | ||
| 903 | - // Traverse the field, copying kids, and preserving integrity. | ||
| 904 | - std::list<QPDFObjectHandle> queue; | ||
| 905 | - QPDFObjGen::set seen; | ||
| 906 | - if (maybe_copy_object(top_field)) { | ||
| 907 | - queue.push_back(top_field); | ||
| 908 | - } | ||
| 909 | - for (; !queue.empty(); queue.pop_front()) { | ||
| 910 | - auto& obj = queue.front(); | ||
| 911 | - if (seen.add(obj)) { | ||
| 912 | - auto parent = obj.getKey("/Parent"); | ||
| 913 | - if (parent.isIndirect()) { | ||
| 914 | - auto parent_og = parent.getObjGen(); | ||
| 915 | - if (orig_to_copy.contains(parent_og)) { | ||
| 916 | - obj.replaceKey("/Parent", orig_to_copy[parent_og]); | ||
| 917 | - } else { | ||
| 918 | - parent.warn( | ||
| 919 | - "while traversing field " + obj.getObjGen().unparse(',') + | ||
| 920 | - ", found parent (" + parent_og.unparse(',') + | ||
| 921 | - ") that had not been seen, indicating likely invalid field " | ||
| 922 | - "structure"); | ||
| 923 | - } | ||
| 924 | - } | ||
| 925 | - auto kids = obj.getKey("/Kids"); | ||
| 926 | - int sz = static_cast<int>(kids.size()); | ||
| 927 | - if (sz != 1 || kids.isArray()) { | ||
| 928 | - for (int i = 0; i < sz; ++i) { | ||
| 929 | - auto kid = kids.getArrayItem(i); | ||
| 930 | - if (maybe_copy_object(kid)) { | ||
| 931 | - kids.setArrayItem(i, kid); | ||
| 932 | - queue.emplace_back(kid); | ||
| 933 | - } | ||
| 934 | - } | ||
| 935 | - } | 947 | + if (maybe_copy_object(top_field)) { |
| 948 | + traverse_field(top_field); | ||
| 949 | + } | ||
| 936 | 950 | ||
| 937 | - if (override_da || override_q) { | ||
| 938 | - adjustInheritedFields( | ||
| 939 | - obj, override_da, from_default_da, override_q, from_default_q); | ||
| 940 | - } | ||
| 941 | - if (foreign) { | ||
| 942 | - // Lazily initialize our /DR and the conflict map. | ||
| 943 | - init_dr_map(); | ||
| 944 | - // The spec doesn't say anything about /DR on the field, but lots of writers | ||
| 945 | - // put one there, and it is frequently the same as the document-level /DR. | ||
| 946 | - // To avoid having the field's /DR point to information that we are not | ||
| 947 | - // maintaining, just reset it to that if it exists. Empirical evidence | ||
| 948 | - // suggests that many readers, including Acrobat, Adobe Acrobat Reader, | ||
| 949 | - // chrome, firefox, the mac Preview application, and several of the free | ||
| 950 | - // readers on Linux all ignore /DR at the field level. | ||
| 951 | - if (obj.hasKey("/DR")) { | ||
| 952 | - obj.replaceKey("/DR", dr); | ||
| 953 | - } | ||
| 954 | - } | ||
| 955 | - if (foreign && obj.getKey("/DA").isString() && (!dr_map.empty())) { | ||
| 956 | - adjustDefaultAppearances(obj, dr_map); | ||
| 957 | - } | ||
| 958 | - } | ||
| 959 | - } | 951 | + // Now switch to copies. We already switched for top_field |
| 952 | + maybe_copy_object(ffield_oh); | ||
| 953 | + return {top_field, true, have_parent}; | ||
| 954 | + }; | ||
| 955 | + | ||
| 956 | + // Now do the actual copies. | ||
| 960 | 957 | ||
| 961 | - // Now switch to copies. We already switched for top_field | ||
| 962 | - maybe_copy_object(ffield_oh); | ||
| 963 | - ffield = QPDFFormFieldObjectHelper(ffield_oh); | 958 | + QPDFObjGen::set added_new_fields; |
| 959 | + for (auto annot: old_annots) { | ||
| 960 | + if (annot.isStream()) { | ||
| 961 | + annot.warn("ignoring annotation that's a stream"); | ||
| 962 | + continue; | ||
| 964 | } | 963 | } |
| 964 | + auto [top_field, have_field, have_parent] = transform_annotation(annot); | ||
| 965 | 965 | ||
| 966 | QTC::TC( | 966 | QTC::TC( |
| 967 | "qpdf", | 967 | "qpdf", |
| @@ -983,24 +983,25 @@ QPDFAcroFormDocumentHelper::transformAnnotations( | @@ -983,24 +983,25 @@ QPDFAcroFormDocumentHelper::transformAnnotations( | ||
| 983 | 983 | ||
| 984 | // Now we have copies, so we can safely mutate. | 984 | // Now we have copies, so we can safely mutate. |
| 985 | if (have_field && added_new_fields.add(top_field)) { | 985 | if (have_field && added_new_fields.add(top_field)) { |
| 986 | - new_fields.push_back(top_field); | 986 | + new_fields.emplace_back(top_field); |
| 987 | } | 987 | } |
| 988 | - new_annots.push_back(annot); | 988 | + new_annots.emplace_back(annot); |
| 989 | 989 | ||
| 990 | // Identify and copy any appearance streams | 990 | // Identify and copy any appearance streams |
| 991 | 991 | ||
| 992 | auto ah = QPDFAnnotationObjectHelper(annot); | 992 | auto ah = QPDFAnnotationObjectHelper(annot); |
| 993 | - auto apdict = ah.getAppearanceDictionary(); | 993 | + Dictionary apdict = ah.getAppearanceDictionary(); |
| 994 | std::vector<QPDFObjectHandle> streams; | 994 | std::vector<QPDFObjectHandle> streams; |
| 995 | auto replace_stream = [](auto& dict, auto& key, auto& old) { | 995 | auto replace_stream = [](auto& dict, auto& key, auto& old) { |
| 996 | - return dict.replaceKeyAndGetNew(key, old.copyStream()); | 996 | + dict.replaceKey(key, old.copyStream()); |
| 997 | + return dict[key]; | ||
| 997 | }; | 998 | }; |
| 998 | 999 | ||
| 999 | - for (auto& [key1, value1]: apdict.as_dictionary()) { | 1000 | + for (auto& [key1, value1]: apdict) { |
| 1000 | if (value1.isStream()) { | 1001 | if (value1.isStream()) { |
| 1001 | streams.emplace_back(replace_stream(apdict, key1, value1)); | 1002 | streams.emplace_back(replace_stream(apdict, key1, value1)); |
| 1002 | } else { | 1003 | } else { |
| 1003 | - for (auto& [key2, value2]: value1.as_dictionary()) { | 1004 | + for (auto& [key2, value2]: Dictionary(value1)) { |
| 1004 | if (value2.isStream()) { | 1005 | if (value2.isStream()) { |
| 1005 | streams.emplace_back(replace_stream(value1, key2, value2)); | 1006 | streams.emplace_back(replace_stream(value1, key2, value2)); |
| 1006 | } | 1007 | } |
| @@ -1010,25 +1011,23 @@ QPDFAcroFormDocumentHelper::transformAnnotations( | @@ -1010,25 +1011,23 @@ QPDFAcroFormDocumentHelper::transformAnnotations( | ||
| 1010 | 1011 | ||
| 1011 | // Now we can safely mutate the annotation and its appearance streams. | 1012 | // Now we can safely mutate the annotation and its appearance streams. |
| 1012 | for (auto& stream: streams) { | 1013 | for (auto& stream: streams) { |
| 1013 | - auto dict = stream.getDict(); | ||
| 1014 | - auto omatrix = dict.getKey("/Matrix"); | 1014 | + Dictionary dict = stream.getDict(); |
| 1015 | + | ||
| 1015 | QPDFMatrix apcm; | 1016 | QPDFMatrix apcm; |
| 1016 | - if (omatrix.isArray()) { | ||
| 1017 | - QTC::TC("qpdf", "QPDFAcroFormDocumentHelper modify ap matrix"); | ||
| 1018 | - auto m1 = omatrix.getArrayAsMatrix(); | ||
| 1019 | - apcm = QPDFMatrix(m1); | 1017 | + Array omatrix = dict["/Matrix"]; |
| 1018 | + if (omatrix) { | ||
| 1019 | + apcm = QPDFMatrix(QPDFObjectHandle(omatrix).getArrayAsMatrix()); | ||
| 1020 | } | 1020 | } |
| 1021 | apcm.concat(cm); | 1021 | apcm.concat(cm); |
| 1022 | - auto new_matrix = QPDFObjectHandle::newFromMatrix(apcm); | ||
| 1023 | - if (omatrix.isArray() || (apcm != QPDFMatrix())) { | ||
| 1024 | - dict.replaceKey("/Matrix", new_matrix); | 1022 | + if (omatrix || apcm != QPDFMatrix()) { |
| 1023 | + dict.replaceKey("/Matrix", QPDFObjectHandle::newFromMatrix(apcm)); | ||
| 1025 | } | 1024 | } |
| 1026 | - auto resources = dict.getKey("/Resources"); | ||
| 1027 | - if ((!dr_map.empty()) && resources.isDictionary()) { | 1025 | + Dictionary resources = dict["/Resources"]; |
| 1026 | + if (!dr_map.empty() && resources) { | ||
| 1028 | adjustAppearanceStream(stream, dr_map); | 1027 | adjustAppearanceStream(stream, dr_map); |
| 1029 | } | 1028 | } |
| 1030 | } | 1029 | } |
| 1031 | - auto rect = cm.transformRectangle(annot.getKey("/Rect").getArrayAsRectangle()); | 1030 | + auto rect = cm.transformRectangle(annot["/Rect"].getArrayAsRectangle()); |
| 1032 | annot.replaceKey("/Rect", QPDFObjectHandle::newFromRectangle(rect)); | 1031 | annot.replaceKey("/Rect", QPDFObjectHandle::newFromRectangle(rect)); |
| 1033 | } | 1032 | } |
| 1034 | } | 1033 | } |
qpdf/qpdf.testcov
| @@ -487,7 +487,6 @@ QPDFFileSpecObjectHelper empty compat_name 0 | @@ -487,7 +487,6 @@ QPDFFileSpecObjectHelper empty compat_name 0 | ||
| 487 | QPDFFileSpecObjectHelper non-empty compat_name 0 | 487 | QPDFFileSpecObjectHelper non-empty compat_name 0 |
| 488 | QPDFAcroFormDocumentHelper copy annotation 3 | 488 | QPDFAcroFormDocumentHelper copy annotation 3 |
| 489 | QPDFAcroFormDocumentHelper field with parent 3 | 489 | QPDFAcroFormDocumentHelper field with parent 3 |
| 490 | -QPDFAcroFormDocumentHelper modify ap matrix 0 | ||
| 491 | QPDFJob pages keeping field from original 0 | 490 | QPDFJob pages keeping field from original 0 |
| 492 | QPDFObjectHandle merge reuse 0 | 491 | QPDFObjectHandle merge reuse 0 |
| 493 | QPDFObjectHandle merge generate 0 | 492 | QPDFObjectHandle merge generate 0 |