Commit bd67a468e42942e6e4166f0887b15b95ce2cf818
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.
Showing
4 changed files
with
189 additions
and
57 deletions
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& qpdf) : | @@ -31,9 +31,6 @@ QPDFAcroFormDocumentHelper::QPDFAcroFormDocumentHelper(QPDF& 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& name) | @@ -213,7 +210,7 @@ QPDFAcroFormDocumentHelper::getFieldsWithQualifiedName(std::string const& 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& token) | @@ -592,7 +593,7 @@ ResourceReplacer::handleToken(QPDFTokenizer::Token const& 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& pdf, char const* arg2) | @@ -3567,7 +3567,6 @@ test_101(QPDF& 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 { |