Commit 12ecd2019a3186d11ebb6083b3813ce722fb2329

Authored by Jay Berkenbilt
1 parent 3f9191a3

Add QPDFObjectHandle::setFilterOnWrite

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-&gt;runtest(&quot;check output&quot;, @@ -1181,6 +1181,19 @@ $td-&gt;runtest(&quot;check output&quot;,
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-&gt;runtest(&quot;object with zero offset&quot;, @@ -1197,7 +1210,7 @@ $td-&gt;runtest(&quot;object with zero offset&quot;,
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 ") +