Commit 8a217eb3a26931453b4f003c6c18ad8569230cf1
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.
Showing
13 changed files
with
280 additions
and
7 deletions
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& 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<int>& 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
libqpdf/build.mk
libqpdf/qpdf/QPDF_Reserved.hh
0 โ 100644
qpdf/qpdf.testcov
qpdf/qtest/qpdf.test
| ... | ... | @@ -149,7 +149,7 @@ $td->runtest("remove page we don't have", |
| 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->runtest("warn for unknown key in Pages", |
| 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 ") + | ... | ... |