Commit 12ecd2019a3186d11ebb6083b3813ce722fb2329
1 parent
3f9191a3
Add QPDFObjectHandle::setFilterOnWrite
Showing
11 changed files
with
131 additions
and
5 deletions
ChangeLog
| 1 | 2020-12-26 Jay Berkenbilt <ejb@ql.org> | 1 | 2020-12-26 Jay Berkenbilt <ejb@ql.org> |
| 2 | 2 | ||
| 3 | + * Add QPDFObjectHandle::setFilterOnWrite, which can be used to | ||
| 4 | + tell QPDFWriter not to filter a stream on output even if it can. | ||
| 5 | + You can use this to prevent QPDFWriter from touching a stream | ||
| 6 | + (either uncompressing or compressing) that you have optimized or | ||
| 7 | + otherwise ensured looks exactly the way you want it, even if | ||
| 8 | + decode level or stream compression would otherwise cause | ||
| 9 | + QPDFWriter to modify the stream. | ||
| 10 | + | ||
| 3 | * Add ostream << for QPDFObjGen. (Don't ask why it took 7.5 years | 11 | * Add ostream << for QPDFObjGen. (Don't ask why it took 7.5 years |
| 4 | for me to decide to do this.) | 12 | for me to decide to do this.) |
| 5 | 13 |
include/qpdf/QPDFObjectHandle.hh
| @@ -786,6 +786,22 @@ class QPDFObjectHandle | @@ -786,6 +786,22 @@ class QPDFObjectHandle | ||
| 786 | QPDF_DLL | 786 | QPDF_DLL |
| 787 | QPDFObjectHandle getDict(); | 787 | QPDFObjectHandle getDict(); |
| 788 | 788 | ||
| 789 | + // By default, or if true passed, QPDFWriter will attempt to | ||
| 790 | + // filter a stream based on decode level, whether compression is | ||
| 791 | + // enabled, and its ability to filter. Passing false will prevent | ||
| 792 | + // QPDFWriter from attempting to filter the stream even if it can. | ||
| 793 | + // This includes both decoding and compressing. This makes it | ||
| 794 | + // possible for you to prevent QPDFWriter from uncompressing and | ||
| 795 | + // recompressing a stream that it knows how to operate on for any | ||
| 796 | + // application-specific reason, such as that you have already | ||
| 797 | + // optimized its filtering. Note that this doesn't affect any | ||
| 798 | + // other ways to get the stream's data, such as pipeStreamData or | ||
| 799 | + // getStreamData. | ||
| 800 | + QPDF_DLL | ||
| 801 | + void setFilterOnWrite(bool); | ||
| 802 | + QPDF_DLL | ||
| 803 | + bool getFilterOnWrite(); | ||
| 804 | + | ||
| 789 | // If addTokenFilter has been called for this stream, then the | 805 | // If addTokenFilter has been called for this stream, then the |
| 790 | // original data should be considered to be modified. This means we | 806 | // original data should be considered to be modified. This means we |
| 791 | // should avoid optimizations such as not filtering a stream that | 807 | // should avoid optimizations such as not filtering a stream that |
libqpdf/QPDFObjectHandle.cc
| @@ -1176,6 +1176,20 @@ QPDFObjectHandle::getDict() | @@ -1176,6 +1176,20 @@ QPDFObjectHandle::getDict() | ||
| 1176 | return dynamic_cast<QPDF_Stream*>(obj.getPointer())->getDict(); | 1176 | return dynamic_cast<QPDF_Stream*>(obj.getPointer())->getDict(); |
| 1177 | } | 1177 | } |
| 1178 | 1178 | ||
| 1179 | +void | ||
| 1180 | +QPDFObjectHandle::setFilterOnWrite(bool val) | ||
| 1181 | +{ | ||
| 1182 | + assertStream(); | ||
| 1183 | + dynamic_cast<QPDF_Stream*>(obj.getPointer())->setFilterOnWrite(val); | ||
| 1184 | +} | ||
| 1185 | + | ||
| 1186 | +bool | ||
| 1187 | +QPDFObjectHandle::getFilterOnWrite() | ||
| 1188 | +{ | ||
| 1189 | + assertStream(); | ||
| 1190 | + return dynamic_cast<QPDF_Stream*>(obj.getPointer())->getFilterOnWrite(); | ||
| 1191 | +} | ||
| 1192 | + | ||
| 1179 | bool | 1193 | bool |
| 1180 | QPDFObjectHandle::isDataModified() | 1194 | QPDFObjectHandle::isDataModified() |
| 1181 | { | 1195 | { |
libqpdf/QPDFWriter.cc
| @@ -1470,6 +1470,7 @@ QPDFWriter::willFilterStream(QPDFObjectHandle stream, | @@ -1470,6 +1470,7 @@ QPDFWriter::willFilterStream(QPDFObjectHandle stream, | ||
| 1470 | { | 1470 | { |
| 1471 | compress_stream = false; | 1471 | compress_stream = false; |
| 1472 | is_metadata = false; | 1472 | is_metadata = false; |
| 1473 | + | ||
| 1473 | QPDFObjGen old_og = stream.getObjGen(); | 1474 | QPDFObjGen old_og = stream.getObjGen(); |
| 1474 | QPDFObjectHandle stream_dict = stream.getDict(); | 1475 | QPDFObjectHandle stream_dict = stream.getDict(); |
| 1475 | 1476 | ||
| @@ -1481,7 +1482,13 @@ QPDFWriter::willFilterStream(QPDFObjectHandle stream, | @@ -1481,7 +1482,13 @@ QPDFWriter::willFilterStream(QPDFObjectHandle stream, | ||
| 1481 | bool filter = (stream.isDataModified() || | 1482 | bool filter = (stream.isDataModified() || |
| 1482 | this->m->compress_streams || | 1483 | this->m->compress_streams || |
| 1483 | this->m->stream_decode_level); | 1484 | this->m->stream_decode_level); |
| 1484 | - if (this->m->compress_streams) | 1485 | + bool filter_on_write = stream.getFilterOnWrite(); |
| 1486 | + if (! filter_on_write) | ||
| 1487 | + { | ||
| 1488 | + QTC::TC("qpdf", "QPDFWriter getFilterOnWrite false"); | ||
| 1489 | + filter = false; | ||
| 1490 | + } | ||
| 1491 | + if (filter_on_write && this->m->compress_streams) | ||
| 1485 | { | 1492 | { |
| 1486 | // Don't filter if the stream is already compressed with | 1493 | // Don't filter if the stream is already compressed with |
| 1487 | // FlateDecode. This way we don't make it worse if the | 1494 | // FlateDecode. This way we don't make it worse if the |
| @@ -1502,7 +1509,7 @@ QPDFWriter::willFilterStream(QPDFObjectHandle stream, | @@ -1502,7 +1509,7 @@ QPDFWriter::willFilterStream(QPDFObjectHandle stream, | ||
| 1502 | } | 1509 | } |
| 1503 | bool normalize = false; | 1510 | bool normalize = false; |
| 1504 | bool uncompress = false; | 1511 | bool uncompress = false; |
| 1505 | - if (is_metadata && | 1512 | + if (filter_on_write && is_metadata && |
| 1506 | ((! this->m->encrypted) || (this->m->encrypt_metadata == false))) | 1513 | ((! this->m->encrypted) || (this->m->encrypt_metadata == false))) |
| 1507 | { | 1514 | { |
| 1508 | QTC::TC("qpdf", "QPDFWriter not compressing metadata"); | 1515 | QTC::TC("qpdf", "QPDFWriter not compressing metadata"); |
| @@ -1510,13 +1517,13 @@ QPDFWriter::willFilterStream(QPDFObjectHandle stream, | @@ -1510,13 +1517,13 @@ QPDFWriter::willFilterStream(QPDFObjectHandle stream, | ||
| 1510 | compress_stream = false; | 1517 | compress_stream = false; |
| 1511 | uncompress = true; | 1518 | uncompress = true; |
| 1512 | } | 1519 | } |
| 1513 | - else if (this->m->normalize_content && | 1520 | + else if (filter_on_write && this->m->normalize_content && |
| 1514 | this->m->normalized_streams.count(old_og)) | 1521 | this->m->normalized_streams.count(old_og)) |
| 1515 | { | 1522 | { |
| 1516 | normalize = true; | 1523 | normalize = true; |
| 1517 | filter = true; | 1524 | filter = true; |
| 1518 | } | 1525 | } |
| 1519 | - else if (filter && this->m->compress_streams) | 1526 | + else if (filter_on_write && filter && this->m->compress_streams) |
| 1520 | { | 1527 | { |
| 1521 | compress_stream = true; | 1528 | compress_stream = true; |
| 1522 | QTC::TC("qpdf", "QPDFWriter compressing uncompressed stream"); | 1529 | QTC::TC("qpdf", "QPDFWriter compressing uncompressed stream"); |
libqpdf/QPDF_Stream.cc
| @@ -90,6 +90,7 @@ QPDF_Stream::QPDF_Stream(QPDF* qpdf, int objid, int generation, | @@ -90,6 +90,7 @@ QPDF_Stream::QPDF_Stream(QPDF* qpdf, int objid, int generation, | ||
| 90 | qpdf(qpdf), | 90 | qpdf(qpdf), |
| 91 | objid(objid), | 91 | objid(objid), |
| 92 | generation(generation), | 92 | generation(generation), |
| 93 | + filter_on_write(true), | ||
| 93 | stream_dict(stream_dict), | 94 | stream_dict(stream_dict), |
| 94 | offset(offset), | 95 | offset(offset), |
| 95 | length(length) | 96 | length(length) |
| @@ -116,6 +117,18 @@ QPDF_Stream::registerStreamFilter( | @@ -116,6 +117,18 @@ QPDF_Stream::registerStreamFilter( | ||
| 116 | } | 117 | } |
| 117 | 118 | ||
| 118 | void | 119 | void |
| 120 | +QPDF_Stream::setFilterOnWrite(bool val) | ||
| 121 | +{ | ||
| 122 | + this->filter_on_write = val; | ||
| 123 | +} | ||
| 124 | + | ||
| 125 | +bool | ||
| 126 | +QPDF_Stream::getFilterOnWrite() const | ||
| 127 | +{ | ||
| 128 | + return this->filter_on_write; | ||
| 129 | +} | ||
| 130 | + | ||
| 131 | +void | ||
| 119 | QPDF_Stream::releaseResolved() | 132 | QPDF_Stream::releaseResolved() |
| 120 | { | 133 | { |
| 121 | this->stream_provider = 0; | 134 | this->stream_provider = 0; |
libqpdf/qpdf/QPDF_Stream.hh
| @@ -27,6 +27,8 @@ class QPDF_Stream: public QPDFObject | @@ -27,6 +27,8 @@ class QPDF_Stream: public QPDFObject | ||
| 27 | virtual void setDescription(QPDF*, std::string const&); | 27 | virtual void setDescription(QPDF*, std::string const&); |
| 28 | QPDFObjectHandle getDict() const; | 28 | QPDFObjectHandle getDict() const; |
| 29 | bool isDataModified() const; | 29 | bool isDataModified() const; |
| 30 | + void setFilterOnWrite(bool); | ||
| 31 | + bool getFilterOnWrite() const; | ||
| 30 | 32 | ||
| 31 | // Methods to help QPDF copy foreign streams | 33 | // Methods to help QPDF copy foreign streams |
| 32 | qpdf_offset_t getOffset() const; | 34 | qpdf_offset_t getOffset() const; |
| @@ -83,6 +85,7 @@ class QPDF_Stream: public QPDFObject | @@ -83,6 +85,7 @@ class QPDF_Stream: public QPDFObject | ||
| 83 | QPDF* qpdf; | 85 | QPDF* qpdf; |
| 84 | int objid; | 86 | int objid; |
| 85 | int generation; | 87 | int generation; |
| 88 | + bool filter_on_write; | ||
| 86 | QPDFObjectHandle stream_dict; | 89 | QPDFObjectHandle stream_dict; |
| 87 | qpdf_offset_t offset; | 90 | qpdf_offset_t offset; |
| 88 | size_t length; | 91 | size_t length; |
qpdf/qpdf.testcov
| @@ -520,3 +520,4 @@ qpdf-c called qpdf_oh_get_generation 0 | @@ -520,3 +520,4 @@ qpdf-c called qpdf_oh_get_generation 0 | ||
| 520 | qpdf-c called qpdf_oh_unparse 0 | 520 | qpdf-c called qpdf_oh_unparse 0 |
| 521 | qpdf-c called qpdf_oh_unparse_resolved 0 | 521 | qpdf-c called qpdf_oh_unparse_resolved 0 |
| 522 | qpdf-c called qpdf_oh_unparse_binary 0 | 522 | qpdf-c called qpdf_oh_unparse_binary 0 |
| 523 | +QPDFWriter getFilterOnWrite false 0 |
qpdf/qtest/qpdf.test
| @@ -1181,6 +1181,19 @@ $td->runtest("check output", | @@ -1181,6 +1181,19 @@ $td->runtest("check output", | ||
| 1181 | 1181 | ||
| 1182 | show_ntests(); | 1182 | show_ntests(); |
| 1183 | # ---------- | 1183 | # ---------- |
| 1184 | +$td->notify("--- Disable filter on write ---"); | ||
| 1185 | +$n_tests += 2; | ||
| 1186 | + | ||
| 1187 | +$td->runtest("no filter on write", | ||
| 1188 | + {$td->COMMAND => "test_driver 70 filter-on-write.pdf"}, | ||
| 1189 | + {$td->STRING => "test 70 done\n", $td->EXIT_STATUS => 0}, | ||
| 1190 | + $td->NORMALIZE_NEWLINES); | ||
| 1191 | +$td->runtest("check output", | ||
| 1192 | + {$td->FILE => "a.pdf"}, | ||
| 1193 | + {$td->FILE => "filter-on-write-out.pdf"}); | ||
| 1194 | + | ||
| 1195 | +show_ntests(); | ||
| 1196 | +# ---------- | ||
| 1184 | $td->notify("--- Invalid objects ---"); | 1197 | $td->notify("--- Invalid objects ---"); |
| 1185 | $n_tests += 2; | 1198 | $n_tests += 2; |
| 1186 | 1199 | ||
| @@ -1197,7 +1210,7 @@ $td->runtest("object with zero offset", | @@ -1197,7 +1210,7 @@ $td->runtest("object with zero offset", | ||
| 1197 | 1210 | ||
| 1198 | show_ntests(); | 1211 | show_ntests(); |
| 1199 | # ---------- | 1212 | # ---------- |
| 1200 | -$td->notify("--- Error/output rediction ---"); | 1213 | +$td->notify("--- Error/output redirection ---"); |
| 1201 | $n_tests += 2; | 1214 | $n_tests += 2; |
| 1202 | 1215 | ||
| 1203 | $td->runtest("error/output redirection to null", | 1216 | $td->runtest("error/output redirection to null", |
qpdf/qtest/qpdf/filter-on-write-out.pdf
0 → 100644
No preview for this file type
qpdf/qtest/qpdf/filter-on-write.pdf
0 → 100644
| 1 | +%PDF-1.3 | ||
| 2 | +%¿÷¢þ | ||
| 3 | +1 0 obj | ||
| 4 | +<< /Pages 2 0 R /Type /Catalog >> | ||
| 5 | +endobj | ||
| 6 | +2 0 obj | ||
| 7 | +<< /Count 0 /Kids [ ] /Type /Pages >> | ||
| 8 | +endobj | ||
| 9 | +3 0 obj | ||
| 10 | +<< /Filter /RunLengthDecode /Length 5 >> | ||
| 11 | +stream | ||
| 12 | +w¹w€endstream | ||
| 13 | +endobj | ||
| 14 | +4 0 obj | ||
| 15 | +<< /Length 6 >> | ||
| 16 | +stream | ||
| 17 | +potatoendstream | ||
| 18 | +endobj | ||
| 19 | +5 0 obj | ||
| 20 | +<< /Filter /RunLengthDecode /Length 5 >> | ||
| 21 | +stream | ||
| 22 | +w¹w€endstream | ||
| 23 | +endobj | ||
| 24 | +6 0 obj | ||
| 25 | +<< /Length 5 >> | ||
| 26 | +stream | ||
| 27 | +saladendstream | ||
| 28 | +endobj | ||
| 29 | +xref | ||
| 30 | +0 7 | ||
| 31 | +0000000000 65535 f | ||
| 32 | +0000000015 00000 n | ||
| 33 | +0000000064 00000 n | ||
| 34 | +0000000117 00000 n | ||
| 35 | +0000000195 00000 n | ||
| 36 | +0000000249 00000 n | ||
| 37 | +0000000327 00000 n | ||
| 38 | +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 >> | ||
| 39 | +startxref | ||
| 40 | +380 | ||
| 41 | +%%EOF |
qpdf/test_driver.cc
| @@ -2219,6 +2219,16 @@ void runtest(int n, char const* filename1, char const* arg2) | @@ -2219,6 +2219,16 @@ void runtest(int n, char const* filename1, char const* arg2) | ||
| 2219 | w.write(); | 2219 | w.write(); |
| 2220 | } | 2220 | } |
| 2221 | } | 2221 | } |
| 2222 | + else if (n == 70) | ||
| 2223 | + { | ||
| 2224 | + auto trailer = pdf.getTrailer(); | ||
| 2225 | + trailer.getKey("/S1").setFilterOnWrite(false); | ||
| 2226 | + trailer.getKey("/S2").setFilterOnWrite(false); | ||
| 2227 | + QPDFWriter w(pdf, "a.pdf"); | ||
| 2228 | + w.setStaticID(true); | ||
| 2229 | + w.setDecodeLevel(qpdf_dl_specialized); | ||
| 2230 | + w.write(); | ||
| 2231 | + } | ||
| 2222 | else | 2232 | else |
| 2223 | { | 2233 | { |
| 2224 | throw std::runtime_error(std::string("invalid test ") + | 2234 | throw std::runtime_error(std::string("invalid test ") + |