diff --git a/.gitignore b/.gitignore index 8708f82..12a4601 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,7 @@ Doxyfile # ./Testing is created if you run ctest from the wrong place. /Testing + +# macOS junk files +.DS_Store +._* diff --git a/libqpdf/QPDFWriter.cc b/libqpdf/QPDFWriter.cc index cf97ea3..1f9cd6b 100644 --- a/libqpdf/QPDFWriter.cc +++ b/libqpdf/QPDFWriter.cc @@ -1293,6 +1293,12 @@ QPDFWriter::willFilterStream( QTC::TC("qpdf", "QPDFWriter compressing uncompressed stream"); } + // Disable compression for empty streams to improve compatibility + if (stream_dict.getKey("/Length").isInteger() && stream_dict.getKey("/Length").getIntValue() == 0) { + filter = true; + compress_stream = false; + } + bool filtered = false; for (bool first_attempt: {true, false}) { PipelinePopper pp_stream_data(this); diff --git a/libqpdf/QPDF_Stream.cc b/libqpdf/QPDF_Stream.cc index ca718c5..1bb98c2 100644 --- a/libqpdf/QPDF_Stream.cc +++ b/libqpdf/QPDF_Stream.cc @@ -432,7 +432,16 @@ Stream::pipeStreamData( filterp = &ignored; } bool& filter = *filterp; - filter = encode_flags || decode_level != qpdf_dl_none; + + const bool empty_stream = !s->stream_provider && !s->stream_data && s->length == 0; + const bool empty_stream_data = s->stream_data && s->stream_data->getSize() == 0; + const bool empty = empty_stream || empty_stream_data; + + if(empty_stream || empty_stream_data) { + filter = true; + } + + filter = empty || encode_flags || decode_level != qpdf_dl_none; if (filter) { filter = filterable(decode_level, filters); } diff --git a/manual/release-notes.rst b/manual/release-notes.rst index e8b802b..514626a 100644 --- a/manual/release-notes.rst +++ b/manual/release-notes.rst @@ -23,6 +23,13 @@ more detail. not work on some older Linux distributions. If you need support for an older distribution, please use version 12.2.0 or below. + - Other enhancements + + - ``QPDFWriter`` will no longer add filters when writing empty streams. + + - More sanity checks have been added when files with damaged xref tables + are recovered. + 12.2.0: May 4, 2025 - Upcoming C++ Version Change diff --git a/qpdf/qtest/object-stream.test b/qpdf/qtest/object-stream.test index 7c3eb20..72ac158 100644 --- a/qpdf/qtest/object-stream.test +++ b/qpdf/qtest/object-stream.test @@ -16,7 +16,7 @@ cleanup(); my $td = new TestDriver('object-stream'); -my $n_tests = 10 + (36 * 4) + (12 * 2); +my $n_tests = 10 + (36 * 4) + (12 * 2) + 4; my $n_compare_pdfs = 36; for (my $n = 16; $n <= 19; ++$n) @@ -126,5 +126,23 @@ $td->runtest("adjacent compressed objects", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); +# Never compress empty streams +$td->runtest("never compress empty streams", + {$td->COMMAND => "qpdf --compress-streams=y --static-id" . + " empty-stream-uncompressed.pdf a.pdf"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}); +$td->runtest("check file", + {$td->FILE => "a.pdf"}, + {$td->FILE => "empty-stream-uncompressed.pdf"}); + +# Always remove filters from compressed empty streams +$td->runtest("always remove filters from empty streams", + {$td->COMMAND => "qpdf --compress-streams=y --static-id" . + " empty-stream-compressed.pdf a.pdf"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}); +$td->runtest("check file", + {$td->FILE => "a.pdf"}, + {$td->FILE => "empty-stream-uncompressed.pdf"}); + cleanup(); $td->report(calc_ntests($n_tests, $n_compare_pdfs)); diff --git a/qpdf/qtest/qpdf/empty-stream-compressed.pdf b/qpdf/qtest/qpdf/empty-stream-compressed.pdf new file mode 100644 index 0000000..7fa8967 --- /dev/null +++ b/qpdf/qtest/qpdf/empty-stream-compressed.pdf @@ -0,0 +1,31 @@ +%PDF-1.4 +%¿÷¢þ +1 0 obj +<< /Pages 3 0 R /Type /Catalog /ViewerPreferences << /DisplayDocTitle true /Type /ViewerPreferences >> >> +endobj +2 0 obj +<< /CreationDate (D:20250409064524+00'00') /Creator (Mozilla/5.0 \(X11; Linux x86_64\) AppleWebKit/537.36 \(KHTML, like Gecko\) Chrome/135.0.0.0 Safari/537.36) /ModDate (D:20250409064524+00'00') /Producer (Skia/PDF m135) /Title (about:blank) >> +endobj +3 0 obj +<< /Count 1 /Kids [ 4 0 R ] /Type /Pages >> +endobj +4 0 obj +<< /Contents 5 0 R /MediaBox [ 0 0 594.95996 841.91998 ] /Parent 3 0 R /Resources << /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] >> /StructParents 0 /Tabs /S /Type /Page >> +endobj +5 0 obj +<< /Length 0 /Filter /FlateDecode >> +stream +endstream +endobj +xref +0 6 +0000000000 65535 f +0000000015 00000 n +0000000136 00000 n +0000000396 00000 n +0000000455 00000 n +0000000647 00000 n +trailer << /Info 2 0 R /Root 1 0 R /Size 6 /ID [<31415926535897932384626433832795>] >> +startxref +716 +%%EOF diff --git a/qpdf/qtest/qpdf/empty-stream-uncompressed.pdf b/qpdf/qtest/qpdf/empty-stream-uncompressed.pdf new file mode 100644 index 0000000..0599e99 --- /dev/null +++ b/qpdf/qtest/qpdf/empty-stream-uncompressed.pdf @@ -0,0 +1,31 @@ +%PDF-1.4 +%¿÷¢þ +1 0 obj +<< /Pages 3 0 R /Type /Catalog /ViewerPreferences << /DisplayDocTitle true /Type /ViewerPreferences >> >> +endobj +2 0 obj +<< /CreationDate (D:20250409064524+00'00') /Creator (Mozilla/5.0 \(X11; Linux x86_64\) AppleWebKit/537.36 \(KHTML, like Gecko\) Chrome/135.0.0.0 Safari/537.36) /ModDate (D:20250409064524+00'00') /Producer (Skia/PDF m135) /Title (about:blank) >> +endobj +3 0 obj +<< /Count 1 /Kids [ 4 0 R ] /Type /Pages >> +endobj +4 0 obj +<< /Contents 5 0 R /MediaBox [ 0 0 594.95996 841.91998 ] /Parent 3 0 R /Resources << /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] >> /StructParents 0 /Tabs /S /Type /Page >> +endobj +5 0 obj +<< /Length 0 >> +stream +endstream +endobj +xref +0 6 +0000000000 65535 f +0000000015 00000 n +0000000136 00000 n +0000000396 00000 n +0000000455 00000 n +0000000647 00000 n +trailer << /Info 2 0 R /Root 1 0 R /Size 6 /ID [<31415926535897932384626433832795>] >> +startxref +695 +%%EOF