diff --git a/ChangeLog b/ChangeLog index 9621b3ff..2adee2ba 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,8 @@ 2021-02-21 Jay Berkenbilt + * Add QPDFObjectHandle::copyStream() for making a copy of a stream + within the same QPDF instance. + * Allow QPDFObjectHandle::newArray and QPDFObjectHandle::newFromMatrix take QPDFMatrix as well as QPDFObjectHandle::Matrix diff --git a/include/qpdf/QPDF.hh b/include/qpdf/QPDF.hh index ee00b23f..d4b7e775 100644 --- a/include/qpdf/QPDF.hh +++ b/include/qpdf/QPDF.hh @@ -700,6 +700,21 @@ class QPDF }; friend class Resolver; + // StreamCopier class is restricted to QPDFObjectHandle so it can + // copy stream data. + class StreamCopier + { + friend class QPDFObjectHandle; + private: + static void copyStreamData(QPDF* qpdf, + QPDFObjectHandle const& dest, + QPDFObjectHandle const& src) + { + qpdf->copyStreamData(dest, src); + } + }; + friend class Resolver; + // ParseGuard class allows QPDFObjectHandle to detect re-entrant // resolution class ParseGuard diff --git a/include/qpdf/QPDFObjectHandle.hh b/include/qpdf/QPDFObjectHandle.hh index bb877622..07c5b427 100644 --- a/include/qpdf/QPDFObjectHandle.hh +++ b/include/qpdf/QPDFObjectHandle.hh @@ -761,7 +761,8 @@ class QPDFObjectHandle // the same place. In the strictest sense, this is not a shallow // copy because it recursively descends arrays and dictionaries; // it just doesn't cross over indirect objects. See also - // unsafeShallowCopy(). + // unsafeShallowCopy(). You can't copy a stream this way. See + // copyStream() instead. QPDF_DLL QPDFObjectHandle shallowCopy(); @@ -776,6 +777,19 @@ class QPDFObjectHandle QPDF_DLL QPDFObjectHandle unsafeShallowCopy(); + // Create a copy of this stream. The new stream and the old stream + // are independent: after the copy, either the original or the + // copy's dictionary or data can be modified without affecting the + // other. This uses StreamDataProvider internally, so no + // unnecessary copies of the stream's data are made. If the source + // stream's data is already being provided by a + // StreamDataProvider, the new stream will use the same one, so + // you have to make sure your StreamDataProvider can handle that + // case. But if you're already using a StreamDataProvider, you + // probably don't need to call this method. + QPDF_DLL + QPDFObjectHandle copyStream(); + // Mutator methods. Use with caution. // Recursively copy this object, making it direct. An exception is diff --git a/libqpdf/QPDF.cc b/libqpdf/QPDF.cc index 5dfc9224..023e0469 100644 --- a/libqpdf/QPDF.cc +++ b/libqpdf/QPDF.cc @@ -2596,6 +2596,10 @@ QPDF::replaceForeignIndirectObjects( void QPDF::copyStreamData(QPDFObjectHandle result, QPDFObjectHandle foreign) { + // This method was originally written for copying foreign streams, + // but it is used by QPDFObjectHandle to copy streams from the + // same QPDF object as well. + QPDFObjectHandle dict = result.getDict(); QPDFObjectHandle old_dict = foreign.getDict(); if (this->m->copied_stream_data_provider == 0) diff --git a/libqpdf/QPDFObjectHandle.cc b/libqpdf/QPDFObjectHandle.cc index ceb91630..f6ba3093 100644 --- a/libqpdf/QPDFObjectHandle.cc +++ b/libqpdf/QPDFObjectHandle.cc @@ -2877,6 +2877,28 @@ QPDFObjectHandle::copyObject(std::set& visited, } } +QPDFObjectHandle +QPDFObjectHandle::copyStream() +{ + assertStream(); + QPDFObjectHandle result = newStream(this->getOwningQPDF()); + QPDFObjectHandle dict = result.getDict(); + QPDFObjectHandle old_dict = getDict(); + for (auto& iter: QPDFDictItems(old_dict)) + { + if (iter.second.isIndirect()) + { + dict.replaceKey(iter.first, iter.second); + } + else + { + dict.replaceKey(iter.first, iter.second.shallowCopy()); + } + } + QPDF::StreamCopier::copyStreamData(getOwningQPDF(), result, *this); + return result; +} + void QPDFObjectHandle::makeDirect() { diff --git a/manual/qpdf-manual.xml b/manual/qpdf-manual.xml index bffd52cb..f7146221 100644 --- a/manual/qpdf-manual.xml +++ b/manual/qpdf-manual.xml @@ -5200,6 +5200,13 @@ print "\n"; details. + + + Add QPDFObjectHandle::copyStream for + making a copy of a stream within the same + QPDF instance. + + Add QUtil::get_current_qpdf_time, diff --git a/qpdf/qtest/qpdf.test b/qpdf/qtest/qpdf.test index 90e7730f..100520f3 100644 --- a/qpdf/qtest/qpdf.test +++ b/qpdf/qtest/qpdf.test @@ -1549,7 +1549,7 @@ unlink "a.pdf" or die; show_ntests(); # ---------- $td->notify("--- Object copying ---"); -$n_tests += 7; +$n_tests += 9; $td->runtest("shallow copy an array", {$td->COMMAND => "test_driver 20 shallow_array.pdf"}, @@ -1578,6 +1578,13 @@ $td->runtest("detect foreign object in write", " copy-foreign-objects-in.pdf minimal.pdf"}, {$td->FILE => "foreign-in-write.out", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); +$td->runtest("copy a stream", + {$td->COMMAND => "test_driver 79 minimal.pdf"}, + {$td->STRING => "test 79 done\n", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("check output", + {$td->FILE => "a.pdf"}, + {$td->FILE => "test79.pdf"}); show_ntests(); # ---------- diff --git a/qpdf/qtest/qpdf/test79.pdf b/qpdf/qtest/qpdf/test79.pdf new file mode 100644 index 00000000..2dbbc903 --- /dev/null +++ b/qpdf/qtest/qpdf/test79.pdf @@ -0,0 +1,214 @@ +%PDF-1.3 +%¿÷¢þ +%QDF-1.0 + +%% Original object ID: 1 0 +1 0 obj +<< + /Pages 14 0 R + /Type /Catalog +>> +endobj + +%% Original object ID: 10 0 +2 0 obj +<< + /Other (other: 1) + /Length 3 0 R +>> +stream +BT + /F1 24 Tf + 72 720 Td + (Potato) Tj +ET +endstream +endobj + +3 0 obj +44 +endobj + +%% Original object ID: 11 0 +4 0 obj +<< + /Other (other: 2) + /Stuff << + /Direct 3 + /Indirect 15 0 R + >> + /Length 5 0 R +>> +stream +from string +endstream +endobj + +%QDF: ignore_newline +5 0 obj +11 +endobj + +%% Original object ID: 12 0 +6 0 obj +<< + /Other (other: 3) + /Length 7 0 R +>> +stream +from buffer +endstream +endobj + +%QDF: ignore_newline +7 0 obj +11 +endobj + +%% Contents for page 1 +%% Original object ID: 4 0 +8 0 obj +<< + /Length 9 0 R +>> +stream +something new 1 +endstream +endobj + +%QDF: ignore_newline +9 0 obj +15 +endobj + +%% Original object ID: 7 0 +10 0 obj +<< + /Other (other stuff) + /Stuff << + /Direct 3 + /Indirect 15 0 R + >> + /Length 11 0 R +>> +stream +something new 2 +endstream +endobj + +%QDF: ignore_newline +11 0 obj +15 +endobj + +%% Original object ID: 9 0 +12 0 obj +<< + /Length 13 0 R +>> +stream +something new 3 +endstream +endobj + +%QDF: ignore_newline +13 0 obj +15 +endobj + +%% Original object ID: 2 0 +14 0 obj +<< + /Count 1 + /Kids [ + 16 0 R + ] + /Type /Pages +>> +endobj + +%% Original object ID: 8 0 +15 0 obj +16059 +endobj + +%% Page 1 +%% Original object ID: 3 0 +16 0 obj +<< + /Contents 8 0 R + /MediaBox [ + 0 + 0 + 612 + 792 + ] + /Parent 14 0 R + /Resources << + /Font << + /F1 17 0 R + >> + /ProcSet 18 0 R + >> + /Type /Page +>> +endobj + +%% Original object ID: 6 0 +17 0 obj +<< + /BaseFont /Helvetica + /Encoding /WinAnsiEncoding + /Name /F1 + /Subtype /Type1 + /Type /Font +>> +endobj + +%% Original object ID: 5 0 +18 0 obj +[ + /PDF + /Text +] +endobj + +xref +0 19 +0000000000 65535 f +0000000052 00000 n +0000000135 00000 n +0000000254 00000 n +0000000301 00000 n +0000000461 00000 n +0000000508 00000 n +0000000616 00000 n +0000000685 00000 n +0000000777 00000 n +0000000823 00000 n +0000000992 00000 n +0000001039 00000 n +0000001133 00000 n +0000001180 00000 n +0000001281 00000 n +0000001341 00000 n +0000001564 00000 n +0000001710 00000 n +trailer << + /Copies [ + 2 0 R + 4 0 R + 6 0 R + ] + /Originals [ + 8 0 R + 10 0 R + 12 0 R + ] + /Root 1 0 R + /Size 19 + /ID [<31415926535897932384626433832795><31415926535897932384626433832795>] +>> +startxref +1746 +%%EOF diff --git a/qpdf/test_driver.cc b/qpdf/test_driver.cc index 2ad5bd62..356d9312 100644 --- a/qpdf/test_driver.cc +++ b/qpdf/test_driver.cc @@ -2844,6 +2844,67 @@ void runtest(int n, char const* filename1, char const* arg2) w.setQDFMode(true); w.write(); } + else if (n == 79) + { + // Exercise stream copier + + // Copy streams. Modify the original and make sure the copy is + // unaffected. + auto copies = QPDFObjectHandle::newArray(); + pdf.getTrailer().replaceKey("/Copies", copies); + auto null = QPDFObjectHandle::newNull(); + + // Get a regular stream from the file + auto p1 = pdf.getAllPages().at(0); + auto s1 = p1.getKey("/Contents"); + + // Create a stream from a string + auto s2 = QPDFObjectHandle::newStream(&pdf, "from string"); + // Add direct and indirect objects to the dictionary + s2.getDict().replaceKey( + "/Stuff", + QPDFObjectHandle::parse( + &pdf, + "<< /Direct 3 /Indirect " + + pdf.makeIndirectObject( + QPDFObjectHandle::newInteger(16059)).unparse() + ">>")); + s2.getDict().replaceKey( + "/Other", QPDFObjectHandle::newString("other stuff")); + + // Use a provider + Pl_Buffer b("buffer"); + b.write(QUtil::unsigned_char_pointer("from buffer"), 11); + b.finish(); + PointerHolder bp = b.getBuffer(); + auto s3 = QPDFObjectHandle::newStream(&pdf, bp); + + std::vector streams = {s1, s2, s3}; + pdf.getTrailer().replaceKey( + "/Originals", QPDFObjectHandle::newArray(streams)); + + int i = 0; + for (auto orig: streams) + { + ++i; + auto istr = QUtil::int_to_string(i); + auto orig_data = orig.getStreamData(); + auto copy = orig.copyStream(); + copy.getDict().replaceKey( + "/Other", QPDFObjectHandle::newString("other: " + istr)); + orig.replaceStreamData("something new " + istr, null, null); + auto copy_data = copy.getStreamData(); + assert(orig_data->getSize() == copy_data->getSize()); + assert(memcmp(orig_data->getBuffer(), + copy_data->getBuffer(), + orig_data->getSize()) == 0); + copies.appendItem(copy); + } + + QPDFWriter w(pdf, "a.pdf"); + w.setStaticID(true); + w.setQDFMode(true); + w.write(); + } else { throw std::runtime_error(std::string("invalid test ") +