Commit 133da3b6d3844670e349281d5fc1d116620d92d9

Authored by Jay Berkenbilt
1 parent a2fc5b52

Add zopfli support (fixes #1323)

This requires a special build option.
.github/workflows/main.yml
@@ -123,6 +123,13 @@ jobs: @@ -123,6 +123,13 @@ jobs:
123 - uses: actions/checkout@v4 123 - uses: actions/checkout@v4
124 - name: 'Sanitizer Tests' 124 - name: 'Sanitizer Tests'
125 run: build-scripts/test-sanitizers 125 run: build-scripts/test-sanitizers
  126 + Zopfli:
  127 + runs-on: ubuntu-latest
  128 + needs: Prebuild
  129 + steps:
  130 + - uses: actions/checkout@v4
  131 + - name: 'Zopfli Tests'
  132 + run: build-scripts/test-zopfli
126 CodeCov: 133 CodeCov:
127 runs-on: ubuntu-latest 134 runs-on: ubuntu-latest
128 needs: Prebuild 135 needs: Prebuild
CMakeLists.txt
@@ -98,6 +98,8 @@ set(DEFAULT_CRYPTO CACHE STRING "") @@ -98,6 +98,8 @@ set(DEFAULT_CRYPTO CACHE STRING "")
98 option(DEFAULT_CRYPTO 98 option(DEFAULT_CRYPTO
99 "Specify default crypto; otherwise chosen automatically" "") 99 "Specify default crypto; otherwise chosen automatically" "")
100 100
  101 +option(ZOPFLI, "Use zopfli for zlib-compatible compression")
  102 +
101 # INSTALL_MANUAL is not dependent on building docs. When creating some 103 # INSTALL_MANUAL is not dependent on building docs. When creating some
102 # distributions, we build the doc in one run, copy doc-dist in, and 104 # distributions, we build the doc in one run, copy doc-dist in, and
103 # install it elsewhere. 105 # install it elsewhere.
ChangeLog
1 2025-02-02 Jay Berkenbilt <ejb@ql.org> 1 2025-02-02 Jay Berkenbilt <ejb@ql.org>
2 2
  3 + * Add support for the zopfli compression library. See manual for
  4 + details. Fixes #1323.
  5 +
3 * Have fix-qdf accept a second argument, interpreted as the output 6 * Have fix-qdf accept a second argument, interpreted as the output
4 file. Fixes #1330. 7 file. Fixes #1330.
5 8
README-maintainer.md
@@ -67,6 +67,10 @@ Note that, in early 2024, branch coverage information is not very accurate with @@ -67,6 +67,10 @@ Note that, in early 2024, branch coverage information is not very accurate with
67 67
68 Memory checks: 68 Memory checks:
69 69
  70 +Note: if clang++ fails to create output, it may be necessary to install a specific version of
  71 +libstdc++-dev. For example, with clang++ version 20 on Ubuntu 24.04, `clang++ -v` indicates the
  72 +selected GCC installation is 14, so it is necessary to install `libstdc++-14-dev`.
  73 +
70 ``` 74 ```
71 CFLAGS="-fsanitize=address -fsanitize=undefined" \ 75 CFLAGS="-fsanitize=address -fsanitize=undefined" \
72 CXXFLAGS="-fsanitize=address -fsanitize=undefined" \ 76 CXXFLAGS="-fsanitize=address -fsanitize=undefined" \
@@ -298,8 +302,8 @@ Building docs from pull requests is also enabled. @@ -298,8 +302,8 @@ Building docs from pull requests is also enabled.
298 ## ZLIB COMPATIBILITY 302 ## ZLIB COMPATIBILITY
299 303
300 The qpdf test suite is designed to be independent of the output of any 304 The qpdf test suite is designed to be independent of the output of any
301 -particular version of zlib. There are several strategies to make this  
302 -work: 305 +particular version of zlib. (See also `ZOPFLI` in README.md.) There
  306 +are several strategies to make this work:
303 307
304 * `build-scripts/test-alt-zlib` runs in CI and runs the test suite 308 * `build-scripts/test-alt-zlib` runs in CI and runs the test suite
305 with a non-default zlib. Please refer to that code for an example of 309 with a non-default zlib. Please refer to that code for an example of
README.md
@@ -66,6 +66,16 @@ below. @@ -66,6 +66,16 @@ below.
66 66
67 Detailed information appears in the [manual](https://qpdf.readthedocs.io/en/latest/installation.html). 67 Detailed information appears in the [manual](https://qpdf.readthedocs.io/en/latest/installation.html).
68 68
  69 +## Zopfli
  70 +
  71 +If qpdf is built with [zopfli](https://github.com/google/zopfli) support and the `QPDF_ZOPFLI` environment variable is set to any value other than `disabled`, qpdf will use the zopfli compression library instead of zlib to generate flate-compressed streams. The zopfli algorithm is much slower (about 100x according to their website) than zlib but produces slightly smaller output, making it suitable for cases such as generation of archival PDFs where size is important regardless of speed. To build with zopfli support, you must have the zopfli library and header file installed.
  72 +
  73 +The environment variable `QPDF_ZOPFLI` can be set to the following values:
  74 +* `disabled` (or unset): do not use zopfli
  75 +* `force`: use zopfli; fail if zopfli is not compiled in
  76 +* `silent`: use zopfli if available; otherwise silently fall back to zlib
  77 +* any other value: use zopfli if available, and warn if not
  78 +
69 # Licensing terms of embedded software 79 # Licensing terms of embedded software
70 80
71 qpdf makes use of zlib and jpeg libraries for its functionality. These packages can be downloaded separately from their 81 qpdf makes use of zlib and jpeg libraries for its functionality. These packages can be downloaded separately from their
build-scripts/test-zopfli 0 โ†’ 100755
  1 +#!/bin/bash
  2 +set -eo pipefail
  3 +sudo apt-get update
  4 +sudo apt-get -y install \
  5 + build-essential cmake \
  6 + zlib1g-dev libjpeg-dev libgnutls28-dev libssl-dev \
  7 + libzopfli-dev
  8 +
  9 +cmake -S . -B build \
  10 + -DCI_MODE=1 -DBUILD_STATIC_LIBS=0 -DCMAKE_BUILD_TYPE=Release \
  11 + -DREQUIRE_CRYPTO_OPENSSL=1 -DREQUIRE_CRYPTO_GNUTLS=1 \
  12 + -DENABLE_QTC=1 -DZOPFLI=1
  13 +cmake --build build --verbose -j$(nproc) -- -k
  14 +
  15 +# Make sure we are using zopfli
  16 +export QPDF_ZOPFLI=force
  17 +zopfli="$(./build/zlib-flate/zlib-flate --_zopfli)"
  18 +if [ "$zopfli" != "11" ]; then
  19 + echo "zopfli is not working"
  20 + exit 2
  21 +fi
  22 +
  23 +# If this fails, please see ZLIB COMPATIBILITY in README-maintainer.md.
  24 +# The tests are very slow with this option. Just run essential tests.
  25 +# If zlib-flate and qpdf tests all pass, we can be pretty sure it works.
  26 +(cd build; ctest --verbose -R zlib-flate)
  27 +(cd build; ctest --verbose -R qpdf)
include/qpdf/Pl_Flate.hh
@@ -24,8 +24,10 @@ @@ -24,8 +24,10 @@
24 #define PL_FLATE_HH 24 #define PL_FLATE_HH
25 25
26 #include <qpdf/Pipeline.hh> 26 #include <qpdf/Pipeline.hh>
  27 +#include <qpdf/QPDFLogger.hh>
27 #include <functional> 28 #include <functional>
28 #include <memory> 29 #include <memory>
  30 +#include <string>
29 31
30 class QPDF_DLL_CLASS Pl_Flate: public Pipeline 32 class QPDF_DLL_CLASS Pl_Flate: public Pipeline
31 { 33 {
@@ -65,6 +67,23 @@ class QPDF_DLL_CLASS Pl_Flate: public Pipeline @@ -65,6 +67,23 @@ class QPDF_DLL_CLASS Pl_Flate: public Pipeline
65 QPDF_DLL 67 QPDF_DLL
66 void setWarnCallback(std::function<void(char const*, int)> callback); 68 void setWarnCallback(std::function<void(char const*, int)> callback);
67 69
  70 + // Returns true if qpdf was built with zopfli support.
  71 + QPDF_DLL
  72 + static bool zopfli_supported();
  73 +
  74 + // Returns true if zopfli is enabled. Zopfli is enabled if QPDF_ZOPFLI is set to a value other
  75 + // than "disabled" and zopfli support is compiled in.
  76 + QPDF_DLL
  77 + static bool zopfli_enabled();
  78 +
  79 + // If zopfli is supported, returns true. Otherwise, check the QPDF_ZOPFLI
  80 + // environment variable as follows:
  81 + // - "disabled" or "silent": return true
  82 + // - "force": qpdf_exit_error, throw an exception
  83 + // - Any other value: issue a warning, and return false
  84 + QPDF_DLL
  85 + static bool zopfli_check_env(QPDFLogger* logger = nullptr);
  86 +
68 private: 87 private:
69 QPDF_DLL_PRIVATE 88 QPDF_DLL_PRIVATE
70 void handleData(unsigned char const* data, size_t len, int flush); 89 void handleData(unsigned char const* data, size_t len, int flush);
@@ -72,6 +91,8 @@ class QPDF_DLL_CLASS Pl_Flate: public Pipeline @@ -72,6 +91,8 @@ class QPDF_DLL_CLASS Pl_Flate: public Pipeline
72 void checkError(char const* prefix, int error_code); 91 void checkError(char const* prefix, int error_code);
73 QPDF_DLL_PRIVATE 92 QPDF_DLL_PRIVATE
74 void warn(char const*, int error_code); 93 void warn(char const*, int error_code);
  94 + QPDF_DLL_PRIVATE
  95 + void finish_zopfli();
75 96
76 QPDF_DLL_PRIVATE 97 QPDF_DLL_PRIVATE
77 static int compression_level; 98 static int compression_level;
@@ -95,6 +116,7 @@ class QPDF_DLL_CLASS Pl_Flate: public Pipeline @@ -95,6 +116,7 @@ class QPDF_DLL_CLASS Pl_Flate: public Pipeline
95 void* zdata; 116 void* zdata;
96 unsigned long long written{0}; 117 unsigned long long written{0};
97 std::function<void(char const*, int)> callback; 118 std::function<void(char const*, int)> callback;
  119 + std::unique_ptr<std::string> zopfli_buf;
98 }; 120 };
99 121
100 std::shared_ptr<Members> m; 122 std::shared_ptr<Members> m;
job.sums
1 # Generated by generate_auto_job 1 # Generated by generate_auto_job
2 -CMakeLists.txt 4aaa3d5df1713d9e3b9c6778101c6af3efa2131a2f4c069095abee269d5eaccc 2 +CMakeLists.txt af74c05aea88512ef9a37a0708c2a03747a4ff23dc7919075c8d3b62b73aad79
3 generate_auto_job f64733b79dcee5a0e3e8ccc6976448e8ddf0e8b6529987a66a7d3ab2ebc10a86 3 generate_auto_job f64733b79dcee5a0e3e8ccc6976448e8ddf0e8b6529987a66a7d3ab2ebc10a86
4 include/qpdf/auto_job_c_att.hh 4c2b171ea00531db54720bf49a43f8b34481586ae7fb6cbf225099ee42bc5bb4 4 include/qpdf/auto_job_c_att.hh 4c2b171ea00531db54720bf49a43f8b34481586ae7fb6cbf225099ee42bc5bb4
5 include/qpdf/auto_job_c_copy_att.hh 50609012bff14fd82f0649185940d617d05d530cdc522185c7f3920a561ccb42 5 include/qpdf/auto_job_c_copy_att.hh 50609012bff14fd82f0649185940d617d05d530cdc522185c7f3920a561ccb42
@@ -7,14 +7,14 @@ include/qpdf/auto_job_c_enc.hh 28446f3c32153a52afa239ea40503e6cc8ac2c026813526a3 @@ -7,14 +7,14 @@ include/qpdf/auto_job_c_enc.hh 28446f3c32153a52afa239ea40503e6cc8ac2c026813526a3
7 include/qpdf/auto_job_c_main.hh 84f463237235b2c095b747a4f5dd00f109ee596a1c207b944efb296c0c568cae 7 include/qpdf/auto_job_c_main.hh 84f463237235b2c095b747a4f5dd00f109ee596a1c207b944efb296c0c568cae
8 include/qpdf/auto_job_c_pages.hh 09ca15649cc94fdaf6d9bdae28a20723f2a66616bf15aa86d83df31051d82506 8 include/qpdf/auto_job_c_pages.hh 09ca15649cc94fdaf6d9bdae28a20723f2a66616bf15aa86d83df31051d82506
9 include/qpdf/auto_job_c_uo.hh 9c2f98a355858dd54d0bba444b73177a59c9e56833e02fa6406f429c07f39e62 9 include/qpdf/auto_job_c_uo.hh 9c2f98a355858dd54d0bba444b73177a59c9e56833e02fa6406f429c07f39e62
10 -job.yml 31935064eca625af7657b23f2f12c614d14751ec0b12702482b1768a04905d22  
11 -libqpdf/qpdf/auto_job_decl.hh 20d6affe1e260f5a1af4f1d82a820b933835440ff03020e877382da2e8dac6c6  
12 -libqpdf/qpdf/auto_job_help.hh 9628a4b3f57ed8ecda3c7b8761b4daa48eaf9da0a6a0fc68bf417c467bd737eb  
13 -libqpdf/qpdf/auto_job_init.hh e2a6bb87870c5522a01b15461c9fe909e360f5c7fed06e41acf13a125bd1d03e 10 +job.yml 2c424c7be0c02545191969e849e1d8f7fdb4ab65bbf799b9a190e21343899751
  11 +libqpdf/qpdf/auto_job_decl.hh 34ba07d3891c3e5cdd8712f991e508a0652c9db314c5d5bcdf4421b76e6f6e01
  12 +libqpdf/qpdf/auto_job_help.hh a36476d0c823033b2af0e4170651e1fa31173887c310f2f208e9ed7e6e36a2ce
  13 +libqpdf/qpdf/auto_job_init.hh f89e7f9950a185372732d2ff7f113161f275f45ee7937dd7fd37e38013bf22e7
14 libqpdf/qpdf/auto_job_json_decl.hh 843892c8e8652a86b7eb573893ef24050b7f36fe313f7251874be5cd4cdbe3fd 14 libqpdf/qpdf/auto_job_json_decl.hh 843892c8e8652a86b7eb573893ef24050b7f36fe313f7251874be5cd4cdbe3fd
15 libqpdf/qpdf/auto_job_json_init.hh 344c2fb473f88fe829c93b1efe6c70a0e4796537b8eb35e421d955fff481ba7d 15 libqpdf/qpdf/auto_job_json_init.hh 344c2fb473f88fe829c93b1efe6c70a0e4796537b8eb35e421d955fff481ba7d
16 libqpdf/qpdf/auto_job_schema.hh 6d3eef5137b8828eaa301a1b3cf75cb7bb812aa6e2d8301de865b42d238d7a7c 16 libqpdf/qpdf/auto_job_schema.hh 6d3eef5137b8828eaa301a1b3cf75cb7bb812aa6e2d8301de865b42d238d7a7c
17 manual/_ext/qpdf.py 6add6321666031d55ed4aedf7c00e5662bba856dfcd66ccb526563bffefbb580 17 manual/_ext/qpdf.py 6add6321666031d55ed4aedf7c00e5662bba856dfcd66ccb526563bffefbb580
18 -manual/cli.rst 45629c81bb407e7a1d2302ff1a9ef87f706565904aa5c21e64adefb34eee575c  
19 -manual/qpdf.1 e058bd97a2bbc1e39282c709fb3beb1c3f8d3e2372b2974e11a849fd0bfb3505 18 +manual/cli.rst 67357688f9a52fafa9a4f231fe4ce74c3cd8977130da7501efe54439a1ee22d4
  19 +manual/qpdf.1 cf5fc00789744c619f2af285fd715e5f85ced53f0126f8df5f97e27f920a9a7a
20 manual/qpdf.1.in 436ecc85d45c4c9e2dbd1725fb7f0177fb627179469f114561adf3cb6cbb677b 20 manual/qpdf.1.in 436ecc85d45c4c9e2dbd1725fb7f0177fb627179469f114561adf3cb6cbb677b
@@ -81,6 +81,7 @@ options: @@ -81,6 +81,7 @@ options:
81 - copyright 81 - copyright
82 - show-crypto 82 - show-crypto
83 - job-json-help 83 - job-json-help
  84 + - zopfli
84 optional_choices: 85 optional_choices:
85 json-help: json_version 86 json-help: json_version
86 - table: main 87 - table: main
libqpdf/CMakeLists.txt
@@ -190,6 +190,18 @@ if(NOT EXTERNAL_LIBS) @@ -190,6 +190,18 @@ if(NOT EXTERNAL_LIBS)
190 endif() 190 endif()
191 endif() 191 endif()
192 192
  193 +if(ZOPFLI)
  194 + find_path(ZOPFLI_H_PATH zopfli/zopfli.h)
  195 + find_library(ZOPFLI_LIB_PATH NAMES zopfli)
  196 + if(ZOPFLI_H_PATH AND ZOPFLI_LIB_PATH)
  197 + list(APPEND dep_include_directories ${ZOPFLI_H_PATH})
  198 + list(APPEND dep_link_libraries ${ZOPFLI_LIB_PATH})
  199 + else()
  200 + message(SEND_ERROR "zopfli not found")
  201 + set(ANYTHING_MISSING 1)
  202 + endif()
  203 +endif()
  204 +
193 # Update JPEG_INCLUDE in PARENT_SCOPE after we have finished setting it. 205 # Update JPEG_INCLUDE in PARENT_SCOPE after we have finished setting it.
194 set(JPEG_INCLUDE ${JPEG_INCLUDE} PARENT_SCOPE) 206 set(JPEG_INCLUDE ${JPEG_INCLUDE} PARENT_SCOPE)
195 207
libqpdf/Pl_Flate.cc
@@ -6,6 +6,11 @@ @@ -6,6 +6,11 @@
6 6
7 #include <qpdf/QIntC.hh> 7 #include <qpdf/QIntC.hh>
8 #include <qpdf/QUtil.hh> 8 #include <qpdf/QUtil.hh>
  9 +#include <qpdf/qpdf-config.h>
  10 +
  11 +#ifdef ZOPFLI
  12 +# include <zopfli/zopfli.h>
  13 +#endif
9 14
10 namespace 15 namespace
11 { 16 {
@@ -39,6 +44,10 @@ Pl_Flate::Members::Members(size_t out_bufsize, action_e action) : @@ -39,6 +44,10 @@ Pl_Flate::Members::Members(size_t out_bufsize, action_e action) :
39 zstream.avail_in = 0; 44 zstream.avail_in = 0;
40 zstream.next_out = this->outbuf.get(); 45 zstream.next_out = this->outbuf.get();
41 zstream.avail_out = QIntC::to_uint(out_bufsize); 46 zstream.avail_out = QIntC::to_uint(out_bufsize);
  47 +
  48 + if (action == a_deflate && Pl_Flate::zopfli_enabled()) {
  49 + zopfli_buf = std::make_unique<std::string>();
  50 + }
42 } 51 }
43 52
44 Pl_Flate::Members::~Members() 53 Pl_Flate::Members::~Members()
@@ -59,7 +68,7 @@ Pl_Flate::Members::~Members() @@ -59,7 +68,7 @@ Pl_Flate::Members::~Members()
59 Pl_Flate::Pl_Flate( 68 Pl_Flate::Pl_Flate(
60 char const* identifier, Pipeline* next, action_e action, unsigned int out_bufsize_int) : 69 char const* identifier, Pipeline* next, action_e action, unsigned int out_bufsize_int) :
61 Pipeline(identifier, next), 70 Pipeline(identifier, next),
62 - m(new Members(QIntC::to_size(out_bufsize_int), action)) 71 + m(std::shared_ptr<Members>(new Members(QIntC::to_size(out_bufsize_int), action)))
63 { 72 {
64 if (!next) { 73 if (!next) {
65 throw std::logic_error("Attempt to create Pl_Flate with nullptr as next"); 74 throw std::logic_error("Attempt to create Pl_Flate with nullptr as next");
@@ -98,6 +107,10 @@ Pl_Flate::write(unsigned char const* data, size_t len) @@ -98,6 +107,10 @@ Pl_Flate::write(unsigned char const* data, size_t len)
98 throw std::logic_error( 107 throw std::logic_error(
99 this->identifier + ": Pl_Flate: write() called after finish() called"); 108 this->identifier + ": Pl_Flate: write() called after finish() called");
100 } 109 }
  110 + if (m->zopfli_buf) {
  111 + m->zopfli_buf->append(reinterpret_cast<char const*>(data), len);
  112 + return;
  113 + }
101 114
102 // Write in chunks in case len is too big to fit in an int. Assume int is at least 32 bits. 115 // Write in chunks in case len is too big to fit in an int. Assume int is at least 32 bits.
103 static size_t const max_bytes = 1 << 30; 116 static size_t const max_bytes = 1 << 30;
@@ -211,7 +224,9 @@ Pl_Flate::finish() @@ -211,7 +224,9 @@ Pl_Flate::finish()
211 throw std::runtime_error("PL_Flate memory limit exceeded"); 224 throw std::runtime_error("PL_Flate memory limit exceeded");
212 } 225 }
213 try { 226 try {
214 - if (m->outbuf.get()) { 227 + if (m->zopfli_buf) {
  228 + finish_zopfli();
  229 + } else if (m->outbuf.get()) {
215 if (m->initialized) { 230 if (m->initialized) {
216 z_stream& zstream = *(static_cast<z_stream*>(m->zdata)); 231 z_stream& zstream = *(static_cast<z_stream*>(m->zdata));
217 unsigned char buf[1]; 232 unsigned char buf[1];
@@ -291,3 +306,76 @@ Pl_Flate::checkError(char const* prefix, int error_code) @@ -291,3 +306,76 @@ Pl_Flate::checkError(char const* prefix, int error_code)
291 throw std::runtime_error(msg); 306 throw std::runtime_error(msg);
292 } 307 }
293 } 308 }
  309 +
  310 +void
  311 +Pl_Flate::finish_zopfli()
  312 +{
  313 +#ifdef ZOPFLI
  314 + if (!m->zopfli_buf) {
  315 + return;
  316 + }
  317 + auto buf = std::move(*m->zopfli_buf.release());
  318 + ZopfliOptions z_opt;
  319 + ZopfliInitOptions(&z_opt);
  320 + unsigned char* out{nullptr};
  321 + size_t out_size{0};
  322 + ZopfliCompress(
  323 + &z_opt,
  324 + ZOPFLI_FORMAT_ZLIB,
  325 + reinterpret_cast<unsigned char const*>(buf.c_str()),
  326 + buf.size(),
  327 + &out,
  328 + &out_size);
  329 + std::unique_ptr<unsigned char, decltype(&free)> p(out, &free);
  330 + next()->write(out, out_size);
  331 + // next()->finish is called by finish()
  332 +#endif
  333 +}
  334 +
  335 +bool
  336 +Pl_Flate::zopfli_supported()
  337 +{
  338 +#ifdef ZOPFLI
  339 + return true;
  340 +#else
  341 + return false;
  342 +#endif
  343 +}
  344 +
  345 +bool
  346 +Pl_Flate::zopfli_enabled()
  347 +{
  348 + if (zopfli_supported()) {
  349 + std::string value;
  350 + static bool enabled = QUtil::get_env("QPDF_ZOPFLI", &value) && value != "disabled";
  351 + return enabled;
  352 + } else {
  353 + return false;
  354 + }
  355 +}
  356 +
  357 +bool
  358 +Pl_Flate::zopfli_check_env(QPDFLogger* logger)
  359 +{
  360 + if (Pl_Flate::zopfli_supported()) {
  361 + return true;
  362 + }
  363 + std::string value;
  364 + auto is_set = QUtil::get_env("QPDF_ZOPFLI", &value);
  365 + if (!is_set || value == "disabled" || value == "silent") {
  366 + return true;
  367 + }
  368 + if (!logger) {
  369 + logger = QPDFLogger::defaultLogger().get();
  370 + }
  371 +
  372 + // This behavior is known in QPDFJob (for the --zopfli argument), Pl_Flate.hh, README.md,
  373 + // and the manual. Do a case-insensitive search for zopfli if changing the behavior.
  374 + if (value == "force") {
  375 + throw std::runtime_error("QPDF_ZOPFLI=force, and zopfli support is not enabled");
  376 + }
  377 + logger->warn("QPDF_ZOPFLI is set, but libqpdf was not built with zopfli support\n");
  378 + logger->warn(
  379 + "Set QPDF_ZOPFLI=silent to suppress this warning and use zopfli when available.\n");
  380 + return false;
  381 +}
libqpdf/QPDFJob.cc
@@ -498,6 +498,11 @@ QPDFJob::createQPDF() @@ -498,6 +498,11 @@ QPDFJob::createQPDF()
498 void 498 void
499 QPDFJob::writeQPDF(QPDF& pdf) 499 QPDFJob::writeQPDF(QPDF& pdf)
500 { 500 {
  501 + if (createsOutput()) {
  502 + if (!Pl_Flate::zopfli_check_env(pdf.getLogger().get())) {
  503 + m->warnings = true;
  504 + }
  505 + }
501 if (!createsOutput()) { 506 if (!createsOutput()) {
502 doInspection(pdf); 507 doInspection(pdf);
503 } else if (m->split_pages) { 508 } else if (m->split_pages) {
libqpdf/QPDFJob_argv.cc
@@ -6,6 +6,7 @@ @@ -6,6 +6,7 @@
6 #include <iostream> 6 #include <iostream>
7 #include <memory> 7 #include <memory>
8 8
  9 +#include <qpdf/Pl_Flate.hh>
9 #include <qpdf/QPDFArgParser.hh> 10 #include <qpdf/QPDFArgParser.hh>
10 #include <qpdf/QPDFCryptoProvider.hh> 11 #include <qpdf/QPDFCryptoProvider.hh>
11 #include <qpdf/QPDFLogger.hh> 12 #include <qpdf/QPDFLogger.hh>
@@ -105,6 +106,27 @@ ArgParser::argVersion() @@ -105,6 +106,27 @@ ArgParser::argVersion()
105 } 106 }
106 107
107 void 108 void
  109 +ArgParser::argZopfli()
  110 +{
  111 + auto logger = QPDFLogger::defaultLogger();
  112 + if (Pl_Flate::zopfli_supported()) {
  113 + if (Pl_Flate::zopfli_enabled()) {
  114 + logger->info("zopfli support is enabled, and zopfli is active\n");
  115 + } else {
  116 + logger->info("zopfli support is enabled but not active\n");
  117 + logger->info("Set the environment variable QPDF_ZOPFLI to activate.\n");
  118 + logger->info("* QPDF_ZOPFLI=disabled or QPDF_ZOPFLI not set: don't use zopfli.\n");
  119 + logger->info("* QPDF_ZOPFLI=force: use zopfli, and fail if not available.\n");
  120 + logger->info("* QPDF_ZOPFLI=silent: use zopfli if available and silently fall back if not.\n");
  121 + logger->info("* QPDF_ZOPFLI= any other value: use zopfli if available, and warn if not.\n");
  122 + }
  123 + } else {
  124 + logger->error("zopfli support is not enabled\n");
  125 + std::exit(qpdf_exit_error);
  126 + }
  127 +}
  128 +
  129 +void
108 ArgParser::argCopyright() 130 ArgParser::argCopyright()
109 { 131 {
110 // clang-format off 132 // clang-format off
libqpdf/qpdf/auto_job_decl.hh
@@ -19,6 +19,7 @@ void argVersion(); @@ -19,6 +19,7 @@ void argVersion();
19 void argCopyright(); 19 void argCopyright();
20 void argShowCrypto(); 20 void argShowCrypto();
21 void argJobJsonHelp(); 21 void argJobJsonHelp();
  22 +void argZopfli();
22 void argJsonHelp(std::string const&); 23 void argJsonHelp(std::string const&);
23 void argPositional(std::string const&); 24 void argPositional(std::string const&);
24 void argAddAttachment(); 25 void argAddAttachment();
libqpdf/qpdf/auto_job_help.hh
@@ -73,6 +73,11 @@ default provider is shown first. @@ -73,6 +73,11 @@ default provider is shown first.
73 ap.addOptionHelp("--job-json-help", "help", "show format of job JSON", R"(Describe the format of the QPDFJob JSON input used by 73 ap.addOptionHelp("--job-json-help", "help", "show format of job JSON", R"(Describe the format of the QPDFJob JSON input used by
74 --job-json-file. 74 --job-json-file.
75 )"); 75 )");
  76 +ap.addOptionHelp("--zopfli", "help", "indicate whether zopfli is enabled and active", R"(If zopfli support is compiled in, indicate whether it is active,
  77 +and exit normally. Otherwise, indicate that it is not compiled
  78 +in, and exit with an error code. If zopfli is compiled in,
  79 +activate it by setting the ``QPDF_ZOPFLI`` environment variable.
  80 +)");
76 ap.addHelpTopic("general", "general options", R"(General options control qpdf's behavior in ways that are not 81 ap.addHelpTopic("general", "general options", R"(General options control qpdf's behavior in ways that are not
77 directly related to the operation it is performing. 82 directly related to the operation it is performing.
78 )"); 83 )");
@@ -86,13 +91,13 @@ ap.addOptionHelp(&quot;--password-file&quot;, &quot;general&quot;, &quot;read password from a file&quot;, R&quot;(- @@ -86,13 +91,13 @@ ap.addOptionHelp(&quot;--password-file&quot;, &quot;general&quot;, &quot;read password from a file&quot;, R&quot;(-
86 The first line of the specified file is used as the password. 91 The first line of the specified file is used as the password.
87 This is used in place of the --password option. 92 This is used in place of the --password option.
88 )"); 93 )");
  94 +}
  95 +static void add_help_2(QPDFArgParser& ap)
  96 +{
89 ap.addOptionHelp("--verbose", "general", "print additional information", R"(Output additional information about various things qpdf is 97 ap.addOptionHelp("--verbose", "general", "print additional information", R"(Output additional information about various things qpdf is
90 doing, including information about files created and operations 98 doing, including information about files created and operations
91 performed. 99 performed.
92 )"); 100 )");
93 -}  
94 -static void add_help_2(QPDFArgParser& ap)  
95 -{  
96 ap.addOptionHelp("--progress", "general", "show progress when writing", R"(Indicate progress when writing files. 101 ap.addOptionHelp("--progress", "general", "show progress when writing", R"(Indicate progress when writing files.
97 )"); 102 )");
98 ap.addOptionHelp("--no-warn", "general", "suppress printing of warning messages", R"(Suppress printing of warning messages. If warnings were 103 ap.addOptionHelp("--no-warn", "general", "suppress printing of warning messages", R"(Suppress printing of warning messages. If warnings were
@@ -168,14 +173,14 @@ Copy encryption details from the specified file instead of @@ -168,14 +173,14 @@ Copy encryption details from the specified file instead of
168 preserving the input file's encryption. Use --encryption-file-password 173 preserving the input file's encryption. Use --encryption-file-password
169 to specify the encryption file's password. 174 to specify the encryption file's password.
170 )"); 175 )");
  176 +}
  177 +static void add_help_3(QPDFArgParser& ap)
  178 +{
171 ap.addOptionHelp("--encryption-file-password", "transformation", "supply password for --copy-encryption", R"(--encryption-file-password=password 179 ap.addOptionHelp("--encryption-file-password", "transformation", "supply password for --copy-encryption", R"(--encryption-file-password=password
172 180
173 If the file named in --copy-encryption requires a password, use 181 If the file named in --copy-encryption requires a password, use
174 this option to supply the password. 182 this option to supply the password.
175 )"); 183 )");
176 -}  
177 -static void add_help_3(QPDFArgParser& ap)  
178 -{  
179 ap.addOptionHelp("--qdf", "transformation", "enable viewing PDF code in a text editor", R"(Create a PDF file suitable for viewing in a text editor and even 184 ap.addOptionHelp("--qdf", "transformation", "enable viewing PDF code in a text editor", R"(Create a PDF file suitable for viewing in a text editor and even
180 editing. This is for editing the PDF code, not the page contents. 185 editing. This is for editing the PDF code, not the page contents.
181 All streams that can be uncompressed are uncompressed, and 186 All streams that can be uncompressed are uncompressed, and
@@ -285,6 +290,9 @@ Force the output PDF file&#39;s PDF version header to be the specified @@ -285,6 +290,9 @@ Force the output PDF file&#39;s PDF version header to be the specified
285 value, even if the file uses features that may not be available 290 value, even if the file uses features that may not be available
286 in that version. 291 in that version.
287 )"); 292 )");
  293 +}
  294 +static void add_help_4(QPDFArgParser& ap)
  295 +{
288 ap.addHelpTopic("page-ranges", "page range syntax", R"(A full description of the page range syntax, with examples, can be 296 ap.addHelpTopic("page-ranges", "page range syntax", R"(A full description of the page range syntax, with examples, can be
289 found in the manual. In summary, a range is a comma-separated list 297 found in the manual. In summary, a range is a comma-separated list
290 of groups. A group is a number or a range of numbers separated by a 298 of groups. A group is a number or a range of numbers separated by a
@@ -305,9 +313,6 @@ resulting set of pages, where :odd starts with the first page and @@ -305,9 +313,6 @@ resulting set of pages, where :odd starts with the first page and
305 :even starts with the second page. These are odd and even pages 313 :even starts with the second page. These are odd and even pages
306 from the resulting set, not based on the original page numbers. 314 from the resulting set, not based on the original page numbers.
307 )"); 315 )");
308 -}  
309 -static void add_help_4(QPDFArgParser& ap)  
310 -{  
311 ap.addHelpTopic("modification", "change parts of the PDF", R"(Modification options make systematic changes to certain parts of 316 ap.addHelpTopic("modification", "change parts of the PDF", R"(Modification options make systematic changes to certain parts of
312 the PDF, causing the PDF to render differently from the original. 317 the PDF, causing the PDF to render differently from the original.
313 )"); 318 )");
@@ -416,11 +421,11 @@ ap.addOptionHelp(&quot;--keep-inline-images&quot;, &quot;modification&quot;, &quot;exclude inline images @@ -416,11 +421,11 @@ ap.addOptionHelp(&quot;--keep-inline-images&quot;, &quot;modification&quot;, &quot;exclude inline images
416 )"); 421 )");
417 ap.addOptionHelp("--remove-info", "modification", "remove file information", R"(Exclude file information (except modification date) from the output file. 422 ap.addOptionHelp("--remove-info", "modification", "remove file information", R"(Exclude file information (except modification date) from the output file.
418 )"); 423 )");
419 -ap.addOptionHelp("--remove-metadata", "modification", "remove metadata", R"(Exclude metadata from the output file.  
420 -)");  
421 } 424 }
422 static void add_help_5(QPDFArgParser& ap) 425 static void add_help_5(QPDFArgParser& ap)
423 { 426 {
  427 +ap.addOptionHelp("--remove-metadata", "modification", "remove metadata", R"(Exclude metadata from the output file.
  428 +)");
424 ap.addOptionHelp("--remove-page-labels", "modification", "remove explicit page numbers", R"(Exclude page labels (explicit page numbers) from the output file. 429 ap.addOptionHelp("--remove-page-labels", "modification", "remove explicit page numbers", R"(Exclude page labels (explicit page numbers) from the output file.
425 )"); 430 )");
426 ap.addOptionHelp("--set-page-labels", "modification", "number pages for the entire document", R"(--set-page-labels label-spec ... -- 431 ap.addOptionHelp("--set-page-labels", "modification", "number pages for the entire document", R"(--set-page-labels label-spec ... --
@@ -641,13 +646,13 @@ non-empty user passwords when using 256-bit encryption. @@ -641,13 +646,13 @@ non-empty user passwords when using 256-bit encryption.
641 ap.addOptionHelp("--force-V4", "encryption", "force V=4 in encryption dictionary", R"(This option is for testing and is never needed in practice since 646 ap.addOptionHelp("--force-V4", "encryption", "force V=4 in encryption dictionary", R"(This option is for testing and is never needed in practice since
642 qpdf does this automatically when needed. 647 qpdf does this automatically when needed.
643 )"); 648 )");
  649 +}
  650 +static void add_help_6(QPDFArgParser& ap)
  651 +{
644 ap.addOptionHelp("--force-R5", "encryption", "use unsupported R=5 encryption", R"(Use an undocumented, unsupported, deprecated encryption 652 ap.addOptionHelp("--force-R5", "encryption", "use unsupported R=5 encryption", R"(Use an undocumented, unsupported, deprecated encryption
645 algorithm that existed only in Acrobat version IX. This option 653 algorithm that existed only in Acrobat version IX. This option
646 should not be used except for compatibility testing. 654 should not be used except for compatibility testing.
647 )"); 655 )");
648 -}  
649 -static void add_help_6(QPDFArgParser& ap)  
650 -{  
651 ap.addHelpTopic("page-selection", "select pages from one or more files", R"(Use the --pages option to select pages from multiple files. Usage: 656 ap.addHelpTopic("page-selection", "select pages from one or more files", R"(Use the --pages option to select pages from multiple files. Usage:
652 657
653 qpdf in.pdf --pages --file=input-file \ 658 qpdf in.pdf --pages --file=input-file \
@@ -827,15 +832,15 @@ ap.addOptionHelp(&quot;--replace&quot;, &quot;add-attachment&quot;, &quot;replace attachment with same ke @@ -827,15 +832,15 @@ ap.addOptionHelp(&quot;--replace&quot;, &quot;add-attachment&quot;, &quot;replace attachment with same ke
827 be replaced by the new attachment. Otherwise, qpdf gives an 832 be replaced by the new attachment. Otherwise, qpdf gives an
828 error if an attachment with that key is already present. 833 error if an attachment with that key is already present.
829 )"); 834 )");
  835 +}
  836 +static void add_help_7(QPDFArgParser& ap)
  837 +{
830 ap.addHelpTopic("copy-attachments", "copy attachments from another file", R"(The options listed below appear between --copy-attachments-from and 838 ap.addHelpTopic("copy-attachments", "copy attachments from another file", R"(The options listed below appear between --copy-attachments-from and
831 its terminating "--". 839 its terminating "--".
832 840
833 To copy attachments from a password-protected file, use 841 To copy attachments from a password-protected file, use
834 the --password option after the file name. 842 the --password option after the file name.
835 )"); 843 )");
836 -}  
837 -static void add_help_7(QPDFArgParser& ap)  
838 -{  
839 ap.addOptionHelp("--prefix", "copy-attachments", "key prefix for copying attachments", R"(--prefix=prefix 844 ap.addOptionHelp("--prefix", "copy-attachments", "key prefix for copying attachments", R"(--prefix=prefix
840 845
841 Prepend a prefix to each key; may be needed if there are 846 Prepend a prefix to each key; may be needed if there are
@@ -920,12 +925,12 @@ ap.addOptionHelp(&quot;--show-attachment&quot;, &quot;inspection&quot;, &quot;export an embedded file&quot;, R @@ -920,12 +925,12 @@ ap.addOptionHelp(&quot;--show-attachment&quot;, &quot;inspection&quot;, &quot;export an embedded file&quot;, R
920 Write the contents of the specified attachment to standard 925 Write the contents of the specified attachment to standard
921 output as binary data. Get the key with --list-attachments. 926 output as binary data. Get the key with --list-attachments.
922 )"); 927 )");
923 -ap.addHelpTopic("json", "JSON output for PDF information", R"(Show information about the PDF file in JSON format. Please see the  
924 -JSON chapter in the qpdf manual for details.  
925 -)");  
926 } 928 }
927 static void add_help_8(QPDFArgParser& ap) 929 static void add_help_8(QPDFArgParser& ap)
928 { 930 {
  931 +ap.addHelpTopic("json", "JSON output for PDF information", R"(Show information about the PDF file in JSON format. Please see the
  932 +JSON chapter in the qpdf manual for details.
  933 +)");
929 ap.addOptionHelp("--json", "json", "show file in JSON format", R"(--json[=version] 934 ap.addOptionHelp("--json", "json", "show file in JSON format", R"(--json[=version]
930 935
931 Generate a JSON representation of the file. This is described in 936 Generate a JSON representation of the file. This is described in
libqpdf/qpdf/auto_job_init.hh
@@ -32,6 +32,7 @@ this-&gt;ap.addBare(&quot;version&quot;, b(&amp;ArgParser::argVersion)); @@ -32,6 +32,7 @@ this-&gt;ap.addBare(&quot;version&quot;, b(&amp;ArgParser::argVersion));
32 this->ap.addBare("copyright", b(&ArgParser::argCopyright)); 32 this->ap.addBare("copyright", b(&ArgParser::argCopyright));
33 this->ap.addBare("show-crypto", b(&ArgParser::argShowCrypto)); 33 this->ap.addBare("show-crypto", b(&ArgParser::argShowCrypto));
34 this->ap.addBare("job-json-help", b(&ArgParser::argJobJsonHelp)); 34 this->ap.addBare("job-json-help", b(&ArgParser::argJobJsonHelp));
  35 +this->ap.addBare("zopfli", b(&ArgParser::argZopfli));
35 this->ap.addChoices("json-help", p(&ArgParser::argJsonHelp), false, json_version_choices); 36 this->ap.addChoices("json-help", p(&ArgParser::argJsonHelp), false, json_version_choices);
36 this->ap.selectMainOptionTable(); 37 this->ap.selectMainOptionTable();
37 this->ap.addPositional(p(&ArgParser::argPositional)); 38 this->ap.addPositional(p(&ArgParser::argPositional));
libqpdf/qpdf/qpdf-config.h.in
@@ -6,6 +6,7 @@ @@ -6,6 +6,7 @@
6 #cmakedefine USE_CRYPTO_OPENSSL 1 6 #cmakedefine USE_CRYPTO_OPENSSL 1
7 #cmakedefine USE_INSECURE_RANDOM 1 7 #cmakedefine USE_INSECURE_RANDOM 1
8 #cmakedefine SKIP_OS_SECURE_RANDOM 1 8 #cmakedefine SKIP_OS_SECURE_RANDOM 1
  9 +#cmakedefine ZOPFLI 1
9 10
10 /* large file support -- may be needed for 32-bit systems */ 11 /* large file support -- may be needed for 32-bit systems */
11 #cmakedefine _FILE_OFFSET_BITS ${_FILE_OFFSET_BITS} 12 #cmakedefine _FILE_OFFSET_BITS ${_FILE_OFFSET_BITS}
manual/cli.rst
@@ -354,6 +354,21 @@ Related Options @@ -354,6 +354,21 @@ Related Options
354 :qpdf:ref:`--job-json-file`. For more information about QPDFJob, 354 :qpdf:ref:`--job-json-file`. For more information about QPDFJob,
355 see :ref:`qpdf-job`. 355 see :ref:`qpdf-job`.
356 356
  357 +.. qpdf:option:: --zopfli
  358 +
  359 + .. help: indicate whether zopfli is enabled and active
  360 +
  361 + If zopfli support is compiled in, indicate whether it is active,
  362 + and exit normally. Otherwise, indicate that it is not compiled
  363 + in, and exit with an error code. If zopfli is compiled in,
  364 + activate it by setting the ``QPDF_ZOPFLI`` environment variable.
  365 +
  366 + If zopfli support is compiled in, indicate whether it is active,
  367 + and exit normally. Otherwise, indicate that it is not compiled in,
  368 + and exit with an error code. If zopfli is compiled in, activate it
  369 + by setting the ``QPDF_ZOPFLI`` environment variable. See
  370 + :ref:`zopfli`.
  371 +
357 .. _general-options: 372 .. _general-options:
358 373
359 General Options 374 General Options
@@ -936,7 +951,7 @@ Related Options @@ -936,7 +951,7 @@ Related Options
936 951
937 As a special case, streams already compressed with ``/FlateDecode`` 952 As a special case, streams already compressed with ``/FlateDecode``
938 are not uncompressed and recompressed. You can change this behavior 953 are not uncompressed and recompressed. You can change this behavior
939 - with :qpdf:ref:`--recompress-flate`. 954 + with :qpdf:ref:`--recompress-flate`. See also :ref:`small-files`.
940 955
941 .. qpdf:option:: --stream-data=parameter 956 .. qpdf:option:: --stream-data=parameter
942 957
@@ -986,7 +1001,8 @@ Related Options @@ -986,7 +1001,8 @@ Related Options
986 tells :command:`qpdf` to uncompress and recompress streams 1001 tells :command:`qpdf` to uncompress and recompress streams
987 compressed with flate. This can be useful when combined with 1002 compressed with flate. This can be useful when combined with
988 :qpdf:ref:`--compression-level`. Using this option may make 1003 :qpdf:ref:`--compression-level`. Using this option may make
989 - :command:`qpdf` much slower when writing output files. 1004 + :command:`qpdf` much slower when writing output files. See also
  1005 + :ref:`small-files`.
990 1006
991 .. qpdf:option:: --compression-level=level 1007 .. qpdf:option:: --compression-level=level
992 1008
@@ -1009,7 +1025,8 @@ Related Options @@ -1009,7 +1025,8 @@ Related Options
1009 :qpdf:ref:`--recompress-flate`. If your goal is to shrink the size 1025 :qpdf:ref:`--recompress-flate`. If your goal is to shrink the size
1010 of PDF files, you should also use 1026 of PDF files, you should also use
1011 :samp:`--object-streams=generate`. If you omit this option, qpdf 1027 :samp:`--object-streams=generate`. If you omit this option, qpdf
1012 - defers to the compression library's default behavior. 1028 + defers to the compression library's default behavior. See also
  1029 + :ref:`small-files`.
1013 1030
1014 .. qpdf:option:: --normalize-content=[y|n] 1031 .. qpdf:option:: --normalize-content=[y|n]
1015 1032
@@ -1734,7 +1751,7 @@ Related Options @@ -1734,7 +1751,7 @@ Related Options
1734 and :qpdf:ref:`--oi-min-area` options. By default, inline images 1751 and :qpdf:ref:`--oi-min-area` options. By default, inline images
1735 are converted to regular images and optimized as well. Use 1752 are converted to regular images and optimized as well. Use
1736 :qpdf:ref:`--keep-inline-images` to prevent inline images from 1753 :qpdf:ref:`--keep-inline-images` to prevent inline images from
1737 - being included. 1754 + being included. See also :ref:`small-files`.
1738 1755
1739 .. qpdf:option:: --oi-min-width=width 1756 .. qpdf:option:: --oi-min-width=width
1740 1757
@@ -3941,3 +3958,86 @@ from password to encryption key entirely, allowing the raw @@ -3941,3 +3958,86 @@ from password to encryption key entirely, allowing the raw
3941 encryption key to be specified directly. That behavior is useful for 3958 encryption key to be specified directly. That behavior is useful for
3942 forensic purposes or for brute-force recovery of files with unknown 3959 forensic purposes or for brute-force recovery of files with unknown
3943 passwords and has nothing to do with the document's actual passwords. 3960 passwords and has nothing to do with the document's actual passwords.
  3961 +
  3962 +.. _small-files:
  3963 +
  3964 +Optimizing File Size
  3965 +--------------------
  3966 +
  3967 +While qpdf's primary function is not to optimize the size of PDF
  3968 +files, there are a number of things you can do to make files smaller.
  3969 +Note that qpdf will not resample images or make optimizations that
  3970 +modify content with the exception of possibly recompressing images
  3971 +using DCT (JPEG) compression.
  3972 +
  3973 +The following options will give you the smallest files that qpdf can
  3974 +generate:
  3975 +
  3976 +- ``--compress-streams=y``: make sure streams are compressed (see
  3977 + :qpdf:ref:`--compress-streams`)
  3978 +
  3979 +- ``--decode-level=generalized``: apply any non-specialized filters
  3980 + (see :qpdf:ref:`--decode-level`)
  3981 +
  3982 +- :qpdf:ref:`--recompress-flate`: uncompress and recompress streams
  3983 + that are already compressed with zlib (flate) compression
  3984 +
  3985 +- ``--compression-level=9``: use the highest possible compression
  3986 + level (see :ref:`zopfli` and :qpdf:ref:`--compression-level`)
  3987 +
  3988 +- :qpdf:ref:`--optimize-images`: replace non-JPEG images with JPEG if
  3989 + doing so reduces their size. Not all types of images are supported,
  3990 + but qpdf will only keep the result if it is supported and reduces
  3991 + the size. Images are not resampled, but bear in mind that JPEG is
  3992 + lossy, so images may have artifacts. These are not usually
  3993 + noticeable to the casual observer.
  3994 +
  3995 +- ``--object-streams=generate``: generate object streams, which means
  3996 + that more of the PDF file's structural content will be compressed
  3997 + (see :qpdf:ref:`--object-streams`)
  3998 +
  3999 +.. _zopfli:
  4000 +
  4001 +Zopfli Compression Algorithm
  4002 +----------------------------
  4003 +
  4004 +If qpdf is built with `zopfli <https://github.com/google/zopfli>`__
  4005 +support (see :ref:`build-zopfli`), you can have qpdf use the Zopfli
  4006 +Compression Algorithm in place of zlib. In this mode, qpdf is *much
  4007 +slower*, but produces slightly smaller compressed output. (According
  4008 +to their documentation, zopfli is about 100 times slower than zlib and
  4009 +produces output that's about 5% better than the best compression
  4010 +available with other libraries.) For this to be useful, you should run
  4011 +qpdf with options to recompress compressed streams. See
  4012 +:ref:`small-files` for tips. In order to use zopfli, in addition to
  4013 +building with zopfli support, you must set the ``QPDF_ZOPFLI``
  4014 +environment variable to some value other than ``disabled``. Note that
  4015 +:qpdf:ref:`--compression-level` has no effect when zopfli is in use,
  4016 +since zopfli always optimizes for size over everything else.
  4017 +
  4018 +Here are the supported values for ``QPDF_ZOPFLI``:
  4019 +
  4020 +.. list-table:: ``QPDF_ZOPFLI`` values
  4021 + :widths: 20 60
  4022 + :header-rows: 0
  4023 +
  4024 + - - ``disabled`` or unset
  4025 + - do not use zopfli even if available
  4026 +
  4027 + - - ``silent``
  4028 + - use zopfli if available; otherwise silently fall back to zlib
  4029 +
  4030 + - - ``force``
  4031 + - use zopfli if available; fail with an error if not available
  4032 +
  4033 + - - any other value
  4034 + - use zopfli if available; otherwise issue a warning and fall
  4035 + back to zlib
  4036 +
  4037 +Note that the warning and error behavior are managed in ``QPDFJob``
  4038 +and affect the ``qpdf`` executable. For code that directly uses the
  4039 +qpdf library, the behavior is that zopfli is enabled with any value
  4040 +other than ``disabled`` but silently falls back to zlib. If you want
  4041 +your application to behave the same as the ``qpdf`` executable with
  4042 +respect to zopfli, you can call ``Pl_Flate::zopfli_check_env()``. See
  4043 +its documentation in the ``qpdf/Pl_Flate.hh`` header file.
manual/installation.rst
@@ -30,6 +30,9 @@ Basic Dependencies @@ -30,6 +30,9 @@ Basic Dependencies
30 <https://openssl.org/>`__ to be able to use the openssl crypto 30 <https://openssl.org/>`__ to be able to use the openssl crypto
31 provider 31 provider
32 32
  33 +- If the ``ZOPFLI`` build option is specified (off by default), the
  34 + `zopfli <https://github.com/google/zopfli>`__ library.
  35 +
33 The qpdf source tree includes a few automatically generated files. The 36 The qpdf source tree includes a few automatically generated files. The
34 code generator uses Python 3. Automatic code generation is off by 37 code generator uses Python 3. Automatic code generation is off by
35 default. For a discussion, refer to :ref:`build-options`. 38 default. For a discussion, refer to :ref:`build-options`.
@@ -291,6 +294,10 @@ QTEST_COLOR @@ -291,6 +294,10 @@ QTEST_COLOR
291 Turn this on or off to control whether qtest uses color in its 294 Turn this on or off to control whether qtest uses color in its
292 output. 295 output.
293 296
  297 +ZOPFLI
  298 + Use the `zopfli <https://github.com/google/zopfli>`__ library for
  299 + zlib-compatible compression. See :ref:`zopfli`.
  300 +
294 Options for Working on qpdf 301 Options for Working on qpdf
295 ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 302 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
296 303
@@ -648,6 +655,16 @@ Implementing the registration functions and internal storage of @@ -648,6 +655,16 @@ Implementing the registration functions and internal storage of
648 registered providers was also easier using C++-11's functional 655 registered providers was also easier using C++-11's functional
649 interfaces, which was another reason to require C++-11 at this time. 656 interfaces, which was another reason to require C++-11 at this time.
650 657
  658 +.. _build-zopfli:
  659 +
  660 +Building with zopfli support
  661 +----------------------------
  662 +
  663 +If you compile with ``-DZOPFLI-ON`` and have the `zopfli
  664 +<https://github.com/google/zopfli>`__ development files available,
  665 +qpdf will be built with zopfli support. See :ref:`zopfli` for
  666 +information about using zopfli with qpdf.
  667 +
651 .. _autoconf-to-cmake: 668 .. _autoconf-to-cmake:
652 669
653 Converting From autoconf to cmake 670 Converting From autoconf to cmake
manual/packaging.rst
@@ -44,6 +44,15 @@ particularly useful to packagers. @@ -44,6 +44,15 @@ particularly useful to packagers.
44 11, this was a recommendation for packagers but was not done 44 11, this was a recommendation for packagers but was not done
45 automatically. 45 automatically.
46 46
  47 +- Starting with qpdf 11.10, qpdf can be built with zopfli support (see
  48 + :ref:`build-zopfli`). It is recommended not to build qpdf with zopfli
  49 + for distributions since it adds zopfli as a dependency, and this
  50 + library is less widely used that qpdf's other dependencies. Users
  51 + who want that probably know they want it, and they can compile from
  52 + source. Note that, per zopfli's own documentation, zopfli is about
  53 + 100 times slower than zlib and produces compression output about 5%
  54 + smaller.
  55 +
47 .. _package-tests: 56 .. _package-tests:
48 57
49 Package Tests 58 Package Tests
manual/qpdf.1
@@ -115,6 +115,12 @@ default provider is shown first. @@ -115,6 +115,12 @@ default provider is shown first.
115 .B --job-json-help \-\- show format of job JSON 115 .B --job-json-help \-\- show format of job JSON
116 Describe the format of the QPDFJob JSON input used by 116 Describe the format of the QPDFJob JSON input used by
117 --job-json-file. 117 --job-json-file.
  118 +.TP
  119 +.B --zopfli \-\- indicate whether zopfli is enabled and active
  120 +If zopfli support is compiled in, indicate whether it is active,
  121 +and exit normally. Otherwise, indicate that it is not compiled
  122 +in, and exit with an error code. If zopfli is compiled in,
  123 +activate it by setting the ``QPDF_ZOPFLI`` environment variable.
118 .SH GENERAL (general options) 124 .SH GENERAL (general options)
119 General options control qpdf's behavior in ways that are not 125 General options control qpdf's behavior in ways that are not
120 directly related to the operation it is performing. 126 directly related to the operation it is performing.
manual/release-notes.rst
@@ -46,6 +46,11 @@ Planned changes for future 12.x (subject to change): @@ -46,6 +46,11 @@ Planned changes for future 12.x (subject to change):
46 environments in which writing a binary file to standard output 46 environments in which writing a binary file to standard output
47 doesn't work (such as PowerShell 5). 47 doesn't work (such as PowerShell 5).
48 48
  49 + - Library Enhancements
  50 +
  51 + - qpdf can now be built with zopfli support. For details, see
  52 + :ref:`zopfli`.
  53 +
49 11.9.1: June 7, 2024 54 11.9.1: June 7, 2024
50 - Bug Fixes 55 - Bug Fixes
51 56
qpdf/qtest/copy-foreign-objects.test
@@ -51,8 +51,9 @@ $td-&gt;runtest(&quot;indirect filters&quot;, @@ -51,8 +51,9 @@ $td-&gt;runtest(&quot;indirect filters&quot;,
51 foreach my $i (0, 1) 51 foreach my $i (0, 1)
52 { 52 {
53 $td->runtest("check output", 53 $td->runtest("check output",
54 - {$td->FILE => "auto-$i.pdf"},  
55 - {$td->FILE => "indirect-filter-out-$i.pdf"}); 54 + {$td->COMMAND =>
  55 + "qpdf-test-compare auto-$i.pdf indirect-filter-out-$i.pdf"},
  56 + {$td->FILE => "indirect-filter-out-$i.pdf", $td->EXIT_STATUS => 0});
56 } 57 }
57 $td->runtest("issue 449", 58 $td->runtest("issue 449",
58 {$td->COMMAND => "test_driver 69 issue-449.pdf"}, 59 {$td->COMMAND => "test_driver 69 issue-449.pdf"},
qpdf/qtest/disable-filter-on-write.test
@@ -21,8 +21,8 @@ $td-&gt;runtest(&quot;no filter on write&quot;, @@ -21,8 +21,8 @@ $td-&gt;runtest(&quot;no filter on write&quot;,
21 {$td->STRING => "test 70 done\n", $td->EXIT_STATUS => 0}, 21 {$td->STRING => "test 70 done\n", $td->EXIT_STATUS => 0},
22 $td->NORMALIZE_NEWLINES); 22 $td->NORMALIZE_NEWLINES);
23 $td->runtest("check output", 23 $td->runtest("check output",
24 - {$td->FILE => "a.pdf"},  
25 - {$td->FILE => "filter-on-write-out.pdf"}); 24 + {$td->COMMAND => "qpdf-test-compare a.pdf filter-on-write-out.pdf"},
  25 + {$td->FILE => "filter-on-write-out.pdf", $td->EXIT_STATUS => 0});
26 26
27 cleanup(); 27 cleanup();
28 $td->report($n_tests); 28 $td->report($n_tests);
qpdf/qtest/qpdf/minimal-out.pdf 0 โ†’ 100644
No preview for this file type
qpdf/qtest/qpdf/zopfli-warning.out 0 โ†’ 100644
  1 +QPDF_ZOPFLI is set, but libqpdf was not built with zopfli support
  2 +Set QPDF_ZOPFLI=silent to suppress this warning and use zopfli when available.
  3 +qpdf: operation succeeded with warnings; resulting file may have some problems
qpdf/qtest/qpdf_test_helpers.pm
1 use File::Spec; 1 use File::Spec;
2 2
3 -my $devNull = File::Spec->devnull(); 3 +my $dev_null = File::Spec->devnull();
4 4
5 my $compare_images = 0; 5 my $compare_images = 0;
6 if ((exists $ENV{'QPDF_TEST_COMPARE_IMAGES'}) && 6 if ((exists $ENV{'QPDF_TEST_COMPARE_IMAGES'}) &&
@@ -95,7 +95,7 @@ sub compare_pdfs @@ -95,7 +95,7 @@ sub compare_pdfs
95 $td->runtest("convert original file to image", 95 $td->runtest("convert original file to image",
96 {$td->COMMAND => 96 {$td->COMMAND =>
97 "(cd tif1;" . 97 "(cd tif1;" .
98 - " gs 2>$devNull $x_gs_args" . 98 + " gs 2>$dev_null $x_gs_args" .
99 " -q -dNOPAUSE -sDEVICE=tiff24nc" . 99 " -q -dNOPAUSE -sDEVICE=tiff24nc" .
100 " -sOutputFile=a.tif - < ../$f1)"}, 100 " -sOutputFile=a.tif - < ../$f1)"},
101 {$td->STRING => "", 101 {$td->STRING => "",
@@ -115,7 +115,7 @@ sub compare_pdfs @@ -115,7 +115,7 @@ sub compare_pdfs
115 $td->runtest("convert new file to image", 115 $td->runtest("convert new file to image",
116 {$td->COMMAND => 116 {$td->COMMAND =>
117 "(cd tif2;" . 117 "(cd tif2;" .
118 - " gs 2>$devNull $x_gs_args" . 118 + " gs 2>$dev_null $x_gs_args" .
119 " -q -dNOPAUSE -sDEVICE=tiff24nc" . 119 " -q -dNOPAUSE -sDEVICE=tiff24nc" .
120 " -sOutputFile=a.tif - < ../$f2)"}, 120 " -sOutputFile=a.tif - < ../$f2)"},
121 {$td->STRING => "", 121 {$td->STRING => "",
qpdf/qtest/split-pages.test
@@ -172,8 +172,8 @@ $td-&gt;runtest(&quot;merge for compare&quot;, @@ -172,8 +172,8 @@ $td-&gt;runtest(&quot;merge for compare&quot;,
172 " split-out-shared-form*.pdf -- a.pdf"}, 172 " split-out-shared-form*.pdf -- a.pdf"},
173 {$td->STRING => "", $td->EXIT_STATUS => 0}); 173 {$td->STRING => "", $td->EXIT_STATUS => 0});
174 $td->runtest("check output", 174 $td->runtest("check output",
175 - {$td->FILE => "a.pdf"},  
176 - {$td->FILE => "shared-form-images-merged.pdf"}); 175 + {$td->COMMAND => "qpdf-test-compare a.pdf shared-form-images-merged.pdf"},
  176 + {$td->FILE => "shared-form-images-merged.pdf", $td->EXIT_STATUS => 0});
177 compare_pdfs($td, "shared-form-images.pdf", "a.pdf"); 177 compare_pdfs($td, "shared-form-images.pdf", "a.pdf");
178 178
179 $td->runtest("shared form xobject subkey", 179 $td->runtest("shared form xobject subkey",
qpdf/qtest/stream-replacements.test
@@ -28,8 +28,8 @@ $td-&gt;runtest(&quot;replace stream data compressed&quot;, @@ -28,8 +28,8 @@ $td-&gt;runtest(&quot;replace stream data compressed&quot;,
28 {$td->FILE => "test8.out", $td->EXIT_STATUS => 0}, 28 {$td->FILE => "test8.out", $td->EXIT_STATUS => 0},
29 $td->NORMALIZE_NEWLINES); 29 $td->NORMALIZE_NEWLINES);
30 $td->runtest("check output", 30 $td->runtest("check output",
31 - {$td->FILE => "a.pdf"},  
32 - {$td->FILE => "replaced-stream-data-flate.pdf"}); 31 + {$td->COMMAND => "qpdf-test-compare a.pdf replaced-stream-data-flate.pdf"},
  32 + {$td->FILE => "replaced-stream-data-flate.pdf", $td->EXIT_STATUS => 0});
33 $td->runtest("new streams", 33 $td->runtest("new streams",
34 {$td->COMMAND => "test_driver 9 minimal.pdf"}, 34 {$td->COMMAND => "test_driver 9 minimal.pdf"},
35 {$td->FILE => "test9.out", $td->EXIT_STATUS => 0}, 35 {$td->FILE => "test9.out", $td->EXIT_STATUS => 0},
qpdf/qtest/zopfli.test 0 โ†’ 100644
  1 +#!/usr/bin/env perl
  2 +require 5.008;
  3 +use warnings;
  4 +use strict;
  5 +
  6 +unshift(@INC, '.');
  7 +require qpdf_test_helpers;
  8 +
  9 +chdir("qpdf") or die "chdir testdir failed: $!\n";
  10 +
  11 +require TestDriver;
  12 +
  13 +my $dev_null = File::Spec->devnull();
  14 +cleanup();
  15 +
  16 +my $td = new TestDriver('zopfli');
  17 +
  18 +my $n_tests = 0;
  19 +
  20 +my $zopfli_enabled = (system("qpdf --zopfli >$dev_null 2>&1") == 0);
  21 +
  22 +if (! $zopfli_enabled) {
  23 + # Variables are not checked
  24 + $n_tests = 8;
  25 + $td->runtest("zopfli not enabled",
  26 + {$td->COMMAND => "QPDF_ZOPFLI=force qpdf --zopfli"},
  27 + {$td->STRING => "zopfli support is not enabled\n",
  28 + $td->EXIT_STATUS => 2},
  29 + $td->NORMALIZE_NEWLINES);
  30 +
  31 + $td->runtest("zopfli disabled",
  32 + {$td->COMMAND => "QPDF_ZOPFLI=disabled qpdf minimal.pdf a.pdf"},
  33 + {$td->STRING => "", $td->EXIT_STATUS => 0},
  34 + $td->NORMALIZE_NEWLINES);
  35 + $td->runtest("check output",
  36 + {$td->COMMAND => "qpdf-test-compare a.pdf minimal-out.pdf"},
  37 + {$td->FILE => "minimal-out.pdf", $td->EXIT_STATUS => 0});
  38 + $td->runtest("zopfli silent",
  39 + {$td->COMMAND => "QPDF_ZOPFLI=silent qpdf minimal.pdf a.pdf"},
  40 + {$td->STRING => "", $td->EXIT_STATUS => 0},
  41 + $td->NORMALIZE_NEWLINES);
  42 + $td->runtest("check output",
  43 + {$td->COMMAND => "qpdf-test-compare a.pdf minimal-out.pdf"},
  44 + {$td->FILE => "minimal-out.pdf", $td->EXIT_STATUS => 0});
  45 +
  46 + $td->runtest("zopfli warning",
  47 + {$td->COMMAND => "QPDF_ZOPFLI=on qpdf minimal.pdf a.pdf"},
  48 + {$td->FILE => "zopfli-warning.out", $td->EXIT_STATUS => 3},
  49 + $td->NORMALIZE_NEWLINES);
  50 + $td->runtest("check output",
  51 + {$td->COMMAND => "qpdf-test-compare a.pdf minimal-out.pdf"},
  52 + {$td->FILE => "minimal-out.pdf", $td->EXIT_STATUS => 0});
  53 +
  54 + $td->runtest("zopfli error",
  55 + {$td->COMMAND => "QPDF_ZOPFLI=force qpdf minimal.pdf a.pdf"},
  56 + {$td->REGEXP => "QPDF_ZOPFLI=force, and zopfli support is not enabled",
  57 + $td->EXIT_STATUS => 2},
  58 + $td->NORMALIZE_NEWLINES);
  59 +
  60 +} else {
  61 + # Check variables
  62 + $n_tests = 4;
  63 + $td->runtest("zopfli supported and enabled",
  64 + {$td->COMMAND => "QPDF_ZOPFLI=on qpdf --zopfli"},
  65 + {$td->STRING => "zopfli support is enabled, and zopfli is active\n",
  66 + $td->EXIT_STATUS => 0},
  67 + $td->NORMALIZE_NEWLINES);
  68 + $td->runtest("zopfli supported and disabled",
  69 + {$td->COMMAND => "QPDF_ZOPFLI=disabled qpdf --zopfli"},
  70 + {$td->REGEXP => "(?s)zopfli support is enabled but not active.*QPDF_ZOPFLI.*\n",
  71 + $td->EXIT_STATUS => 0},
  72 + $td->NORMALIZE_NEWLINES);
  73 + # CI runs the whole test suite with QPDF_ZOPFLI=force, but run one
  74 + # for a guarantee.
  75 + $td->runtest("run with zopfli",
  76 + {$td->COMMAND => "QPDF_ZOPFLI=force qpdf minimal.pdf a.pdf"},
  77 + {$td->STRING => "", $td->EXIT_STATUS => 0},
  78 + $td->NORMALIZE_NEWLINES);
  79 + $td->runtest("check output",
  80 + {$td->COMMAND => "qpdf-test-compare a.pdf minimal-out.pdf"},
  81 + {$td->FILE => "minimal-out.pdf", $td->EXIT_STATUS => 0});
  82 +}
  83 +
  84 +cleanup();
  85 +$td->report($n_tests);
zlib-flate/qtest/zf.test
@@ -21,6 +21,9 @@ for (my $i = 0; $i &lt; 100; $i++) @@ -21,6 +21,9 @@ for (my $i = 0; $i &lt; 100; $i++)
21 } 21 }
22 close(F); 22 close(F);
23 23
  24 +my $dev_null = File::Spec->devnull();
  25 +my $n_tests = 9;
  26 +
24 foreach my $level ('', '=1', '=9') 27 foreach my $level ('', '=1', '=9')
25 { 28 {
26 my $f = $level; 29 my $f = $level;
@@ -35,11 +38,18 @@ foreach my $level (&#39;&#39;, &#39;=1&#39;, &#39;=9&#39;) @@ -35,11 +38,18 @@ foreach my $level (&#39;&#39;, &#39;=1&#39;, &#39;=9&#39;)
35 {$td->FILE => "a.uncompressed", $td->EXIT_STATUS => 0}); 38 {$td->FILE => "a.uncompressed", $td->EXIT_STATUS => 0});
36 } 39 }
37 40
  41 +chomp(my $zopfli = `zlib-flate --_zopfli`);
38 my $size1 = (stat("a.=1"))[7]; 42 my $size1 = (stat("a.=1"))[7];
39 my $size9 = (stat("a.=9"))[7]; 43 my $size9 = (stat("a.=9"))[7];
40 -$td->runtest("higher compression is smaller",  
41 - {$td->STRING => ($size9 < $size1 ? "YES\n" : "$size9 $size1\n")},  
42 - {$td->STRING => "YES\n"}); 44 +if ($zopfli =~ m/1$/) {
  45 + $td->runtest("compression level is ignored with zopfli",
  46 + {$td->STRING => ($size9 == $size1 ? "YES\n" : "$size9 $size1\n")},
  47 + {$td->STRING => "YES\n"});
  48 +} else {
  49 + $td->runtest("higher compression is smaller",
  50 + {$td->STRING => ($size9 < $size1 ? "YES\n" : "$size9 $size1\n")},
  51 + {$td->STRING => "YES\n"});
  52 +}
43 53
44 $td->runtest("error", 54 $td->runtest("error",
45 {$td->COMMAND => "zlib-flate -uncompress < 1.uncompressed"}, 55 {$td->COMMAND => "zlib-flate -uncompress < 1.uncompressed"},
@@ -54,7 +64,40 @@ $td-&gt;runtest(&quot;corrupted input&quot;, @@ -54,7 +64,40 @@ $td-&gt;runtest(&quot;corrupted input&quot;,
54 $td->EXIT_STATUS => 3}, 64 $td->EXIT_STATUS => 3},
55 $td->NORMALIZE_NEWLINES); 65 $td->NORMALIZE_NEWLINES);
56 66
57 -$td->report(9); 67 +# Exercise different values of the QPDF_ZOPFLI variable
  68 +if ($zopfli =~ m/^0/) {
  69 + $n_tests += 4;
  70 + $td->runtest("disabled",
  71 + {$td->COMMAND => "QPDF_ZOPFLI=disabled zlib-flate --_zopfli"},
  72 + {$td->STRING => "00\n", $td->EXIT_STATUS => 0},
  73 + $td->NORMALIZE_NEWLINES);
  74 + $td->runtest("force",
  75 + {$td->COMMAND => "QPDF_ZOPFLI=force zlib-flate -compress < a.uncompressed"},
  76 + {$td->REGEXP => "QPDF_ZOPFLI=force, and zopfli support is not enabled",
  77 + $td->EXIT_STATUS => 2},
  78 + $td->NORMALIZE_NEWLINES);
  79 + $td->runtest("silent",
  80 + {$td->COMMAND => "QPDF_ZOPFLI=silent zlib-flate -compress < a.uncompressed > $dev_null"},
  81 + {$td->STRING => "", $td->EXIT_STATUS => 0},
  82 + $td->NORMALIZE_NEWLINES);
  83 + $td->runtest("other",
  84 + {$td->COMMAND => "QPDF_ZOPFLI=other zlib-flate -compress < a.uncompressed > $dev_null"},
  85 + {$td->REGEXP => "QPDF_ZOPFLI is set, but libqpdf was not built with zopfli support",
  86 + $td->EXIT_STATUS => 0},
  87 + $td->NORMALIZE_NEWLINES);
  88 +} else {
  89 + $n_tests += 2;
  90 + $td->runtest("disabled",
  91 + {$td->COMMAND => "QPDF_ZOPFLI=disabled zlib-flate --_zopfli"},
  92 + {$td->STRING => "10\n", $td->EXIT_STATUS => 0},
  93 + $td->NORMALIZE_NEWLINES);
  94 + $td->runtest("force",
  95 + {$td->COMMAND => "QPDF_ZOPFLI=force zlib-flate --_zopfli"},
  96 + {$td->STRING => "11\n", $td->EXIT_STATUS => 0},
  97 + $td->NORMALIZE_NEWLINES);
  98 +}
  99 +
  100 +$td->report($n_tests);
58 101
59 cleanup(); 102 cleanup();
60 103
zlib-flate/zlib-flate.cc
@@ -6,7 +6,6 @@ @@ -6,7 +6,6 @@
6 #include <cstdio> 6 #include <cstdio>
7 #include <cstdlib> 7 #include <cstdlib>
8 #include <cstring> 8 #include <cstring>
9 -#include <fcntl.h>  
10 #include <iostream> 9 #include <iostream>
11 10
12 static char const* whoami = nullptr; 11 static char const* whoami = nullptr;
@@ -50,21 +49,29 @@ main(int argc, char* argv[]) @@ -50,21 +49,29 @@ main(int argc, char* argv[])
50 action = Pl_Flate::a_deflate; 49 action = Pl_Flate::a_deflate;
51 int level = QUtil::string_to_int(argv[1] + 10); 50 int level = QUtil::string_to_int(argv[1] + 10);
52 Pl_Flate::setCompressionLevel(level); 51 Pl_Flate::setCompressionLevel(level);
  52 + } else if (strcmp(argv[1], "--_zopfli") == 0) {
  53 + // Undocumented option, but that doesn't mean someone doesn't use it...
  54 + // This is primarily here to support the test suite.
  55 + std::cout << (Pl_Flate::zopfli_supported() ? "1" : "0")
  56 + << (Pl_Flate::zopfli_enabled() ? "1" : "0") << std::endl;
  57 + return 0;
53 } else { 58 } else {
54 usage(); 59 usage();
55 } 60 }
56 61
57 - QUtil::binary_stdout();  
58 - QUtil::binary_stdin();  
59 - auto out = std::make_shared<Pl_StdioFile>("stdout", stdout);  
60 - auto flate = std::make_shared<Pl_Flate>("flate", out.get(), action);  
61 bool warn = false; 62 bool warn = false;
62 - flate->setWarnCallback([&warn](char const* msg, int code) {  
63 - warn = true;  
64 - std::cerr << whoami << ": WARNING: zlib code " << code << ", msg = " << msg << std::endl;  
65 - });  
66 -  
67 try { 63 try {
  64 + QUtil::binary_stdout();
  65 + QUtil::binary_stdin();
  66 + Pl_Flate::zopfli_check_env();
  67 + auto out = std::make_shared<Pl_StdioFile>("stdout", stdout);
  68 + auto flate = std::make_shared<Pl_Flate>("flate", out.get(), action);
  69 + flate->setWarnCallback([&warn](char const* msg, int code) {
  70 + warn = true;
  71 + std::cerr << whoami << ": WARNING: zlib code " << code << ", msg = " << msg
  72 + << std::endl;
  73 + });
  74 +
68 unsigned char buf[10000]; 75 unsigned char buf[10000];
69 bool done = false; 76 bool done = false;
70 while (!done) { 77 while (!done) {