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 2012-07-07 Jay Berkenbilt <ejb@ql.org> 10 2012-07-07 Jay Berkenbilt <ejb@ql.org>
2 11
3 * NOTE: BREAKING API CHANGE. Remove previously required length 12 * NOTE: BREAKING API CHANGE. Remove previously required length
include/qpdf/QPDF.hh
@@ -161,7 +161,8 @@ class QPDF @@ -161,7 +161,8 @@ class QPDF
161 // be associated with the PDF file. Note that replacing an object 161 // be associated with the PDF file. Note that replacing an object
162 // with QPDFObjectHandle::newNull() effectively removes the object 162 // with QPDFObjectHandle::newNull() effectively removes the object
163 // from the file since a non-existent object is treated as a null 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 QPDF_DLL 166 QPDF_DLL
166 void replaceObject(int objid, int generation, QPDFObjectHandle); 167 void replaceObject(int objid, int generation, QPDFObjectHandle);
167 168
@@ -180,6 +181,15 @@ class QPDF @@ -180,6 +181,15 @@ class QPDF
180 void swapObjects(int objid1, int generation1, 181 void swapObjects(int objid1, int generation1,
181 int objid2, int generation2); 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 // Encryption support 193 // Encryption support
184 194
185 enum encryption_method_e { e_none, e_unknown, e_rc4, e_aes }; 195 enum encryption_method_e { e_none, e_unknown, e_rc4, e_aes };
include/qpdf/QPDFObjectHandle.hh
@@ -81,6 +81,8 @@ class QPDFObjectHandle @@ -81,6 +81,8 @@ class QPDFObjectHandle
81 bool isDictionary(); 81 bool isDictionary();
82 QPDF_DLL 82 QPDF_DLL
83 bool isStream(); 83 bool isStream();
  84 + QPDF_DLL
  85 + bool isReserved();
84 86
85 // This returns true in addition to the query for the specific 87 // This returns true in addition to the query for the specific
86 // type for indirect objects. 88 // type for indirect objects.
@@ -148,6 +150,24 @@ class QPDFObjectHandle @@ -148,6 +150,24 @@ class QPDFObjectHandle
148 QPDF_DLL 150 QPDF_DLL
149 static QPDFObjectHandle newStream(QPDF* qpdf, std::string const& data); 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 // Accessor methods. If an accessor method that is valid for only 171 // Accessor methods. If an accessor method that is valid for only
152 // a particular object type is called on an object of the wrong 172 // a particular object type is called on an object of the wrong
153 // type, an exception is thrown. 173 // type, an exception is thrown.
@@ -430,6 +450,8 @@ class QPDFObjectHandle @@ -430,6 +450,8 @@ class QPDFObjectHandle
430 void assertDictionary(); 450 void assertDictionary();
431 QPDF_DLL 451 QPDF_DLL
432 void assertStream(); 452 void assertStream();
  453 + QPDF_DLL
  454 + void assertReserved();
433 455
434 QPDF_DLL 456 QPDF_DLL
435 void assertScalar(); 457 void assertScalar();
@@ -459,6 +481,7 @@ class QPDFObjectHandle @@ -459,6 +481,7 @@ class QPDFObjectHandle
459 int objid; // 0 for direct object 481 int objid; // 0 for direct object
460 int generation; 482 int generation;
461 PointerHolder<QPDFObject> obj; 483 PointerHolder<QPDFObject> obj;
  484 + bool reserved;
462 }; 485 };
463 486
464 #endif // __QPDFOBJECTHANDLE_HH__ 487 #endif // __QPDFOBJECTHANDLE_HH__
libqpdf/QPDF.cc
@@ -2057,6 +2057,18 @@ QPDF::replaceObject(int objid, int generation, QPDFObjectHandle oh) @@ -2057,6 +2057,18 @@ QPDF::replaceObject(int objid, int generation, QPDFObjectHandle oh)
2057 } 2057 }
2058 2058
2059 void 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 QPDF::swapObjects(int objid1, int generation1, int objid2, int generation2) 2072 QPDF::swapObjects(int objid1, int generation1, int objid2, int generation2)
2061 { 2073 {
2062 // Force objects to be loaded into cache; then swap them in the 2074 // Force objects to be loaded into cache; then swap them in the
libqpdf/QPDFObjectHandle.cc
@@ -10,6 +10,7 @@ @@ -10,6 +10,7 @@
10 #include <qpdf/QPDF_Array.hh> 10 #include <qpdf/QPDF_Array.hh>
11 #include <qpdf/QPDF_Dictionary.hh> 11 #include <qpdf/QPDF_Dictionary.hh>
12 #include <qpdf/QPDF_Stream.hh> 12 #include <qpdf/QPDF_Stream.hh>
  13 +#include <qpdf/QPDF_Reserved.hh>
13 14
14 #include <qpdf/QTC.hh> 15 #include <qpdf/QTC.hh>
15 #include <qpdf/QUtil.hh> 16 #include <qpdf/QUtil.hh>
@@ -20,7 +21,8 @@ @@ -20,7 +21,8 @@
20 QPDFObjectHandle::QPDFObjectHandle() : 21 QPDFObjectHandle::QPDFObjectHandle() :
21 initialized(false), 22 initialized(false),
22 objid(0), 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,7 +30,8 @@ QPDFObjectHandle::QPDFObjectHandle(QPDF* qpdf, int objid, int generation) :
28 initialized(true), 30 initialized(true),
29 qpdf(qpdf), 31 qpdf(qpdf),
30 objid(objid), 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,7 +40,8 @@ QPDFObjectHandle::QPDFObjectHandle(QPDFObject* data) :
37 qpdf(0), 40 qpdf(0),
38 objid(0), 41 objid(0),
39 generation(0), 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,6 +170,14 @@ QPDFObjectHandle::isStream()
166 } 170 }
167 171
168 bool 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 QPDFObjectHandle::isIndirect() 181 QPDFObjectHandle::isIndirect()
170 { 182 {
171 assertInitialized(); 183 assertInitialized();
@@ -568,6 +580,11 @@ QPDFObjectHandle::unparse() @@ -568,6 +580,11 @@ QPDFObjectHandle::unparse()
568 std::string 580 std::string
569 QPDFObjectHandle::unparseResolved() 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 dereference(); 588 dereference();
572 return this->obj->unparse(); 589 return this->obj->unparse();
573 } 590 }
@@ -690,6 +707,19 @@ QPDFObjectHandle::newStream(QPDF* qpdf, std::string const&amp; data) @@ -690,6 +707,19 @@ QPDFObjectHandle::newStream(QPDF* qpdf, std::string const&amp; data)
690 } 707 }
691 708
692 QPDFObjectHandle 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 QPDFObjectHandle::shallowCopy() 723 QPDFObjectHandle::shallowCopy()
694 { 724 {
695 assertInitialized(); 725 assertInitialized();
@@ -746,6 +776,13 @@ QPDFObjectHandle::makeDirectInternal(std::set&lt;int&gt;&amp; visited) @@ -746,6 +776,13 @@ QPDFObjectHandle::makeDirectInternal(std::set&lt;int&gt;&amp; visited)
746 visited.insert(cur_objid); 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 dereference(); 786 dereference();
750 this->objid = 0; 787 this->objid = 0;
751 this->generation = 0; 788 this->generation = 0;
@@ -903,6 +940,12 @@ QPDFObjectHandle::assertStream() @@ -903,6 +940,12 @@ QPDFObjectHandle::assertStream()
903 } 940 }
904 941
905 void 942 void
  943 +QPDFObjectHandle::assertReserved()
  944 +{
  945 + assertType("Reserved", isReserved());
  946 +}
  947 +
  948 +void
906 QPDFObjectHandle::assertScalar() 949 QPDFObjectHandle::assertScalar()
907 { 950 {
908 assertType("Scalar", isScalar()); 951 assertType("Scalar", isScalar());
@@ -929,12 +972,21 @@ QPDFObjectHandle::dereference() @@ -929,12 +972,21 @@ QPDFObjectHandle::dereference()
929 { 972 {
930 if (this->obj.getPointer() == 0) 973 if (this->obj.getPointer() == 0)
931 { 974 {
932 - this->obj = QPDF::Resolver::resolve( 975 + PointerHolder<QPDFObject> obj = QPDF::Resolver::resolve(
933 this->qpdf, this->objid, this->generation); 976 this->qpdf, this->objid, this->generation);
934 - if (this->obj.getPointer() == 0) 977 + if (obj.getPointer() == 0)
935 { 978 {
936 QTC::TC("qpdf", "QPDFObjectHandle indirect to unknown"); 979 QTC::TC("qpdf", "QPDFObjectHandle indirect to unknown");
937 this->obj = new QPDF_Null(); 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,6 +39,7 @@ SRCS_libqpdf = \
39 libqpdf/QPDF_Name.cc \ 39 libqpdf/QPDF_Name.cc \
40 libqpdf/QPDF_Null.cc \ 40 libqpdf/QPDF_Null.cc \
41 libqpdf/QPDF_Real.cc \ 41 libqpdf/QPDF_Real.cc \
  42 + libqpdf/QPDF_Reserved.cc \
42 libqpdf/QPDF_Stream.cc \ 43 libqpdf/QPDF_Stream.cc \
43 libqpdf/QPDF_String.cc \ 44 libqpdf/QPDF_String.cc \
44 libqpdf/QPDF_encryption.cc \ 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,3 +217,4 @@ QPDFObjectHandle newStream with string 0
217 QPDF unknown key not inherited 0 217 QPDF unknown key not inherited 0
218 QPDF_Stream provider length not provided 0 218 QPDF_Stream provider length not provided 0
219 QPDF_Stream unknown stream length 0 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,7 +149,7 @@ $td-&gt;runtest(&quot;remove page we don&#39;t have&quot;,
149 $td->NORMALIZE_NEWLINES); 149 $td->NORMALIZE_NEWLINES);
150 # ---------- 150 # ----------
151 $td->notify("--- Miscellaneous Tests ---"); 151 $td->notify("--- Miscellaneous Tests ---");
152 -$n_tests += 41; 152 +$n_tests += 43;
153 153
154 $td->runtest("qpdf version", 154 $td->runtest("qpdf version",
155 {$td->COMMAND => "qpdf --version"}, 155 {$td->COMMAND => "qpdf --version"},
@@ -358,6 +358,13 @@ $td-&gt;runtest(&quot;warn for unknown key in Pages&quot;, @@ -358,6 +358,13 @@ $td-&gt;runtest(&quot;warn for unknown key in Pages&quot;,
358 {$td->COMMAND => "test_driver 23 lin-special.pdf"}, 358 {$td->COMMAND => "test_driver 23 lin-special.pdf"},
359 {$td->FILE => "pages-warning.out", $td->EXIT_STATUS => 0}, 359 {$td->FILE => "pages-warning.out", $td->EXIT_STATUS => 0},
360 $td->NORMALIZE_NEWLINES); 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 show_ntests(); 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,6 +840,82 @@ void runtest(int n, char const* filename)
840 std::vector<QPDFObjectHandle> const& pages = pdf.getAllPages(); 840 std::vector<QPDFObjectHandle> const& pages = pdf.getAllPages();
841 pdf.removePage(pages.back()); 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 else 919 else
844 { 920 {
845 throw std::runtime_error(std::string("invalid test ") + 921 throw std::runtime_error(std::string("invalid test ") +