From 8a217eb3a26931453b4f003c6c18ad8569230cf1 Mon Sep 17 00:00:00 2001 From: Jay Berkenbilt Date: Sun, 8 Jul 2012 14:19:19 -0400 Subject: [PATCH] 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 | 9 ++++ include/qpdf/QPDF.hh | 12 ++++- include/qpdf/QPDFObjectHandle.hh | 23 +++++++++ libqpdf/QPDF.cc | 12 +++++ libqpdf/QPDFObjectHandle.cc | 62 +++++++++++++++++++++-- libqpdf/QPDF_Reserved.cc | 13 +++++ libqpdf/build.mk | 1 + libqpdf/qpdf/QPDF_Reserved.hh | 13 +++++ qpdf/qpdf.testcov | 1 + qpdf/qtest/qpdf.test | 9 +++- qpdf/qtest/qpdf/reserved-objects.out | 8 +++ qpdf/qtest/qpdf/reserved-objects.pdf | 48 ++++++++++++++++++ qpdf/test_driver.cc | 76 ++++++++++++++++++++++++++++ 13 files changed, 280 insertions(+), 7 deletions(-) create mode 100644 libqpdf/QPDF_Reserved.cc create mode 100644 libqpdf/qpdf/QPDF_Reserved.hh create mode 100644 qpdf/qtest/qpdf/reserved-objects.out create mode 100644 qpdf/qtest/qpdf/reserved-objects.pdf diff --git a/ChangeLog b/ChangeLog index 6fa4c45d..377d79bf 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,12 @@ +2012-07-08 Jay Berkenbilt + + * Add QPDFObjectHandle::newReserved to create a reserved object + and QPDF::replaceReserved to replace it with a real object. + QPDFObjectHandle::newReserved reserves an object ID in a QPDF + object and ensures that any references to it remain unresolved. + When QPDF::replaceReserved is later called, previous references to + the reserved object will properly resolve to the replaced object. + 2012-07-07 Jay Berkenbilt * NOTE: BREAKING API CHANGE. Remove previously required length diff --git a/include/qpdf/QPDF.hh b/include/qpdf/QPDF.hh index fbac2ab2..b5c07abb 100644 --- a/include/qpdf/QPDF.hh +++ b/include/qpdf/QPDF.hh @@ -161,7 +161,8 @@ class QPDF // be associated with the PDF file. Note that replacing an object // with QPDFObjectHandle::newNull() effectively removes the object // from the file since a non-existent object is treated as a null - // object. + // object. To replace a reserved object, call replaceReserved + // instead. QPDF_DLL void replaceObject(int objid, int generation, QPDFObjectHandle); @@ -180,6 +181,15 @@ class QPDF void swapObjects(int objid1, int generation1, int objid2, int generation2); + // Replace a reserved object. This is a wrapper around + // replaceObject but it guarantees that the underlying object is a + // reserved object. After this call, reserved will be a reference + // to replacement. + QPDF_DLL + void + replaceReserved(QPDFObjectHandle reserved, + QPDFObjectHandle replacement); + // Encryption support enum encryption_method_e { e_none, e_unknown, e_rc4, e_aes }; diff --git a/include/qpdf/QPDFObjectHandle.hh b/include/qpdf/QPDFObjectHandle.hh index daa71faa..b21a3b0c 100644 --- a/include/qpdf/QPDFObjectHandle.hh +++ b/include/qpdf/QPDFObjectHandle.hh @@ -81,6 +81,8 @@ class QPDFObjectHandle bool isDictionary(); QPDF_DLL bool isStream(); + QPDF_DLL + bool isReserved(); // This returns true in addition to the query for the specific // type for indirect objects. @@ -148,6 +150,24 @@ class QPDFObjectHandle QPDF_DLL static QPDFObjectHandle newStream(QPDF* qpdf, std::string const& data); + // A reserved object is a special sentinel used for qpdf to + // reserve a spot for an object that is going to be added to the + // QPDF object. Normally you don't have to use this type since + // you can just call QPDF::makeIndirectObject. However, in some + // cases, if you have to create objects with circular references, + // you may need to create a reserved object so that you can have a + // reference to it and then replace the object later. Reserved + // objects have the special property that they can't be resolved + // to direct objects. This makes it possible to replace a + // reserved object with a new object while preserving existing + // references to them. When you are ready to replace a reserved + // object with its replacement, use QPDF::replaceReserved for this + // purpose rather than the more general QPDF::replaceObject. It + // is an error to try to write a QPDF with QPDFWriter if it has + // any reserved objects in it. + QPDF_DLL + static QPDFObjectHandle newReserved(QPDF* qpdf); + // Accessor methods. If an accessor method that is valid for only // a particular object type is called on an object of the wrong // type, an exception is thrown. @@ -430,6 +450,8 @@ class QPDFObjectHandle void assertDictionary(); QPDF_DLL void assertStream(); + QPDF_DLL + void assertReserved(); QPDF_DLL void assertScalar(); @@ -459,6 +481,7 @@ class QPDFObjectHandle int objid; // 0 for direct object int generation; PointerHolder obj; + bool reserved; }; #endif // __QPDFOBJECTHANDLE_HH__ diff --git a/libqpdf/QPDF.cc b/libqpdf/QPDF.cc index a66b4f15..1c4e5d8d 100644 --- a/libqpdf/QPDF.cc +++ b/libqpdf/QPDF.cc @@ -2056,6 +2056,18 @@ QPDF::replaceObject(int objid, int generation, QPDFObjectHandle oh) ObjCache(QPDFObjectHandle::ObjAccessor::getObject(oh), -1, -1); } +void +QPDF::replaceReserved(QPDFObjectHandle reserved, + QPDFObjectHandle replacement) +{ + QTC::TC("qpdf", "QPDF replaceReserved"); + reserved.assertReserved(); + replaceObject(reserved.getObjectID(), + reserved.getGeneration(), + replacement); +} + + void QPDF::swapObjects(int objid1, int generation1, int objid2, int generation2) { diff --git a/libqpdf/QPDFObjectHandle.cc b/libqpdf/QPDFObjectHandle.cc index 73d0019c..25298bee 100644 --- a/libqpdf/QPDFObjectHandle.cc +++ b/libqpdf/QPDFObjectHandle.cc @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -20,7 +21,8 @@ QPDFObjectHandle::QPDFObjectHandle() : initialized(false), objid(0), - generation(0) + generation(0), + reserved(false) { } @@ -28,7 +30,8 @@ QPDFObjectHandle::QPDFObjectHandle(QPDF* qpdf, int objid, int generation) : initialized(true), qpdf(qpdf), objid(objid), - generation(generation) + generation(generation), + reserved(false) { } @@ -37,7 +40,8 @@ QPDFObjectHandle::QPDFObjectHandle(QPDFObject* data) : qpdf(0), objid(0), generation(0), - obj(data) + obj(data), + reserved(false) { } @@ -165,6 +169,14 @@ QPDFObjectHandle::isStream() return QPDFObjectTypeAccessor::check(obj.getPointer()); } +bool +QPDFObjectHandle::isReserved() +{ + // dereference will clear reserved if this has been replaced + dereference(); + return this->reserved; +} + bool QPDFObjectHandle::isIndirect() { @@ -568,6 +580,11 @@ QPDFObjectHandle::unparse() std::string QPDFObjectHandle::unparseResolved() { + if (this->reserved) + { + throw std::logic_error( + "QPDFObjectHandle: attempting to unparse a reserved object"); + } dereference(); return this->obj->unparse(); } @@ -689,6 +706,19 @@ QPDFObjectHandle::newStream(QPDF* qpdf, std::string const& data) return QPDFObjectHandle::newStream(qpdf, b); } +QPDFObjectHandle +QPDFObjectHandle::newReserved(QPDF* qpdf) +{ + // Reserve a spot for this object by assigning it an object + // number, but then return an unresolved handle to the object. + QPDFObjectHandle reserved = qpdf->makeIndirectObject( + QPDFObjectHandle(new QPDF_Reserved())); + QPDFObjectHandle result = + newIndirect(qpdf, reserved.objid, reserved.generation); + result.reserved = true; + return result; +} + QPDFObjectHandle QPDFObjectHandle::shallowCopy() { @@ -746,6 +776,13 @@ QPDFObjectHandle::makeDirectInternal(std::set& visited) visited.insert(cur_objid); } + if (isReserved()) + { + throw std::logic_error( + "QPDFObjectHandle: attempting to make a" + " reserved object handle direct"); + } + dereference(); this->objid = 0; this->generation = 0; @@ -902,6 +939,12 @@ QPDFObjectHandle::assertStream() assertType("Stream", isStream()); } +void +QPDFObjectHandle::assertReserved() +{ + assertType("Reserved", isReserved()); +} + void QPDFObjectHandle::assertScalar() { @@ -929,12 +972,21 @@ QPDFObjectHandle::dereference() { if (this->obj.getPointer() == 0) { - this->obj = QPDF::Resolver::resolve( + PointerHolder obj = QPDF::Resolver::resolve( this->qpdf, this->objid, this->generation); - if (this->obj.getPointer() == 0) + if (obj.getPointer() == 0) { QTC::TC("qpdf", "QPDFObjectHandle indirect to unknown"); this->obj = new QPDF_Null(); } + else if (dynamic_cast(obj.getPointer())) + { + // Do not resolve + } + else + { + this->reserved = false; + this->obj = obj; + } } } diff --git a/libqpdf/QPDF_Reserved.cc b/libqpdf/QPDF_Reserved.cc new file mode 100644 index 00000000..368db3b4 --- /dev/null +++ b/libqpdf/QPDF_Reserved.cc @@ -0,0 +1,13 @@ +#include +#include + +QPDF_Reserved::~QPDF_Reserved() +{ +} + +std::string +QPDF_Reserved::unparse() +{ + throw std::logic_error("attempt to unparse QPDF_Reserved"); + return ""; +} diff --git a/libqpdf/build.mk b/libqpdf/build.mk index 7efbbd85..422878f2 100644 --- a/libqpdf/build.mk +++ b/libqpdf/build.mk @@ -39,6 +39,7 @@ SRCS_libqpdf = \ libqpdf/QPDF_Name.cc \ libqpdf/QPDF_Null.cc \ libqpdf/QPDF_Real.cc \ + libqpdf/QPDF_Reserved.cc \ libqpdf/QPDF_Stream.cc \ libqpdf/QPDF_String.cc \ libqpdf/QPDF_encryption.cc \ diff --git a/libqpdf/qpdf/QPDF_Reserved.hh b/libqpdf/qpdf/QPDF_Reserved.hh new file mode 100644 index 00000000..b149f776 --- /dev/null +++ b/libqpdf/qpdf/QPDF_Reserved.hh @@ -0,0 +1,13 @@ +#ifndef __QPDF_RESERVED_HH__ +#define __QPDF_RESERVED_HH__ + +#include + +class QPDF_Reserved: public QPDFObject +{ + public: + virtual ~QPDF_Reserved(); + virtual std::string unparse(); +}; + +#endif // __QPDF_RESERVED_HH__ diff --git a/qpdf/qpdf.testcov b/qpdf/qpdf.testcov index b2c98496..eea5475a 100644 --- a/qpdf/qpdf.testcov +++ b/qpdf/qpdf.testcov @@ -217,3 +217,4 @@ QPDFObjectHandle newStream with string 0 QPDF unknown key not inherited 0 QPDF_Stream provider length not provided 0 QPDF_Stream unknown stream length 0 +QPDF replaceReserved 0 diff --git a/qpdf/qtest/qpdf.test b/qpdf/qtest/qpdf.test index 9a7f7b99..b4171735 100644 --- a/qpdf/qtest/qpdf.test +++ b/qpdf/qtest/qpdf.test @@ -149,7 +149,7 @@ $td->runtest("remove page we don't have", $td->NORMALIZE_NEWLINES); # ---------- $td->notify("--- Miscellaneous Tests ---"); -$n_tests += 41; +$n_tests += 43; $td->runtest("qpdf version", {$td->COMMAND => "qpdf --version"}, @@ -358,6 +358,13 @@ $td->runtest("warn for unknown key in Pages", {$td->COMMAND => "test_driver 23 lin-special.pdf"}, {$td->FILE => "pages-warning.out", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); +$td->runtest("reserved objects", + {$td->COMMAND => "test_driver 24 minimal.pdf"}, + {$td->FILE => "reserved-objects.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("check output", + {$td->FILE => "a.pdf"}, + {$td->FILE => "reserved-objects.pdf"}); show_ntests(); # ---------- diff --git a/qpdf/qtest/qpdf/reserved-objects.out b/qpdf/qtest/qpdf/reserved-objects.out new file mode 100644 index 00000000..a1611e11 --- /dev/null +++ b/qpdf/qtest/qpdf/reserved-objects.out @@ -0,0 +1,8 @@ +res1 is still reserved after checking if array +res1 is no longer reserved +res1 is an array +logic error: QPDFObjectHandle: attempting to unparse a reserved object +logic error: QPDFObjectHandle: attempting to make a reserved object handle direct +res2 is an array +circular access and lazy resolution worked +test 24 done diff --git a/qpdf/qtest/qpdf/reserved-objects.pdf b/qpdf/qtest/qpdf/reserved-objects.pdf new file mode 100644 index 00000000..592a51de --- /dev/null +++ b/qpdf/qtest/qpdf/reserved-objects.pdf @@ -0,0 +1,48 @@ +%PDF-1.3 +%¿÷¢þ +1 0 obj +<< /Pages 4 0 R /Type /Catalog >> +endobj +2 0 obj +[ 3 0 R 1 ] +endobj +3 0 obj +[ 2 0 R 2 ] +endobj +4 0 obj +<< /Count 1 /Kids [ 5 0 R ] /Type /Pages >> +endobj +5 0 obj +<< /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 >> +endobj +6 0 obj +<< /Length 44 >> +stream +BT + /F1 24 Tf + 72 720 Td + (Potato) Tj +ET +endstream +endobj +7 0 obj +<< /BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font >> +endobj +8 0 obj +[ /PDF /Text ] +endobj +xref +0 9 +0000000000 65535 f +0000000015 00000 n +0000000064 00000 n +0000000091 00000 n +0000000118 00000 n +0000000177 00000 n +0000000320 00000 n +0000000413 00000 n +0000000520 00000 n +trailer << /Root 1 0 R /Size 9 Array1 2 0 R Array2 3 0 R /ID [<31415926535897932384626433832795><31415926535897932384626433832795>] >> +startxref +550 +%%EOF diff --git a/qpdf/test_driver.cc b/qpdf/test_driver.cc index dd8de73e..1e1bd7d8 100644 --- a/qpdf/test_driver.cc +++ b/qpdf/test_driver.cc @@ -840,6 +840,82 @@ void runtest(int n, char const* filename) std::vector const& pages = pdf.getAllPages(); pdf.removePage(pages.back()); } + else if (n == 24) + { + // Test behavior of reserved objects + QPDFObjectHandle res1 = QPDFObjectHandle::newReserved(&pdf); + QPDFObjectHandle res2 = QPDFObjectHandle::newReserved(&pdf); + QPDFObjectHandle trailer = pdf.getTrailer(); + trailer.replaceKey("Array1", res1); + trailer.replaceKey("Array2", res2); + + QPDFObjectHandle array1 = QPDFObjectHandle::newArray(); + QPDFObjectHandle array2 = QPDFObjectHandle::newArray(); + array1.appendItem(res2); + array1.appendItem(QPDFObjectHandle::newInteger(1)); + array2.appendItem(res1); + array2.appendItem(QPDFObjectHandle::newInteger(2)); + // Make sure trying to ask questions about a reserved object + // doesn't break it. + if (res1.isArray()) + { + std::cout << "oops -- res1 is an array" << std::endl; + } + if (res1.isReserved()) + { + std::cout << "res1 is still reserved after checking if array" + << std::endl; + } + pdf.replaceReserved(res1, array1); + if (res1.isReserved()) + { + std::cout << "oops -- res1 is still reserved" << std::endl; + } + else + { + std::cout << "res1 is no longer reserved" << std::endl; + } + res1.assertArray(); + std::cout << "res1 is an array" << std::endl; + + try + { + res2.unparseResolved(); + std::cout << "oops -- didn't throw" << std::endl; + } + catch (std::logic_error e) + { + std::cout << "logic error: " << e.what() << std::endl; + } + try + { + res2.makeDirect(); + std::cout << "oops -- didn't throw" << std::endl; + } + catch (std::logic_error e) + { + std::cout << "logic error: " << e.what() << std::endl; + } + + pdf.replaceReserved(res2, array2); + + res2.assertArray(); + std::cout << "res2 is an array" << std::endl; + + // Verify that the previously added reserved keys can be + // dereferenced properly now + int i1 = res1.getArrayItem(0).getArrayItem(1).getIntValue(); + int i2 = res2.getArrayItem(0).getArrayItem(1).getIntValue(); + if ((i1 == 2) && (i2 == 1)) + { + std::cout << "circular access and lazy resolution worked" << std::endl; + } + + QPDFWriter w(pdf, "a.pdf"); + w.setStaticID(true); + w.setStreamDataMode(qpdf_s_preserve); + w.write(); + } else { throw std::runtime_error(std::string("invalid test ") +