Commit a139d2b36da39fbfb018ef6973e9316a64a4ca6c

Authored by Jay Berkenbilt
1 parent afb48d23

Add several methods for working with form XObjects (fixes #436)

Make some more methods in QPDFPageObjectHelper work with form
XObjects, provide forEach methods to walk through nested form
XObjects, possibly recursively. This should make it easier to work
with form XObjects from user code.
ChangeLog
  1 +2021-01-01 Jay Berkenbilt <ejb@ql.org>
  2 +
  3 + * Add methods to QPDFPageObjectHelper: forEachXObject,
  4 + forEachImage, forEachFormXObject to call a function on each
  5 + XObject (or image or form XObject) in a page or form XObject,
  6 + possibly recursing into nested form XObjects.
  7 +
  8 + * Add method QPDFPageObjectHelper::getFormXObjects to return a map
  9 + of keys to form XObjects (non-recursively) from a page or form
  10 + XObject.
  11 +
  12 + * Add method QPDFObjectHandle::isImage to test whether an object
  13 + is an image.
  14 +
1 15 2020-12-31 Jay Berkenbilt <ejb@ql.org>
2 16  
3 17 * QPDFPageObjectHelper::removeUnreferencedResources can now be
... ...
... ... @@ -8,7 +8,6 @@ Candidates for upcoming release
8 8 * #473: zsh completion with directories
9 9 * Investigate how serverless does completion
10 10 * Non-bugs
11   - * #436: parsing of document with form xobject
12 11 * #488: allow specification of jpeg compression quality; may also
13 12 need an option to recompress jpeg
14 13  
... ...
include/qpdf/QPDFObjectHandle.hh
... ... @@ -1150,6 +1150,11 @@ class QPDFObjectHandle
1150 1150 QPDF_DLL
1151 1151 bool isFormXObject();
1152 1152  
  1153 + // Indicate if this is an image. If exclude_imagemask is true,
  1154 + // don't count image masks as images.
  1155 + QPDF_DLL
  1156 + bool isImage(bool exclude_imagemask=true);
  1157 +
1153 1158 private:
1154 1159 QPDFObjectHandle(QPDF*, int objid, int generation);
1155 1160 QPDFObjectHandle(QPDFObject*);
... ...
include/qpdf/QPDFPageObjectHelper.hh
... ... @@ -72,11 +72,42 @@ class QPDFPageObjectHelper: public QPDFObjectHelper
72 72 QPDFObjectHandle
73 73 getMediaBox(bool copy_if_shared = false);
74 74  
  75 + // Iterate through XObjects, possibly recursing into form
  76 + // XObjects. This works with pages or form XObjects. Call action
  77 + // on each XObject for which selector, if specified, returns true.
  78 + // With no selector, calls action for every object. In addition to
  79 + // the object being passed to action, the containing XObject
  80 + // dictionary and key are passed in. Remember that the XObject
  81 + // dictionary may be shared, and the object may appear in multiple
  82 + // XObject dictionaries.
  83 + QPDF_DLL
  84 + void forEachXObject(
  85 + bool recursive,
  86 + std::function<void(QPDFObjectHandle& obj,
  87 + QPDFObjectHandle& xobj_dict,
  88 + std::string const& key)> action,
  89 + std::function<bool(QPDFObjectHandle)> selector=nullptr);
  90 + // Only call action for images
  91 + QPDF_DLL
  92 + void forEachImage(
  93 + bool recursive,
  94 + std::function<void(QPDFObjectHandle& obj,
  95 + QPDFObjectHandle& xobj_dict,
  96 + std::string const& key)> action);
  97 + // Only call action for form XObjects
  98 + QPDF_DLL
  99 + void forEachFormXObject(
  100 + bool recursive,
  101 + std::function<void(QPDFObjectHandle& obj,
  102 + QPDFObjectHandle& xobj_dict,
  103 + std::string const& key)> action);
  104 +
75 105 // Returns an empty map if there are no images or no resources.
76 106 // Prior to qpdf 8.4.0, this function did not support inherited
77 107 // resources, but it does now. Return value is a map from XObject
78 108 // name to the image object, which is always a stream. Works with
79   - // form XObjects as well as pages.
  109 + // form XObjects as well as pages. This method does not recurse
  110 + // into nested form XObjects. For that, use forEachImage.
80 111 QPDF_DLL
81 112 std::map<std::string, QPDFObjectHandle> getImages();
82 113  
... ... @@ -84,6 +115,14 @@ class QPDFPageObjectHelper: public QPDFObjectHelper
84 115 QPDF_DLL
85 116 std::map<std::string, QPDFObjectHandle> getPageImages();
86 117  
  118 + // Returns an empty map if there are no form XObjects or no
  119 + // resources. Otherwise, returns a map of keys to form XObjects
  120 + // directly referenced from this page or form XObjects. This does
  121 + // not recurse into nested form XObjects. For that, use
  122 + // forEachFormXObject.
  123 + QPDF_DLL
  124 + std::map<std::string, QPDFObjectHandle> getFormXObjects();
  125 +
87 126 // Convert each inline image to an external (normal) image if the
88 127 // size is at least the specified number of bytes.
89 128 QPDF_DLL
... ...
libqpdf/QPDFObjectHandle.cc
... ... @@ -2948,6 +2948,21 @@ QPDFObjectHandle::isFormXObject()
2948 2948 ("/Form" == dict.getKey("/Subtype").getName()));
2949 2949 }
2950 2950  
  2951 +bool
  2952 +QPDFObjectHandle::isImage(bool exclude_imagemask)
  2953 +{
  2954 + if (! this->isStream())
  2955 + {
  2956 + return false;
  2957 + }
  2958 + QPDFObjectHandle dict = this->getDict();
  2959 + return (dict.hasKey("/Subtype") &&
  2960 + (dict.getKey("/Subtype").getName() == "/Image") &&
  2961 + ((! exclude_imagemask) ||
  2962 + (! (dict.getKey("/ImageMask").isBool() &&
  2963 + dict.getKey("/ImageMask").getBoolValue()))));
  2964 +}
  2965 +
2951 2966 void
2952 2967 QPDFObjectHandle::assertPageObject()
2953 2968 {
... ...
libqpdf/QPDFPageObjectHelper.cc
... ... @@ -386,6 +386,73 @@ QPDFPageObjectHelper::getMediaBox(bool copy_if_shared)
386 386 return getAttribute("/MediaBox", copy_if_shared);
387 387 }
388 388  
  389 +void
  390 +QPDFPageObjectHelper::forEachXObject(
  391 + bool recursive,
  392 + std::function<void(QPDFObjectHandle& obj,
  393 + QPDFObjectHandle& xobj_dict,
  394 + std::string const& key)> action,
  395 + std::function<bool(QPDFObjectHandle)> selector)
  396 +{
  397 + QTC::TC("qpdf", "QPDFPageObjectHelper::forEachXObject",
  398 + recursive
  399 + ? (this->oh.isFormXObject() ? 0 : 1)
  400 + : (this->oh.isFormXObject() ? 2 : 3));
  401 + std::set<QPDFObjGen> seen;
  402 + std::list<QPDFPageObjectHelper> queue;
  403 + queue.push_back(*this);
  404 + while (! queue.empty())
  405 + {
  406 + QPDFPageObjectHelper ph = queue.front();
  407 + queue.pop_front();
  408 + QPDFObjGen og = ph.oh.getObjGen();
  409 + if (seen.count(og))
  410 + {
  411 + continue;
  412 + }
  413 + seen.insert(og);
  414 + QPDFObjectHandle resources = ph.getAttribute("/Resources", false);
  415 + if (resources.isDictionary() && resources.hasKey("/XObject"))
  416 + {
  417 + QPDFObjectHandle xobj_dict = resources.getKey("/XObject");
  418 + for (auto const& key: xobj_dict.getKeys())
  419 + {
  420 + QPDFObjectHandle obj = xobj_dict.getKey(key);
  421 + if ((! selector) || selector(obj))
  422 + {
  423 + action(obj, xobj_dict, key);
  424 + }
  425 + if (recursive && obj.isFormXObject())
  426 + {
  427 + queue.push_back(QPDFPageObjectHelper(obj));
  428 + }
  429 + }
  430 + }
  431 + }
  432 +}
  433 +
  434 +void
  435 +QPDFPageObjectHelper::forEachImage(
  436 + bool recursive,
  437 + std::function<void(QPDFObjectHandle& obj,
  438 + QPDFObjectHandle& xobj_dict,
  439 + std::string const& key)> action)
  440 +{
  441 + forEachXObject(recursive, action,
  442 + [](QPDFObjectHandle obj) { return obj.isImage(); });
  443 +}
  444 +
  445 +void
  446 +QPDFPageObjectHelper::forEachFormXObject(
  447 + bool recursive,
  448 + std::function<void(QPDFObjectHandle& obj,
  449 + QPDFObjectHandle& xobj_dict,
  450 + std::string const& key)> action)
  451 +{
  452 + forEachXObject(recursive, action,
  453 + [](QPDFObjectHandle obj) { return obj.isFormXObject(); });
  454 +}
  455 +
389 456 std::map<std::string, QPDFObjectHandle>
390 457 QPDFPageObjectHelper::getPageImages()
391 458 {
... ... @@ -396,32 +463,23 @@ std::map&lt;std::string, QPDFObjectHandle&gt;
396 463 QPDFPageObjectHelper::getImages()
397 464 {
398 465 std::map<std::string, QPDFObjectHandle> result;
399   - QPDFObjectHandle resources = getAttribute("/Resources", false);
400   - if (resources.isDictionary())
401   - {
402   - if (resources.hasKey("/XObject"))
403   - {
404   - QPDFObjectHandle xobject = resources.getKey("/XObject");
405   - std::set<std::string> keys = xobject.getKeys();
406   - for (std::set<std::string>::iterator iter = keys.begin();
407   - iter != keys.end(); ++iter)
408   - {
409   - std::string key = (*iter);
410   - QPDFObjectHandle value = xobject.getKey(key);
411   - if (value.isStream())
412   - {
413   - QPDFObjectHandle dict = value.getDict();
414   - if (dict.hasKey("/Subtype") &&
415   - (dict.getKey("/Subtype").getName() == "/Image") &&
416   - (! dict.hasKey("/ImageMask")))
417   - {
418   - result[key] = value;
419   - }
420   - }
421   - }
422   - }
423   - }
  466 + forEachImage(false, [&result](QPDFObjectHandle& obj,
  467 + QPDFObjectHandle&,
  468 + std::string const& key) {
  469 + result[key] = obj;
  470 + });
  471 + return result;
  472 +}
424 473  
  474 +std::map<std::string, QPDFObjectHandle>
  475 +QPDFPageObjectHelper::getFormXObjects()
  476 +{
  477 + std::map<std::string, QPDFObjectHandle> result;
  478 + forEachFormXObject(false, [&result](QPDFObjectHandle& obj,
  479 + QPDFObjectHandle&,
  480 + std::string const& key) {
  481 + result[key] = obj;
  482 + });
425 483 return result;
426 484 }
427 485  
... ...
manual/qpdf-manual.xml
... ... @@ -4859,6 +4859,60 @@ print &quot;\n&quot;;
4859 4859 </listitem>
4860 4860 <listitem>
4861 4861 <para>
  4862 + Add methods to <classname>QPDFPageObjectHelper</classname>
  4863 + to iterate through XObjects on a page or form XObjects,
  4864 + possibly recursing into nested form XObjects:
  4865 + <function>forEachXObject</function>,
  4866 + <function>ForEachImage</function>,
  4867 + <function>forEachFormXObject</function>.
  4868 + </para>
  4869 + </listitem>
  4870 + <listitem>
  4871 + <para>
  4872 + Enhance several methods in
  4873 + <classname>QPDFPageObjectHelper</classname> to work with
  4874 + form XObjects as well as pages, as noted in comments. See
  4875 + <filename>ChangeLog</filename> for a full list.
  4876 + </para>
  4877 + </listitem>
  4878 + <listitem>
  4879 + <para>
  4880 + Rename some functions in
  4881 + <classname>QPDFPageObjectHelper</classname>, while keeping
  4882 + old names for compatibility:
  4883 + <itemizedlist>
  4884 + <listitem>
  4885 + <para>
  4886 + <function>getPageImages</function> to
  4887 + <function>getImages</function>
  4888 + </para>
  4889 + </listitem>
  4890 + <listitem>
  4891 + <para>
  4892 + <function>filterPageContents</function> to
  4893 + <function>filterContents</function>
  4894 + </para>
  4895 + </listitem>
  4896 + </itemizedlist>
  4897 + </para>
  4898 + </listitem>
  4899 + <listitem>
  4900 + <para>
  4901 + Add method
  4902 + <function>QPDFPageObjectHelper::getFormXObjects</function>
  4903 + to return a map of form XObjects directly on a page or form
  4904 + XObject
  4905 + </para>
  4906 + </listitem>
  4907 + <listitem>
  4908 + <para>
  4909 + Add new helper methods to
  4910 + <classname>QPDFObjectHandle</classname>:
  4911 + <function>isFormXObject</function>, <function>isImage</function>
  4912 + </para>
  4913 + </listitem>
  4914 + <listitem>
  4915 + <para>
4862 4916 Add the optional <function>allow_streams</function>
4863 4917 parameter <function>QPDFObjectHandle::makeDirect</function>.
4864 4918 When <function>QPDFObjectHandle::makeDirect</function> is
... ...
qpdf/qpdf.testcov
... ... @@ -521,3 +521,4 @@ qpdf-c called qpdf_oh_unparse 0
521 521 qpdf-c called qpdf_oh_unparse_resolved 0
522 522 qpdf-c called qpdf_oh_unparse_binary 0
523 523 QPDFWriter getFilterOnWrite false 0
  524 +QPDFPageObjectHelper::forEachXObject 3
... ...
qpdf/qtest/qpdf.test
... ... @@ -423,7 +423,7 @@ foreach my $i (@choice_values)
423 423 show_ntests();
424 424 # ----------
425 425 $td->notify("--- Form XObject, underlay, overlay ---");
426   -$n_tests += 18;
  426 +$n_tests += 19;
427 427  
428 428 $td->runtest("form xobject creation",
429 429 {$td->COMMAND => "test_driver 55 fxo-red.pdf"},
... ... @@ -486,6 +486,11 @@ for (my $i = 1; $i &lt;= scalar(@uo_cases); ++$i)
486 486 {$td->FILE => "a.pdf"},
487 487 {$td->FILE => "$outbase.pdf"});
488 488 }
  489 +$td->runtest("foreach",
  490 + {$td->COMMAND => "test_driver 71 nested-form-xobjects.pdf"},
  491 + {$td->FILE => "nested-form-xobjects.out",
  492 + $td->EXIT_STATUS => 0},
  493 + $td->NORMALIZE_NEWLINES);
489 494  
490 495 show_ntests();
491 496 # ----------
... ...
qpdf/qtest/qpdf/nested-form-xobjects.out 0 โ†’ 100644
  1 +--- recursive, all ---
  2 +<< /Fx1 12 0 R /Im1 14 0 R /Im2 16 0 R >> -> /Fx1 -> 12 0 R
  3 +<< /Fx1 12 0 R /Im1 14 0 R /Im2 16 0 R >> -> /Im1 -> 14 0 R
  4 +<< /Fx1 12 0 R /Im1 14 0 R /Im2 16 0 R >> -> /Im2 -> 16 0 R
  5 +<< /Fx1 20 0 R /Im1 22 0 R /Im2 24 0 R >> -> /Fx1 -> 20 0 R
  6 +<< /Fx1 20 0 R /Im1 22 0 R /Im2 24 0 R >> -> /Im1 -> 22 0 R
  7 +<< /Fx1 20 0 R /Im1 22 0 R /Im2 24 0 R >> -> /Im2 -> 24 0 R
  8 +<< /Im1 26 0 R /Im2 28 0 R >> -> /Im1 -> 26 0 R
  9 +<< /Im1 26 0 R /Im2 28 0 R >> -> /Im2 -> 28 0 R
  10 +--- non-recursive, all ---
  11 +<< /Fx1 12 0 R /Im1 14 0 R /Im2 16 0 R >> -> /Fx1 -> 12 0 R
  12 +<< /Fx1 12 0 R /Im1 14 0 R /Im2 16 0 R >> -> /Im1 -> 14 0 R
  13 +<< /Fx1 12 0 R /Im1 14 0 R /Im2 16 0 R >> -> /Im2 -> 16 0 R
  14 +--- recursive, images ---
  15 +<< /Fx1 12 0 R /Im1 14 0 R /Im2 16 0 R >> -> /Im1 -> 14 0 R
  16 +<< /Fx1 12 0 R /Im1 14 0 R /Im2 16 0 R >> -> /Im2 -> 16 0 R
  17 +<< /Fx1 20 0 R /Im1 22 0 R /Im2 24 0 R >> -> /Im1 -> 22 0 R
  18 +<< /Fx1 20 0 R /Im1 22 0 R /Im2 24 0 R >> -> /Im2 -> 24 0 R
  19 +<< /Im1 26 0 R /Im2 28 0 R >> -> /Im1 -> 26 0 R
  20 +<< /Im1 26 0 R /Im2 28 0 R >> -> /Im2 -> 28 0 R
  21 +--- non-recursive, images ---
  22 +<< /Fx1 12 0 R /Im1 14 0 R /Im2 16 0 R >> -> /Im1 -> 14 0 R
  23 +<< /Fx1 12 0 R /Im1 14 0 R /Im2 16 0 R >> -> /Im2 -> 16 0 R
  24 +--- recursive, form XObjects ---
  25 +<< /Fx1 12 0 R /Im1 14 0 R /Im2 16 0 R >> -> /Fx1 -> 12 0 R
  26 +<< /Fx1 20 0 R /Im1 22 0 R /Im2 24 0 R >> -> /Fx1 -> 20 0 R
  27 +--- non-recursive, form XObjects ---
  28 +<< /Fx1 12 0 R /Im1 14 0 R /Im2 16 0 R >> -> /Fx1 -> 12 0 R
  29 +--- recursive, all, from fx1 ---
  30 +<< /Fx1 20 0 R /Im1 22 0 R /Im2 24 0 R >> -> /Fx1 -> 20 0 R
  31 +<< /Fx1 20 0 R /Im1 22 0 R /Im2 24 0 R >> -> /Im1 -> 22 0 R
  32 +<< /Fx1 20 0 R /Im1 22 0 R /Im2 24 0 R >> -> /Im2 -> 24 0 R
  33 +<< /Im1 26 0 R /Im2 28 0 R >> -> /Im1 -> 26 0 R
  34 +<< /Im1 26 0 R /Im2 28 0 R >> -> /Im2 -> 28 0 R
  35 +--- non-recursive, all, from fx1 ---
  36 +<< /Fx1 20 0 R /Im1 22 0 R /Im2 24 0 R >> -> /Fx1 -> 20 0 R
  37 +<< /Fx1 20 0 R /Im1 22 0 R /Im2 24 0 R >> -> /Im1 -> 22 0 R
  38 +<< /Fx1 20 0 R /Im1 22 0 R /Im2 24 0 R >> -> /Im2 -> 24 0 R
  39 +--- get images, page ---
  40 +/Im1 -> 14 0 R
  41 +/Im2 -> 16 0 R
  42 +--- get images, fx ---
  43 +/Im1 -> 22 0 R
  44 +/Im2 -> 24 0 R
  45 +--- get form XObjects, page ---
  46 +/Fx1 -> 12 0 R
  47 +--- get form XObjects, fx ---
  48 +/Fx1 -> 20 0 R
  49 +test 71 done
... ...
qpdf/qtest/qpdf/nested-form-xobjects.pdf 0 โ†’ 100644
  1 +%PDF-1.3
  2 +%ยฟรทยขรพ
  3 +%QDF-1.0
  4 +
  5 +1 0 obj
  6 +<<
  7 + /Pages 2 0 R
  8 + /Type /Catalog
  9 +>>
  10 +endobj
  11 +
  12 +2 0 obj
  13 +<<
  14 + /Count 1
  15 + /Kids [
  16 + 3 0 R
  17 + ]
  18 + /Type /Pages
  19 +>>
  20 +endobj
  21 +
  22 +%% Page 1
  23 +3 0 obj
  24 +<<
  25 + /Contents [
  26 + 4 0 R
  27 + 6 0 R
  28 + 8 0 R
  29 + ]
  30 + /MediaBox [
  31 + 0
  32 + 0
  33 + 612
  34 + 792
  35 + ]
  36 + /Parent 2 0 R
  37 + /Resources <<
  38 + /Font <<
  39 + /F1 10 0 R
  40 + >>
  41 + /ProcSet 11 0 R
  42 + /XObject <<
  43 + /Fx1 12 0 R
  44 + /Im1 14 0 R
  45 + /Im2 16 0 R
  46 + >>
  47 + >>
  48 + /Type /Page
  49 +>>
  50 +endobj
  51 +
  52 +%% Contents for page 1
  53 +4 0 obj
  54 +<<
  55 + /Length 5 0 R
  56 +>>
  57 +stream
  58 +q
  59 +endstream
  60 +endobj
  61 +
  62 +5 0 obj
  63 +2
  64 +endobj
  65 +
  66 +%% Contents for page 1
  67 +6 0 obj
  68 +<<
  69 + /Length 7 0 R
  70 +>>
  71 +stream
  72 +BT
  73 + /F1 24 Tf
  74 + 72 720 Td
  75 + (Page) Tj
  76 +ET
  77 +q
  78 +100 0 0 100 72 600 cm
  79 +/Im1 Do
  80 +Q
  81 +q
  82 +100 0 0 100 192 600 cm
  83 +/Im2 Do
  84 +Q
  85 +endstream
  86 +endobj
  87 +
  88 +7 0 obj
  89 +111
  90 +endobj
  91 +
  92 +%% Contents for page 1
  93 +8 0 obj
  94 +<<
  95 + /Length 9 0 R
  96 +>>
  97 +stream
  98 +
  99 +Q
  100 +q
  101 +1.00000 0.00000 0.00000 1.00000 72.00000 200.00000 cm
  102 +/Fx1 Do
  103 +Q
  104 +endstream
  105 +endobj
  106 +
  107 +9 0 obj
  108 +69
  109 +endobj
  110 +
  111 +10 0 obj
  112 +<<
  113 + /BaseFont /Helvetica
  114 + /Encoding /WinAnsiEncoding
  115 + /Name /F1
  116 + /Subtype /Type1
  117 + /Type /Font
  118 +>>
  119 +endobj
  120 +
  121 +11 0 obj
  122 +[
  123 + /PDF
  124 + /Text
  125 + /ImageC
  126 +]
  127 +endobj
  128 +
  129 +12 0 obj
  130 +<<
  131 + /BBox [
  132 + 0
  133 + 0
  134 + 300
  135 + 500
  136 + ]
  137 + /Resources <<
  138 + /Font <<
  139 + /F1 18 0 R
  140 + >>
  141 + /ProcSet 19 0 R
  142 + /XObject <<
  143 + /Fx1 20 0 R
  144 + /Im1 22 0 R
  145 + /Im2 24 0 R
  146 + >>
  147 + >>
  148 + /Subtype /Form
  149 + /Type /XObject
  150 + /Length 13 0 R
  151 +>>
  152 +stream
  153 +BT
  154 + /F1 24 Tf
  155 + 0 320 Td
  156 + (FX1) Tj
  157 +ET
  158 +q
  159 +100 0 0 100 000 200 cm
  160 +/Im1 Do
  161 +Q
  162 +q
  163 +100 0 0 100 120 200 cm
  164 +/Im2 Do
  165 +Q
  166 +q
  167 +1.00000 0.00000 0.00000 1.00000 0.00000 0.00000 cm
  168 +/Fx1 Do
  169 +Q
  170 +endstream
  171 +endobj
  172 +
  173 +13 0 obj
  174 +173
  175 +endobj
  176 +
  177 +14 0 obj
  178 +<<
  179 + /BitsPerComponent 8
  180 + /ColorSpace /DeviceGray
  181 + /Height 15
  182 + /Subtype /Image
  183 + /Type /XObject
  184 + /Width 15
  185 + /Length 15 0 R
  186 +>>
  187 +stream
  188 +`````@@@@@@@@@@`````@@@@@@@@@@`````@@@@@@@@@@`````@@@@@@@@@@`````@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
  189 +endstream
  190 +endobj
  191 +
  192 +%QDF: ignore_newline
  193 +15 0 obj
  194 +225
  195 +endobj
  196 +
  197 +16 0 obj
  198 +<<
  199 + /BitsPerComponent 8
  200 + /ColorSpace /DeviceGray
  201 + /Height 15
  202 + /Subtype /Image
  203 + /Type /XObject
  204 + /Width 15
  205 + /Length 17 0 R
  206 +>>
  207 +stream
  208 +@@@@@`````@@@@@@@@@@`````@@@@@@@@@@`````@@@@@@@@@@`````@@@@@@@@@@`````@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
  209 +endstream
  210 +endobj
  211 +
  212 +%QDF: ignore_newline
  213 +17 0 obj
  214 +225
  215 +endobj
  216 +
  217 +18 0 obj
  218 +<<
  219 + /BaseFont /Helvetica
  220 + /Encoding /WinAnsiEncoding
  221 + /Name /F1
  222 + /Subtype /Type1
  223 + /Type /Font
  224 +>>
  225 +endobj
  226 +
  227 +19 0 obj
  228 +[
  229 + /PDF
  230 + /Text
  231 + /ImageC
  232 +]
  233 +endobj
  234 +
  235 +20 0 obj
  236 +<<
  237 + /BBox [
  238 + 0
  239 + 0
  240 + 300
  241 + 200
  242 + ]
  243 + /Resources <<
  244 + /Font <<
  245 + /F1 18 0 R
  246 + >>
  247 + /ProcSet 19 0 R
  248 + /XObject <<
  249 + /Im1 26 0 R
  250 + /Im2 28 0 R
  251 + >>
  252 + >>
  253 + /Subtype /Form
  254 + /Type /XObject
  255 + /Length 21 0 R
  256 +>>
  257 +stream
  258 +BT
  259 + /F1 24 Tf
  260 + 0 120 Td
  261 + (FX2) Tj
  262 +ET
  263 +q
  264 +100 0 0 100 0 0 cm
  265 +/Im1 Do
  266 +Q
  267 +q
  268 +100 0 0 100 120 0 cm
  269 +/Im2 Do
  270 +Q
  271 +endstream
  272 +endobj
  273 +
  274 +21 0 obj
  275 +104
  276 +endobj
  277 +
  278 +22 0 obj
  279 +<<
  280 + /BitsPerComponent 8
  281 + /ColorSpace /DeviceGray
  282 + /Height 15
  283 + /Subtype /Image
  284 + /Type /XObject
  285 + /Width 15
  286 + /Length 23 0 R
  287 +>>
  288 +stream
  289 +@@@@@@@@@@`````@@@@@@@@@@`````@@@@@@@@@@`````@@@@@@@@@@`````@@@@@@@@@@`````@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
  290 +endstream
  291 +endobj
  292 +
  293 +%QDF: ignore_newline
  294 +23 0 obj
  295 +225
  296 +endobj
  297 +
  298 +24 0 obj
  299 +<<
  300 + /BitsPerComponent 8
  301 + /ColorSpace /DeviceGray
  302 + /Height 15
  303 + /Subtype /Image
  304 + /Type /XObject
  305 + /Width 15
  306 + /Length 25 0 R
  307 +>>
  308 +stream
  309 +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@`````@@@@@@@@@@`````@@@@@@@@@@`````@@@@@@@@@@`````@@@@@@@@@@`````@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
  310 +endstream
  311 +endobj
  312 +
  313 +%QDF: ignore_newline
  314 +25 0 obj
  315 +225
  316 +endobj
  317 +
  318 +26 0 obj
  319 +<<
  320 + /BitsPerComponent 8
  321 + /ColorSpace /DeviceGray
  322 + /Height 15
  323 + /Subtype /Image
  324 + /Type /XObject
  325 + /Width 15
  326 + /Length 27 0 R
  327 +>>
  328 +stream
  329 +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@`````@@@@@@@@@@`````@@@@@@@@@@`````@@@@@@@@@@`````@@@@@@@@@@`````@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
  330 +endstream
  331 +endobj
  332 +
  333 +%QDF: ignore_newline
  334 +27 0 obj
  335 +225
  336 +endobj
  337 +
  338 +28 0 obj
  339 +<<
  340 + /BitsPerComponent 8
  341 + /ColorSpace /DeviceGray
  342 + /Height 15
  343 + /Subtype /Image
  344 + /Type /XObject
  345 + /Width 15
  346 + /Length 29 0 R
  347 +>>
  348 +stream
  349 +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@`````@@@@@@@@@@`````@@@@@@@@@@`````@@@@@@@@@@`````@@@@@@@@@@`````@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
  350 +endstream
  351 +endobj
  352 +
  353 +%QDF: ignore_newline
  354 +29 0 obj
  355 +225
  356 +endobj
  357 +
  358 +xref
  359 +0 30
  360 +0000000000 65535 f
  361 +0000000025 00000 n
  362 +0000000079 00000 n
  363 +0000000161 00000 n
  364 +0000000485 00000 n
  365 +0000000542 00000 n
  366 +0000000583 00000 n
  367 +0000000749 00000 n
  368 +0000000792 00000 n
  369 +0000000916 00000 n
  370 +0000000935 00000 n
  371 +0000001054 00000 n
  372 +0000001100 00000 n
  373 +0000001561 00000 n
  374 +0000001582 00000 n
  375 +0000001994 00000 n
  376 +0000002015 00000 n
  377 +0000002427 00000 n
  378 +0000002448 00000 n
  379 +0000002567 00000 n
  380 +0000002613 00000 n
  381 +0000002987 00000 n
  382 +0000003008 00000 n
  383 +0000003420 00000 n
  384 +0000003441 00000 n
  385 +0000003853 00000 n
  386 +0000003874 00000 n
  387 +0000004286 00000 n
  388 +0000004307 00000 n
  389 +0000004719 00000 n
  390 +trailer <<
  391 + /Root 1 0 R
  392 + /Size 30
  393 + /ID [<55269d37282af9edc76855e4cb859987><633b13d949c2f1d115e24c686f36e362>]
  394 +>>
  395 +startxref
  396 +4740
  397 +%%EOF
... ...
qpdf/test_driver.cc
... ... @@ -2228,6 +2228,57 @@ void runtest(int n, char const* filename1, char const* arg2)
2228 2228 w.setDecodeLevel(qpdf_dl_specialized);
2229 2229 w.write();
2230 2230 }
  2231 + else if (n == 71)
  2232 + {
  2233 + auto show = [](QPDFObjectHandle& obj,
  2234 + QPDFObjectHandle& xobj_dict,
  2235 + std::string const& key) {
  2236 + std::cout << xobj_dict.unparse() << " -> "
  2237 + << key << " -> " << obj.unparse() << std::endl;
  2238 + };
  2239 + auto page = QPDFPageDocumentHelper(pdf).getAllPages().at(0);
  2240 + std::cout << "--- recursive, all ---" << std::endl;
  2241 + page.forEachXObject(true, show);
  2242 + std::cout << "--- non-recursive, all ---" << std::endl;
  2243 + page.forEachXObject(false, show);
  2244 + std::cout << "--- recursive, images ---" << std::endl;
  2245 + page.forEachImage(true, show);
  2246 + std::cout << "--- non-recursive, images ---" << std::endl;
  2247 + page.forEachImage(false, show);
  2248 + std::cout << "--- recursive, form XObjects ---" << std::endl;
  2249 + page.forEachFormXObject(true, show);
  2250 + std::cout << "--- non-recursive, form XObjects ---" << std::endl;
  2251 + page.forEachFormXObject(false, show);
  2252 + auto fx1 = QPDFPageObjectHelper(
  2253 + page.getObjectHandle()
  2254 + .getKey("/Resources")
  2255 + .getKey("/XObject")
  2256 + .getKey("/Fx1"));
  2257 + std::cout << "--- recursive, all, from fx1 ---" << std::endl;
  2258 + fx1.forEachXObject(true, show);
  2259 + std::cout << "--- non-recursive, all, from fx1 ---" << std::endl;
  2260 + fx1.forEachXObject(false, show);
  2261 + std::cout << "--- get images, page ---" << std::endl;
  2262 + for (auto& i: page.getImages())
  2263 + {
  2264 + std::cout << i.first << " -> " << i.second.unparse() << std::endl;
  2265 + }
  2266 + std::cout << "--- get images, fx ---" << std::endl;
  2267 + for (auto& i: fx1.getImages())
  2268 + {
  2269 + std::cout << i.first << " -> " << i.second.unparse() << std::endl;
  2270 + }
  2271 + std::cout << "--- get form XObjects, page ---" << std::endl;
  2272 + for (auto& i: page.getFormXObjects())
  2273 + {
  2274 + std::cout << i.first << " -> " << i.second.unparse() << std::endl;
  2275 + }
  2276 + std::cout << "--- get form XObjects, fx ---" << std::endl;
  2277 + for (auto& i: fx1.getFormXObjects())
  2278 + {
  2279 + std::cout << i.first << " -> " << i.second.unparse() << std::endl;
  2280 + }
  2281 + }
2231 2282 else
2232 2283 {
2233 2284 throw std::runtime_error(std::string("invalid test ") +
... ...