diff --git a/include/qpdf/QPDFObjectHandle.hh b/include/qpdf/QPDFObjectHandle.hh index e8d24d40..298e480b 100644 --- a/include/qpdf/QPDFObjectHandle.hh +++ b/include/qpdf/QPDFObjectHandle.hh @@ -28,6 +28,34 @@ class QPDF_Array; class QPDFObjectHandle { public: + // This class is used by replaceStreamData. It provides an + // alternative way of associating stream data with a stream. See + // comments on replaceStreamData and newStream for additional + // details. + class StreamDataProvider + { + public: + QPDF_DLL + virtual ~StreamDataProvider() + { + } + // The implementation of this function must write the + // unencrypted, raw stream data to the given pipeline. Every + // call to provideStreamData for a given stream must write the + // same data. The number of bytes written must agree with the + // length provided at the time the StreamDataProvider object + // was associated with the stream. The object ID and + // generation passed to this method are those that belong to + // the stream on behalf of which the provider is called. They + // may be ignored or used by the implementation for indexing + // or other purposes. This information is made available just + // to make it more convenient to use a single + // StreamDataProvider object to provide data for multiple + // streams. + virtual void provideStreamData(int objid, int generation, + Pipeline* pipeline) = 0; + }; + QPDF_DLL QPDFObjectHandle(); QPDF_DLL @@ -83,6 +111,30 @@ class QPDFObjectHandle static QPDFObjectHandle newDictionary( std::map const& items); + // Create a new stream and associate it with the given qpdf + // object. A subsequent call must be made to replaceStreamData() + // to provide data for the stream. The stream's dictionary may be + // retrieved by calling getDict(), and the resulting dictionary + // may be modified. + QPDF_DLL + static QPDFObjectHandle newStream(QPDF* qpdf); + + // Create a new stream and associate it with the given qpdf + // object. Use the given buffer as the stream data. The stream + // dictionary's /Length key will automatically be set to the size + // of the data buffer. If additional keys are required, the + // stream's dictionary may be retrieved by calling getDict(), and + // the resulting dictionary may be modified. This method is just + // a convient wrapper around the newStream() and + // replaceStreamData(). It is a convenience methods for streams + // that require no parameters beyond the stream length. Note that + // you don't have to deal with compression yourself if you use + // QPDFWriter. By default, QPDFWriter will automatically compress + // uncompressed stream data. Example programs are provided that + // illustrate this. + QPDF_DLL + static QPDFObjectHandle newStream(QPDF* qpdf, PointerHolder data); + // 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. @@ -198,34 +250,17 @@ class QPDFObjectHandle QPDFObjectHandle const& filter, QPDFObjectHandle const& decode_parms); - class StreamDataProvider - { - public: - QPDF_DLL - virtual ~StreamDataProvider() - { - } - // See replaceStreamData below for details on how to override - // this method. - virtual void provideStreamData(int objid, int generation, - Pipeline* pipeline) = 0; - }; // As above, replace this stream's stream data. Instead of // directly providing a buffer with the stream data, call the - // given provider's provideStreamData method. The method is to - // write the unencrypted, raw stream data to the provided - // pipeline. The stream's /Length key will be set to the length - // as provided. This must match the number of bytes written to - // the pipeline. The provider must write exactly the same data to - // the pipeline every time it is called. The method is invoked - // with the object ID and generation number, which are just there - // to be available to the handler in case it is useful for - // indexing purposes. This makes it easier to reuse the same - // StreamDataProvider object for multiple streams. Although it is - // more complex to use this form of replaceStreamData, it makes it - // possible to avoid allocating memory for the stream data. - // Example programs are provided that use both forms of - // replaceStreamData. + // given provider's provideStreamData method. See comments on the + // StreamDataProvider class (defined above) for details on the + // method. The provider must write the number of bytes as + // indicated by the length parameter, and the data must be + // consistent with filter and decode_parms as provided. Although + // it is more complex to use this form of replaceStreamData than + // the one that takes a buffer, it makes it possible to avoid + // allocating memory for the stream data. Example programs are + // provided that use both forms of replaceStreamData. QPDF_DLL void replaceStreamData(PointerHolder provider, QPDFObjectHandle const& filter, diff --git a/libqpdf/QPDF.cc b/libqpdf/QPDF.cc index cbc2450c..3869e00c 100644 --- a/libqpdf/QPDF.cc +++ b/libqpdf/QPDF.cc @@ -1779,7 +1779,11 @@ QPDF::resolveObjectsInStream(int obj_stream_number) QPDFObjectHandle QPDF::makeIndirectObject(QPDFObjectHandle oh) { - ObjGen o1 = (*(this->obj_cache.rbegin())).first; + ObjGen o1(0, 0); + if (! this->obj_cache.empty()) + { + o1 = (*(this->obj_cache.rbegin())).first; + } ObjGen o2 = (*(this->xref_table.rbegin())).first; QTC::TC("qpdf", "QPDF indirect last obj from xref", (o2.obj > o1.obj) ? 1 : 0); diff --git a/libqpdf/QPDFObjectHandle.cc b/libqpdf/QPDFObjectHandle.cc index 649ce3f0..19b4f94e 100644 --- a/libqpdf/QPDFObjectHandle.cc +++ b/libqpdf/QPDFObjectHandle.cc @@ -561,6 +561,30 @@ QPDFObjectHandle::newStream(QPDF* qpdf, int objid, int generation, stream_dict, offset, length)); } +QPDFObjectHandle +QPDFObjectHandle::newStream(QPDF* qpdf) +{ + QTC::TC("qpdf", "QPDFObjectHandle newStream"); + std::map keys; + QPDFObjectHandle stream_dict = newDictionary(keys); + QPDFObjectHandle result = qpdf->makeIndirectObject( + QPDFObjectHandle( + new QPDF_Stream(qpdf, 0, 0, stream_dict, 0, 0))); + result.dereference(); + QPDF_Stream* stream = dynamic_cast(result.obj.getPointer()); + stream->setObjGen(result.getObjectID(), result.getGeneration()); + return result; +} + +QPDFObjectHandle +QPDFObjectHandle::newStream(QPDF* qpdf, PointerHolder data) +{ + QTC::TC("qpdf", "QPDFObjectHandle newStream with data"); + QPDFObjectHandle result = newStream(qpdf); + result.replaceStreamData(data, newNull(), newNull()); + return result; +} + void QPDFObjectHandle::makeDirectInternal(std::set& visited) { @@ -649,7 +673,7 @@ QPDFObjectHandle::makeDirectInternal(std::set& visited) } else { - throw std::logic_error("QPDFObjectHandle::makeIndirect: " + throw std::logic_error("QPDFObjectHandle::makeDirectInternal: " "unknown object type"); } diff --git a/libqpdf/QPDF_Stream.cc b/libqpdf/QPDF_Stream.cc index 1f48b2c1..87c2daa9 100644 --- a/libqpdf/QPDF_Stream.cc +++ b/libqpdf/QPDF_Stream.cc @@ -40,6 +40,19 @@ QPDF_Stream::~QPDF_Stream() { } +void +QPDF_Stream::setObjGen(int objid, int generation) +{ + if (! ((this->objid == 0) && (this->generation == 0))) + { + throw std::logic_error( + "attempt to set object ID and generation of a stream" + " that already has them"); + } + this->objid = objid; + this->generation = generation; +} + std::string QPDF_Stream::unparse() { @@ -353,6 +366,12 @@ QPDF_Stream::pipeStreamData(Pipeline* pipeline, bool filter, QUtil::int_to_string(desired_length) + " bytes"); } } + else if (this->offset == 0) + { + QTC::TC("qpdf", "QPDF_Stream pipe no stream data"); + throw std::logic_error( + "pipeStreamData called for stream with no data"); + } else { QTC::TC("qpdf", "QPDF_Stream pipe original stream data"); diff --git a/libqpdf/qpdf/QPDF_Stream.hh b/libqpdf/qpdf/QPDF_Stream.hh index 1790121e..73261f39 100644 --- a/libqpdf/qpdf/QPDF_Stream.hh +++ b/libqpdf/qpdf/QPDF_Stream.hh @@ -31,6 +31,11 @@ class QPDF_Stream: public QPDFObject QPDFObjectHandle const& decode_parms, size_t length); + // Replace object ID and generation. This may only be called if + // object ID and generation are 0. It is used by QPDFObjectHandle + // when adding streams to files. + void setObjGen(int objid, int generation); + private: void replaceFilterData(QPDFObjectHandle const& filter, QPDFObjectHandle const& decode_parms, diff --git a/qpdf/qpdf.testcov b/qpdf/qpdf.testcov index c880188e..49b03c42 100644 --- a/qpdf/qpdf.testcov +++ b/qpdf/qpdf.testcov @@ -178,3 +178,6 @@ QPDF_Stream pipe original stream data 0 QPDF_Stream pipe replaced stream data 0 QPDF_Stream pipe use stream provider 0 QPDF_Stream provider length mismatch 0 +QPDFObjectHandle newStream 0 +QPDFObjectHandle newStream with data 0 +QPDF_Stream pipe no stream data 0 diff --git a/qpdf/qtest/qpdf.test b/qpdf/qtest/qpdf.test index 6768ebd4..dc47af18 100644 --- a/qpdf/qtest/qpdf.test +++ b/qpdf/qtest/qpdf.test @@ -77,7 +77,7 @@ flush_tiff_cache(); show_ntests(); # ---------- $td->notify("--- Miscellaneous Tests ---"); -$n_tests += 26; +$n_tests += 28; $td->runtest("qpdf version", {$td->COMMAND => "qpdf --version"}, @@ -104,7 +104,6 @@ $td->runtest("replace stream data", $td->runtest("check output", {$td->FILE => "a.pdf"}, {$td->FILE => "replaced-stream-data.out"}); - $td->runtest("replace stream data compressed", {$td->COMMAND => "test_driver 8 qstream.pdf"}, {$td->FILE => "test8.out", $td->EXIT_STATUS => 0}, @@ -112,6 +111,13 @@ $td->runtest("replace stream data compressed", $td->runtest("check output", {$td->FILE => "a.pdf"}, {$td->FILE => "replaced-stream-data-flate.out"}); +$td->runtest("new streams", + {$td->COMMAND => "test_driver 9 minimal.pdf"}, + {$td->FILE => "test9.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("new stream", + {$td->FILE => "a.pdf"}, + {$td->FILE => "new-streams.pdf"}); # Make sure we ignore decode parameters that we don't understand $td->runtest("unknown decode parameters", diff --git a/qpdf/qtest/qpdf/minimal.pdf b/qpdf/qtest/qpdf/minimal.pdf new file mode 100644 index 00000000..a7e01f91 --- /dev/null +++ b/qpdf/qtest/qpdf/minimal.pdf @@ -0,0 +1,79 @@ +%PDF-1.3 +1 0 obj +<< + /Type /Catalog + /Pages 2 0 R +>> +endobj + +2 0 obj +<< + /Type /Pages + /Kids [ + 3 0 R + ] + /Count 1 +>> +endobj + +3 0 obj +<< + /Type /Page + /Parent 2 0 R + /MediaBox [0 0 612 792] + /Contents 4 0 R + /Resources << + /ProcSet 5 0 R + /Font << + /F1 6 0 R + >> + >> +>> +endobj + +4 0 obj +<< + /Length 44 +>> +stream +BT + /F1 24 Tf + 72 720 Td + (Potato) Tj +ET +endstream +endobj + +5 0 obj +[ + /PDF + /Text +] +endobj + +6 0 obj +<< + /Type /Font + /Subtype /Type1 + /Name /F1 + /BaseFont /Helvetica + /Encoding /WinAnsiEncoding +>> +endobj + +xref +0 7 +0000000000 65535 f +0000000009 00000 n +0000000063 00000 n +0000000135 00000 n +0000000307 00000 n +0000000403 00000 n +0000000438 00000 n +trailer << + /Size 7 + /Root 1 0 R +>> +startxref +556 +%%EOF diff --git a/qpdf/qtest/qpdf/new-streams.pdf b/qpdf/qtest/qpdf/new-streams.pdf new file mode 100644 index 00000000..d3966a33 --- /dev/null +++ b/qpdf/qtest/qpdf/new-streams.pdf @@ -0,0 +1,54 @@ +%PDF-1.3 +%¿÷¢þ +1 0 obj +<< /Pages 2 0 R /QStream 3 0 R /RStream 4 0 R /Type /Catalog >> +endobj +2 0 obj +<< /Count 1 /Kids [ 5 0 R ] /Type /Pages >> +endobj +3 0 obj +<< /Length 20 >> +stream +data for new stream +endstream +endobj +4 0 obj +<< /Length 22 >> +stream +data for other stream +endstream +endobj +5 0 obj +<< /Contents 6 0 R /MediaBox [ 0 0 612 792 ] /Parent 2 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 +0000000094 00000 n +0000000153 00000 n +0000000222 00000 n +0000000293 00000 n +0000000436 00000 n +0000000529 00000 n +0000000636 00000 n +trailer << /Root 1 0 R /Size 9 /ID [<31415926535897932384626433832795><31415926535897932384626433832795>] >> +startxref +666 +%%EOF diff --git a/qpdf/qtest/qpdf/test9.out b/qpdf/qtest/qpdf/test9.out new file mode 100644 index 00000000..3489ba0c --- /dev/null +++ b/qpdf/qtest/qpdf/test9.out @@ -0,0 +1,2 @@ +exception: pipeStreamData called for stream with no data +test 9 done diff --git a/qpdf/test_driver.cc b/qpdf/test_driver.cc index e9739e92..09458227 100644 --- a/qpdf/test_driver.cc +++ b/qpdf/test_driver.cc @@ -398,6 +398,35 @@ void runtest(int n, char const* filename) w.setStreamDataMode(qpdf_s_preserve); w.write(); } + else if (n == 9) + { + QPDFObjectHandle root = pdf.getRoot(); + PointerHolder b1 = new Buffer(20); + unsigned char* bp = b1.getPointer()->getBuffer(); + memcpy(bp, (char*)"data for new stream\n", 20); // no null! + QPDFObjectHandle qstream = QPDFObjectHandle::newStream(&pdf, b1); + QPDFObjectHandle rstream = QPDFObjectHandle::newStream(&pdf); + try + { + rstream.getStreamData(); + std::cout << "oops -- getStreamData didn't throw" << std::endl; + } + catch (std::logic_error const& e) + { + std::cout << "exception: " << e.what() << std::endl; + } + PointerHolder b2 = new Buffer(22); + bp = b2.getPointer()->getBuffer(); + memcpy(bp, (char*)"data for other stream\n", 22); // no null! + rstream.replaceStreamData( + b2, QPDFObjectHandle::newNull(), QPDFObjectHandle::newNull()); + root.replaceKey("/QStream", qstream); + root.replaceKey("/RStream", rstream); + QPDFWriter w(pdf, "a.pdf"); + w.setStaticID(true); + w.setStreamDataMode(qpdf_s_preserve); + w.write(); + } else { throw std::runtime_error(std::string("invalid test ") +