Commit c8f1e6bf14c4504fc52c9af8b62f31492f43127d

Authored by m-holger
Committed by GitHub
2 parents 8f455ffa d35c34d8

Merge pull request #1615 from m-holger/ffoh

Refactor QPDFAcroFormDocumentHelper
include/qpdf/QPDFAcroFormDocumentHelper.hh
... ... @@ -225,21 +225,7 @@ class QPDFAcroFormDocumentHelper: public QPDFDocumentHelper
225 225 std::set<QPDFObjGen>* new_fields = nullptr);
226 226  
227 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   -
  228 + friend class QPDF::Doc;
243 229 class Members;
244 230  
245 231 std::shared_ptr<Members> m;
... ...
libqpdf/QPDF.cc
... ... @@ -11,6 +11,7 @@
11 11 #include <sstream>
12 12 #include <vector>
13 13  
  14 +#include <qpdf/AcroForm.hh>
14 15 #include <qpdf/FileInputSource.hh>
15 16 #include <qpdf/InputSource_private.hh>
16 17 #include <qpdf/OffsetInputSource.hh>
... ... @@ -147,6 +148,21 @@ QPDF::QPDF() :
147 148 m->unique_id = unique_id.fetch_add(1ULL);
148 149 }
149 150  
  151 +/// @brief Initializes the AcroForm functionality for the document.
  152 +/// @par
  153 +/// This method creates a unique instance of QPDFAcroFormDocumentHelper and associates it
  154 +/// with the document. It also updates the `acroform_` pointer to reference the AcroForm
  155 +/// instance managed by the helper.
  156 +///
  157 +/// The method has been separated out from `acroform` to avoid it being inlined
  158 +/// unnecessarily.
  159 +void
  160 +QPDF::Doc::init_acroform()
  161 +{
  162 + acroform_dh_ = std::make_unique<QPDFAcroFormDocumentHelper>(qpdf);
  163 + acroform_ = acroform_dh_->m.get();
  164 +}
  165 +
150 166 // Provide access to disconnect(). Disconnect will in due course be merged into the current ObjCache
151 167 // (future Objects::Entry) to centralize all QPDF access to QPDFObject.
152 168 class Disconnect: BaseHandle
... ...
libqpdf/QPDFAcroFormDocumentHelper.cc
1 1 #include <qpdf/QPDFAcroFormDocumentHelper.hh>
2 2  
  3 +#include <qpdf/AcroForm.hh>
  4 +
3 5 #include <qpdf/Pl_Buffer.hh>
4 6 #include <qpdf/QPDFObjectHandle_private.hh>
5 7 #include <qpdf/QPDFPageDocumentHelper.hh>
... ... @@ -7,51 +9,38 @@
7 9 #include <qpdf/QTC.hh>
8 10 #include <qpdf/QUtil.hh>
9 11 #include <qpdf/ResourceFinder.hh>
  12 +#include <qpdf/Util.hh>
10 13  
11 14 #include <deque>
12 15 #include <utility>
13 16  
14 17 using namespace qpdf;
  18 +using namespace qpdf::impl;
15 19 using namespace std::literals;
16 20  
17   -class QPDFAcroFormDocumentHelper::Members
18   -{
19   - public:
20   - Members() = default;
21   - Members(Members const&) = delete;
22   - ~Members() = default;
23   -
24   - struct FieldData
25   - {
26   - std::vector<QPDFAnnotationObjectHelper> annotations;
27   - std::string name;
28   - };
29   -
30   - bool cache_valid{false};
31   - std::map<QPDFObjGen, FieldData> field_to;
32   - std::map<QPDFObjGen, QPDFFormFieldObjectHelper> annotation_to_field;
33   - std::map<std::string, std::set<QPDFObjGen>> name_to_fields;
34   - std::set<QPDFObjGen> bad_fields;
35   -};
  21 +using AcroForm = impl::AcroForm;
36 22  
37 23 QPDFAcroFormDocumentHelper::QPDFAcroFormDocumentHelper(QPDF& qpdf) :
38 24 QPDFDocumentHelper(qpdf),
39   - m(std::make_shared<Members>())
  25 + m(std::make_shared<Members>(qpdf))
40 26 {
41   - // We have to analyze up front. Otherwise, when we are adding annotations and fields, we are in
42   - // a temporarily unstable configuration where some widget annotations are not reachable.
43   - analyze();
44 27 }
45 28  
46 29 QPDFAcroFormDocumentHelper&
47 30 QPDFAcroFormDocumentHelper::get(QPDF& qpdf)
48 31 {
49   - return qpdf.doc().acroform();
  32 + return qpdf.doc().acroform_dh();
50 33 }
51 34  
52 35 void
53 36 QPDFAcroFormDocumentHelper::validate(bool repair)
54 37 {
  38 + m->validate(repair);
  39 +}
  40 +
  41 +void
  42 +AcroForm::validate(bool repair)
  43 +{
55 44 invalidateCache();
56 45 analyze();
57 46 }
... ... @@ -59,19 +48,33 @@ QPDFAcroFormDocumentHelper::validate(bool repair)
59 48 void
60 49 QPDFAcroFormDocumentHelper::invalidateCache()
61 50 {
62   - m->cache_valid = false;
63   - m->field_to.clear();
64   - m->annotation_to_field.clear();
  51 + m->invalidateCache();
  52 +}
  53 +
  54 +void
  55 +AcroForm::invalidateCache()
  56 +{
  57 + cache_valid_ = false;
  58 + fields_.clear();
  59 + annotation_to_field_.clear();
  60 + bad_fields_.clear();
  61 + name_to_fields_.clear();
65 62 }
66 63  
67 64 bool
68 65 QPDFAcroFormDocumentHelper::hasAcroForm()
69 66 {
  67 + return m->hasAcroForm();
  68 +}
  69 +
  70 +bool
  71 +AcroForm::hasAcroForm()
  72 +{
70 73 return qpdf.getRoot().hasKey("/AcroForm");
71 74 }
72 75  
73 76 QPDFObjectHandle
74   -QPDFAcroFormDocumentHelper::getOrCreateAcroForm()
  77 +AcroForm::getOrCreateAcroForm()
75 78 {
76 79 auto acroform = qpdf.getRoot().getKey("/AcroForm");
77 80 if (!acroform.isDictionary()) {
... ... @@ -84,6 +87,12 @@ QPDFAcroFormDocumentHelper::getOrCreateAcroForm()
84 87 void
85 88 QPDFAcroFormDocumentHelper::addFormField(QPDFFormFieldObjectHelper ff)
86 89 {
  90 + m->addFormField(ff);
  91 +}
  92 +
  93 +void
  94 +AcroForm::addFormField(QPDFFormFieldObjectHelper ff)
  95 +{
87 96 auto acroform = getOrCreateAcroForm();
88 97 auto fields = acroform.getKey("/Fields");
89 98 if (!fields.isArray()) {
... ... @@ -96,6 +105,12 @@ QPDFAcroFormDocumentHelper::addFormField(QPDFFormFieldObjectHelper ff)
96 105 void
97 106 QPDFAcroFormDocumentHelper::addAndRenameFormFields(std::vector<QPDFObjectHandle> fields)
98 107 {
  108 + m->addAndRenameFormFields(fields);
  109 +}
  110 +
  111 +void
  112 +AcroForm::addAndRenameFormFields(std::vector<QPDFObjectHandle> fields)
  113 +{
99 114 analyze();
100 115 std::map<std::string, std::string> renames;
101 116 QPDFObjGen::set seen;
... ... @@ -147,6 +162,12 @@ QPDFAcroFormDocumentHelper::addAndRenameFormFields(std::vector&lt;QPDFObjectHandle&gt;
147 162 void
148 163 QPDFAcroFormDocumentHelper::removeFormFields(std::set<QPDFObjGen> const& to_remove)
149 164 {
  165 + m->removeFormFields(to_remove);
  166 +}
  167 +
  168 +void
  169 +AcroForm::removeFormFields(std::set<QPDFObjGen> const& to_remove)
  170 +{
150 171 auto acroform = qpdf.getRoot().getKey("/AcroForm");
151 172 if (!acroform.isDictionary()) {
152 173 return;
... ... @@ -157,19 +178,19 @@ QPDFAcroFormDocumentHelper::removeFormFields(std::set&lt;QPDFObjGen&gt; const&amp; to_remo
157 178 }
158 179  
159 180 for (auto const& og: to_remove) {
160   - auto it = m->field_to.find(og);
161   - if (it != m->field_to.end()) {
  181 + auto it = fields_.find(og);
  182 + if (it != fields_.end()) {
162 183 for (auto aoh: it->second.annotations) {
163   - m->annotation_to_field.erase(aoh.getObjectHandle().getObjGen());
  184 + annotation_to_field_.erase(aoh.getObjectHandle().getObjGen());
164 185 }
165 186 auto const& name = it->second.name;
166 187 if (!name.empty()) {
167   - m->name_to_fields[name].erase(og);
168   - if (m->name_to_fields[name].empty()) {
169   - m->name_to_fields.erase(name);
  188 + name_to_fields_[name].erase(og);
  189 + if (name_to_fields_[name].empty()) {
  190 + name_to_fields_.erase(name);
170 191 }
171 192 }
172   - m->field_to.erase(og);
  193 + fields_.erase(og);
173 194 }
174 195 }
175 196  
... ... @@ -187,6 +208,12 @@ QPDFAcroFormDocumentHelper::removeFormFields(std::set&lt;QPDFObjGen&gt; const&amp; to_remo
187 208 void
188 209 QPDFAcroFormDocumentHelper::setFormFieldName(QPDFFormFieldObjectHelper ff, std::string const& name)
189 210 {
  211 + m->setFormFieldName(ff, name);
  212 +}
  213 +
  214 +void
  215 +AcroForm::setFormFieldName(QPDFFormFieldObjectHelper ff, std::string const& name)
  216 +{
190 217 ff.setFieldAttribute("/T", name);
191 218 traverseField(ff, ff["/Parent"], 0);
192 219 }
... ... @@ -194,9 +221,15 @@ QPDFAcroFormDocumentHelper::setFormFieldName(QPDFFormFieldObjectHelper ff, std::
194 221 std::vector<QPDFFormFieldObjectHelper>
195 222 QPDFAcroFormDocumentHelper::getFormFields()
196 223 {
  224 + return m->getFormFields();
  225 +}
  226 +
  227 +std::vector<QPDFFormFieldObjectHelper>
  228 +AcroForm::getFormFields()
  229 +{
197 230 analyze();
198 231 std::vector<QPDFFormFieldObjectHelper> result;
199   - for (auto const& [og, data]: m->field_to) {
  232 + for (auto const& [og, data]: fields_) {
200 233 if (!data.annotations.empty()) {
201 234 result.emplace_back(qpdf.getObject(og));
202 235 }
... ... @@ -207,10 +240,16 @@ QPDFAcroFormDocumentHelper::getFormFields()
207 240 std::set<QPDFObjGen>
208 241 QPDFAcroFormDocumentHelper::getFieldsWithQualifiedName(std::string const& name)
209 242 {
  243 + return m->getFieldsWithQualifiedName(name);
  244 +}
  245 +
  246 +std::set<QPDFObjGen>
  247 +AcroForm::getFieldsWithQualifiedName(std::string const& name)
  248 +{
210 249 analyze();
211 250 // Keep from creating an empty entry
212   - auto iter = m->name_to_fields.find(name);
213   - if (iter != m->name_to_fields.end()) {
  251 + auto iter = name_to_fields_.find(name);
  252 + if (iter != name_to_fields_.end()) {
214 253 return iter->second;
215 254 }
216 255 return {};
... ... @@ -219,11 +258,17 @@ QPDFAcroFormDocumentHelper::getFieldsWithQualifiedName(std::string const&amp; name)
219 258 std::vector<QPDFAnnotationObjectHelper>
220 259 QPDFAcroFormDocumentHelper::getAnnotationsForField(QPDFFormFieldObjectHelper h)
221 260 {
  261 + return m->getAnnotationsForField(h);
  262 +}
  263 +
  264 +std::vector<QPDFAnnotationObjectHelper>
  265 +AcroForm::getAnnotationsForField(QPDFFormFieldObjectHelper h)
  266 +{
222 267 analyze();
223 268 std::vector<QPDFAnnotationObjectHelper> result;
224 269 QPDFObjGen og(h.getObjectHandle().getObjGen());
225   - if (m->field_to.contains(og)) {
226   - result = m->field_to[og].annotations;
  270 + if (fields_.contains(og)) {
  271 + result = fields_[og].annotations;
227 272 }
228 273 return result;
229 274 }
... ... @@ -231,12 +276,24 @@ QPDFAcroFormDocumentHelper::getAnnotationsForField(QPDFFormFieldObjectHelper h)
231 276 std::vector<QPDFAnnotationObjectHelper>
232 277 QPDFAcroFormDocumentHelper::getWidgetAnnotationsForPage(QPDFPageObjectHelper h)
233 278 {
  279 + return m->getWidgetAnnotationsForPage(h);
  280 +}
  281 +
  282 +std::vector<QPDFAnnotationObjectHelper>
  283 +AcroForm::getWidgetAnnotationsForPage(QPDFPageObjectHelper h)
  284 +{
234 285 return h.getAnnotations("/Widget");
235 286 }
236 287  
237 288 std::vector<QPDFFormFieldObjectHelper>
238 289 QPDFAcroFormDocumentHelper::getFormFieldsForPage(QPDFPageObjectHelper ph)
239 290 {
  291 + return m->getFormFieldsForPage(ph);
  292 +}
  293 +
  294 +std::vector<QPDFFormFieldObjectHelper>
  295 +AcroForm::getFormFieldsForPage(QPDFPageObjectHelper ph)
  296 +{
240 297 analyze();
241 298 QPDFObjGen::set todo;
242 299 std::vector<QPDFFormFieldObjectHelper> result;
... ... @@ -252,25 +309,31 @@ QPDFAcroFormDocumentHelper::getFormFieldsForPage(QPDFPageObjectHelper ph)
252 309 QPDFFormFieldObjectHelper
253 310 QPDFAcroFormDocumentHelper::getFieldForAnnotation(QPDFAnnotationObjectHelper h)
254 311 {
  312 + return m->getFieldForAnnotation(h);
  313 +}
  314 +
  315 +QPDFFormFieldObjectHelper
  316 +AcroForm::getFieldForAnnotation(QPDFAnnotationObjectHelper h)
  317 +{
255 318 QPDFObjectHandle oh = h.getObjectHandle();
256 319 if (!oh.isDictionaryOfType("", "/Widget")) {
257 320 return Null::temp();
258 321 }
259 322 analyze();
260 323 QPDFObjGen og(oh.getObjGen());
261   - if (m->annotation_to_field.contains(og)) {
262   - return m->annotation_to_field[og];
  324 + if (annotation_to_field_.contains(og)) {
  325 + return annotation_to_field_[og];
263 326 }
264 327 return Null::temp();
265 328 }
266 329  
267 330 void
268   -QPDFAcroFormDocumentHelper::analyze()
  331 +AcroForm::analyze()
269 332 {
270   - if (m->cache_valid) {
  333 + if (cache_valid_) {
271 334 return;
272 335 }
273   - m->cache_valid = true;
  336 + cache_valid_ = true;
274 337 QPDFObjectHandle acroform = qpdf.getRoot().getKey("/AcroForm");
275 338 if (!(acroform.isDictionary() && acroform.hasKey("/Fields"))) {
276 339 return;
... ... @@ -293,11 +356,11 @@ QPDFAcroFormDocumentHelper::analyze()
293 356 // a file that contains this kind of error will probably not
294 357 // actually work with most viewers.
295 358  
296   - for (auto const& ph: QPDFPageDocumentHelper(qpdf).getAllPages()) {
  359 + for (QPDFPageObjectHelper ph: pages) {
297 360 for (auto const& iter: getWidgetAnnotationsForPage(ph)) {
298 361 QPDFObjectHandle annot(iter.getObjectHandle());
299 362 QPDFObjGen og(annot.getObjGen());
300   - if (!m->annotation_to_field.contains(og)) {
  363 + if (!annotation_to_field_.contains(og)) {
301 364 // This is not supposed to happen, but it's easy enough for us to handle this case.
302 365 // Treat the annotation as its own field. This could allow qpdf to sensibly handle a
303 366 // case such as a PDF creator adding a self-contained annotation (merged with the
... ... @@ -306,16 +369,15 @@ QPDFAcroFormDocumentHelper::analyze()
306 369 annot.warn(
307 370 "this widget annotation is not reachable from /AcroForm in the document "
308 371 "catalog");
309   - m->annotation_to_field[og] = QPDFFormFieldObjectHelper(annot);
310   - m->field_to[og].annotations.emplace_back(annot);
  372 + annotation_to_field_[og] = QPDFFormFieldObjectHelper(annot);
  373 + fields_[og].annotations.emplace_back(annot);
311 374 }
312 375 }
313 376 }
314 377 }
315 378  
316 379 bool
317   -QPDFAcroFormDocumentHelper::traverseField(
318   - QPDFObjectHandle field, QPDFObjectHandle const& parent, int depth)
  380 +AcroForm::traverseField(QPDFObjectHandle field, QPDFObjectHandle const& parent, int depth)
319 381 {
320 382 if (depth > 100) {
321 383 // Arbitrarily cut off recursion at a fixed depth to avoid specially crafted files that
... ... @@ -339,8 +401,7 @@ QPDFAcroFormDocumentHelper::traverseField(
339 401 return false;
340 402 }
341 403 QPDFObjGen og(field.getObjGen());
342   - if (m->field_to.contains(og) || m->annotation_to_field.contains(og) ||
343   - m->bad_fields.contains(og)) {
  404 + if (fields_.contains(og) || annotation_to_field_.contains(og) || bad_fields_.contains(og)) {
344 405 field.warn("loop detected while traversing /AcroForm");
345 406 return false;
346 407 }
... ... @@ -368,8 +429,8 @@ QPDFAcroFormDocumentHelper::traverseField(
368 429  
369 430 if (is_annotation) {
370 431 QPDFObjectHandle our_field = (is_field ? field : parent);
371   - m->field_to[our_field.getObjGen()].annotations.emplace_back(field);
372   - m->annotation_to_field[og] = QPDFFormFieldObjectHelper(our_field);
  432 + fields_[our_field.getObjGen()].annotations.emplace_back(field);
  433 + annotation_to_field_[og] = QPDFFormFieldObjectHelper(our_field);
373 434 }
374 435  
375 436 if (is_field && depth != 0 && field["/Parent"] != parent) {
... ... @@ -392,22 +453,22 @@ QPDFAcroFormDocumentHelper::traverseField(
392 453 if (is_field && field.hasKey("/T")) {
393 454 QPDFFormFieldObjectHelper foh(field);
394 455 std::string name = foh.getFullyQualifiedName();
395   - auto old = m->field_to.find(og);
396   - if (old != m->field_to.end() && !old->second.name.empty()) {
  456 + auto old = fields_.find(og);
  457 + if (old != fields_.end() && !old->second.name.empty()) {
397 458 // We might be updating after a name change, so remove any old information
398   - m->name_to_fields[old->second.name].erase(og);
  459 + name_to_fields_[old->second.name].erase(og);
399 460 }
400   - m->field_to[og].name = name;
401   - m->name_to_fields[name].insert(og);
  461 + fields_[og].name = name;
  462 + name_to_fields_[name].insert(og);
402 463 }
403 464  
404 465 for (auto const& kid: Kids) {
405   - if (m->bad_fields.contains(kid)) {
  466 + if (bad_fields_.contains(kid)) {
406 467 continue;
407 468 }
408 469  
409 470 if (!traverseField(kid, field, 1 + depth)) {
410   - m->bad_fields.insert(kid);
  471 + bad_fields_.insert(kid);
411 472 }
412 473 }
413 474 return true;
... ... @@ -416,6 +477,12 @@ QPDFAcroFormDocumentHelper::traverseField(
416 477 bool
417 478 QPDFAcroFormDocumentHelper::getNeedAppearances()
418 479 {
  480 + return m->getNeedAppearances();
  481 +}
  482 +
  483 +bool
  484 +AcroForm::getNeedAppearances()
  485 +{
419 486 bool result = false;
420 487 QPDFObjectHandle acroform = qpdf.getRoot().getKey("/AcroForm");
421 488 if (acroform.isDictionary() && acroform.getKey("/NeedAppearances").isBool()) {
... ... @@ -427,6 +494,12 @@ QPDFAcroFormDocumentHelper::getNeedAppearances()
427 494 void
428 495 QPDFAcroFormDocumentHelper::setNeedAppearances(bool val)
429 496 {
  497 + m->setNeedAppearances(val);
  498 +}
  499 +
  500 +void
  501 +AcroForm::setNeedAppearances(bool val)
  502 +{
430 503 QPDFObjectHandle acroform = qpdf.getRoot().getKey("/AcroForm");
431 504 if (!acroform.isDictionary()) {
432 505 qpdf.getRoot().warn(
... ... @@ -444,6 +517,12 @@ QPDFAcroFormDocumentHelper::setNeedAppearances(bool val)
444 517 void
445 518 QPDFAcroFormDocumentHelper::generateAppearancesIfNeeded()
446 519 {
  520 + m->generateAppearancesIfNeeded();
  521 +}
  522 +
  523 +void
  524 +AcroForm::generateAppearancesIfNeeded()
  525 +{
447 526 if (!getNeedAppearances()) {
448 527 return;
449 528 }
... ... @@ -470,6 +549,12 @@ QPDFAcroFormDocumentHelper::generateAppearancesIfNeeded()
470 549 void
471 550 QPDFAcroFormDocumentHelper::disableDigitalSignatures()
472 551 {
  552 + m->disableDigitalSignatures();
  553 +}
  554 +
  555 +void
  556 +AcroForm::disableDigitalSignatures()
  557 +{
473 558 qpdf.removeSecurityRestrictions();
474 559 std::set<QPDFObjGen> to_remove;
475 560 auto fields = getFormFields();
... ... @@ -491,7 +576,7 @@ QPDFAcroFormDocumentHelper::disableDigitalSignatures()
491 576 }
492 577  
493 578 void
494   -QPDFAcroFormDocumentHelper::adjustInheritedFields(
  579 +AcroForm::adjustInheritedFields(
495 580 QPDFObjectHandle obj,
496 581 bool override_da,
497 582 std::string const& from_default_da,
... ... @@ -598,7 +683,7 @@ ResourceReplacer::handleToken(QPDFTokenizer::Token const&amp; token)
598 683 }
599 684  
600 685 void
601   -QPDFAcroFormDocumentHelper::adjustDefaultAppearances(
  686 +AcroForm::adjustDefaultAppearances(
602 687 QPDFObjectHandle obj, std::map<std::string, std::map<std::string, std::string>> const& dr_map)
603 688 {
604 689 // This method is called on a field that has been copied from another file but whose /DA still
... ... @@ -656,7 +741,7 @@ QPDFAcroFormDocumentHelper::adjustDefaultAppearances(
656 741 }
657 742  
658 743 void
659   -QPDFAcroFormDocumentHelper::adjustAppearanceStream(
  744 +AcroForm::adjustAppearanceStream(
660 745 QPDFObjectHandle stream, std::map<std::string, std::map<std::string, std::string>> dr_map)
661 746 {
662 747 // We don't have to modify appearance streams or their resource dictionaries for them to display
... ... @@ -748,7 +833,6 @@ QPDFAcroFormDocumentHelper::transformAnnotations(
748 833 QPDF* from_qpdf,
749 834 QPDFAcroFormDocumentHelper* from_afdh)
750 835 {
751   - Array old_annots = std::move(a_old_annots);
752 836 if (!from_qpdf) {
753 837 // Assume these are from the same QPDF.
754 838 from_qpdf = &qpdf;
... ... @@ -756,6 +840,23 @@ QPDFAcroFormDocumentHelper::transformAnnotations(
756 840 } else if (from_qpdf != &qpdf && !from_afdh) {
757 841 from_afdh = &QPDFAcroFormDocumentHelper::get(*from_qpdf);
758 842 }
  843 + m->transformAnnotations(
  844 + a_old_annots, new_annots, new_fields, old_fields, cm, from_qpdf, from_afdh->m.get());
  845 +}
  846 +
  847 +void
  848 +AcroForm::transformAnnotations(
  849 + QPDFObjectHandle a_old_annots,
  850 + std::vector<QPDFObjectHandle>& new_annots,
  851 + std::vector<QPDFObjectHandle>& new_fields,
  852 + std::set<QPDFObjGen>& old_fields,
  853 + QPDFMatrix const& cm,
  854 + QPDF* from_qpdf,
  855 + AcroForm* from_afdh)
  856 +{
  857 + qpdf_expect(from_qpdf);
  858 + qpdf_expect(from_afdh);
  859 + Array old_annots = std::move(a_old_annots);
759 860 const bool foreign = from_qpdf != &qpdf;
760 861  
761 862 // It's possible that we will transform annotations that don't include any form fields. This
... ... @@ -1056,6 +1157,16 @@ QPDFAcroFormDocumentHelper::fixCopiedAnnotations(
1056 1157 QPDFAcroFormDocumentHelper& from_afdh,
1057 1158 std::set<QPDFObjGen>* added_fields)
1058 1159 {
  1160 + m->fixCopiedAnnotations(to_page, from_page, *from_afdh.m, added_fields);
  1161 +}
  1162 +
  1163 +void
  1164 +AcroForm::fixCopiedAnnotations(
  1165 + QPDFObjectHandle to_page,
  1166 + QPDFObjectHandle from_page,
  1167 + AcroForm& from_afdh,
  1168 + std::set<QPDFObjGen>* added_fields)
  1169 +{
1059 1170 auto old_annots = from_page.getKey("/Annots");
1060 1171 if (old_annots.empty() || !old_annots.isArray()) {
1061 1172 return;
... ... @@ -1070,7 +1181,7 @@ QPDFAcroFormDocumentHelper::fixCopiedAnnotations(
1070 1181 new_fields,
1071 1182 old_fields,
1072 1183 QPDFMatrix(),
1073   - &(from_afdh.getQPDF()),
  1184 + &(from_afdh.qpdf),
1074 1185 &from_afdh);
1075 1186  
1076 1187 to_page.replaceKey("/Annots", QPDFObjectHandle::newArray(new_annots));
... ...
libqpdf/QPDFFormFieldObjectHelper.cc
1 1 #include <qpdf/QPDFFormFieldObjectHelper.hh>
2 2  
3   -#include <qpdf/FormField.hh>
  3 +#include <qpdf/AcroForm.hh>
4 4  
5 5 #include <qpdf/Pl_QPDFTokenizer.hh>
6 6 #include <qpdf/QIntC.hh>
... ... @@ -16,15 +16,15 @@
16 16  
17 17 using namespace qpdf;
18 18  
19   -using FormField = qpdf::impl::FormField;
  19 +using FormNode = qpdf::impl::FormNode;
20 20  
21   -const QPDFObjectHandle FormField::null_oh;
  21 +const QPDFObjectHandle FormNode::null_oh;
22 22  
23   -class QPDFFormFieldObjectHelper::Members: public FormField
  23 +class QPDFFormFieldObjectHelper::Members: public FormNode
24 24 {
25 25 public:
26 26 Members(QPDFObjectHandle const& oh) :
27   - FormField(oh)
  27 + FormNode(oh)
28 28 {
29 29 }
30 30 };
... ... @@ -59,8 +59,8 @@ QPDFFormFieldObjectHelper::getTopLevelField(bool* is_different)
59 59 return Null::if_null(m->root_field(is_different).oh());
60 60 }
61 61  
62   -FormField
63   -FormField::root_field(bool* is_different)
  62 +FormNode
  63 +FormNode::root_field(bool* is_different)
64 64 {
65 65 if (is_different) {
66 66 *is_different = false;
... ... @@ -87,7 +87,7 @@ QPDFFormFieldObjectHelper::getInheritableFieldValue(std::string const&amp; name)
87 87 }
88 88  
89 89 QPDFObjectHandle const&
90   -FormField::inherited(std::string const& name, bool acroform) const
  90 +FormNode::inherited(std::string const& name, bool acroform) const
91 91 {
92 92 if (!obj) {
93 93 return null_oh;
... ... @@ -111,7 +111,7 @@ QPDFFormFieldObjectHelper::getInheritableFieldValueAsString(std::string const&amp; n
111 111 }
112 112  
113 113 std::string
114   -FormField::inheritable_string(std::string const& name) const
  114 +FormNode::inheritable_string(std::string const& name) const
115 115 {
116 116 if (auto fv = inheritable_value<String>(name)) {
117 117 return fv.utf8_value();
... ... @@ -144,7 +144,7 @@ QPDFFormFieldObjectHelper::getFullyQualifiedName()
144 144 }
145 145  
146 146 std::string
147   -FormField::fully_qualified_name() const
  147 +FormNode::fully_qualified_name() const
148 148 {
149 149 std::string result;
150 150 auto node = *this;
... ... @@ -169,7 +169,7 @@ QPDFFormFieldObjectHelper::getPartialName()
169 169 }
170 170  
171 171 std::string
172   -FormField::partial_name() const
  172 +FormNode::partial_name() const
173 173 {
174 174 if (auto pn = T()) {
175 175 return pn.utf8_value();
... ... @@ -184,7 +184,7 @@ QPDFFormFieldObjectHelper::getAlternativeName()
184 184 }
185 185  
186 186 std::string
187   -FormField::alternative_name() const
  187 +FormNode::alternative_name() const
188 188 {
189 189 if (auto an = TU()) {
190 190 return an.utf8_value();
... ... @@ -199,7 +199,7 @@ QPDFFormFieldObjectHelper::getMappingName()
199 199 }
200 200  
201 201 std::string
202   -FormField::mapping_name() const
  202 +FormNode::mapping_name() const
203 203 {
204 204 if (auto mn = TM()) {
205 205 return mn.utf8_value();
... ... @@ -220,7 +220,7 @@ QPDFFormFieldObjectHelper::getValueAsString()
220 220 }
221 221  
222 222 std::string
223   -FormField::value() const
  223 +FormNode::value() const
224 224 {
225 225 return inheritable_string("/V");
226 226 }
... ... @@ -238,7 +238,7 @@ QPDFFormFieldObjectHelper::getDefaultValueAsString()
238 238 }
239 239  
240 240 std::string
241   -FormField::default_value() const
  241 +FormNode::default_value() const
242 242 {
243 243 return inheritable_string("/DV");
244 244 }
... ... @@ -250,7 +250,7 @@ QPDFFormFieldObjectHelper::getDefaultResources()
250 250 }
251 251  
252 252 QPDFObjectHandle
253   -FormField::getDefaultResources()
  253 +FormNode::getDefaultResources()
254 254 {
255 255 return from_AcroForm("/DR");
256 256 }
... ... @@ -262,7 +262,7 @@ QPDFFormFieldObjectHelper::getDefaultAppearance()
262 262 }
263 263  
264 264 std::string
265   -FormField::default_appearance() const
  265 +FormNode::default_appearance() const
266 266 {
267 267 if (auto DA = inheritable_value<String>("/DA")) {
268 268 return DA.utf8_value();
... ... @@ -280,7 +280,7 @@ QPDFFormFieldObjectHelper::getQuadding()
280 280 }
281 281  
282 282 int
283   -FormField::getQuadding()
  283 +FormNode::getQuadding()
284 284 {
285 285 auto fv = inheritable_value<QPDFObjectHandle>("/Q");
286 286 bool looked_in_acroform = false;
... ... @@ -302,7 +302,7 @@ QPDFFormFieldObjectHelper::getFlags()
302 302 }
303 303  
304 304 int
305   -FormField::getFlags()
  305 +FormNode::getFlags()
306 306 {
307 307 auto f = inheritable_value<QPDFObjectHandle>("/Ff");
308 308 return f.isInteger() ? f.getIntValueAsInt() : 0;
... ... @@ -315,7 +315,7 @@ QPDFFormFieldObjectHelper::isText()
315 315 }
316 316  
317 317 bool
318   -FormField::isText()
  318 +FormNode::isText()
319 319 {
320 320 return FT() == "/Tx";
321 321 }
... ... @@ -327,7 +327,7 @@ QPDFFormFieldObjectHelper::isCheckbox()
327 327 }
328 328  
329 329 bool
330   -FormField::isCheckbox()
  330 +FormNode::isCheckbox()
331 331 {
332 332 return FT() == "/Btn" && (getFlags() & (ff_btn_radio | ff_btn_pushbutton)) == 0;
333 333 }
... ... @@ -339,7 +339,7 @@ QPDFFormFieldObjectHelper::isChecked()
339 339 }
340 340  
341 341 bool
342   -FormField::isChecked()
  342 +FormNode::isChecked()
343 343 {
344 344 return isCheckbox() && V<Name>() != "/Off";
345 345 }
... ... @@ -351,7 +351,7 @@ QPDFFormFieldObjectHelper::isRadioButton()
351 351 }
352 352  
353 353 bool
354   -FormField::isRadioButton()
  354 +FormNode::isRadioButton()
355 355 {
356 356 return FT() == "/Btn" && (getFlags() & ff_btn_radio) == ff_btn_radio;
357 357 }
... ... @@ -363,7 +363,7 @@ QPDFFormFieldObjectHelper::isPushbutton()
363 363 }
364 364  
365 365 bool
366   -FormField::isPushbutton()
  366 +FormNode::isPushbutton()
367 367 {
368 368 return FT() == "/Btn" && (getFlags() & ff_btn_pushbutton) == ff_btn_pushbutton;
369 369 }
... ... @@ -375,7 +375,7 @@ QPDFFormFieldObjectHelper::isChoice()
375 375 }
376 376  
377 377 bool
378   -FormField::isChoice()
  378 +FormNode::isChoice()
379 379 {
380 380 return FT() == "/Ch";
381 381 }
... ... @@ -387,7 +387,7 @@ QPDFFormFieldObjectHelper::getChoices()
387 387 }
388 388  
389 389 std::vector<std::string>
390   -FormField::getChoices()
  390 +FormNode::getChoices()
391 391 {
392 392 if (!isChoice()) {
393 393 return {};
... ... @@ -413,7 +413,7 @@ QPDFFormFieldObjectHelper::setFieldAttribute(std::string const&amp; key, QPDFObjectH
413 413 }
414 414  
415 415 void
416   -FormField::setFieldAttribute(std::string const& key, QPDFObjectHandle value)
  416 +FormNode::setFieldAttribute(std::string const& key, QPDFObjectHandle value)
417 417 {
418 418 oh().replaceKey(key, value);
419 419 }
... ... @@ -425,7 +425,7 @@ QPDFFormFieldObjectHelper::setFieldAttribute(std::string const&amp; key, std::string
425 425 }
426 426  
427 427 void
428   -FormField::setFieldAttribute(std::string const& key, std::string const& utf8_value)
  428 +FormNode::setFieldAttribute(std::string const& key, std::string const& utf8_value)
429 429 {
430 430 oh().replaceKey(key, QPDFObjectHandle::newUnicodeString(utf8_value));
431 431 }
... ... @@ -437,7 +437,7 @@ QPDFFormFieldObjectHelper::setV(QPDFObjectHandle value, bool need_appearances)
437 437 }
438 438  
439 439 void
440   -FormField::setV(QPDFObjectHandle value, bool need_appearances)
  440 +FormNode::setV(QPDFObjectHandle value, bool need_appearances)
441 441 {
442 442 Name name = value;
443 443 if (FT() == "/Btn") {
... ... @@ -485,13 +485,13 @@ QPDFFormFieldObjectHelper::setV(std::string const&amp; utf8_value, bool need_appeara
485 485 }
486 486  
487 487 void
488   -FormField::setV(std::string const& utf8_value, bool need_appearances)
  488 +FormNode::setV(std::string const& utf8_value, bool need_appearances)
489 489 {
490 490 setV(QPDFObjectHandle::newUnicodeString(utf8_value), need_appearances);
491 491 }
492 492  
493 493 void
494   -FormField::setRadioButtonValue(QPDFObjectHandle name)
  494 +FormNode::setRadioButtonValue(QPDFObjectHandle name)
495 495 {
496 496 // Set the value of a radio button field. This has the following specific behavior:
497 497 // * If this is a radio button field that has a parent that is also a radio button field and has
... ... @@ -503,7 +503,7 @@ FormField::setRadioButtonValue(QPDFObjectHandle name)
503 503 // Note that we never turn on /NeedAppearances when setting a radio button field.
504 504 QPDFObjectHandle parent = oh().getKey("/Parent");
505 505 if (parent.isDictionary() && parent.getKey("/Parent").null()) {
506   - FormField ph(parent);
  506 + FormNode ph(parent);
507 507 if (ph.isRadioButton()) {
508 508 // This is most likely one of the individual buttons. Try calling on the parent.
509 509 ph.setRadioButtonValue(name);
... ... @@ -546,7 +546,7 @@ FormField::setRadioButtonValue(QPDFObjectHandle name)
546 546 }
547 547  
548 548 void
549   -FormField::setCheckBoxValue(bool value)
  549 +FormNode::setCheckBoxValue(bool value)
550 550 {
551 551 QPDFObjectHandle AP = oh().getKey("/AP");
552 552 QPDFObjectHandle annot;
... ... @@ -601,7 +601,7 @@ QPDFFormFieldObjectHelper::generateAppearance(QPDFAnnotationObjectHelper&amp; aoh)
601 601 }
602 602  
603 603 void
604   -FormField::generateAppearance(QPDFAnnotationObjectHelper& aoh)
  604 +FormNode::generateAppearance(QPDFAnnotationObjectHelper& aoh)
605 605 {
606 606 // Ignore field types we don't know how to generate appearances for. Button fields don't really
607 607 // need them -- see code in QPDFAcroFormDocumentHelper::generateAppearancesIfNeeded.
... ... @@ -877,7 +877,7 @@ namespace
877 877 } // namespace
878 878  
879 879 QPDFObjectHandle
880   -FormField::getFontFromResource(QPDFObjectHandle resources, std::string const& name)
  880 +FormNode::getFontFromResource(QPDFObjectHandle resources, std::string const& name)
881 881 {
882 882 QPDFObjectHandle result;
883 883 if (resources.isDictionary() && resources.getKey("/Font").isDictionary() &&
... ... @@ -888,7 +888,7 @@ FormField::getFontFromResource(QPDFObjectHandle resources, std::string const&amp; na
888 888 }
889 889  
890 890 void
891   -FormField::generateTextAppearance(QPDFAnnotationObjectHelper& aoh)
  891 +FormNode::generateTextAppearance(QPDFAnnotationObjectHelper& aoh)
892 892 {
893 893 QPDFObjectHandle AS = aoh.getAppearanceStream("/N");
894 894 if (AS.null()) {
... ...
libqpdf/QPDFJob.cc
... ... @@ -4,6 +4,7 @@
4 4 #include <iostream>
5 5 #include <memory>
6 6  
  7 +#include <qpdf/AcroForm.hh>
7 8 #include <qpdf/ClosedFileInputSource.hh>
8 9 #include <qpdf/FileInputSource.hh>
9 10 #include <qpdf/Pipeline_private.hh>
... ... @@ -1866,7 +1867,7 @@ QPDFJob::doUnderOverlayForPage(
1866 1867 if (!(uo.pdf && pagenos[pageno.idx].contains(uo_idx))) {
1867 1868 return "";
1868 1869 }
1869   - auto& dest_afdh = dest_page.qpdf()->doc().acroform();
  1870 + auto& dest_afdh = dest_page.qpdf()->doc().acroform_dh();
1870 1871  
1871 1872 auto const& pages = uo.pdf->doc().pages().all();
1872 1873 std::string content;
... ... @@ -1887,7 +1888,8 @@ QPDFJob::doUnderOverlayForPage(
1887 1888 QPDFMatrix cm;
1888 1889 std::string new_content = dest_page.placeFormXObject(
1889 1890 fo[from_no.no][uo_idx], name, dest_page.getTrimBox().getArrayAsRectangle(), cm);
1890   - dest_page.copyAnnotations(from_page, cm, &dest_afdh, &from_page.qpdf()->doc().acroform());
  1891 + dest_page.copyAnnotations(
  1892 + from_page, cm, &dest_afdh, &from_page.qpdf()->doc().acroform_dh());
1891 1893 if (!new_content.empty()) {
1892 1894 resources.mergeResources(Dictionary({{"/XObject", Dictionary::empty()}}));
1893 1895 auto xobject = resources.getKey("/XObject");
... ... @@ -2107,7 +2109,7 @@ QPDFJob::handleTransformations(QPDF&amp; pdf)
2107 2109 QPDFAcroFormDocumentHelper* afdh_ptr = nullptr;
2108 2110 auto afdh = [&]() -> QPDFAcroFormDocumentHelper& {
2109 2111 if (!afdh_ptr) {
2110   - afdh_ptr = &pdf.doc().acroform();
  2112 + afdh_ptr = &pdf.doc().acroform_dh();
2111 2113 }
2112 2114 return *afdh_ptr;
2113 2115 };
... ... @@ -2978,8 +2980,7 @@ QPDFJob::doSplitPages(QPDF&amp; pdf)
2978 2980 QPDF outpdf;
2979 2981 outpdf.doc().config(m->d_cfg);
2980 2982 outpdf.emptyPDF();
2981   - QPDFAcroFormDocumentHelper* out_afdh =
2982   - afdh.hasAcroForm() ? &outpdf.doc().acroform() : nullptr;
  2983 + impl::AcroForm* out_afdh = afdh.hasAcroForm() ? &outpdf.doc().acroform() : nullptr;
2983 2984 for (size_t pageno = first; pageno <= last; ++pageno) {
2984 2985 QPDFObjectHandle page = pages.at(pageno - 1);
2985 2986 outpdf.addPage(page, false);
... ...
libqpdf/QPDF_pages.cc
1 1 #include <qpdf/QPDFPageDocumentHelper.hh>
2 2 #include <qpdf/QPDF_private.hh>
3 3  
4   -#include <qpdf/QPDFAcroFormDocumentHelper.hh>
  4 +#include <qpdf/AcroForm.hh>
5 5 #include <qpdf/QPDFExc.hh>
6 6 #include <qpdf/QPDFObjectHandle_private.hh>
7 7 #include <qpdf/QTC.hh>
... ... @@ -660,7 +660,7 @@ void
660 660 Pages::flatten_annotations_for_page(
661 661 QPDFPageObjectHelper& page,
662 662 QPDFObjectHandle& resources,
663   - QPDFAcroFormDocumentHelper& afdh,
  663 + impl::AcroForm& afdh,
664 664 int required_flags,
665 665 int forbidden_flags)
666 666 {
... ...
libqpdf/qpdf/AcroForm.hh 0 โ†’ 100644
  1 +#ifndef ACRO_FORM_HH
  2 +#define ACRO_FORM_HH
  3 +
  4 +#include <qpdf/QPDFObjectHandle_private.hh>
  5 +#include <qpdf/QPDFObjectHelper.hh>
  6 +#include <qpdf/QPDF_private.hh>
  7 +
  8 +#include <vector>
  9 +
  10 +class QPDFAnnotationObjectHelper;
  11 +
  12 +namespace qpdf::impl
  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
  27 + class AcroForm: public Doc::Common
  28 + {
  29 + public:
  30 + AcroForm() = delete;
  31 + AcroForm(AcroForm const&) = delete;
  32 + AcroForm(AcroForm&&) = delete;
  33 + AcroForm& operator=(AcroForm const&) = delete;
  34 + AcroForm& operator=(AcroForm&&) = delete;
  35 + ~AcroForm() = default;
  36 +
  37 + AcroForm(impl::Doc& doc) :
  38 + Common(doc)
  39 + {
  40 + // We have to analyze up front. Otherwise, when we are adding annotations and fields, we
  41 + // are in a temporarily unstable configuration where some widget annotations are not
  42 + // reachable.
  43 + validate();
  44 + }
  45 +
  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);
  98 +
  99 + /// Retrieves a list of widget annotations for the specified page.
  100 + ///
  101 + /// A widget annotation represents the visual part of a form field in a PDF.
  102 + /// This function filters annotations on the given page, returning only those
  103 + /// annotations whose subtype is "/Widget".
  104 + ///
  105 + /// @param page A `QPDFPageObjectHelper` representing the page from which to
  106 + /// extract widget annotations.
  107 + ///
  108 + /// @return A vector of `QPDFAnnotationObjectHelper` objects corresponding to
  109 + /// the widget annotations found on the specified page.
  110 + std::vector<QPDFAnnotationObjectHelper>
  111 + getWidgetAnnotationsForPage(QPDFPageObjectHelper page);
  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 +
  193 + /// Analyzes the AcroForm structure in the PDF document and updates the internal
  194 + /// cache with the form fields and their corresponding widget annotations.
  195 + ///
  196 + /// The function performs the following steps:
  197 + /// - Checks if the cache is valid. If it is, the function exits early.
  198 + /// - Retrieves the `/AcroForm` dictionary from the PDF and checks if it contains
  199 + /// a `/Fields` key.
  200 + /// - If `/Fields` exist and is an array, iterates through the fields and traverses
  201 + /// them to map annotations bidirectionally to form fields.
  202 + /// - Logs a warning if the `/Fields` key is present but not an array, and initializes
  203 + /// it to an empty array.
  204 + /// - Ensures that all widget annotations are processed, including any annotations
  205 + /// that might not be reachable from the `/AcroForm`. Treats such annotations as
  206 + /// their own fields.
  207 + /// - Provides a workaround for PDF documents containing inconsistencies, such as
  208 + /// widget annotations on a page not being referenced in `/AcroForm`.
  209 + ///
  210 + /// This function allows precise navigation and manipulation of form fields and
  211 + /// their related annotations, facilitating advanced PDF document processing.
  212 + void analyze();
  213 +
  214 + /// Recursively traverses the structure of form fields and annotations in a PDF's /AcroForm.
  215 + ///
  216 + /// The method is designed to process form fields in a hierarchical /AcroForm structure.
  217 + /// It captures field and annotation data, resolves parent-child relationships, detects
  218 + /// loops, and avoids stack overflow from excessive recursion depth.
  219 + ///
  220 + /// @param field The current field or annotation to process.
  221 + /// @param parent The parent field object. If the current field is a top-level field, parent
  222 + /// will be a null object.
  223 + /// @param depth The current recursion depth to limit stack usage and avoid infinite loops.
  224 + ///
  225 + /// @return True if the field was processed successfully, false otherwise.
  226 + ///
  227 + /// - Recursion is limited to a depth of 100 to prevent stack overflow with maliciously
  228 + /// crafted files.
  229 + /// - The function skips non-indirect and invalid objects (e.g., non-dictionaries or objects
  230 + /// with invalid parent references).
  231 + /// - Detects and warns about loops in the /AcroForm hierarchy.
  232 + /// - Differentiates between terminal fields, annotations, and composite fields based on
  233 + /// dictionary keys.
  234 + /// - Tracks processed fields and annotations using internal maps to prevent reprocessing
  235 + /// and detect loops.
  236 + /// - Updates name-to-field mappings for terminal fields with a valid fully qualified name.
  237 + /// - Ensures the integrity of parent-child relationships within the field hierarchy.
  238 + /// - Any invalid child objects are logged and skipped during traversal.
  239 + bool traverseField(QPDFObjectHandle field, QPDFObjectHandle const& parent, int depth);
  240 +
  241 + /// Retrieves or creates the /AcroForm dictionary in the PDF document's root.
  242 + ///
  243 + /// - If the /AcroForm key exists in the document root and is a dictionary,
  244 + /// it is returned as is.
  245 + /// - If the /AcroForm key does not exist or is not a dictionary, a new
  246 + /// dictionary is created, stored as the /AcroForm entry in the document root,
  247 + /// and then returned.
  248 + ///
  249 + /// @return A QPDFObjectHandle representing the /AcroForm dictionary.
  250 + QPDFObjectHandle getOrCreateAcroForm();
  251 +
  252 + /// Adjusts inherited field properties for an AcroForm field object.
  253 + ///
  254 + /// This method ensures that the `/DA` (default appearance) and `/Q` (quadding) keys
  255 + /// of the specified field object are overridden if necessary, based on the provided
  256 + /// parameters. The overriding is performed only if the respective `override_da` or
  257 + /// `override_q` flags are set to true, and when the original object's values differ from
  258 + /// the provided defaults. No changes are made to fields that have explicit values for `/DA`
  259 + /// or `/Q`.
  260 + ///
  261 + /// The function is primarily used for adjusting inherited form field properties in cases
  262 + /// where the document structure or inherited values have changed (e.g., when working with
  263 + /// fields in a PDF document).
  264 + ///
  265 + /// @param obj The `QPDFObjectHandle` instance representing the form field object to be
  266 + /// adjusted.
  267 + /// @param override_da A boolean flag indicating whether to override the `/DA` key.
  268 + /// @param from_default_da The default appearance string to apply if overriding the `/DA`
  269 + /// key.
  270 + /// @param override_q A boolean flag indicating whether to override the `/Q` key.
  271 + /// @param from_default_q The default quadding value (alignment) to apply if overriding the
  272 + /// `/Q` key.
  273 + void adjustInheritedFields(
  274 + QPDFObjectHandle obj,
  275 + bool override_da,
  276 + std::string const& from_default_da,
  277 + bool override_q,
  278 + int from_default_q);
  279 +
  280 + /// Adjusts the default appearances (/DA) of an AcroForm field object.
  281 + ///
  282 + /// This method ensures that form fields copied from another PDF document
  283 + /// have their default appearances resource references updated to correctly
  284 + /// point to the appropriate resources in the current document's resource
  285 + /// dictionary (/DR). It resolves name conflicts between the dictionaries
  286 + /// of the source and destination documents by using a mapping provided in
  287 + /// `dr_map`.
  288 + ///
  289 + /// The method parses the /DA string, processes its resource references,
  290 + /// and regenerates the /DA with updated references.
  291 + ///
  292 + /// @param obj The AcroForm field object whose /DA is being adjusted.
  293 + /// @param dr_map A mapping between resource names in the source document's
  294 + /// resource dictionary and their corresponding names in the current
  295 + /// document's resource dictionary.
  296 + void adjustDefaultAppearances(
  297 + QPDFObjectHandle obj,
  298 + std::map<std::string, std::map<std::string, std::string>> const& dr_map);
  299 +
  300 + /// Modifies the appearance stream of an AcroForm field to ensure its resources
  301 + /// align with the resource dictionary and appearance settings. This method
  302 + /// ensures proper resource handling to avoid any conflicts when regenerating
  303 + /// the appearance stream.
  304 + ///
  305 + /// Adjustments include:
  306 + /// - Creating a private resource dictionary for the stream if not already present.
  307 + /// - Merging top-level resource keys into the stream's resource dictionary.
  308 + /// - Resolving naming conflicts between existing and remapped resource keys.
  309 + /// - Removing empty sub-dictionaries from the resource dictionary.
  310 + /// - Attaching a token filter to rewrite resource references in the stream content.
  311 + ///
  312 + /// If conflicts between keys are encountered or the stream cannot be parsed successfully,
  313 + /// appropriate warnings will be generated instead of halting execution.
  314 + ///
  315 + /// @param stream The QPDFObjectHandle representation of the PDF appearance stream to be
  316 + /// adjusted.
  317 + /// @param dr_map A mapping of resource types and their corresponding name remappings
  318 + /// used for resolving resource conflicts and regenerating appearances.
  319 + void adjustAppearanceStream(
  320 + QPDFObjectHandle stream,
  321 + std::map<std::string, std::map<std::string, std::string>> dr_map);
  322 +
  323 + std::map<QPDFObjGen, FieldData> fields_;
  324 + std::map<QPDFObjGen, QPDFFormFieldObjectHelper> annotation_to_field_;
  325 + std::map<std::string, std::set<QPDFObjGen>> name_to_fields_;
  326 + std::set<QPDFObjGen> bad_fields_;
  327 + bool cache_valid_{false};
  328 +
  329 + }; // class Acroform
  330 +
  331 + /// @class FormNode
  332 + /// @brief Represents a node in the interactive forms tree of a PDF document.
  333 + ///
  334 + /// This class models nodes that may be either form field dictionaries or widget annotation
  335 + /// dictionaries, as defined in the PDF specification (sections 12.7 and 12.5.6.19).
  336 + ///
  337 + /// For a detailed description of the attributes that this class can expose, refer to the
  338 + /// corresponding tables in the PDF 2.0 (Table 226) or PDF 1.7 (Table 220) specifications.
  339 + class FormNode: public qpdf::BaseDictionary
  340 + {
  341 + public:
  342 + FormNode() = default;
  343 + FormNode(FormNode const&) = default;
  344 + FormNode& operator=(FormNode const&) = default;
  345 + FormNode(FormNode&&) = default;
  346 + FormNode& operator=(FormNode&&) = default;
  347 + ~FormNode() = default;
  348 +
  349 + FormNode(QPDFObjectHandle const& oh) :
  350 + BaseDictionary(oh)
  351 + {
  352 + }
  353 +
  354 + FormNode(QPDFObjectHandle&& oh) :
  355 + BaseDictionary(std::move(oh))
  356 + {
  357 + }
  358 +
  359 + /// Retrieves the /Parent form field of the current field.
  360 + ///
  361 + /// This function accesses the parent field in the hierarchical structure of form fields, if
  362 + /// it exists. The parent is determined based on the /Parent attribute in the field
  363 + /// dictionary.
  364 + ///
  365 + /// @return A FormNode object representing the parent field. If the current field has no
  366 + /// parent, an empty FormNode object is returned.
  367 + FormNode
  368 + Parent()
  369 + {
  370 + return {get("/Parent")};
  371 + }
  372 +
  373 + /// @brief Returns the top-level field associated with the current field.
  374 + ///
  375 + /// The function traverses the hierarchy of parent fields to identify the highest-level
  376 + /// field in the tree. Typically, this will be the current field itself unless it has a
  377 + /// parent field. Optionally, it can indicate whether the top-level field is different from
  378 + /// the current field.
  379 + ///
  380 + /// @param is_different A pointer to a boolean that, if provided, will be set to true if the
  381 + /// top-level field differs from the current field; otherwise, it will be set to
  382 + /// false.
  383 + ///
  384 + /// @return The top-level field in the form field hierarchy.
  385 + FormNode root_field(bool* is_different = nullptr);
  386 +
  387 + /// @brief Retrieves the inherited value of the specified attribute.
  388 + ///
  389 + /// @param name The name of the attribute to retrieve.
  390 + /// @param acroform If true, checks the document's /AcroForm dictionary for the attribute
  391 + /// if it is not found in the field hierarchy.
  392 + ///
  393 + /// @return A constant reference to the QPDFObjectHandle representing the value of the
  394 + /// specified attribute, if found. If the attribute is not found in the field
  395 + /// hierarchy or the /AcroForm dictionary (when `acroform` is true), returns a
  396 + /// reference to a static null object handle.
  397 + QPDFObjectHandle const& inherited(std::string const& name, bool acroform = false) const;
  398 +
  399 + /// @brief Retrieves the value of a specified field, accounting for inheritance through the
  400 + /// hierarchy of ancestor nodes in the form field tree.
  401 + ///
  402 + /// This function attempts to retrieve the value of the specified field. If the `inherit`
  403 + /// parameter is set to `true` and the field value is not found at the current level, the
  404 + /// method traverses up the parent hierarchy to find the value. The traversal stops when a
  405 + /// value is found, when the root node is reached, or when a loop detection mechanism
  406 + /// prevents further traversal.
  407 + ///
  408 + /// @tparam T The return type of the field value.
  409 + /// @param name The name of the field to retrieve the value for.
  410 + /// @param inherit If set to `true`, the function will attempt to retrieve the value by
  411 + /// inheritance from the parent hierarchy of the form field. Defaults to `true`.
  412 + /// @return Returns the field's value if found; otherwise, returns a default-constructed
  413 + /// value of type `T`.
  414 + template <class T>
  415 + T
  416 + inheritable_value(std::string const& name, bool inherit = true, bool acroform = false) const
  417 + {
  418 + if (auto& v = get(name)) {
  419 + return {v};
  420 + }
  421 + return {inherit ? inherited(name, acroform) : null_oh};
  422 + }
  423 +
  424 + /// @brief Retrieves an inherited field string attribute as a string.
  425 + ///
  426 + /// @param name The name of the field for which the value is to be retrieved.
  427 + /// @return The inherited field value as a UTF-8 encoded string, or an empty string if the
  428 + /// value does not exist or is not of String type.
  429 + std::string inheritable_string(std::string const& name) const;
  430 +
  431 + /// @brief Retrieves the field type (/FT attribute).
  432 + ///
  433 + /// @param inherit If set to `true`, the function will attempt to retrieve the value by
  434 + /// inheritance from the parent hierarchy of the form field. Defaults to `true`.
  435 + /// @return Returns the field type if found; otherwise, returns a default-constructed
  436 + /// `Name`.
  437 + Name
  438 + FT(bool inherit = true) const
  439 + {
  440 + return inheritable_value<Name>("/FT");
  441 + }
  442 +
  443 + /// @brief Retrieves the partial field name (/T attribute).
  444 + ///
  445 + /// @return Returns the partial field name if found; otherwise, returns a
  446 + /// default-constructed `String`.
  447 + String
  448 + T() const
  449 + {
  450 + return {get("/T")};
  451 + }
  452 +
  453 + /// @brief Retrieves the alternative name (/TU attribute).
  454 + ///
  455 + /// @return Returns the alternative name if found; otherwise, returns a default-constructed
  456 + /// `String`.
  457 + String
  458 + TU() const
  459 + {
  460 + return {get("/TU")};
  461 + }
  462 +
  463 + /// @brief Retrieves the mapping name (/TM attribute).
  464 + ///
  465 + /// @return Returns the mapping name if found; otherwise, returns a default-constructed
  466 + /// `String`.
  467 + String
  468 + TM() const
  469 + {
  470 + return {get("/TM")};
  471 + }
  472 +
  473 + /// @brief Retrieves the fully qualified name of the form field.
  474 + ///
  475 + /// This method constructs the fully qualified name of the form field by traversing through
  476 + /// its parent hierarchy. The fully qualified name is constructed by concatenating the /T
  477 + /// (field name) attribute of each parent node with periods as separators, starting from the
  478 + /// root of the hierarchy.
  479 + ///
  480 + /// If the field has no parent hierarchy, the result will simply be the /T attribute of the
  481 + /// current field. In cases of potential circular references, loop detection is applied.
  482 + ///
  483 + /// @return A string representing the fully qualified name of the field.
  484 + std::string fully_qualified_name() const;
  485 +
  486 + /// @brief Retrieves the partial name (/T attribute) of the form field.
  487 + ///
  488 + /// This method returns the value of the field's /T attribute, which is the partial name
  489 + /// used to identify the field within its parent hierarchy. If the attribute is not set, an
  490 + /// empty string is returned.
  491 + ///
  492 + /// @return A string representing the partial name of the field in UTF-8 encoding, or an
  493 + /// empty string if the /T attribute is not present.
  494 + std::string partial_name() const;
  495 +
  496 + /// @brief Retrieves the alternative name for the form field.
  497 + ///
  498 + /// This method attempts to return the alternative name (/TU) of the form field, which is
  499 + /// the field name intended to be presented, to users as a UTF-8 string, if it exists. If
  500 + /// the alternative name is not present, the method falls back to the fully qualified name
  501 + /// of the form field.
  502 + ///
  503 + /// @return The alternative name of the form field as a string, or the
  504 + /// fully qualified name if the alternative name is unavailable.
  505 + std::string alternative_name() const;
  506 +
  507 + /// @brief Retrieves the mapping field name (/TM) for the form field.
  508 + ///
  509 + /// If the mapping name (/TM) is present, it is returned as a UTF-8 string. If not, it falls
  510 + /// back to the 'alternative name', which is obtained using the `alternative_name()` method.
  511 + ///
  512 + /// @return The mapping field name (/TM) as a UTF-8 string or the alternative name if the
  513 + /// mapping name is absent.
  514 + std::string mapping_name() const;
  515 +
  516 + /// @brief Retrieves the field value (`/V` attribute) of a specified field, accounting for
  517 + /// inheritance through the hierarchy of ancestor nodes in the form field tree.
  518 + ///
  519 + /// This function attempts to retrieve the `/V` attribute. If the `inherit`
  520 + /// parameter is set to `true` and the `/V` is not found at the current level, the
  521 + /// method traverses up the parent hierarchy to find the value. The traversal stops when
  522 + /// `/V` is found, when the root node is reached, or when a loop detection mechanism
  523 + /// prevents further traversal.
  524 + ///
  525 + /// @tparam T The return type.
  526 + /// @param inherit If set to `true`, the function will attempt to retrieve `/V` by
  527 + /// inheritance from the parent hierarchy of the form field. Defaults to `true`.
  528 + /// @return Returns the field's value if found; otherwise, returns a default-constructed
  529 + /// value of type `T`.
  530 + template <class T>
  531 + T
  532 + V(bool inherit = true) const
  533 + {
  534 + return inheritable_value<T>("/V", inherit);
  535 + }
  536 +
  537 + /// @brief Retrieves the field value (`/V` attribute) of a specified field, accounting for
  538 + /// inheritance through the hierarchy of ancestor nodes in the form field tree.
  539 + ///
  540 + /// This function attempts to retrieve the `/V` attribute. If the `inherit`
  541 + /// parameter is set to `true` and the `/V` is not found at the current level, the
  542 + /// method traverses up the parent hierarchy to find the value. The traversal stops when
  543 + /// `/V` is found, when the root node is reached, or when a loop detection mechanism
  544 + /// prevents further traversal.
  545 + ///
  546 + /// @param inherit If set to `true`, the function will attempt to retrieve `/V` by
  547 + /// inheritance from the parent hierarchy of the form field. Defaults to `true`.
  548 + /// @return Returns the field's value if found; otherwise, returns a default-constructed
  549 + /// object handle.
  550 + QPDFObjectHandle const&
  551 + V(bool inherit = true) const
  552 + {
  553 + if (auto& v = get("/V")) {
  554 + return v;
  555 + }
  556 + return {inherit ? inherited("/V") : null_oh};
  557 + }
  558 +
  559 + /// @brief Retrieves the field value `/V` attribute of the form field, considering
  560 + /// inheritance, if the value is a String.
  561 + ///
  562 + /// This function extracts the value of the form field, accounting for potential inheritance
  563 + /// through the form hierarchy. It returns the value if it is a String, and an empty string
  564 + /// otherwise.
  565 + ///
  566 + /// @return A string containing the actual or inherited `/V` attribute of the form field, or
  567 + /// an empty string if the value is not present or not a String.
  568 + std::string value() const;
  569 +
  570 + /// @brief Retrieves the field default value (`/DV` attribute) of a specified field,
  571 + /// accounting for inheritance through the hierarchy of ancestor nodes in the form
  572 + /// field tree.
  573 + ///
  574 + /// This function attempts to retrieve the `/DV` attribute. If the `inherit` parameter is
  575 + /// set to `true` and the `/DV` is not found at the current level, the method traverses up
  576 + /// the parent hierarchy to find the value. The traversal stops when
  577 + /// `/DV` is found, when the root node is reached, or when a loop detection mechanism
  578 + /// prevents further traversal.
  579 + ///
  580 + /// @tparam T The return type.
  581 + /// @param inherit If set to `true`, the function will attempt to retrieve `/DV` by
  582 + /// inheritance from the parent hierarchy of the form field. Defaults to `true`.
  583 + /// @return Returns the field's default value if found; otherwise, returns a
  584 + /// default-constructed value of type `T`.
  585 + QPDFObjectHandle const&
  586 + DV(bool inherit = true) const
  587 + {
  588 + if (auto& v = get("/DV")) {
  589 + return v;
  590 + }
  591 + return {inherit ? inherited("/DV") : null_oh};
  592 + }
  593 +
  594 + /// @brief Retrieves the default value `/DV` attribute of the form field, considering
  595 + /// inheritance, if the default value is a String.
  596 + ///
  597 + /// This function extracts the default value of the form field, accounting for potential
  598 + /// inheritance through the form hierarchy. It returns the value if it is a String, and an
  599 + /// empty string otherwise.
  600 + ///
  601 + /// @return A string containing the actual or inherited `/DV` attribute of the form field,
  602 + /// or an empty string if the value is not present or not a String.
  603 + std::string default_value() const;
  604 +
  605 + /// @brief Returns the default appearance string for the form field, considering inheritance
  606 + /// from the field tree hierarchy and the document's /AcroForm dictionary.
  607 + ///
  608 + /// This method retrieves the field's /DA (default appearance) attribute. If the attribute
  609 + /// is not directly available, it checks the parent fields in the hierarchy for an inherited
  610 + /// value. If no value is found in the field hierarchy, it attempts to retrieve the /DA
  611 + /// attribute from the document's /AcroForm dictionary. The method returns an empty string
  612 + /// if no default appearance string is available or applicable.
  613 + ///
  614 + /// @return A string representing the default appearance, or an empty string if
  615 + /// no value is found.
  616 + std::string default_appearance() const;
  617 +
  618 + // Return the default resource dictionary for the field. This comes not from the field but
  619 + // from the document-level /AcroForm dictionary. While several PDF generates put a /DR key
  620 + // in the form field's dictionary, experimentation suggests that many popular readers,
  621 + // including Adobe Acrobat and Acrobat Reader, ignore any /DR item on the field.
  622 + QPDFObjectHandle getDefaultResources();
  623 +
  624 + // Return the quadding value, taking inheritance from the field tree into account. Returns 0
  625 + // if quadding is not specified. Look in /AcroForm if not found in the field hierarchy.
  626 + int getQuadding();
  627 +
  628 + // Return field flags from /Ff. The value is a logical or of pdf_form_field_flag_e as
  629 + // defined in qpdf/Constants.h//
  630 + int getFlags();
  631 +
  632 + // Methods for testing for particular types of form fields
  633 +
  634 + // Returns true if field is of type /Tx
  635 + bool isText();
  636 + // Returns true if field is of type /Btn and flags do not indicate some other type of
  637 + // button.
  638 + bool isCheckbox();
  639 +
  640 + // Returns true if field is a checkbox and is checked.
  641 + bool isChecked();
  642 +
  643 + // Returns true if field is of type /Btn and flags indicate that it is a radio button
  644 + bool isRadioButton();
  645 +
  646 + // Returns true if field is of type /Btn and flags indicate that it is a pushbutton
  647 + bool isPushbutton();
  648 +
  649 + // Returns true if fields if of type /Ch
  650 + bool isChoice();
  651 +
  652 + // Returns choices display values as UTF-8 strings
  653 + std::vector<std::string> getChoices();
  654 +
  655 + // Set an attribute to the given value. If you have a QPDFAcroFormDocumentHelper and you
  656 + // want to set the name of a field, use QPDFAcroFormDocumentHelper::setFormFieldName
  657 + // instead.
  658 + void setFieldAttribute(std::string const& key, QPDFObjectHandle value);
  659 +
  660 + // Set an attribute to the given value as a Unicode string (UTF-16 BE encoded). The input
  661 + // string should be UTF-8 encoded. If you have a QPDFAcroFormDocumentHelper and you want to
  662 + // set the name of a field, use QPDFAcroFormDocumentHelper::setFormFieldName instead.
  663 + void setFieldAttribute(std::string const& key, std::string const& utf8_value);
  664 +
  665 + // Set /V (field value) to the given value. If need_appearances is true and the field type
  666 + // is either /Tx (text) or /Ch (choice), set /NeedAppearances to true. You can explicitly
  667 + // tell this method not to set /NeedAppearances if you are going to generate an appearance
  668 + // stream yourself. Starting with qpdf 8.3.0, this method handles fields of type /Btn
  669 + // (checkboxes, radio buttons, pushbuttons) specially. When setting a checkbox value, any
  670 + // value other than /Off will be treated as on, and the actual value set will be based on
  671 + // the appearance stream's /N dictionary, so the value that ends up in /V may not exactly
  672 + // match the value you pass in.
  673 + void setV(QPDFObjectHandle value, bool need_appearances = true);
  674 +
  675 + // Set /V (field value) to the given string value encoded as a Unicode string. The input
  676 + // value should be UTF-8 encoded. See comments above about /NeedAppearances.
  677 + void setV(std::string const& utf8_value, bool need_appearances = true);
  678 +
  679 + // Update the appearance stream for this field. Note that qpdf's ability to generate
  680 + // appearance streams is limited. We only generate appearance streams for streams of type
  681 + // text or choice. The appearance uses the default parameters provided in the file, and it
  682 + // only supports ASCII characters. Quadding is currently ignored. While this functionality
  683 + // is limited, it should do a decent job on properly constructed PDF files when field values
  684 + // are restricted to ASCII characters.
  685 + void generateAppearance(QPDFAnnotationObjectHelper&);
  686 +
  687 + private:
  688 + /// @brief Retrieves an entry from the document's /AcroForm dictionary using the specified
  689 + /// name.
  690 + ///
  691 + /// The method accesses the AcroForm dictionary within the root object of the PDF document.
  692 + /// If the AcroForm dictionary contains the given field name, it retrieves the
  693 + /// corresponding entry. Otherwise, it returns a default-constructed object handle.
  694 + ///
  695 + /// @param name The name of the form field to retrieve.
  696 + /// @return An object handle corresponding to the specified name within the AcroForm
  697 + /// dictionary.
  698 + QPDFObjectHandle const&
  699 + from_AcroForm(std::string const& name) const
  700 + {
  701 + return {qpdf() ? qpdf()->getRoot()["/AcroForm"][name] : null_oh};
  702 + }
  703 +
  704 + void setRadioButtonValue(QPDFObjectHandle name);
  705 + void setCheckBoxValue(bool value);
  706 + void generateTextAppearance(QPDFAnnotationObjectHelper&);
  707 + QPDFObjectHandle
  708 + getFontFromResource(QPDFObjectHandle resources, std::string const& font_name);
  709 +
  710 + static const QPDFObjectHandle null_oh;
  711 + }; // class FormNode
  712 +} // namespace qpdf::impl
  713 +
  714 +class QPDFAcroFormDocumentHelper::Members: public qpdf::impl::AcroForm
  715 +{
  716 + public:
  717 + Members(QPDF& qpdf) :
  718 + AcroForm(qpdf.doc())
  719 + {
  720 + }
  721 +};
  722 +
  723 +#endif // ACRO_FORM_HH
... ...
libqpdf/qpdf/FormField.hh deleted
1   -#ifndef FORMFIELD_HH
2   -#define FORMFIELD_HH
3   -
4   -#include <qpdf/QPDFObjectHandle_private.hh>
5   -#include <qpdf/QPDFObjectHelper.hh>
6   -
7   -#include <vector>
8   -
9   -class QPDFAnnotationObjectHelper;
10   -
11   -namespace qpdf::impl
12   -{
13   - // This object helper helps with form fields for interactive forms. Please see comments in
14   - // QPDFAcroFormDocumentHelper.hh for additional details.
15   - class FormField: public qpdf::BaseDictionary
16   - {
17   - public:
18   - FormField() = default;
19   - FormField(FormField const&) = default;
20   - FormField& operator=(FormField const&) = default;
21   - FormField(FormField&&) = default;
22   - FormField& operator=(FormField&&) = default;
23   - ~FormField() = default;
24   -
25   - FormField(QPDFObjectHandle const& oh) :
26   - BaseDictionary(oh)
27   - {
28   - }
29   -
30   - FormField(QPDFObjectHandle&& oh) :
31   - BaseDictionary(std::move(oh))
32   - {
33   - }
34   -
35   - /// Retrieves the /Parent form field of the current field.
36   - ///
37   - /// This function accesses the parent field in the hierarchical structure of form fields, if
38   - /// it exists. The parent is determined based on the /Parent attribute in the field
39   - /// dictionary.
40   - ///
41   - /// @return A FormField object representing the parent field. If the current field has no
42   - /// parent, an empty FormField object is returned.
43   - FormField
44   - Parent()
45   - {
46   - return {get("/Parent")};
47   - }
48   -
49   - /// @brief Returns the top-level field associated with the current field.
50   - ///
51   - /// The function traverses the hierarchy of parent fields to identify the highest-level
52   - /// field in the tree. Typically, this will be the current field itself unless it has a
53   - /// parent field. Optionally, it can indicate whether the top-level field is different from
54   - /// the current field.
55   - ///
56   - /// @param is_different A pointer to a boolean that, if provided, will be set to true if the
57   - /// top-level field differs from the current field; otherwise, it will be set to
58   - /// false.
59   - ///
60   - /// @return The top-level field in the form field hierarchy.
61   - FormField root_field(bool* is_different = nullptr);
62   -
63   - /// @brief Retrieves the inherited value of the specified attribute.
64   - ///
65   - /// @param name The name of the attribute to retrieve.
66   - /// @param acroform If true, checks the document's /AcroForm dictionary for the attribute
67   - /// if it is not found in the field hierarchy.
68   - ///
69   - /// @return A constant reference to the QPDFObjectHandle representing the value of the
70   - /// specified attribute, if found. If the attribute is not found in the field
71   - /// hierarchy or the /AcroForm dictionary (when `acroform` is true), returns a
72   - /// reference to a static null object handle.
73   - QPDFObjectHandle const& inherited(std::string const& name, bool acroform = false) const;
74   -
75   - /// @brief Retrieves the value of a specified field, accounting for inheritance through the
76   - /// hierarchy of ancestor nodes in the form field tree.
77   - ///
78   - /// This function attempts to retrieve the value of the specified field. If the `inherit`
79   - /// parameter is set to `true` and the field value is not found at the current level, the
80   - /// method traverses up the parent hierarchy to find the value. The traversal stops when a
81   - /// value is found, when the root node is reached, or when a loop detection mechanism
82   - /// prevents further traversal.
83   - ///
84   - /// @tparam T The return type of the field value.
85   - /// @param name The name of the field to retrieve the value for.
86   - /// @param inherit If set to `true`, the function will attempt to retrieve the value by
87   - /// inheritance from the parent hierarchy of the form field. Defaults to `true`.
88   - /// @return Returns the field's value if found; otherwise, returns a default-constructed
89   - /// value of type `T`.
90   - template <class T>
91   - T
92   - inheritable_value(std::string const& name, bool inherit = true, bool acroform = false) const
93   - {
94   - if (auto& v = get(name)) {
95   - return {v};
96   - }
97   - return {inherit ? inherited(name, acroform) : null_oh};
98   - }
99   -
100   - /// @brief Retrieves an inherited field string attribute as a string.
101   - ///
102   - /// @param name The name of the field for which the value is to be retrieved.
103   - /// @return The inherited field value as a UTF-8 encoded string, or an empty string if the
104   - /// value does not exist or is not of String type.
105   - std::string inheritable_string(std::string const& name) const;
106   -
107   - /// @brief Retrieves the field type (/FT attribute).
108   - ///
109   - /// @param inherit If set to `true`, the function will attempt to retrieve the value by
110   - /// inheritance from the parent hierarchy of the form field. Defaults to `true`.
111   - /// @return Returns the field type if found; otherwise, returns a default-constructed
112   - /// `Name`.
113   - Name
114   - FT(bool inherit = true) const
115   - {
116   - return inheritable_value<Name>("/FT");
117   - }
118   -
119   - /// @brief Retrieves the partial field name (/T attribute).
120   - ///
121   - /// @return Returns the partial field name if found; otherwise, returns a
122   - /// default-constructed `String`.
123   - String
124   - T() const
125   - {
126   - return {get("/T")};
127   - }
128   -
129   - /// @brief Retrieves the alternative name (/TU attribute).
130   - ///
131   - /// @return Returns the alternative name if found; otherwise, returns a default-constructed
132   - /// `String`.
133   - String
134   - TU() const
135   - {
136   - return {get("/TU")};
137   - }
138   -
139   - /// @brief Retrieves the mapping name (/TM attribute).
140   - ///
141   - /// @return Returns the mapping name if found; otherwise, returns a default-constructed
142   - /// `String`.
143   - String
144   - TM() const
145   - {
146   - return {get("/TM")};
147   - }
148   -
149   - /// @brief Retrieves the fully qualified name of the form field.
150   - ///
151   - /// This method constructs the fully qualified name of the form field by traversing through
152   - /// its parent hierarchy. The fully qualified name is constructed by concatenating the /T
153   - /// (field name) attribute of each parent node with periods as separators, starting from the
154   - /// root of the hierarchy.
155   - ///
156   - /// If the field has no parent hierarchy, the result will simply be the /T attribute of the
157   - /// current field. In cases of potential circular references, loop detection is applied.
158   - ///
159   - /// @return A string representing the fully qualified name of the field.
160   - std::string fully_qualified_name() const;
161   -
162   - /// @brief Retrieves the partial name (/T attribute) of the form field.
163   - ///
164   - /// This method returns the value of the field's /T attribute, which is the partial name
165   - /// used to identify the field within its parent hierarchy. If the attribute is not set, an
166   - /// empty string is returned.
167   - ///
168   - /// @return A string representing the partial name of the field in UTF-8 encoding, or an
169   - /// empty string if the /T attribute is not present.
170   - std::string partial_name() const;
171   -
172   - /// @brief Retrieves the alternative name for the form field.
173   - ///
174   - /// This method attempts to return the alternative name (/TU) of the form field, which is
175   - /// the field name intended to be presented, to users as a UTF-8 string, if it exists. If
176   - /// the alternative name is not present, the method falls back to the fully qualified name
177   - /// of the form field.
178   - ///
179   - /// @return The alternative name of the form field as a string, or the
180   - /// fully qualified name if the alternative name is unavailable.
181   - std::string alternative_name() const;
182   -
183   - /// @brief Retrieves the mapping field name (/TM) for the form field.
184   - ///
185   - /// If the mapping name (/TM) is present, it is returned as a UTF-8 string. If not, it falls
186   - /// back to the 'alternative name', which is obtained using the `alternative_name()` method.
187   - ///
188   - /// @return The mapping field name (/TM) as a UTF-8 string or the alternative name if the
189   - /// mapping name is absent.
190   - std::string mapping_name() const;
191   -
192   - /// @brief Retrieves the field value (`/V` attribute) of a specified field, accounting for
193   - /// inheritance through thehierarchy of ancestor nodes in the form field tree.
194   - ///
195   - /// This function attempts to retrieve the `/V` attribute. If the `inherit`
196   - /// parameter is set to `true` and the `/V` is not found at the current level, the
197   - /// method traverses up the parent hierarchy to find the value. The traversal stops when
198   - /// `/V` is found, when the root node is reached, or when a loop detection mechanism
199   - /// prevents further traversal.
200   - ///
201   - /// @tparam T The return type.
202   - /// @param inherit If set to `true`, the function will attempt to retrieve `/V` by
203   - /// inheritance from the parent hierarchy of the form field. Defaults to `true`.
204   - /// @return Returns the field's value if found; otherwise, returns a default-constructed
205   - /// value of type `T`.
206   - template <class T>
207   - T
208   - V(bool inherit = true) const
209   - {
210   - return inheritable_value<T>("/V", inherit);
211   - }
212   -
213   - /// @brief Retrieves the field value (`/V` attribute) of a specified field, accounting for
214   - /// inheritance through the hierarchy of ancestor nodes in the form field tree.
215   - ///
216   - /// This function attempts to retrieve the `/V` attribute. If the `inherit`
217   - /// parameter is set to `true` and the `/V` is not found at the current level, the
218   - /// method traverses up the parent hierarchy to find the value. The traversal stops when
219   - /// `/V` is found, when the root node is reached, or when a loop detection mechanism
220   - /// prevents further traversal.
221   - ///
222   - /// @param inherit If set to `true`, the function will attempt to retrieve `/V` by
223   - /// inheritance from the parent hierarchy of the form field. Defaults to `true`.
224   - /// @return Returns the field's value if found; otherwise, returns a default-constructed
225   - /// object handle.
226   - QPDFObjectHandle const&
227   - V(bool inherit = true) const
228   - {
229   - if (auto& v = get("/V")) {
230   - return v;
231   - }
232   - return {inherit ? inherited("/V") : null_oh};
233   - }
234   -
235   - /// @brief Retrieves the field value `/V` attribute of the form field, considering
236   - /// inheritance, if the value is a String.
237   - ///
238   - /// This function extracts the value of the form field, accounting for potential inheritance
239   - /// through the form hierarchy. It returns the value if it is a String, and an empty string
240   - /// otherwise.
241   - ///
242   - /// @return A string containing the actual or inherited `/V` attribute of the form field, or
243   - /// an empty string if the value is not present or not a String.
244   - std::string value() const;
245   -
246   - /// @brief Retrieves the field default value (`/DV` attribute) of a specified field,
247   - /// accounting for inheritance through the hierarchy of ancestor nodes in the form
248   - /// field tree.
249   - ///
250   - /// This function attempts to retrieve the `/DV` attribute. If the `inherit` parameter is
251   - /// set to `true` and the `/DV` is not found at the current level, the method traverses up
252   - /// the parent hierarchy to find the value. The traversal stops when
253   - /// `/DV` is found, when the root node is reached, or when a loop detection mechanism
254   - /// prevents further traversal.
255   - ///
256   - /// @tparam T The return type.
257   - /// @param inherit If set to `true`, the function will attempt to retrieve `/DV` by
258   - /// inheritance from the parent hierarchy of the form field. Defaults to `true`.
259   - /// @return Returns the field's default value if found; otherwise, returns a
260   - /// default-constructed value of type `T`.
261   - QPDFObjectHandle const&
262   - DV(bool inherit = true) const
263   - {
264   - if (auto& v = get("/DV")) {
265   - return v;
266   - }
267   - return {inherit ? inherited("/DV") : null_oh};
268   - }
269   -
270   - /// @brief Retrieves the default value `/DV` attribute of the form field, considering
271   - /// inheritance, if the default value is a String.
272   - ///
273   - /// This function extracts the default value of the form field, accounting for potential
274   - /// inheritance through the form hierarchy. It returns the value if it is a String, and an
275   - /// empty string otherwise.
276   - ///
277   - /// @return A string containing the actual or inherited `/V` attribute of the form field, or
278   - /// an empty string if the value is not present or not a String.
279   - std::string default_value() const;
280   -
281   - /// @brief Returns the default appearance string for the form field, considering inheritance
282   - /// from the field tree hierarchy and the document's /AcroForm dictionary.
283   - ///
284   - /// This method retrieves the field's /DA (default appearance) attribute. If the attribute
285   - /// is not directly available, it checks the parent fields in the hierarchy for an inherited
286   - /// value. If no value is found in the field hierarchy, it attempts to retrieve the /DA
287   - /// attribute from the document's /AcroForm dictionary. The method returns an empty string
288   - /// if no default appearance string is available or applicable.
289   - ///
290   - /// @return A string representing the default appearance, or an empty string if
291   - /// no value is found.
292   - std::string default_appearance() const;
293   -
294   - // Return the default resource dictionary for the field. This comes not from the field but
295   - // from the document-level /AcroForm dictionary. While several PDF generates put a /DR key
296   - // in the form field's dictionary, experimentation suggests that many popular readers,
297   - // including Adobe Acrobat and Acrobat Reader, ignore any /DR item on the field.
298   - QPDFObjectHandle getDefaultResources();
299   -
300   - // Return the quadding value, taking inheritance from the field tree into account. Returns 0
301   - // if quadding is not specified. Look in /AcroForm if not found in the field hierarchy.
302   - int getQuadding();
303   -
304   - // Return field flags from /Ff. The value is a logical or of pdf_form_field_flag_e as
305   - // defined in qpdf/Constants.h//
306   - int getFlags();
307   -
308   - // Methods for testing for particular types of form fields
309   -
310   - // Returns true if field is of type /Tx
311   - bool isText();
312   - // Returns true if field is of type /Btn and flags do not indicate some other type of
313   - // button.
314   - bool isCheckbox();
315   -
316   - // Returns true if field is a checkbox and is checked.
317   - bool isChecked();
318   -
319   - // Returns true if field is of type /Btn and flags indicate that it is a radio button
320   - bool isRadioButton();
321   -
322   - // Returns true if field is of type /Btn and flags indicate that it is a pushbutton
323   - bool isPushbutton();
324   -
325   - // Returns true if fields if of type /Ch
326   - bool isChoice();
327   -
328   - // Returns choices display values as UTF-8 strings
329   - std::vector<std::string> getChoices();
330   -
331   - // Set an attribute to the given value. If you have a QPDFAcroFormDocumentHelper and you
332   - // want to set the name of a field, use QPDFAcroFormDocumentHelper::setFormFieldName
333   - // instead.
334   - void setFieldAttribute(std::string const& key, QPDFObjectHandle value);
335   -
336   - // Set an attribute to the given value as a Unicode string (UTF-16 BE encoded). The input
337   - // string should be UTF-8 encoded. If you have a QPDFAcroFormDocumentHelper and you want to
338   - // set the name of a field, use QPDFAcroFormDocumentHelper::setFormFieldName instead.
339   - void setFieldAttribute(std::string const& key, std::string const& utf8_value);
340   -
341   - // Set /V (field value) to the given value. If need_appearances is true and the field type
342   - // is either /Tx (text) or /Ch (choice), set /NeedAppearances to true. You can explicitly
343   - // tell this method not to set /NeedAppearances if you are going to generate an appearance
344   - // stream yourself. Starting with qpdf 8.3.0, this method handles fields of type /Btn
345   - // (checkboxes, radio buttons, pushbuttons) specially. When setting a checkbox value, any
346   - // value other than /Off will be treated as on, and the actual value set will be based on
347   - // the appearance stream's /N dictionary, so the value that ends up in /V may not exactly
348   - // match the value you pass in.
349   - void setV(QPDFObjectHandle value, bool need_appearances = true);
350   -
351   - // Set /V (field value) to the given string value encoded as a Unicode string. The input
352   - // value should be UTF-8 encoded. See comments above about /NeedAppearances.
353   - void setV(std::string const& utf8_value, bool need_appearances = true);
354   -
355   - // Update the appearance stream for this field. Note that qpdf's ability to generate
356   - // appearance streams is limited. We only generate appearance streams for streams of type
357   - // text or choice. The appearance uses the default parameters provided in the file, and it
358   - // only supports ASCII characters. Quadding is currently ignored. While this functionality
359   - // is limited, it should do a decent job on properly constructed PDF files when field values
360   - // are restricted to ASCII characters.
361   - void generateAppearance(QPDFAnnotationObjectHelper&);
362   -
363   - private:
364   - /// @brief Retrieves an entry from the document's /AcroForm dictionary using the specified
365   - /// name.
366   - ///
367   - /// The method accesses the AcroForm dictionary within the root object of the PDF document.
368   - /// If the the AcroForm dictionary contains the given field name, it retrieves the
369   - /// corresponding entry. Otherwise, it returns a default-constructed object handle.
370   - ///
371   - /// @param name The name of the form field to retrieve.
372   - /// @return A object handle corresponding to the specified name within the AcroForm
373   - /// dictionary.
374   - QPDFObjectHandle const&
375   - from_AcroForm(std::string const& name) const
376   - {
377   - return {qpdf() ? qpdf()->getRoot()["/AcroForm"][name] : null_oh};
378   - }
379   -
380   - void setRadioButtonValue(QPDFObjectHandle name);
381   - void setCheckBoxValue(bool value);
382   - void generateTextAppearance(QPDFAnnotationObjectHelper&);
383   - QPDFObjectHandle
384   - getFontFromResource(QPDFObjectHandle resources, std::string const& font_name);
385   -
386   - static const QPDFObjectHandle null_oh;
387   - };
388   -} // namespace qpdf::impl
389   -
390   -#endif // FORMFIELD_HH
libqpdf/qpdf/QPDF_private.hh
... ... @@ -28,8 +28,9 @@ namespace qpdf
28 28  
29 29 namespace impl
30 30 {
  31 + class AcroForm;
31 32 using Doc = QPDF::Doc;
32   - }
  33 + } // namespace impl
33 34  
34 35 class Doc: public QPDF
35 36 {
... ... @@ -374,11 +375,33 @@ class QPDF::Doc
374 375 bool reconstructed_xref() const;
375 376  
376 377 QPDFAcroFormDocumentHelper&
  378 + acroform_dh()
  379 + {
  380 + if (!acroform_) {
  381 + no_inspection();
  382 + init_acroform();
  383 + }
  384 + return *acroform_dh_;
  385 + }
  386 +
  387 + /// @brief Retrieves the shared impl::AcroForm instance associated with the document.
  388 + ///
  389 + /// @note The AcroForm class caches the form field structure for efficiency. If any part
  390 + /// of the form field structure is modified directly the `validate` method MUST be
  391 + /// called before calling any other AcroForm methods in order to refresh the cache.
  392 + ///
  393 + /// If the AcroForm instance has not already been initialized, the `init_acroform()`
  394 + /// function is called to initialize it.
  395 + ///
  396 + /// @return A reference to the shared AcroForm object of the document.
  397 + ///
  398 + /// @since 12.3
  399 + impl::AcroForm&
377 400 acroform()
378 401 {
379 402 if (!acroform_) {
380 403 no_inspection();
381   - acroform_ = std::make_unique<QPDFAcroFormDocumentHelper>(qpdf);
  404 + init_acroform();
382 405 }
383 406 return *acroform_;
384 407 }
... ... @@ -438,8 +461,11 @@ class QPDF::Doc
438 461 qpdf::Doc::Config cf;
439 462  
440 463 private:
  464 + void init_acroform();
  465 +
441 466 // Document Helpers;
442   - std::unique_ptr<QPDFAcroFormDocumentHelper> acroform_;
  467 + std::unique_ptr<QPDFAcroFormDocumentHelper> acroform_dh_;
  468 + impl::AcroForm* acroform_{nullptr};
443 469 std::unique_ptr<QPDFEmbeddedFileDocumentHelper> embedded_files_;
444 470 std::unique_ptr<QPDFOutlineDocumentHelper> outlines_;
445 471 std::unique_ptr<QPDFPageDocumentHelper> page_dh_;
... ... @@ -1173,7 +1199,7 @@ class QPDF::Doc::Pages: Common
1173 1199 void flatten_annotations_for_page(
1174 1200 QPDFPageObjectHelper& page,
1175 1201 QPDFObjectHandle& resources,
1176   - QPDFAcroFormDocumentHelper& afdh,
  1202 + impl::AcroForm& afdh,
1177 1203 int required_flags,
1178 1204 int forbidden_flags);
1179 1205  
... ...
qpdf/test_driver.cc
... ... @@ -3567,7 +3567,6 @@ test_101(QPDF&amp; pdf, char const* arg2)
3567 3567 std::cout << oh.unparseResolved() << '\n';
3568 3568 }
3569 3569  
3570   -
3571 3570 auto test_helper_throws = [&qpdf](auto helper_func) {
3572 3571 bool thrown = false;
3573 3572 try {
... ...