Commit bd67a468e42942e6e4166f0887b15b95ce2cf818

Authored by m-holger
1 parent f26327a3

Refactor `AcroForm` implementation to improve encapsulation and reusability.

- Move AcroForm-related methods (`analyze`, `traverseField`, `getOrCreateAcroForm`, etc.) from `QPDFAcroFormDocumentHelper` to the `AcroForm` class.
- Update method calls across files to reflect changes.
- Improve comments for methods to align with PDF specifications.
include/qpdf/QPDFAcroFormDocumentHelper.hh
@@ -225,21 +225,6 @@ class QPDFAcroFormDocumentHelper: public QPDFDocumentHelper @@ -225,21 +225,6 @@ class QPDFAcroFormDocumentHelper: public QPDFDocumentHelper
225 std::set<QPDFObjGen>* new_fields = nullptr); 225 std::set<QPDFObjGen>* new_fields = nullptr);
226 226
227 private: 227 private:
228 - void analyze();  
229 - bool traverseField(QPDFObjectHandle field, QPDFObjectHandle const& parent, int depth);  
230 - QPDFObjectHandle getOrCreateAcroForm();  
231 - void adjustInheritedFields(  
232 - QPDFObjectHandle obj,  
233 - bool override_da,  
234 - std::string const& from_default_da,  
235 - bool override_q,  
236 - int from_default_q);  
237 - void adjustDefaultAppearances(  
238 - QPDFObjectHandle obj,  
239 - std::map<std::string, std::map<std::string, std::string>> const& dr_map);  
240 - void adjustAppearanceStream(  
241 - QPDFObjectHandle stream, std::map<std::string, std::map<std::string, std::string>> dr_map);  
242 -  
243 class Members; 228 class Members;
244 229
245 std::shared_ptr<Members> m; 230 std::shared_ptr<Members> m;
libqpdf/QPDFAcroFormDocumentHelper.cc
@@ -31,9 +31,6 @@ QPDFAcroFormDocumentHelper::QPDFAcroFormDocumentHelper(QPDF&amp; qpdf) : @@ -31,9 +31,6 @@ QPDFAcroFormDocumentHelper::QPDFAcroFormDocumentHelper(QPDF&amp; qpdf) :
31 QPDFDocumentHelper(qpdf), 31 QPDFDocumentHelper(qpdf),
32 m(std::make_shared<Members>(qpdf)) 32 m(std::make_shared<Members>(qpdf))
33 { 33 {
34 - // We have to analyze up front. Otherwise, when we are adding annotations and fields, we are in  
35 - // a temporarily unstable configuration where some widget annotations are not reachable.  
36 - analyze();  
37 } 34 }
38 35
39 QPDFAcroFormDocumentHelper& 36 QPDFAcroFormDocumentHelper&
@@ -46,7 +43,7 @@ void @@ -46,7 +43,7 @@ void
46 QPDFAcroFormDocumentHelper::validate(bool repair) 43 QPDFAcroFormDocumentHelper::validate(bool repair)
47 { 44 {
48 invalidateCache(); 45 invalidateCache();
49 - analyze(); 46 + m->analyze();
50 } 47 }
51 48
52 void 49 void
@@ -65,7 +62,7 @@ QPDFAcroFormDocumentHelper::hasAcroForm() @@ -65,7 +62,7 @@ QPDFAcroFormDocumentHelper::hasAcroForm()
65 } 62 }
66 63
67 QPDFObjectHandle 64 QPDFObjectHandle
68 -QPDFAcroFormDocumentHelper::getOrCreateAcroForm() 65 +AcroForm::getOrCreateAcroForm()
69 { 66 {
70 auto acroform = qpdf.getRoot().getKey("/AcroForm"); 67 auto acroform = qpdf.getRoot().getKey("/AcroForm");
71 if (!acroform.isDictionary()) { 68 if (!acroform.isDictionary()) {
@@ -78,19 +75,19 @@ QPDFAcroFormDocumentHelper::getOrCreateAcroForm() @@ -78,19 +75,19 @@ QPDFAcroFormDocumentHelper::getOrCreateAcroForm()
78 void 75 void
79 QPDFAcroFormDocumentHelper::addFormField(QPDFFormFieldObjectHelper ff) 76 QPDFAcroFormDocumentHelper::addFormField(QPDFFormFieldObjectHelper ff)
80 { 77 {
81 - auto acroform = getOrCreateAcroForm(); 78 + auto acroform = m->getOrCreateAcroForm();
82 auto fields = acroform.getKey("/Fields"); 79 auto fields = acroform.getKey("/Fields");
83 if (!fields.isArray()) { 80 if (!fields.isArray()) {
84 fields = acroform.replaceKeyAndGetNew("/Fields", QPDFObjectHandle::newArray()); 81 fields = acroform.replaceKeyAndGetNew("/Fields", QPDFObjectHandle::newArray());
85 } 82 }
86 fields.appendItem(ff.getObjectHandle()); 83 fields.appendItem(ff.getObjectHandle());
87 - traverseField(ff.getObjectHandle(), {}, 0); 84 + m->traverseField(ff.getObjectHandle(), {}, 0);
88 } 85 }
89 86
90 void 87 void
91 QPDFAcroFormDocumentHelper::addAndRenameFormFields(std::vector<QPDFObjectHandle> fields) 88 QPDFAcroFormDocumentHelper::addAndRenameFormFields(std::vector<QPDFObjectHandle> fields)
92 { 89 {
93 - analyze(); 90 + m->analyze();
94 std::map<std::string, std::string> renames; 91 std::map<std::string, std::string> renames;
95 QPDFObjGen::set seen; 92 QPDFObjGen::set seen;
96 for (std::list<QPDFObjectHandle> queue{fields.begin(), fields.end()}; !queue.empty(); 93 for (std::list<QPDFObjectHandle> queue{fields.begin(), fields.end()}; !queue.empty();
@@ -182,13 +179,13 @@ void @@ -182,13 +179,13 @@ void
182 QPDFAcroFormDocumentHelper::setFormFieldName(QPDFFormFieldObjectHelper ff, std::string const& name) 179 QPDFAcroFormDocumentHelper::setFormFieldName(QPDFFormFieldObjectHelper ff, std::string const& name)
183 { 180 {
184 ff.setFieldAttribute("/T", name); 181 ff.setFieldAttribute("/T", name);
185 - traverseField(ff, ff["/Parent"], 0); 182 + m->traverseField(ff, ff["/Parent"], 0);
186 } 183 }
187 184
188 std::vector<QPDFFormFieldObjectHelper> 185 std::vector<QPDFFormFieldObjectHelper>
189 QPDFAcroFormDocumentHelper::getFormFields() 186 QPDFAcroFormDocumentHelper::getFormFields()
190 { 187 {
191 - analyze(); 188 + m->analyze();
192 std::vector<QPDFFormFieldObjectHelper> result; 189 std::vector<QPDFFormFieldObjectHelper> result;
193 for (auto const& [og, data]: m->field_to) { 190 for (auto const& [og, data]: m->field_to) {
194 if (!data.annotations.empty()) { 191 if (!data.annotations.empty()) {
@@ -201,7 +198,7 @@ QPDFAcroFormDocumentHelper::getFormFields() @@ -201,7 +198,7 @@ QPDFAcroFormDocumentHelper::getFormFields()
201 std::set<QPDFObjGen> 198 std::set<QPDFObjGen>
202 QPDFAcroFormDocumentHelper::getFieldsWithQualifiedName(std::string const& name) 199 QPDFAcroFormDocumentHelper::getFieldsWithQualifiedName(std::string const& name)
203 { 200 {
204 - analyze(); 201 + m->analyze();
205 // Keep from creating an empty entry 202 // Keep from creating an empty entry
206 auto iter = m->name_to_fields.find(name); 203 auto iter = m->name_to_fields.find(name);
207 if (iter != m->name_to_fields.end()) { 204 if (iter != m->name_to_fields.end()) {
@@ -213,7 +210,7 @@ QPDFAcroFormDocumentHelper::getFieldsWithQualifiedName(std::string const&amp; name) @@ -213,7 +210,7 @@ QPDFAcroFormDocumentHelper::getFieldsWithQualifiedName(std::string const&amp; name)
213 std::vector<QPDFAnnotationObjectHelper> 210 std::vector<QPDFAnnotationObjectHelper>
214 QPDFAcroFormDocumentHelper::getAnnotationsForField(QPDFFormFieldObjectHelper h) 211 QPDFAcroFormDocumentHelper::getAnnotationsForField(QPDFFormFieldObjectHelper h)
215 { 212 {
216 - analyze(); 213 + m->analyze();
217 std::vector<QPDFAnnotationObjectHelper> result; 214 std::vector<QPDFAnnotationObjectHelper> result;
218 QPDFObjGen og(h.getObjectHandle().getObjGen()); 215 QPDFObjGen og(h.getObjectHandle().getObjGen());
219 if (m->field_to.contains(og)) { 216 if (m->field_to.contains(og)) {
@@ -225,13 +222,19 @@ QPDFAcroFormDocumentHelper::getAnnotationsForField(QPDFFormFieldObjectHelper h) @@ -225,13 +222,19 @@ QPDFAcroFormDocumentHelper::getAnnotationsForField(QPDFFormFieldObjectHelper h)
225 std::vector<QPDFAnnotationObjectHelper> 222 std::vector<QPDFAnnotationObjectHelper>
226 QPDFAcroFormDocumentHelper::getWidgetAnnotationsForPage(QPDFPageObjectHelper h) 223 QPDFAcroFormDocumentHelper::getWidgetAnnotationsForPage(QPDFPageObjectHelper h)
227 { 224 {
  225 + return m->getWidgetAnnotationsForPage(h);
  226 +}
  227 +
  228 +std::vector<QPDFAnnotationObjectHelper>
  229 +AcroForm::getWidgetAnnotationsForPage(QPDFPageObjectHelper h)
  230 +{
228 return h.getAnnotations("/Widget"); 231 return h.getAnnotations("/Widget");
229 } 232 }
230 233
231 std::vector<QPDFFormFieldObjectHelper> 234 std::vector<QPDFFormFieldObjectHelper>
232 QPDFAcroFormDocumentHelper::getFormFieldsForPage(QPDFPageObjectHelper ph) 235 QPDFAcroFormDocumentHelper::getFormFieldsForPage(QPDFPageObjectHelper ph)
233 { 236 {
234 - analyze(); 237 + m->analyze();
235 QPDFObjGen::set todo; 238 QPDFObjGen::set todo;
236 std::vector<QPDFFormFieldObjectHelper> result; 239 std::vector<QPDFFormFieldObjectHelper> result;
237 for (auto& annot: getWidgetAnnotationsForPage(ph)) { 240 for (auto& annot: getWidgetAnnotationsForPage(ph)) {
@@ -250,7 +253,7 @@ QPDFAcroFormDocumentHelper::getFieldForAnnotation(QPDFAnnotationObjectHelper h) @@ -250,7 +253,7 @@ QPDFAcroFormDocumentHelper::getFieldForAnnotation(QPDFAnnotationObjectHelper h)
250 if (!oh.isDictionaryOfType("", "/Widget")) { 253 if (!oh.isDictionaryOfType("", "/Widget")) {
251 return Null::temp(); 254 return Null::temp();
252 } 255 }
253 - analyze(); 256 + m->analyze();
254 QPDFObjGen og(oh.getObjGen()); 257 QPDFObjGen og(oh.getObjGen());
255 if (m->annotation_to_field.contains(og)) { 258 if (m->annotation_to_field.contains(og)) {
256 return m->annotation_to_field[og]; 259 return m->annotation_to_field[og];
@@ -259,12 +262,12 @@ QPDFAcroFormDocumentHelper::getFieldForAnnotation(QPDFAnnotationObjectHelper h) @@ -259,12 +262,12 @@ QPDFAcroFormDocumentHelper::getFieldForAnnotation(QPDFAnnotationObjectHelper h)
259 } 262 }
260 263
261 void 264 void
262 -QPDFAcroFormDocumentHelper::analyze() 265 +AcroForm::analyze()
263 { 266 {
264 - if (m->cache_valid) { 267 + if (cache_valid) {
265 return; 268 return;
266 } 269 }
267 - m->cache_valid = true; 270 + cache_valid = true;
268 QPDFObjectHandle acroform = qpdf.getRoot().getKey("/AcroForm"); 271 QPDFObjectHandle acroform = qpdf.getRoot().getKey("/AcroForm");
269 if (!(acroform.isDictionary() && acroform.hasKey("/Fields"))) { 272 if (!(acroform.isDictionary() && acroform.hasKey("/Fields"))) {
270 return; 273 return;
@@ -287,11 +290,11 @@ QPDFAcroFormDocumentHelper::analyze() @@ -287,11 +290,11 @@ QPDFAcroFormDocumentHelper::analyze()
287 // a file that contains this kind of error will probably not 290 // a file that contains this kind of error will probably not
288 // actually work with most viewers. 291 // actually work with most viewers.
289 292
290 - for (auto const& ph: QPDFPageDocumentHelper(qpdf).getAllPages()) { 293 + for (QPDFPageObjectHelper ph: pages) {
291 for (auto const& iter: getWidgetAnnotationsForPage(ph)) { 294 for (auto const& iter: getWidgetAnnotationsForPage(ph)) {
292 QPDFObjectHandle annot(iter.getObjectHandle()); 295 QPDFObjectHandle annot(iter.getObjectHandle());
293 QPDFObjGen og(annot.getObjGen()); 296 QPDFObjGen og(annot.getObjGen());
294 - if (!m->annotation_to_field.contains(og)) { 297 + if (!annotation_to_field.contains(og)) {
295 // This is not supposed to happen, but it's easy enough for us to handle this case. 298 // This is not supposed to happen, but it's easy enough for us to handle this case.
296 // Treat the annotation as its own field. This could allow qpdf to sensibly handle a 299 // Treat the annotation as its own field. This could allow qpdf to sensibly handle a
297 // case such as a PDF creator adding a self-contained annotation (merged with the 300 // case such as a PDF creator adding a self-contained annotation (merged with the
@@ -300,16 +303,15 @@ QPDFAcroFormDocumentHelper::analyze() @@ -300,16 +303,15 @@ QPDFAcroFormDocumentHelper::analyze()
300 annot.warn( 303 annot.warn(
301 "this widget annotation is not reachable from /AcroForm in the document " 304 "this widget annotation is not reachable from /AcroForm in the document "
302 "catalog"); 305 "catalog");
303 - m->annotation_to_field[og] = QPDFFormFieldObjectHelper(annot);  
304 - m->field_to[og].annotations.emplace_back(annot); 306 + annotation_to_field[og] = QPDFFormFieldObjectHelper(annot);
  307 + field_to[og].annotations.emplace_back(annot);
305 } 308 }
306 } 309 }
307 } 310 }
308 } 311 }
309 312
310 bool 313 bool
311 -QPDFAcroFormDocumentHelper::traverseField(  
312 - QPDFObjectHandle field, QPDFObjectHandle const& parent, int depth) 314 +AcroForm::traverseField(QPDFObjectHandle field, QPDFObjectHandle const& parent, int depth)
313 { 315 {
314 if (depth > 100) { 316 if (depth > 100) {
315 // Arbitrarily cut off recursion at a fixed depth to avoid specially crafted files that 317 // Arbitrarily cut off recursion at a fixed depth to avoid specially crafted files that
@@ -333,8 +335,7 @@ QPDFAcroFormDocumentHelper::traverseField( @@ -333,8 +335,7 @@ QPDFAcroFormDocumentHelper::traverseField(
333 return false; 335 return false;
334 } 336 }
335 QPDFObjGen og(field.getObjGen()); 337 QPDFObjGen og(field.getObjGen());
336 - if (m->field_to.contains(og) || m->annotation_to_field.contains(og) ||  
337 - m->bad_fields.contains(og)) { 338 + if (field_to.contains(og) || annotation_to_field.contains(og) || bad_fields.contains(og)) {
338 field.warn("loop detected while traversing /AcroForm"); 339 field.warn("loop detected while traversing /AcroForm");
339 return false; 340 return false;
340 } 341 }
@@ -362,8 +363,8 @@ QPDFAcroFormDocumentHelper::traverseField( @@ -362,8 +363,8 @@ QPDFAcroFormDocumentHelper::traverseField(
362 363
363 if (is_annotation) { 364 if (is_annotation) {
364 QPDFObjectHandle our_field = (is_field ? field : parent); 365 QPDFObjectHandle our_field = (is_field ? field : parent);
365 - m->field_to[our_field.getObjGen()].annotations.emplace_back(field);  
366 - m->annotation_to_field[og] = QPDFFormFieldObjectHelper(our_field); 366 + field_to[our_field.getObjGen()].annotations.emplace_back(field);
  367 + annotation_to_field[og] = QPDFFormFieldObjectHelper(our_field);
367 } 368 }
368 369
369 if (is_field && depth != 0 && field["/Parent"] != parent) { 370 if (is_field && depth != 0 && field["/Parent"] != parent) {
@@ -386,22 +387,22 @@ QPDFAcroFormDocumentHelper::traverseField( @@ -386,22 +387,22 @@ QPDFAcroFormDocumentHelper::traverseField(
386 if (is_field && field.hasKey("/T")) { 387 if (is_field && field.hasKey("/T")) {
387 QPDFFormFieldObjectHelper foh(field); 388 QPDFFormFieldObjectHelper foh(field);
388 std::string name = foh.getFullyQualifiedName(); 389 std::string name = foh.getFullyQualifiedName();
389 - auto old = m->field_to.find(og);  
390 - if (old != m->field_to.end() && !old->second.name.empty()) { 390 + auto old = field_to.find(og);
  391 + if (old != field_to.end() && !old->second.name.empty()) {
391 // We might be updating after a name change, so remove any old information 392 // We might be updating after a name change, so remove any old information
392 - m->name_to_fields[old->second.name].erase(og); 393 + name_to_fields[old->second.name].erase(og);
393 } 394 }
394 - m->field_to[og].name = name;  
395 - m->name_to_fields[name].insert(og); 395 + field_to[og].name = name;
  396 + name_to_fields[name].insert(og);
396 } 397 }
397 398
398 for (auto const& kid: Kids) { 399 for (auto const& kid: Kids) {
399 - if (m->bad_fields.contains(kid)) { 400 + if (bad_fields.contains(kid)) {
400 continue; 401 continue;
401 } 402 }
402 403
403 if (!traverseField(kid, field, 1 + depth)) { 404 if (!traverseField(kid, field, 1 + depth)) {
404 - m->bad_fields.insert(kid); 405 + bad_fields.insert(kid);
405 } 406 }
406 } 407 }
407 return true; 408 return true;
@@ -485,7 +486,7 @@ QPDFAcroFormDocumentHelper::disableDigitalSignatures() @@ -485,7 +486,7 @@ QPDFAcroFormDocumentHelper::disableDigitalSignatures()
485 } 486 }
486 487
487 void 488 void
488 -QPDFAcroFormDocumentHelper::adjustInheritedFields( 489 +AcroForm::adjustInheritedFields(
489 QPDFObjectHandle obj, 490 QPDFObjectHandle obj,
490 bool override_da, 491 bool override_da,
491 std::string const& from_default_da, 492 std::string const& from_default_da,
@@ -592,7 +593,7 @@ ResourceReplacer::handleToken(QPDFTokenizer::Token const&amp; token) @@ -592,7 +593,7 @@ ResourceReplacer::handleToken(QPDFTokenizer::Token const&amp; token)
592 } 593 }
593 594
594 void 595 void
595 -QPDFAcroFormDocumentHelper::adjustDefaultAppearances( 596 +AcroForm::adjustDefaultAppearances(
596 QPDFObjectHandle obj, std::map<std::string, std::map<std::string, std::string>> const& dr_map) 597 QPDFObjectHandle obj, std::map<std::string, std::map<std::string, std::string>> const& dr_map)
597 { 598 {
598 // This method is called on a field that has been copied from another file but whose /DA still 599 // This method is called on a field that has been copied from another file but whose /DA still
@@ -650,7 +651,7 @@ QPDFAcroFormDocumentHelper::adjustDefaultAppearances( @@ -650,7 +651,7 @@ QPDFAcroFormDocumentHelper::adjustDefaultAppearances(
650 } 651 }
651 652
652 void 653 void
653 -QPDFAcroFormDocumentHelper::adjustAppearanceStream( 654 +AcroForm::adjustAppearanceStream(
654 QPDFObjectHandle stream, std::map<std::string, std::map<std::string, std::string>> dr_map) 655 QPDFObjectHandle stream, std::map<std::string, std::map<std::string, std::string>> dr_map)
655 { 656 {
656 // We don't have to modify appearance streams or their resource dictionaries for them to display 657 // We don't have to modify appearance streams or their resource dictionaries for them to display
@@ -807,7 +808,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations( @@ -807,7 +808,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations(
807 // Ensure that we have a /DR that is an indirect 808 // Ensure that we have a /DR that is an indirect
808 // dictionary object. 809 // dictionary object.
809 if (!acroform) { 810 if (!acroform) {
810 - acroform = getOrCreateAcroForm(); 811 + acroform = m->getOrCreateAcroForm();
811 } 812 }
812 dr = acroform["/DR"]; 813 dr = acroform["/DR"];
813 if (!dr) { 814 if (!dr) {
@@ -872,7 +873,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations( @@ -872,7 +873,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations(
872 } 873 }
873 ++i; 874 ++i;
874 } 875 }
875 - adjustInheritedFields( 876 + m->adjustInheritedFields(
876 obj, override_da, from_default_da, override_q, from_default_q); 877 obj, override_da, from_default_da, override_q, from_default_q);
877 if (foreign) { 878 if (foreign) {
878 // Lazily initialize our /DR and the conflict map. 879 // Lazily initialize our /DR and the conflict map.
@@ -888,7 +889,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations( @@ -888,7 +889,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations(
888 obj.replace("/DR", dr); 889 obj.replace("/DR", dr);
889 } 890 }
890 if (obj["/DA"].isString() && !dr_map.empty()) { 891 if (obj["/DA"].isString() && !dr_map.empty()) {
891 - adjustDefaultAppearances(obj, dr_map); 892 + m->adjustDefaultAppearances(obj, dr_map);
892 } 893 }
893 } 894 }
894 } 895 }
@@ -1035,7 +1036,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations( @@ -1035,7 +1036,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations(
1035 } 1036 }
1036 Dictionary resources = dict["/Resources"]; 1037 Dictionary resources = dict["/Resources"];
1037 if (!dr_map.empty() && resources) { 1038 if (!dr_map.empty() && resources) {
1038 - adjustAppearanceStream(stream, dr_map); 1039 + m->adjustAppearanceStream(stream, dr_map);
1039 } 1040 }
1040 } 1041 }
1041 auto rect = cm.transformRectangle(annot["/Rect"].getArrayAsRectangle()); 1042 auto rect = cm.transformRectangle(annot["/Rect"].getArrayAsRectangle());
libqpdf/qpdf/AcroForm.hh
@@ -24,6 +24,10 @@ namespace qpdf::impl @@ -24,6 +24,10 @@ namespace qpdf::impl
24 AcroForm(impl::Doc& doc) : 24 AcroForm(impl::Doc& doc) :
25 Common(doc) 25 Common(doc)
26 { 26 {
  27 + // We have to analyze up front. Otherwise, when we are adding annotations and fields, we
  28 + // are in a temporarily unstable configuration where some widget annotations are not
  29 + // reachable.
  30 + analyze();
27 } 31 }
28 32
29 struct FieldData 33 struct FieldData
@@ -32,6 +36,149 @@ namespace qpdf::impl @@ -32,6 +36,149 @@ namespace qpdf::impl
32 std::string name; 36 std::string name;
33 }; 37 };
34 38
  39 + /// Retrieves a list of widget annotations for the specified page.
  40 + ///
  41 + /// A widget annotation represents the visual part of a form field in a PDF.
  42 + /// This function filters annotations on the given page, returning only those
  43 + /// annotations whose subtype is "/Widget".
  44 + ///
  45 + /// @param page A `QPDFPageObjectHelper` representing the page from which to
  46 + /// extract widget annotations.
  47 + ///
  48 + /// @return A vector of `QPDFAnnotationObjectHelper` objects corresponding to
  49 + /// the widget annotations found on the specified page.
  50 + std::vector<QPDFAnnotationObjectHelper> getWidgetAnnotationsForPage(QPDFPageObjectHelper page);
  51 +
  52 + /// Analyzes the AcroForm structure in the PDF document and updates the internal
  53 + /// cache with the form fields and their corresponding widget annotations.
  54 + ///
  55 + /// The function performs the following steps:
  56 + /// - Checks if the cache is valid. If it is, the function exits early.
  57 + /// - Retrieves the `/AcroForm` dictionary from the PDF and checks if it contains
  58 + /// a `/Fields` key.
  59 + /// - If `/Fields` exist and is an array, iterates through the fields and traverses
  60 + /// them to map annotations bidirectionally to form fields.
  61 + /// - Logs a warning if the `/Fields` key is present but not an array, and initializes
  62 + /// it to an empty array.
  63 + /// - Ensures that all widget annotations are processed, including any annotations
  64 + /// that might not be reachable from the `/AcroForm`. Treats such annotations as
  65 + /// their own fields.
  66 + /// - Provides a workaround for PDF documents containing inconsistencies, such as
  67 + /// widget annotations on a page not being referenced in `/AcroForm`.
  68 + ///
  69 + /// This function allows precise navigation and manipulation of form fields and
  70 + /// their related annotations, facilitating advanced PDF document processing.
  71 + void analyze();
  72 +
  73 + /// Recursively traverses the structure of form fields and annotations in a PDF's /AcroForm.
  74 + ///
  75 + /// The method is designed to process form fields in a hierarchical /AcroForm structure.
  76 + /// It captures field and annotation data, resolves parent-child relationships, detects
  77 + /// loops, and avoids stack overflow from excessive recursion depth.
  78 + ///
  79 + /// @param field The current field or annotation to process.
  80 + /// @param parent The parent field object. If the current field is a top-level field, parent
  81 + /// will be a null object.
  82 + /// @param depth The current recursion depth to limit stack usage and avoid infinite loops.
  83 + ///
  84 + /// @return True if the field was processed successfully, false otherwise.
  85 + ///
  86 + /// - Recursion is limited to a depth of 100 to prevent stack overflow with maliciously
  87 + /// crafted files.
  88 + /// - The function skips non-indirect and invalid objects (e.g., non-dictionaries or objects
  89 + /// with invalid parent references).
  90 + /// - Detects and warns about loops in the /AcroForm hierarchy.
  91 + /// - Differentiates between terminal fields, annotations, and composite fields based on
  92 + /// dictionary keys.
  93 + /// - Tracks processed fields and annotations using internal maps to prevent reprocessing
  94 + /// and detect loops.
  95 + /// - Updates name-to-field mappings for terminal fields with a valid fully qualified name.
  96 + /// - Ensures the integrity of parent-child relationships within the field hierarchy.
  97 + /// - Any invalid child objects are logged and skipped during traversal.
  98 + bool traverseField(QPDFObjectHandle field, QPDFObjectHandle const& parent, int depth);
  99 +
  100 + /// Retrieves or creates the /AcroForm dictionary in the PDF document's root.
  101 + ///
  102 + /// - If the /AcroForm key exists in the document root and is a dictionary,
  103 + /// it is returned as is.
  104 + /// - If the /AcroForm key does not exist or is not a dictionary, a new
  105 + /// dictionary is created, stored as the /AcroForm entry in the document root,
  106 + /// and then returned.
  107 + ///
  108 + /// @return A QPDFObjectHandle representing the /AcroForm dictionary.
  109 + QPDFObjectHandle getOrCreateAcroForm();
  110 +
  111 + /// Adjusts inherited field properties for an AcroForm field object.
  112 + ///
  113 + /// This method ensures that the `/DA` (default appearance) and `/Q` (quadding) keys
  114 + /// of the specified field object are overridden if necessary, based on the provided
  115 + /// parameters. The overriding is performed only if the respective `override_da` or
  116 + /// `override_q` flags are set to true, and when the original object's values differ from
  117 + /// the provided defaults. No changes are made to fields that have explicit values for `/DA`
  118 + /// or `/Q`.
  119 + ///
  120 + /// The function is primarily used for adjusting inherited form field properties in cases
  121 + /// where the document structure or inherited values have changed (e.g., when working with
  122 + /// fields in a PDF document).
  123 + ///
  124 + /// @param obj The `QPDFObjectHandle` instance representing the form field object to be
  125 + /// adjusted.
  126 + /// @param override_da A boolean flag indicating whether to override the `/DA` key.
  127 + /// @param from_default_da The default appearance string to apply if overriding the `/DA`
  128 + /// key.
  129 + /// @param override_q A boolean flag indicating whether to override the `/Q` key.
  130 + /// @param from_default_q The default quadding value (alignment) to apply if overriding the
  131 + /// `/Q` key.
  132 + void adjustInheritedFields(
  133 + QPDFObjectHandle obj,
  134 + bool override_da,
  135 + std::string const& from_default_da,
  136 + bool override_q,
  137 + int from_default_q);
  138 +
  139 + /// Adjusts the default appearances (/DA) of an AcroForm field object.
  140 + ///
  141 + /// This method ensures that form fields copied from another PDF document
  142 + /// have their default appearances resource references updated to correctly
  143 + /// point to the appropriate resources in the current document's resource
  144 + /// dictionary (/DR). It resolves name conflicts between the dictionaries
  145 + /// of the source and destination documents by using a mapping provided in
  146 + /// `dr_map`.
  147 + ///
  148 + /// The method parses the /DA string, processes its resource references,
  149 + /// and regenerates the /DA with updated references.
  150 + ///
  151 + /// @param obj The AcroForm field object whose /DA is being adjusted.
  152 + /// @param dr_map A mapping between resource names in the source document's
  153 + /// resource dictionary and their corresponding names in the current
  154 + /// document's resource dictionary.
  155 + void adjustDefaultAppearances(
  156 + QPDFObjectHandle obj,
  157 + std::map<std::string, std::map<std::string, std::string>> const& dr_map);
  158 +
  159 + /// Modifies the appearance stream of an AcroForm field to ensure its resources
  160 + /// align with the resource dictionary and appearance settings. This method
  161 + /// ensures proper resource handling to avoid any conflicts when regenerating
  162 + /// the appearance stream.
  163 + ///
  164 + /// Adjustments include:
  165 + /// - Creating a private resource dictionary for the stream if not already present.
  166 + /// - Merging top-level resource keys into the stream's resource dictionary.
  167 + /// - Resolving naming conflicts between existing and remapped resource keys.
  168 + /// - Removing empty sub-dictionaries from the resource dictionary.
  169 + /// - Attaching a token filter to rewrite resource references in the stream content.
  170 + ///
  171 + /// If conflicts between keys are encountered or the stream cannot be parsed successfully,
  172 + /// appropriate warnings will be generated instead of halting execution.
  173 + ///
  174 + /// @param stream The QPDFObjectHandle representation of the PDF appearance stream to be
  175 + /// adjusted.
  176 + /// @param dr_map A mapping of resource types and their corresponding name remappings
  177 + /// used for resolving resource conflicts and regenerating appearances.
  178 + void adjustAppearanceStream(
  179 + QPDFObjectHandle stream,
  180 + std::map<std::string, std::map<std::string, std::string>> dr_map);
  181 +
35 bool cache_valid{false}; 182 bool cache_valid{false};
36 std::map<QPDFObjGen, FieldData> field_to; 183 std::map<QPDFObjGen, FieldData> field_to;
37 std::map<QPDFObjGen, QPDFFormFieldObjectHelper> annotation_to_field; 184 std::map<QPDFObjGen, QPDFFormFieldObjectHelper> annotation_to_field;
qpdf/test_driver.cc
@@ -3567,7 +3567,6 @@ test_101(QPDF&amp; pdf, char const* arg2) @@ -3567,7 +3567,6 @@ test_101(QPDF&amp; pdf, char const* arg2)
3567 std::cout << oh.unparseResolved() << '\n'; 3567 std::cout << oh.unparseResolved() << '\n';
3568 } 3568 }
3569 3569
3570 -  
3571 auto test_helper_throws = [&qpdf](auto helper_func) { 3570 auto test_helper_throws = [&qpdf](auto helper_func) {
3572 bool thrown = false; 3571 bool thrown = false;
3573 try { 3572 try {