diff --git a/ChangeLog b/ChangeLog index c94c76e6..32f0fb34 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,13 @@ 2020-12-26 Jay Berkenbilt + * Add QPDFObjectHandle::setFilterOnWrite, which can be used to + tell QPDFWriter not to filter a stream on output even if it can. + You can use this to prevent QPDFWriter from touching a stream + (either uncompressing or compressing) that you have optimized or + otherwise ensured looks exactly the way you want it, even if + decode level or stream compression would otherwise cause + QPDFWriter to modify the stream. + * Add ostream << for QPDFObjGen. (Don't ask why it took 7.5 years for me to decide to do this.) diff --git a/include/qpdf/QPDFObjectHandle.hh b/include/qpdf/QPDFObjectHandle.hh index 0cd10569..060d21e1 100644 --- a/include/qpdf/QPDFObjectHandle.hh +++ b/include/qpdf/QPDFObjectHandle.hh @@ -786,6 +786,22 @@ class QPDFObjectHandle QPDF_DLL QPDFObjectHandle getDict(); + // By default, or if true passed, QPDFWriter will attempt to + // filter a stream based on decode level, whether compression is + // enabled, and its ability to filter. Passing false will prevent + // QPDFWriter from attempting to filter the stream even if it can. + // This includes both decoding and compressing. This makes it + // possible for you to prevent QPDFWriter from uncompressing and + // recompressing a stream that it knows how to operate on for any + // application-specific reason, such as that you have already + // optimized its filtering. Note that this doesn't affect any + // other ways to get the stream's data, such as pipeStreamData or + // getStreamData. + QPDF_DLL + void setFilterOnWrite(bool); + QPDF_DLL + bool getFilterOnWrite(); + // If addTokenFilter has been called for this stream, then the // original data should be considered to be modified. This means we // should avoid optimizations such as not filtering a stream that diff --git a/libqpdf/QPDFObjectHandle.cc b/libqpdf/QPDFObjectHandle.cc index e09146e4..7b36fffe 100644 --- a/libqpdf/QPDFObjectHandle.cc +++ b/libqpdf/QPDFObjectHandle.cc @@ -1176,6 +1176,20 @@ QPDFObjectHandle::getDict() return dynamic_cast(obj.getPointer())->getDict(); } +void +QPDFObjectHandle::setFilterOnWrite(bool val) +{ + assertStream(); + dynamic_cast(obj.getPointer())->setFilterOnWrite(val); +} + +bool +QPDFObjectHandle::getFilterOnWrite() +{ + assertStream(); + return dynamic_cast(obj.getPointer())->getFilterOnWrite(); +} + bool QPDFObjectHandle::isDataModified() { diff --git a/libqpdf/QPDFWriter.cc b/libqpdf/QPDFWriter.cc index 7057930b..689fef74 100644 --- a/libqpdf/QPDFWriter.cc +++ b/libqpdf/QPDFWriter.cc @@ -1470,6 +1470,7 @@ QPDFWriter::willFilterStream(QPDFObjectHandle stream, { compress_stream = false; is_metadata = false; + QPDFObjGen old_og = stream.getObjGen(); QPDFObjectHandle stream_dict = stream.getDict(); @@ -1481,7 +1482,13 @@ QPDFWriter::willFilterStream(QPDFObjectHandle stream, bool filter = (stream.isDataModified() || this->m->compress_streams || this->m->stream_decode_level); - if (this->m->compress_streams) + bool filter_on_write = stream.getFilterOnWrite(); + if (! filter_on_write) + { + QTC::TC("qpdf", "QPDFWriter getFilterOnWrite false"); + filter = false; + } + if (filter_on_write && this->m->compress_streams) { // Don't filter if the stream is already compressed with // FlateDecode. This way we don't make it worse if the @@ -1502,7 +1509,7 @@ QPDFWriter::willFilterStream(QPDFObjectHandle stream, } bool normalize = false; bool uncompress = false; - if (is_metadata && + if (filter_on_write && is_metadata && ((! this->m->encrypted) || (this->m->encrypt_metadata == false))) { QTC::TC("qpdf", "QPDFWriter not compressing metadata"); @@ -1510,13 +1517,13 @@ QPDFWriter::willFilterStream(QPDFObjectHandle stream, compress_stream = false; uncompress = true; } - else if (this->m->normalize_content && + else if (filter_on_write && this->m->normalize_content && this->m->normalized_streams.count(old_og)) { normalize = true; filter = true; } - else if (filter && this->m->compress_streams) + else if (filter_on_write && filter && this->m->compress_streams) { compress_stream = true; QTC::TC("qpdf", "QPDFWriter compressing uncompressed stream"); diff --git a/libqpdf/QPDF_Stream.cc b/libqpdf/QPDF_Stream.cc index 8f9b4b52..05cde8d8 100644 --- a/libqpdf/QPDF_Stream.cc +++ b/libqpdf/QPDF_Stream.cc @@ -90,6 +90,7 @@ QPDF_Stream::QPDF_Stream(QPDF* qpdf, int objid, int generation, qpdf(qpdf), objid(objid), generation(generation), + filter_on_write(true), stream_dict(stream_dict), offset(offset), length(length) @@ -115,6 +116,18 @@ QPDF_Stream::registerStreamFilter( filter_factories[filter_name] = factory; } +void +QPDF_Stream::setFilterOnWrite(bool val) +{ + this->filter_on_write = val; +} + +bool +QPDF_Stream::getFilterOnWrite() const +{ + return this->filter_on_write; +} + void QPDF_Stream::releaseResolved() { diff --git a/libqpdf/qpdf/QPDF_Stream.hh b/libqpdf/qpdf/QPDF_Stream.hh index b11de6a2..c4fdd379 100644 --- a/libqpdf/qpdf/QPDF_Stream.hh +++ b/libqpdf/qpdf/QPDF_Stream.hh @@ -27,6 +27,8 @@ class QPDF_Stream: public QPDFObject virtual void setDescription(QPDF*, std::string const&); QPDFObjectHandle getDict() const; bool isDataModified() const; + void setFilterOnWrite(bool); + bool getFilterOnWrite() const; // Methods to help QPDF copy foreign streams qpdf_offset_t getOffset() const; @@ -83,6 +85,7 @@ class QPDF_Stream: public QPDFObject QPDF* qpdf; int objid; int generation; + bool filter_on_write; QPDFObjectHandle stream_dict; qpdf_offset_t offset; size_t length; diff --git a/qpdf/qpdf.testcov b/qpdf/qpdf.testcov index 908f06e5..04e3e044 100644 --- a/qpdf/qpdf.testcov +++ b/qpdf/qpdf.testcov @@ -520,3 +520,4 @@ qpdf-c called qpdf_oh_get_generation 0 qpdf-c called qpdf_oh_unparse 0 qpdf-c called qpdf_oh_unparse_resolved 0 qpdf-c called qpdf_oh_unparse_binary 0 +QPDFWriter getFilterOnWrite false 0 diff --git a/qpdf/qtest/qpdf.test b/qpdf/qtest/qpdf.test index 6ae330a4..b166db84 100644 --- a/qpdf/qtest/qpdf.test +++ b/qpdf/qtest/qpdf.test @@ -1179,6 +1179,19 @@ $td->runtest("check output", {$td->FILE => "a.pdf"}, {$td->FILE => "filter-abbreviation.out"}); +show_ntests(); +# ---------- +$td->notify("--- Disable filter on write ---"); +$n_tests += 2; + +$td->runtest("no filter on write", + {$td->COMMAND => "test_driver 70 filter-on-write.pdf"}, + {$td->STRING => "test 70 done\n", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("check output", + {$td->FILE => "a.pdf"}, + {$td->FILE => "filter-on-write-out.pdf"}); + show_ntests(); # ---------- $td->notify("--- Invalid objects ---"); @@ -1197,7 +1210,7 @@ $td->runtest("object with zero offset", show_ntests(); # ---------- -$td->notify("--- Error/output rediction ---"); +$td->notify("--- Error/output redirection ---"); $n_tests += 2; $td->runtest("error/output redirection to null", diff --git a/qpdf/qtest/qpdf/filter-on-write-out.pdf b/qpdf/qtest/qpdf/filter-on-write-out.pdf new file mode 100644 index 00000000..46cdbbc9 Binary files /dev/null and b/qpdf/qtest/qpdf/filter-on-write-out.pdf differ diff --git a/qpdf/qtest/qpdf/filter-on-write.pdf b/qpdf/qtest/qpdf/filter-on-write.pdf new file mode 100644 index 00000000..1e35a3e8 --- /dev/null +++ b/qpdf/qtest/qpdf/filter-on-write.pdf @@ -0,0 +1,41 @@ +%PDF-1.3 +%¿÷¢þ +1 0 obj +<< /Pages 2 0 R /Type /Catalog >> +endobj +2 0 obj +<< /Count 0 /Kids [ ] /Type /Pages >> +endobj +3 0 obj +<< /Filter /RunLengthDecode /Length 5 >> +stream +w¹w€endstream +endobj +4 0 obj +<< /Length 6 >> +stream +potatoendstream +endobj +5 0 obj +<< /Filter /RunLengthDecode /Length 5 >> +stream +w¹w€endstream +endobj +6 0 obj +<< /Length 5 >> +stream +saladendstream +endobj +xref +0 7 +0000000000 65535 f +0000000015 00000 n +0000000064 00000 n +0000000117 00000 n +0000000195 00000 n +0000000249 00000 n +0000000327 00000 n +trailer << /Root 1 0 R /Size 7 /ID [<5ab7a0329a828e2f46377e16247bc367><5ab7a0329a828e2f46377e16247bc367>] /S1 3 0 R /S2 4 0 R /S3 5 0 R /S4 6 0 R >> +startxref +380 +%%EOF diff --git a/qpdf/test_driver.cc b/qpdf/test_driver.cc index 68519e09..b373e5bc 100644 --- a/qpdf/test_driver.cc +++ b/qpdf/test_driver.cc @@ -2219,6 +2219,16 @@ void runtest(int n, char const* filename1, char const* arg2) w.write(); } } + else if (n == 70) + { + auto trailer = pdf.getTrailer(); + trailer.getKey("/S1").setFilterOnWrite(false); + trailer.getKey("/S2").setFilterOnWrite(false); + QPDFWriter w(pdf, "a.pdf"); + w.setStaticID(true); + w.setDecodeLevel(qpdf_dl_specialized); + w.write(); + } else { throw std::runtime_error(std::string("invalid test ") +