Commit 8d36ca4d851ce62356a58f5455b23de5f84d8dbf

Authored by m-holger
1 parent 6379f3f1

Refactor `AcroForm`: centralize functionality within `AcroForm` class for improv…

…ed modularity and reusability.

- Move methods from `QPDFAcroFormDocumentHelper` to `AcroForm` (`validate`, `invalidateCache`, `addFormField`, and more) to reduce duplication.
- Update function calls and improve encapsulation by leveraging `AcroForm`.
- Enhance comments and align with PDF specifications for better clarity.
libqpdf/QPDFAcroFormDocumentHelper.cc
... ... @@ -9,15 +9,15 @@
9 9 #include <qpdf/QTC.hh>
10 10 #include <qpdf/QUtil.hh>
11 11 #include <qpdf/ResourceFinder.hh>
  12 +#include <qpdf/Util.hh>
12 13  
13 14 #include <deque>
14 15 #include <utility>
15 16  
16 17 using namespace qpdf;
  18 +using namespace qpdf::impl;
17 19 using namespace std::literals;
18 20  
19   -using AcroForm = impl::AcroForm;
20   -
21 21 class QPDFAcroFormDocumentHelper::Members: public AcroForm
22 22 {
23 23 public:
... ... @@ -42,23 +42,41 @@ QPDFAcroFormDocumentHelper::get(QPDF&amp; qpdf)
42 42 void
43 43 QPDFAcroFormDocumentHelper::validate(bool repair)
44 44 {
  45 + m->validate(repair);
  46 +}
  47 +
  48 +void
  49 +AcroForm::validate(bool repair)
  50 +{
45 51 invalidateCache();
46   - m->analyze();
  52 + analyze();
47 53 }
48 54  
49 55 void
50 56 QPDFAcroFormDocumentHelper::invalidateCache()
51 57 {
52   - m->cache_valid_ = false;
53   - m->fields_.clear();
54   - m->annotation_to_field_.clear();
55   - m->bad_fields_.clear();
56   - m->name_to_fields_.clear();
  58 + m->invalidateCache();
  59 +}
  60 +
  61 +void
  62 +AcroForm::invalidateCache()
  63 +{
  64 + cache_valid_ = false;
  65 + fields_.clear();
  66 + annotation_to_field_.clear();
  67 + bad_fields_.clear();
  68 + name_to_fields_.clear();
57 69 }
58 70  
59 71 bool
60 72 QPDFAcroFormDocumentHelper::hasAcroForm()
61 73 {
  74 + return m->hasAcroForm();
  75 +}
  76 +
  77 +bool
  78 +AcroForm::hasAcroForm()
  79 +{
62 80 return qpdf.getRoot().hasKey("/AcroForm");
63 81 }
64 82  
... ... @@ -76,19 +94,31 @@ AcroForm::getOrCreateAcroForm()
76 94 void
77 95 QPDFAcroFormDocumentHelper::addFormField(QPDFFormFieldObjectHelper ff)
78 96 {
79   - auto acroform = m->getOrCreateAcroForm();
  97 + m->addFormField(ff);
  98 +}
  99 +
  100 +void
  101 +AcroForm::addFormField(QPDFFormFieldObjectHelper ff)
  102 +{
  103 + auto acroform = getOrCreateAcroForm();
80 104 auto fields = acroform.getKey("/Fields");
81 105 if (!fields.isArray()) {
82 106 fields = acroform.replaceKeyAndGetNew("/Fields", QPDFObjectHandle::newArray());
83 107 }
84 108 fields.appendItem(ff.getObjectHandle());
85   - m->traverseField(ff.getObjectHandle(), {}, 0);
  109 + traverseField(ff.getObjectHandle(), {}, 0);
86 110 }
87 111  
88 112 void
89 113 QPDFAcroFormDocumentHelper::addAndRenameFormFields(std::vector<QPDFObjectHandle> fields)
90 114 {
91   - m->analyze();
  115 + m->addAndRenameFormFields(fields);
  116 +}
  117 +
  118 +void
  119 +AcroForm::addAndRenameFormFields(std::vector<QPDFObjectHandle> fields)
  120 +{
  121 + analyze();
92 122 std::map<std::string, std::string> renames;
93 123 QPDFObjGen::set seen;
94 124 for (std::list<QPDFObjectHandle> queue{fields.begin(), fields.end()}; !queue.empty();
... ... @@ -139,6 +169,12 @@ QPDFAcroFormDocumentHelper::addAndRenameFormFields(std::vector&lt;QPDFObjectHandle&gt;
139 169 void
140 170 QPDFAcroFormDocumentHelper::removeFormFields(std::set<QPDFObjGen> const& to_remove)
141 171 {
  172 + m->removeFormFields(to_remove);
  173 +}
  174 +
  175 +void
  176 +AcroForm::removeFormFields(std::set<QPDFObjGen> const& to_remove)
  177 +{
142 178 auto acroform = qpdf.getRoot().getKey("/AcroForm");
143 179 if (!acroform.isDictionary()) {
144 180 return;
... ... @@ -149,19 +185,19 @@ QPDFAcroFormDocumentHelper::removeFormFields(std::set&lt;QPDFObjGen&gt; const&amp; to_remo
149 185 }
150 186  
151 187 for (auto const& og: to_remove) {
152   - auto it = m->fields_.find(og);
153   - if (it != m->fields_.end()) {
  188 + auto it = fields_.find(og);
  189 + if (it != fields_.end()) {
154 190 for (auto aoh: it->second.annotations) {
155   - m->annotation_to_field_.erase(aoh.getObjectHandle().getObjGen());
  191 + annotation_to_field_.erase(aoh.getObjectHandle().getObjGen());
156 192 }
157 193 auto const& name = it->second.name;
158 194 if (!name.empty()) {
159   - m->name_to_fields_[name].erase(og);
160   - if (m->name_to_fields_[name].empty()) {
161   - m->name_to_fields_.erase(name);
  195 + name_to_fields_[name].erase(og);
  196 + if (name_to_fields_[name].empty()) {
  197 + name_to_fields_.erase(name);
162 198 }
163 199 }
164   - m->fields_.erase(og);
  200 + fields_.erase(og);
165 201 }
166 202 }
167 203  
... ... @@ -179,16 +215,28 @@ QPDFAcroFormDocumentHelper::removeFormFields(std::set&lt;QPDFObjGen&gt; const&amp; to_remo
179 215 void
180 216 QPDFAcroFormDocumentHelper::setFormFieldName(QPDFFormFieldObjectHelper ff, std::string const& name)
181 217 {
  218 + m->setFormFieldName(ff, name);
  219 +}
  220 +
  221 +void
  222 +AcroForm::setFormFieldName(QPDFFormFieldObjectHelper ff, std::string const& name)
  223 +{
182 224 ff.setFieldAttribute("/T", name);
183   - m->traverseField(ff, ff["/Parent"], 0);
  225 + traverseField(ff, ff["/Parent"], 0);
184 226 }
185 227  
186 228 std::vector<QPDFFormFieldObjectHelper>
187 229 QPDFAcroFormDocumentHelper::getFormFields()
188 230 {
189   - m->analyze();
  231 + return m->getFormFields();
  232 +}
  233 +
  234 +std::vector<QPDFFormFieldObjectHelper>
  235 +AcroForm::getFormFields()
  236 +{
  237 + analyze();
190 238 std::vector<QPDFFormFieldObjectHelper> result;
191   - for (auto const& [og, data]: m->fields_) {
  239 + for (auto const& [og, data]: fields_) {
192 240 if (!data.annotations.empty()) {
193 241 result.emplace_back(qpdf.getObject(og));
194 242 }
... ... @@ -199,10 +247,16 @@ QPDFAcroFormDocumentHelper::getFormFields()
199 247 std::set<QPDFObjGen>
200 248 QPDFAcroFormDocumentHelper::getFieldsWithQualifiedName(std::string const& name)
201 249 {
202   - m->analyze();
  250 + return m->getFieldsWithQualifiedName(name);
  251 +}
  252 +
  253 +std::set<QPDFObjGen>
  254 +AcroForm::getFieldsWithQualifiedName(std::string const& name)
  255 +{
  256 + analyze();
203 257 // Keep from creating an empty entry
204   - auto iter = m->name_to_fields_.find(name);
205   - if (iter != m->name_to_fields_.end()) {
  258 + auto iter = name_to_fields_.find(name);
  259 + if (iter != name_to_fields_.end()) {
206 260 return iter->second;
207 261 }
208 262 return {};
... ... @@ -211,11 +265,17 @@ QPDFAcroFormDocumentHelper::getFieldsWithQualifiedName(std::string const&amp; name)
211 265 std::vector<QPDFAnnotationObjectHelper>
212 266 QPDFAcroFormDocumentHelper::getAnnotationsForField(QPDFFormFieldObjectHelper h)
213 267 {
214   - m->analyze();
  268 + return m->getAnnotationsForField(h);
  269 +}
  270 +
  271 +std::vector<QPDFAnnotationObjectHelper>
  272 +AcroForm::getAnnotationsForField(QPDFFormFieldObjectHelper h)
  273 +{
  274 + analyze();
215 275 std::vector<QPDFAnnotationObjectHelper> result;
216 276 QPDFObjGen og(h.getObjectHandle().getObjGen());
217   - if (m->fields_.contains(og)) {
218   - result = m->fields_[og].annotations;
  277 + if (fields_.contains(og)) {
  278 + result = fields_[og].annotations;
219 279 }
220 280 return result;
221 281 }
... ... @@ -235,7 +295,13 @@ AcroForm::getWidgetAnnotationsForPage(QPDFPageObjectHelper h)
235 295 std::vector<QPDFFormFieldObjectHelper>
236 296 QPDFAcroFormDocumentHelper::getFormFieldsForPage(QPDFPageObjectHelper ph)
237 297 {
238   - m->analyze();
  298 + return m->getFormFieldsForPage(ph);
  299 +}
  300 +
  301 +std::vector<QPDFFormFieldObjectHelper>
  302 +AcroForm::getFormFieldsForPage(QPDFPageObjectHelper ph)
  303 +{
  304 + analyze();
239 305 QPDFObjGen::set todo;
240 306 std::vector<QPDFFormFieldObjectHelper> result;
241 307 for (auto& annot: getWidgetAnnotationsForPage(ph)) {
... ... @@ -250,14 +316,20 @@ QPDFAcroFormDocumentHelper::getFormFieldsForPage(QPDFPageObjectHelper ph)
250 316 QPDFFormFieldObjectHelper
251 317 QPDFAcroFormDocumentHelper::getFieldForAnnotation(QPDFAnnotationObjectHelper h)
252 318 {
  319 + return m->getFieldForAnnotation(h);
  320 +}
  321 +
  322 +QPDFFormFieldObjectHelper
  323 +AcroForm::getFieldForAnnotation(QPDFAnnotationObjectHelper h)
  324 +{
253 325 QPDFObjectHandle oh = h.getObjectHandle();
254 326 if (!oh.isDictionaryOfType("", "/Widget")) {
255 327 return Null::temp();
256 328 }
257   - m->analyze();
  329 + analyze();
258 330 QPDFObjGen og(oh.getObjGen());
259   - if (m->annotation_to_field_.contains(og)) {
260   - return m->annotation_to_field_[og];
  331 + if (annotation_to_field_.contains(og)) {
  332 + return annotation_to_field_[og];
261 333 }
262 334 return Null::temp();
263 335 }
... ... @@ -412,6 +484,12 @@ AcroForm::traverseField(QPDFObjectHandle field, QPDFObjectHandle const&amp; parent,
412 484 bool
413 485 QPDFAcroFormDocumentHelper::getNeedAppearances()
414 486 {
  487 + return m->getNeedAppearances();
  488 +}
  489 +
  490 +bool
  491 +AcroForm::getNeedAppearances()
  492 +{
415 493 bool result = false;
416 494 QPDFObjectHandle acroform = qpdf.getRoot().getKey("/AcroForm");
417 495 if (acroform.isDictionary() && acroform.getKey("/NeedAppearances").isBool()) {
... ... @@ -423,6 +501,12 @@ QPDFAcroFormDocumentHelper::getNeedAppearances()
423 501 void
424 502 QPDFAcroFormDocumentHelper::setNeedAppearances(bool val)
425 503 {
  504 + m->setNeedAppearances(val);
  505 +}
  506 +
  507 +void
  508 +AcroForm::setNeedAppearances(bool val)
  509 +{
426 510 QPDFObjectHandle acroform = qpdf.getRoot().getKey("/AcroForm");
427 511 if (!acroform.isDictionary()) {
428 512 qpdf.getRoot().warn(
... ... @@ -440,6 +524,12 @@ QPDFAcroFormDocumentHelper::setNeedAppearances(bool val)
440 524 void
441 525 QPDFAcroFormDocumentHelper::generateAppearancesIfNeeded()
442 526 {
  527 + m->generateAppearancesIfNeeded();
  528 +}
  529 +
  530 +void
  531 +AcroForm::generateAppearancesIfNeeded()
  532 +{
443 533 if (!getNeedAppearances()) {
444 534 return;
445 535 }
... ... @@ -466,6 +556,12 @@ QPDFAcroFormDocumentHelper::generateAppearancesIfNeeded()
466 556 void
467 557 QPDFAcroFormDocumentHelper::disableDigitalSignatures()
468 558 {
  559 + m->disableDigitalSignatures();
  560 +}
  561 +
  562 +void
  563 +AcroForm::disableDigitalSignatures()
  564 +{
469 565 qpdf.removeSecurityRestrictions();
470 566 std::set<QPDFObjGen> to_remove;
471 567 auto fields = getFormFields();
... ... @@ -744,7 +840,6 @@ QPDFAcroFormDocumentHelper::transformAnnotations(
744 840 QPDF* from_qpdf,
745 841 QPDFAcroFormDocumentHelper* from_afdh)
746 842 {
747   - Array old_annots = std::move(a_old_annots);
748 843 if (!from_qpdf) {
749 844 // Assume these are from the same QPDF.
750 845 from_qpdf = &qpdf;
... ... @@ -752,6 +847,23 @@ QPDFAcroFormDocumentHelper::transformAnnotations(
752 847 } else if (from_qpdf != &qpdf && !from_afdh) {
753 848 from_afdh = &QPDFAcroFormDocumentHelper::get(*from_qpdf);
754 849 }
  850 + m->transformAnnotations(
  851 + a_old_annots, new_annots, new_fields, old_fields, cm, from_qpdf, from_afdh->m.get());
  852 +}
  853 +
  854 +void
  855 +AcroForm::transformAnnotations(
  856 + QPDFObjectHandle a_old_annots,
  857 + std::vector<QPDFObjectHandle>& new_annots,
  858 + std::vector<QPDFObjectHandle>& new_fields,
  859 + std::set<QPDFObjGen>& old_fields,
  860 + QPDFMatrix const& cm,
  861 + QPDF* from_qpdf,
  862 + AcroForm* from_afdh)
  863 +{
  864 + qpdf_expect(from_qpdf);
  865 + qpdf_expect(from_afdh);
  866 + Array old_annots = std::move(a_old_annots);
755 867 const bool foreign = from_qpdf != &qpdf;
756 868  
757 869 // It's possible that we will transform annotations that don't include any form fields. This
... ... @@ -809,7 +921,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations(
809 921 // Ensure that we have a /DR that is an indirect
810 922 // dictionary object.
811 923 if (!acroform) {
812   - acroform = m->getOrCreateAcroForm();
  924 + acroform = getOrCreateAcroForm();
813 925 }
814 926 dr = acroform["/DR"];
815 927 if (!dr) {
... ... @@ -874,7 +986,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations(
874 986 }
875 987 ++i;
876 988 }
877   - m->adjustInheritedFields(
  989 + adjustInheritedFields(
878 990 obj, override_da, from_default_da, override_q, from_default_q);
879 991 if (foreign) {
880 992 // Lazily initialize our /DR and the conflict map.
... ... @@ -890,7 +1002,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations(
890 1002 obj.replace("/DR", dr);
891 1003 }
892 1004 if (obj["/DA"].isString() && !dr_map.empty()) {
893   - m->adjustDefaultAppearances(obj, dr_map);
  1005 + adjustDefaultAppearances(obj, dr_map);
894 1006 }
895 1007 }
896 1008 }
... ... @@ -1037,7 +1149,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations(
1037 1149 }
1038 1150 Dictionary resources = dict["/Resources"];
1039 1151 if (!dr_map.empty() && resources) {
1040   - m->adjustAppearanceStream(stream, dr_map);
  1152 + adjustAppearanceStream(stream, dr_map);
1041 1153 }
1042 1154 }
1043 1155 auto rect = cm.transformRectangle(annot["/Rect"].getArrayAsRectangle());
... ... @@ -1052,6 +1164,16 @@ QPDFAcroFormDocumentHelper::fixCopiedAnnotations(
1052 1164 QPDFAcroFormDocumentHelper& from_afdh,
1053 1165 std::set<QPDFObjGen>* added_fields)
1054 1166 {
  1167 + m->fixCopiedAnnotations(to_page, from_page, *from_afdh.m, added_fields);
  1168 +}
  1169 +
  1170 +void
  1171 +AcroForm::fixCopiedAnnotations(
  1172 + QPDFObjectHandle to_page,
  1173 + QPDFObjectHandle from_page,
  1174 + AcroForm& from_afdh,
  1175 + std::set<QPDFObjGen>* added_fields)
  1176 +{
1055 1177 auto old_annots = from_page.getKey("/Annots");
1056 1178 if (old_annots.empty() || !old_annots.isArray()) {
1057 1179 return;
... ... @@ -1066,7 +1188,7 @@ QPDFAcroFormDocumentHelper::fixCopiedAnnotations(
1066 1188 new_fields,
1067 1189 old_fields,
1068 1190 QPDFMatrix(),
1069   - &(from_afdh.getQPDF()),
  1191 + &(from_afdh.qpdf),
1070 1192 &from_afdh);
1071 1193  
1072 1194 to_page.replaceKey("/Annots", QPDFObjectHandle::newArray(new_annots));
... ...
libqpdf/qpdf/AcroForm.hh
... ... @@ -11,6 +11,19 @@ class QPDFAnnotationObjectHelper;
11 11  
12 12 namespace qpdf::impl
13 13 {
  14 + /// @class AcroForm
  15 + /// @brief Represents the interactive form dictionary and the interactive form tree within a
  16 + /// PDF document.
  17 + /// @par
  18 + /// The AcroForm class deals with interactive forms defined in section 12.7 of the PDF
  19 + /// specification. This defines a tree structure consisting of an interactive form or
  20 + /// `/AcroForm` dictionary (section 12.7.3) at its root. The attributes of the
  21 + /// `/AcroForm` dictionary are defined in table 224 of the PDF 2.0 / table 220 of the
  22 + /// PDF 1.7 specification.
  23 + /// @par
  24 + /// The nodes of the interactive forms tree are represented by the FormNode class.
  25 + ///
  26 + /// @since 12.3
14 27 class AcroForm: public Doc::Common
15 28 {
16 29 public:
... ... @@ -27,14 +40,61 @@ namespace qpdf::impl
27 40 // We have to analyze up front. Otherwise, when we are adding annotations and fields, we
28 41 // are in a temporarily unstable configuration where some widget annotations are not
29 42 // reachable.
30   - analyze();
  43 + validate();
31 44 }
32 45  
33   - struct FieldData
34   - {
35   - std::vector<QPDFAnnotationObjectHelper> annotations;
36   - std::string name;
37   - };
  46 + // Re-validate the AcroForm structure. This is useful if you have modified the structure of
  47 + // the AcroForm dictionary in a way that would invalidate the cache.
  48 + //
  49 + // If repair is true, the document will be repaired if possible if the validation encounters
  50 + // errors.
  51 + void validate(bool repair = true);
  52 +
  53 + // This class lazily creates an internal cache of the mapping among form fields,
  54 + // annotations, and pages. Methods within this class preserve the validity of this cache.
  55 + // However, if you modify pages' annotation dictionaries, the document's /AcroForm
  56 + // dictionary, or any form fields manually in a way that alters the association between
  57 + // forms, fields, annotations, and pages, it may cause this cache to become invalid. This
  58 + // method marks the cache invalid and forces it to be regenerated the next time it is
  59 + // needed.
  60 + void invalidateCache();
  61 +
  62 + bool hasAcroForm();
  63 +
  64 + // Add a form field, initializing the document's AcroForm dictionary if needed, updating the
  65 + // cache if necessary. Note that if you are adding fields that are copies of other fields,
  66 + // this method may result in multiple fields existing with the same qualified name, which
  67 + // can have unexpected side effects. In that case, you should use addAndRenameFormFields()
  68 + // instead.
  69 + void addFormField(QPDFFormFieldObjectHelper);
  70 +
  71 + // Add a collection of form fields making sure that their fully qualified names don't
  72 + // conflict with already present form fields. Fields within the collection of new fields
  73 + // that have the same name as each other will continue to do so.
  74 + void addAndRenameFormFields(std::vector<QPDFObjectHandle> fields);
  75 +
  76 + // Remove fields from the fields array
  77 + void removeFormFields(std::set<QPDFObjGen> const&);
  78 +
  79 + // Set the name of a field, updating internal records of field names. Name should be UTF-8
  80 + // encoded.
  81 + void setFormFieldName(QPDFFormFieldObjectHelper, std::string const& name);
  82 +
  83 + // Return a vector of all terminal fields in a document. Terminal fields are fields that
  84 + // have no children that are also fields. Terminal fields may still have children that are
  85 + // annotations. Intermediate nodes in the fields tree are not included in this list, but you
  86 + // can still reach them through the getParent method of the field object helper.
  87 + std::vector<QPDFFormFieldObjectHelper> getFormFields();
  88 +
  89 + // Return all the form fields that have the given fully-qualified name and also have an
  90 + // explicit "/T" attribute. For this information to be accurate, any changes to field names
  91 + // must be done through setFormFieldName() above.
  92 + std::set<QPDFObjGen> getFieldsWithQualifiedName(std::string const& name);
  93 +
  94 + // Return the annotations associated with a terminal field. Note that in the case of a field
  95 + // having a single annotation, the underlying object will typically be the same as the
  96 + // underlying object for the field.
  97 + std::vector<QPDFAnnotationObjectHelper> getAnnotationsForField(QPDFFormFieldObjectHelper);
38 98  
39 99 /// Retrieves a list of widget annotations for the specified page.
40 100 ///
... ... @@ -50,6 +110,86 @@ namespace qpdf::impl
50 110 std::vector<QPDFAnnotationObjectHelper>
51 111 getWidgetAnnotationsForPage(QPDFPageObjectHelper page);
52 112  
  113 + // Return top-level form fields for a page.
  114 + std::vector<QPDFFormFieldObjectHelper> getFormFieldsForPage(QPDFPageObjectHelper);
  115 +
  116 + // Return the terminal field that is associated with this annotation. If the annotation
  117 + // dictionary is merged with the field dictionary, the underlying object will be the same,
  118 + // but this is not always the case. Note that if you call this method with an annotation
  119 + // that is not a widget annotation, there will not be an associated field, and this method
  120 + // will return a helper associated with a null object (isNull() == true).
  121 + QPDFFormFieldObjectHelper getFieldForAnnotation(QPDFAnnotationObjectHelper);
  122 +
  123 + // Return the current value of /NeedAppearances. If /NeedAppearances is missing, return
  124 + // false as that is how PDF viewers are supposed to interpret it.
  125 + bool getNeedAppearances();
  126 +
  127 + // Indicate whether appearance streams must be regenerated. If you modify a field value, you
  128 + // should call setNeedAppearances(true) unless you also generate an appearance stream for
  129 + // the corresponding annotation at the same time. If you generate appearance streams for all
  130 + // fields, you can call setNeedAppearances(false). If you use
  131 + // QPDFFormFieldObjectHelper::setV, it will automatically call this method unless you tell
  132 + // it not to.
  133 + void setNeedAppearances(bool);
  134 +
  135 + // If /NeedAppearances is false, do nothing. Otherwise generate appearance streams for all
  136 + // widget annotations that need them. See comments in QPDFFormFieldObjectHelper.hh for
  137 + // generateAppearance for limitations. For checkbox and radio button fields, this code
  138 + // ensures that appearance state is consistent with the field's value and uses any
  139 + // pre-existing appearance streams.
  140 + void generateAppearancesIfNeeded();
  141 +
  142 + // Disable Digital Signature Fields. Remove all digital signature fields from the document,
  143 + // leaving any annotation showing the content of the field intact. This also calls
  144 + // QPDF::removeSecurityRestrictions.
  145 + void disableDigitalSignatures();
  146 +
  147 + // Note: this method works on all annotations, not just ones with associated fields. For
  148 + // each annotation in old_annots, apply the given transformation matrix to create a new
  149 + // annotation. New annotations are appended to new_annots. If the annotation is associated
  150 + // with a form field, a new form field is created that points to the new annotation and is
  151 + // appended to new_fields, and the old field is added to old_fields.
  152 + //
  153 + // old_annots may belong to a different QPDF object. In that case, you should pass in
  154 + // from_qpdf, and copyForeignObject will be called automatically. If this is the case, for
  155 + // efficiency, you may pass in a QPDFAcroFormDocumentHelper for the other file to avoid the
  156 + // expensive process of creating one for each call to transformAnnotations. New fields and
  157 + // annotations are not added to the document or pages. You have to do that yourself after
  158 + // calling transformAnnotations. If this operation will leave orphaned fields behind, such
  159 + // as if you are replacing the old annotations with the new ones on the same page and the
  160 + // fields and annotations are not shared, you will also need to remove the old fields to
  161 + // prevent them from hanging around unreferenced.
  162 + void transformAnnotations(
  163 + QPDFObjectHandle old_annots,
  164 + std::vector<QPDFObjectHandle>& new_annots,
  165 + std::vector<QPDFObjectHandle>& new_fields,
  166 + std::set<QPDFObjGen>& old_fields,
  167 + QPDFMatrix const& cm,
  168 + QPDF* from_qpdf,
  169 + AcroForm* from_afdh);
  170 +
  171 + // Copy form fields and annotations from one page to another, allowing the from page to be
  172 + // in a different QPDF or in the same QPDF. This would typically be called after calling
  173 + // addPage to add field/annotation awareness. When just copying the page by itself,
  174 + // annotations end up being shared, and fields end up being omitted because there is no
  175 + // reference to the field from the page. This method ensures that each separate copy of a
  176 + // page has private annotations and that fields and annotations are properly updated to
  177 + // resolve conflicts that may occur from common resource and field names across documents.
  178 + // It is basically a wrapper around transformAnnotations that handles updating the receiving
  179 + // page. If new_fields is non-null, any newly created fields are added to it.
  180 + void fixCopiedAnnotations(
  181 + QPDFObjectHandle to_page,
  182 + QPDFObjectHandle from_page,
  183 + AcroForm& from_afdh,
  184 + std::set<QPDFObjGen>* new_fields = nullptr);
  185 +
  186 + private:
  187 + struct FieldData
  188 + {
  189 + std::vector<QPDFAnnotationObjectHelper> annotations;
  190 + std::string name;
  191 + };
  192 +
53 193 /// Analyzes the AcroForm structure in the PDF document and updates the internal
54 194 /// cache with the form fields and their corresponding widget annotations.
55 195 ///
... ... @@ -374,7 +514,7 @@ namespace qpdf::impl
374 514 std::string mapping_name() const;
375 515  
376 516 /// @brief Retrieves the field value (`/V` attribute) of a specified field, accounting for
377   - /// inheritance through thehierarchy of ancestor nodes in the form field tree.
  517 + /// inheritance through the hierarchy of ancestor nodes in the form field tree.
378 518 ///
379 519 /// This function attempts to retrieve the `/V` attribute. If the `inherit`
380 520 /// parameter is set to `true` and the `/V` is not found at the current level, the
... ... @@ -549,11 +689,11 @@ namespace qpdf::impl
549 689 /// name.
550 690 ///
551 691 /// The method accesses the AcroForm dictionary within the root object of the PDF document.
552   - /// If the the AcroForm dictionary contains the given field name, it retrieves the
  692 + /// If the AcroForm dictionary contains the given field name, it retrieves the
553 693 /// corresponding entry. Otherwise, it returns a default-constructed object handle.
554 694 ///
555 695 /// @param name The name of the form field to retrieve.
556   - /// @return A object handle corresponding to the specified name within the AcroForm
  696 + /// @return An object handle corresponding to the specified name within the AcroForm
557 697 /// dictionary.
558 698 QPDFObjectHandle const&
559 699 from_AcroForm(std::string const& name) const
... ...