From efbb21673c59cfbf6a74de6866a59cb2dbb8e59f Mon Sep 17 00:00:00 2001 From: Jay Berkenbilt Date: Sun, 14 Feb 2021 14:04:40 -0500 Subject: [PATCH] Add functional versions of QPDFObjectHandle::replaceStreamData Also fix a bug in checking consistency of length for stream data providers. Length should not be checked or recorded if the provider says it failed to generate the data. --- ChangeLog | 8 ++ include/qpdf/QPDFObjectHandle.hh | 21 +++++ libqpdf/QPDFObjectHandle.cc | 56 +++++++++++++ libqpdf/QPDF_Stream.cc | 4 +- manual/qpdf-manual.xml | 11 +++ qpdf/qtest/qpdf.test | 9 +- qpdf/qtest/qpdf/test78.out | 10 +++ qpdf/qtest/qpdf/test78.pdf | 139 +++++++++++++++++++++++++++++++ qpdf/test_driver.cc | 43 ++++++++++ 9 files changed, 298 insertions(+), 3 deletions(-) create mode 100644 qpdf/qtest/qpdf/test78.out create mode 100644 qpdf/qtest/qpdf/test78.pdf diff --git a/ChangeLog b/ChangeLog index b9f44077..7b1db1f2 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,11 @@ +2021-02-14 Jay Berkenbilt + + * Add new versions of QPDFObjectHandle::replaceStreamData that + take std::function objects for cases when you need something + between a static string and a full-fledged StreamDataProvider. + Using this with QUtil::file_provider is a very easy way to create + a stream from the contents of a file. + 2021-02-12 Jay Berkenbilt * Move formerly internal QPDFMatrix class to the public API. This diff --git a/include/qpdf/QPDFObjectHandle.hh b/include/qpdf/QPDFObjectHandle.hh index 41646b79..cb4c5be6 100644 --- a/include/qpdf/QPDFObjectHandle.hh +++ b/include/qpdf/QPDFObjectHandle.hh @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -986,6 +987,26 @@ class QPDFObjectHandle QPDFObjectHandle const& filter, QPDFObjectHandle const& decode_parms); + // Starting in qpdf 10.2, you can use C++-11 function objects + // instead of StreamDataProvider. + + // The provider should write the stream data to the pipeline. For + // a one-liner to replace stream data with the contents of a file, + // pass QUtil::file_provider(filename) as provider. + QPDF_DLL + void replaceStreamData(std::function provider, + QPDFObjectHandle const& filter, + QPDFObjectHandle const& decode_parms); + // The provider should write the stream data to the pipeline, + // returning true if it succeeded without errors. + QPDF_DLL + void replaceStreamData( + std::function provider, + QPDFObjectHandle const& filter, + QPDFObjectHandle const& decode_parms); + // Access object ID and generation. For direct objects, return // object ID 0. diff --git a/libqpdf/QPDFObjectHandle.cc b/libqpdf/QPDFObjectHandle.cc index 94875e13..d4796498 100644 --- a/libqpdf/QPDFObjectHandle.cc +++ b/libqpdf/QPDFObjectHandle.cc @@ -1347,6 +1347,62 @@ QPDFObjectHandle::replaceStreamData(PointerHolder provider, provider, filter, decode_parms); } +class FunctionProvider: public QPDFObjectHandle::StreamDataProvider +{ + public: + FunctionProvider(std::function provider) : + StreamDataProvider(false), + p1(provider), + p2(nullptr) + { + } + FunctionProvider(std::function provider) : + StreamDataProvider(true), + p1(nullptr), + p2(provider) + { + } + + virtual void provideStreamData(int, int, Pipeline* pipeline) override + { + p1(pipeline); + } + + virtual bool provideStreamData(int, int, Pipeline* pipeline, + bool suppress_warnings, + bool will_retry) override + { + return p2(pipeline, suppress_warnings, will_retry); + } + + private: + std::function p1; + std::function p2; +}; + +void +QPDFObjectHandle::replaceStreamData(std::function provider, + QPDFObjectHandle const& filter, + QPDFObjectHandle const& decode_parms) +{ + assertStream(); + PointerHolder sdp = new FunctionProvider(provider); + dynamic_cast(obj.getPointer())->replaceStreamData( + sdp, filter, decode_parms); +} + +void +QPDFObjectHandle::replaceStreamData( + std::function provider, + QPDFObjectHandle const& filter, + QPDFObjectHandle const& decode_parms) +{ + assertStream(); + PointerHolder sdp = new FunctionProvider(provider); + dynamic_cast(obj.getPointer())->replaceStreamData( + sdp, filter, decode_parms); +} + QPDFObjGen QPDFObjectHandle::getObjGen() const { diff --git a/libqpdf/QPDF_Stream.cc b/libqpdf/QPDF_Stream.cc index b05137df..bc3b1b56 100644 --- a/libqpdf/QPDF_Stream.cc +++ b/libqpdf/QPDF_Stream.cc @@ -533,7 +533,7 @@ QPDF_Stream::pipeStreamData(Pipeline* pipeline, bool* filterp, } qpdf_offset_t actual_length = count.getCount(); qpdf_offset_t desired_length = 0; - if (this->stream_dict.hasKey("/Length")) + if (success && this->stream_dict.hasKey("/Length")) { desired_length = this->stream_dict.getKey("/Length").getIntValue(); if (actual_length == desired_length) @@ -555,7 +555,7 @@ QPDF_Stream::pipeStreamData(Pipeline* pipeline, bool* filterp, QUtil::int_to_string(desired_length) + " bytes"); } } - else + else if (success) { QTC::TC("qpdf", "QPDF_Stream provider length not provided"); this->stream_dict.replaceKey( diff --git a/manual/qpdf-manual.xml b/manual/qpdf-manual.xml index 58012e30..ae085d6e 100644 --- a/manual/qpdf-manual.xml +++ b/manual/qpdf-manual.xml @@ -5211,6 +5211,17 @@ print "\n"; contents of a file through a pipeline as binary data. + + + Add new versions of + QPDFObjectHandle::replaceStreamData + that take std::function objects for + cases when you need something between a static string and a + full-fledged StreamDataProvider. Using this with + QUtil::file_provider is a very easy way to create a stream + from the contents of a file. + + Add option to QUtil::double_to_string diff --git a/qpdf/qtest/qpdf.test b/qpdf/qtest/qpdf.test index dba10181..38a2d1b8 100644 --- a/qpdf/qtest/qpdf.test +++ b/qpdf/qtest/qpdf.test @@ -717,7 +717,7 @@ $td->runtest("check dates", show_ntests(); # ---------- $td->notify("--- Stream Replacement Tests ---"); -$n_tests += 8; +$n_tests += 10; $td->runtest("replace stream data", {$td->COMMAND => "test_driver 7 qstream.pdf"}, @@ -747,6 +747,13 @@ $td->runtest("add page contents", $td->runtest("new stream", {$td->FILE => "a.pdf"}, {$td->FILE => "add-contents.pdf"}); +$td->runtest("functional replace stream data", + {$td->COMMAND => "test_driver 78 minimal.pdf"}, + {$td->FILE => "test78.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("check output", + {$td->FILE => "a.pdf"}, + {$td->FILE => "test78.pdf"}); show_ntests(); # ---------- diff --git a/qpdf/qtest/qpdf/test78.out b/qpdf/qtest/qpdf/test78.out new file mode 100644 index 00000000..d726dd37 --- /dev/null +++ b/qpdf/qtest/qpdf/test78.out @@ -0,0 +1,10 @@ +piping with warning suppression +f2 +f2 done +writing +f2 +failing +f2 +warning +f2 done +test 78 done diff --git a/qpdf/qtest/qpdf/test78.pdf b/qpdf/qtest/qpdf/test78.pdf new file mode 100644 index 00000000..d5d4ce4b --- /dev/null +++ b/qpdf/qtest/qpdf/test78.pdf @@ -0,0 +1,139 @@ +%PDF-1.3 +%¿÷¢þ +%QDF-1.0 + +%% Original object ID: 1 0 +1 0 obj +<< + /Pages 6 0 R + /Type /Catalog +>> +endobj + +%% Original object ID: 7 0 +2 0 obj +<< + /Length 3 0 R +>> +stream +potato +endstream +endobj + +%QDF: ignore_newline +3 0 obj +6 +endobj + +%% Original object ID: 8 0 +4 0 obj +<< + /Length 5 0 R +>> +stream +salad +endstream +endobj + +%QDF: ignore_newline +5 0 obj +5 +endobj + +%% Original object ID: 2 0 +6 0 obj +<< + /Count 1 + /Kids [ + 7 0 R + ] + /Type /Pages +>> +endobj + +%% Page 1 +%% Original object ID: 3 0 +7 0 obj +<< + /Contents 8 0 R + /MediaBox [ + 0 + 0 + 612 + 792 + ] + /Parent 6 0 R + /Resources << + /Font << + /F1 10 0 R + >> + /ProcSet 11 0 R + >> + /Type /Page +>> +endobj + +%% Contents for page 1 +%% Original object ID: 4 0 +8 0 obj +<< + /Length 9 0 R +>> +stream +BT + /F1 24 Tf + 72 720 Td + (Potato) Tj +ET +endstream +endobj + +9 0 obj +44 +endobj + +%% Original object ID: 6 0 +10 0 obj +<< + /BaseFont /Helvetica + /Encoding /WinAnsiEncoding + /Name /F1 + /Subtype /Type1 + /Type /Font +>> +endobj + +%% Original object ID: 5 0 +11 0 obj +[ + /PDF + /Text +] +endobj + +xref +0 12 +0000000000 65535 f +0000000052 00000 n +0000000133 00000 n +0000000216 00000 n +0000000261 00000 n +0000000343 00000 n +0000000388 00000 n +0000000497 00000 n +0000000741 00000 n +0000000840 00000 n +0000000886 00000 n +0000001032 00000 n +trailer << + /Root 1 0 R + /Size 12 + /Streams [ + 2 0 R + 4 0 R + ] + /ID [<31415926535897932384626433832795><31415926535897932384626433832795>] +>> +startxref +1068 +%%EOF diff --git a/qpdf/test_driver.cc b/qpdf/test_driver.cc index 28e07c5d..d7c9a352 100644 --- a/qpdf/test_driver.cc +++ b/qpdf/test_driver.cc @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -2798,6 +2799,48 @@ void runtest(int n, char const* filename1, char const* arg2) w.setQDFMode(true); w.write(); } + else if (n == 78) + { + // Test functional versions of replaceStreamData() + + auto f1 = [](Pipeline* p) { + p->write(QUtil::unsigned_char_pointer("potato"), 6); + p->finish(); + }; + auto f2 = [](Pipeline* p, bool suppress_warnings, bool will_retry) { + std::cerr << "f2" << std::endl; + if (will_retry) + { + std::cerr << "failing" << std::endl; + return false; + } + if (! suppress_warnings) + { + std::cerr << "warning" << std::endl; + } + p->write(QUtil::unsigned_char_pointer("salad"), 5); + p->finish(); + std::cerr << "f2 done" << std::endl; + return true; + }; + + auto null = QPDFObjectHandle::newNull(); + auto s1 = QPDFObjectHandle::newStream(&pdf); + s1.replaceStreamData(f1, null, null); + auto s2 = QPDFObjectHandle::newStream(&pdf); + s2.replaceStreamData(f2, null, null); + pdf.getTrailer().replaceKey( + "/Streams", QPDFObjectHandle::newArray({s1, s2})); + std::cout << "piping with warning suppression" << std::endl; + Pl_Discard d; + s2.pipeStreamData(&d, nullptr, 0, qpdf_dl_all, true, false); + + std::cout << "writing" << std::endl; + QPDFWriter w(pdf, "a.pdf"); + w.setStaticID(true); + w.setQDFMode(true); + w.write(); + } else { throw std::runtime_error(std::string("invalid test ") +