Commit 8a217eb3a26931453b4f003c6c18ad8569230cf1

Authored by Jay Berkenbilt
1 parent af64668a

Add concept of reserved objects

QPDFObjectHandle::{new,is,assert}Reserved, QPDF::replaceReserved
provide a mechanism to add objects to a PDF file when there are
circular references.  This is a prerequisite to copying objects from
one PDF to another.
ChangeLog
  1 +2012-07-08 Jay Berkenbilt <ejb@ql.org>
  2 +
  3 + * Add QPDFObjectHandle::newReserved to create a reserved object
  4 + and QPDF::replaceReserved to replace it with a real object.
  5 + QPDFObjectHandle::newReserved reserves an object ID in a QPDF
  6 + object and ensures that any references to it remain unresolved.
  7 + When QPDF::replaceReserved is later called, previous references to
  8 + the reserved object will properly resolve to the replaced object.
  9 +
1 10 2012-07-07 Jay Berkenbilt <ejb@ql.org>
2 11  
3 12 * NOTE: BREAKING API CHANGE. Remove previously required length
... ...
include/qpdf/QPDF.hh
... ... @@ -161,7 +161,8 @@ class QPDF
161 161 // be associated with the PDF file. Note that replacing an object
162 162 // with QPDFObjectHandle::newNull() effectively removes the object
163 163 // from the file since a non-existent object is treated as a null
164   - // object.
  164 + // object. To replace a reserved object, call replaceReserved
  165 + // instead.
165 166 QPDF_DLL
166 167 void replaceObject(int objid, int generation, QPDFObjectHandle);
167 168  
... ... @@ -180,6 +181,15 @@ class QPDF
180 181 void swapObjects(int objid1, int generation1,
181 182 int objid2, int generation2);
182 183  
  184 + // Replace a reserved object. This is a wrapper around
  185 + // replaceObject but it guarantees that the underlying object is a
  186 + // reserved object. After this call, reserved will be a reference
  187 + // to replacement.
  188 + QPDF_DLL
  189 + void
  190 + replaceReserved(QPDFObjectHandle reserved,
  191 + QPDFObjectHandle replacement);
  192 +
183 193 // Encryption support
184 194  
185 195 enum encryption_method_e { e_none, e_unknown, e_rc4, e_aes };
... ...
include/qpdf/QPDFObjectHandle.hh
... ... @@ -81,6 +81,8 @@ class QPDFObjectHandle
81 81 bool isDictionary();
82 82 QPDF_DLL
83 83 bool isStream();
  84 + QPDF_DLL
  85 + bool isReserved();
84 86  
85 87 // This returns true in addition to the query for the specific
86 88 // type for indirect objects.
... ... @@ -148,6 +150,24 @@ class QPDFObjectHandle
148 150 QPDF_DLL
149 151 static QPDFObjectHandle newStream(QPDF* qpdf, std::string const& data);
150 152  
  153 + // A reserved object is a special sentinel used for qpdf to
  154 + // reserve a spot for an object that is going to be added to the
  155 + // QPDF object. Normally you don't have to use this type since
  156 + // you can just call QPDF::makeIndirectObject. However, in some
  157 + // cases, if you have to create objects with circular references,
  158 + // you may need to create a reserved object so that you can have a
  159 + // reference to it and then replace the object later. Reserved
  160 + // objects have the special property that they can't be resolved
  161 + // to direct objects. This makes it possible to replace a
  162 + // reserved object with a new object while preserving existing
  163 + // references to them. When you are ready to replace a reserved
  164 + // object with its replacement, use QPDF::replaceReserved for this
  165 + // purpose rather than the more general QPDF::replaceObject. It
  166 + // is an error to try to write a QPDF with QPDFWriter if it has
  167 + // any reserved objects in it.
  168 + QPDF_DLL
  169 + static QPDFObjectHandle newReserved(QPDF* qpdf);
  170 +
151 171 // Accessor methods. If an accessor method that is valid for only
152 172 // a particular object type is called on an object of the wrong
153 173 // type, an exception is thrown.
... ... @@ -430,6 +450,8 @@ class QPDFObjectHandle
430 450 void assertDictionary();
431 451 QPDF_DLL
432 452 void assertStream();
  453 + QPDF_DLL
  454 + void assertReserved();
433 455  
434 456 QPDF_DLL
435 457 void assertScalar();
... ... @@ -459,6 +481,7 @@ class QPDFObjectHandle
459 481 int objid; // 0 for direct object
460 482 int generation;
461 483 PointerHolder<QPDFObject> obj;
  484 + bool reserved;
462 485 };
463 486  
464 487 #endif // __QPDFOBJECTHANDLE_HH__
... ...
libqpdf/QPDF.cc
... ... @@ -2057,6 +2057,18 @@ QPDF::replaceObject(int objid, int generation, QPDFObjectHandle oh)
2057 2057 }
2058 2058  
2059 2059 void
  2060 +QPDF::replaceReserved(QPDFObjectHandle reserved,
  2061 + QPDFObjectHandle replacement)
  2062 +{
  2063 + QTC::TC("qpdf", "QPDF replaceReserved");
  2064 + reserved.assertReserved();
  2065 + replaceObject(reserved.getObjectID(),
  2066 + reserved.getGeneration(),
  2067 + replacement);
  2068 +}
  2069 +
  2070 +
  2071 +void
2060 2072 QPDF::swapObjects(int objid1, int generation1, int objid2, int generation2)
2061 2073 {
2062 2074 // Force objects to be loaded into cache; then swap them in the
... ...
libqpdf/QPDFObjectHandle.cc
... ... @@ -10,6 +10,7 @@
10 10 #include <qpdf/QPDF_Array.hh>
11 11 #include <qpdf/QPDF_Dictionary.hh>
12 12 #include <qpdf/QPDF_Stream.hh>
  13 +#include <qpdf/QPDF_Reserved.hh>
13 14  
14 15 #include <qpdf/QTC.hh>
15 16 #include <qpdf/QUtil.hh>
... ... @@ -20,7 +21,8 @@
20 21 QPDFObjectHandle::QPDFObjectHandle() :
21 22 initialized(false),
22 23 objid(0),
23   - generation(0)
  24 + generation(0),
  25 + reserved(false)
24 26 {
25 27 }
26 28  
... ... @@ -28,7 +30,8 @@ QPDFObjectHandle::QPDFObjectHandle(QPDF* qpdf, int objid, int generation) :
28 30 initialized(true),
29 31 qpdf(qpdf),
30 32 objid(objid),
31   - generation(generation)
  33 + generation(generation),
  34 + reserved(false)
32 35 {
33 36 }
34 37  
... ... @@ -37,7 +40,8 @@ QPDFObjectHandle::QPDFObjectHandle(QPDFObject* data) :
37 40 qpdf(0),
38 41 objid(0),
39 42 generation(0),
40   - obj(data)
  43 + obj(data),
  44 + reserved(false)
41 45 {
42 46 }
43 47  
... ... @@ -166,6 +170,14 @@ QPDFObjectHandle::isStream()
166 170 }
167 171  
168 172 bool
  173 +QPDFObjectHandle::isReserved()
  174 +{
  175 + // dereference will clear reserved if this has been replaced
  176 + dereference();
  177 + return this->reserved;
  178 +}
  179 +
  180 +bool
169 181 QPDFObjectHandle::isIndirect()
170 182 {
171 183 assertInitialized();
... ... @@ -568,6 +580,11 @@ QPDFObjectHandle::unparse()
568 580 std::string
569 581 QPDFObjectHandle::unparseResolved()
570 582 {
  583 + if (this->reserved)
  584 + {
  585 + throw std::logic_error(
  586 + "QPDFObjectHandle: attempting to unparse a reserved object");
  587 + }
571 588 dereference();
572 589 return this->obj->unparse();
573 590 }
... ... @@ -690,6 +707,19 @@ QPDFObjectHandle::newStream(QPDF* qpdf, std::string const&amp; data)
690 707 }
691 708  
692 709 QPDFObjectHandle
  710 +QPDFObjectHandle::newReserved(QPDF* qpdf)
  711 +{
  712 + // Reserve a spot for this object by assigning it an object
  713 + // number, but then return an unresolved handle to the object.
  714 + QPDFObjectHandle reserved = qpdf->makeIndirectObject(
  715 + QPDFObjectHandle(new QPDF_Reserved()));
  716 + QPDFObjectHandle result =
  717 + newIndirect(qpdf, reserved.objid, reserved.generation);
  718 + result.reserved = true;
  719 + return result;
  720 +}
  721 +
  722 +QPDFObjectHandle
693 723 QPDFObjectHandle::shallowCopy()
694 724 {
695 725 assertInitialized();
... ... @@ -746,6 +776,13 @@ QPDFObjectHandle::makeDirectInternal(std::set&lt;int&gt;&amp; visited)
746 776 visited.insert(cur_objid);
747 777 }
748 778  
  779 + if (isReserved())
  780 + {
  781 + throw std::logic_error(
  782 + "QPDFObjectHandle: attempting to make a"
  783 + " reserved object handle direct");
  784 + }
  785 +
749 786 dereference();
750 787 this->objid = 0;
751 788 this->generation = 0;
... ... @@ -903,6 +940,12 @@ QPDFObjectHandle::assertStream()
903 940 }
904 941  
905 942 void
  943 +QPDFObjectHandle::assertReserved()
  944 +{
  945 + assertType("Reserved", isReserved());
  946 +}
  947 +
  948 +void
906 949 QPDFObjectHandle::assertScalar()
907 950 {
908 951 assertType("Scalar", isScalar());
... ... @@ -929,12 +972,21 @@ QPDFObjectHandle::dereference()
929 972 {
930 973 if (this->obj.getPointer() == 0)
931 974 {
932   - this->obj = QPDF::Resolver::resolve(
  975 + PointerHolder<QPDFObject> obj = QPDF::Resolver::resolve(
933 976 this->qpdf, this->objid, this->generation);
934   - if (this->obj.getPointer() == 0)
  977 + if (obj.getPointer() == 0)
935 978 {
936 979 QTC::TC("qpdf", "QPDFObjectHandle indirect to unknown");
937 980 this->obj = new QPDF_Null();
938 981 }
  982 + else if (dynamic_cast<QPDF_Reserved*>(obj.getPointer()))
  983 + {
  984 + // Do not resolve
  985 + }
  986 + else
  987 + {
  988 + this->reserved = false;
  989 + this->obj = obj;
  990 + }
939 991 }
940 992 }
... ...
libqpdf/QPDF_Reserved.cc 0 โ†’ 100644
  1 +#include <qpdf/QPDF_Reserved.hh>
  2 +#include <stdexcept>
  3 +
  4 +QPDF_Reserved::~QPDF_Reserved()
  5 +{
  6 +}
  7 +
  8 +std::string
  9 +QPDF_Reserved::unparse()
  10 +{
  11 + throw std::logic_error("attempt to unparse QPDF_Reserved");
  12 + return "";
  13 +}
... ...
libqpdf/build.mk
... ... @@ -39,6 +39,7 @@ SRCS_libqpdf = \
39 39 libqpdf/QPDF_Name.cc \
40 40 libqpdf/QPDF_Null.cc \
41 41 libqpdf/QPDF_Real.cc \
  42 + libqpdf/QPDF_Reserved.cc \
42 43 libqpdf/QPDF_Stream.cc \
43 44 libqpdf/QPDF_String.cc \
44 45 libqpdf/QPDF_encryption.cc \
... ...
libqpdf/qpdf/QPDF_Reserved.hh 0 โ†’ 100644
  1 +#ifndef __QPDF_RESERVED_HH__
  2 +#define __QPDF_RESERVED_HH__
  3 +
  4 +#include <qpdf/QPDFObject.hh>
  5 +
  6 +class QPDF_Reserved: public QPDFObject
  7 +{
  8 + public:
  9 + virtual ~QPDF_Reserved();
  10 + virtual std::string unparse();
  11 +};
  12 +
  13 +#endif // __QPDF_RESERVED_HH__
... ...
qpdf/qpdf.testcov
... ... @@ -217,3 +217,4 @@ QPDFObjectHandle newStream with string 0
217 217 QPDF unknown key not inherited 0
218 218 QPDF_Stream provider length not provided 0
219 219 QPDF_Stream unknown stream length 0
  220 +QPDF replaceReserved 0
... ...
qpdf/qtest/qpdf.test
... ... @@ -149,7 +149,7 @@ $td-&gt;runtest(&quot;remove page we don&#39;t have&quot;,
149 149 $td->NORMALIZE_NEWLINES);
150 150 # ----------
151 151 $td->notify("--- Miscellaneous Tests ---");
152   -$n_tests += 41;
  152 +$n_tests += 43;
153 153  
154 154 $td->runtest("qpdf version",
155 155 {$td->COMMAND => "qpdf --version"},
... ... @@ -358,6 +358,13 @@ $td-&gt;runtest(&quot;warn for unknown key in Pages&quot;,
358 358 {$td->COMMAND => "test_driver 23 lin-special.pdf"},
359 359 {$td->FILE => "pages-warning.out", $td->EXIT_STATUS => 0},
360 360 $td->NORMALIZE_NEWLINES);
  361 +$td->runtest("reserved objects",
  362 + {$td->COMMAND => "test_driver 24 minimal.pdf"},
  363 + {$td->FILE => "reserved-objects.out", $td->EXIT_STATUS => 0},
  364 + $td->NORMALIZE_NEWLINES);
  365 +$td->runtest("check output",
  366 + {$td->FILE => "a.pdf"},
  367 + {$td->FILE => "reserved-objects.pdf"});
361 368  
362 369 show_ntests();
363 370 # ----------
... ...
qpdf/qtest/qpdf/reserved-objects.out 0 โ†’ 100644
  1 +res1 is still reserved after checking if array
  2 +res1 is no longer reserved
  3 +res1 is an array
  4 +logic error: QPDFObjectHandle: attempting to unparse a reserved object
  5 +logic error: QPDFObjectHandle: attempting to make a reserved object handle direct
  6 +res2 is an array
  7 +circular access and lazy resolution worked
  8 +test 24 done
... ...
qpdf/qtest/qpdf/reserved-objects.pdf 0 โ†’ 100644
  1 +%PDF-1.3
  2 +%ยฟรทยขรพ
  3 +1 0 obj
  4 +<< /Pages 4 0 R /Type /Catalog >>
  5 +endobj
  6 +2 0 obj
  7 +[ 3 0 R 1 ]
  8 +endobj
  9 +3 0 obj
  10 +[ 2 0 R 2 ]
  11 +endobj
  12 +4 0 obj
  13 +<< /Count 1 /Kids [ 5 0 R ] /Type /Pages >>
  14 +endobj
  15 +5 0 obj
  16 +<< /Contents 6 0 R /MediaBox [ 0 0 612 792 ] /Parent 4 0 R /Resources << /Font << /F1 7 0 R >> /ProcSet 8 0 R >> /Type /Page >>
  17 +endobj
  18 +6 0 obj
  19 +<< /Length 44 >>
  20 +stream
  21 +BT
  22 + /F1 24 Tf
  23 + 72 720 Td
  24 + (Potato) Tj
  25 +ET
  26 +endstream
  27 +endobj
  28 +7 0 obj
  29 +<< /BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font >>
  30 +endobj
  31 +8 0 obj
  32 +[ /PDF /Text ]
  33 +endobj
  34 +xref
  35 +0 9
  36 +0000000000 65535 f
  37 +0000000015 00000 n
  38 +0000000064 00000 n
  39 +0000000091 00000 n
  40 +0000000118 00000 n
  41 +0000000177 00000 n
  42 +0000000320 00000 n
  43 +0000000413 00000 n
  44 +0000000520 00000 n
  45 +trailer << /Root 1 0 R /Size 9 Array1 2 0 R Array2 3 0 R /ID [<31415926535897932384626433832795><31415926535897932384626433832795>] >>
  46 +startxref
  47 +550
  48 +%%EOF
... ...
qpdf/test_driver.cc
... ... @@ -840,6 +840,82 @@ void runtest(int n, char const* filename)
840 840 std::vector<QPDFObjectHandle> const& pages = pdf.getAllPages();
841 841 pdf.removePage(pages.back());
842 842 }
  843 + else if (n == 24)
  844 + {
  845 + // Test behavior of reserved objects
  846 + QPDFObjectHandle res1 = QPDFObjectHandle::newReserved(&pdf);
  847 + QPDFObjectHandle res2 = QPDFObjectHandle::newReserved(&pdf);
  848 + QPDFObjectHandle trailer = pdf.getTrailer();
  849 + trailer.replaceKey("Array1", res1);
  850 + trailer.replaceKey("Array2", res2);
  851 +
  852 + QPDFObjectHandle array1 = QPDFObjectHandle::newArray();
  853 + QPDFObjectHandle array2 = QPDFObjectHandle::newArray();
  854 + array1.appendItem(res2);
  855 + array1.appendItem(QPDFObjectHandle::newInteger(1));
  856 + array2.appendItem(res1);
  857 + array2.appendItem(QPDFObjectHandle::newInteger(2));
  858 + // Make sure trying to ask questions about a reserved object
  859 + // doesn't break it.
  860 + if (res1.isArray())
  861 + {
  862 + std::cout << "oops -- res1 is an array" << std::endl;
  863 + }
  864 + if (res1.isReserved())
  865 + {
  866 + std::cout << "res1 is still reserved after checking if array"
  867 + << std::endl;
  868 + }
  869 + pdf.replaceReserved(res1, array1);
  870 + if (res1.isReserved())
  871 + {
  872 + std::cout << "oops -- res1 is still reserved" << std::endl;
  873 + }
  874 + else
  875 + {
  876 + std::cout << "res1 is no longer reserved" << std::endl;
  877 + }
  878 + res1.assertArray();
  879 + std::cout << "res1 is an array" << std::endl;
  880 +
  881 + try
  882 + {
  883 + res2.unparseResolved();
  884 + std::cout << "oops -- didn't throw" << std::endl;
  885 + }
  886 + catch (std::logic_error e)
  887 + {
  888 + std::cout << "logic error: " << e.what() << std::endl;
  889 + }
  890 + try
  891 + {
  892 + res2.makeDirect();
  893 + std::cout << "oops -- didn't throw" << std::endl;
  894 + }
  895 + catch (std::logic_error e)
  896 + {
  897 + std::cout << "logic error: " << e.what() << std::endl;
  898 + }
  899 +
  900 + pdf.replaceReserved(res2, array2);
  901 +
  902 + res2.assertArray();
  903 + std::cout << "res2 is an array" << std::endl;
  904 +
  905 + // Verify that the previously added reserved keys can be
  906 + // dereferenced properly now
  907 + int i1 = res1.getArrayItem(0).getArrayItem(1).getIntValue();
  908 + int i2 = res2.getArrayItem(0).getArrayItem(1).getIntValue();
  909 + if ((i1 == 2) && (i2 == 1))
  910 + {
  911 + std::cout << "circular access and lazy resolution worked" << std::endl;
  912 + }
  913 +
  914 + QPDFWriter w(pdf, "a.pdf");
  915 + w.setStaticID(true);
  916 + w.setStreamDataMode(qpdf_s_preserve);
  917 + w.write();
  918 + }
843 919 else
844 920 {
845 921 throw std::runtime_error(std::string("invalid test ") +
... ...