Commit ad3ecadf05200e8fd9fb2de6e4fc90a5ec6cb209

Authored by Jay Berkenbilt
Committed by GitHub
2 parents 0ddc4abd 133da3b6

Merge pull request #1346 from jberkenbilt/zopfli

Add zopfli support
.github/workflows/main.yml
... ... @@ -123,6 +123,13 @@ jobs:
123 123 - uses: actions/checkout@v4
124 124 - name: 'Sanitizer Tests'
125 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 133 CodeCov:
127 134 runs-on: ubuntu-latest
128 135 needs: Prebuild
... ...
CMakeLists.txt
... ... @@ -98,6 +98,8 @@ set(DEFAULT_CRYPTO CACHE STRING "")
98 98 option(DEFAULT_CRYPTO
99 99 "Specify default crypto; otherwise chosen automatically" "")
100 100  
  101 +option(ZOPFLI, "Use zopfli for zlib-compatible compression")
  102 +
101 103 # INSTALL_MANUAL is not dependent on building docs. When creating some
102 104 # distributions, we build the doc in one run, copy doc-dist in, and
103 105 # install it elsewhere.
... ...
ChangeLog
1 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 6 * Have fix-qdf accept a second argument, interpreted as the output
4 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 67  
68 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 75 CFLAGS="-fsanitize=address -fsanitize=undefined" \
72 76 CXXFLAGS="-fsanitize=address -fsanitize=undefined" \
... ... @@ -298,8 +302,8 @@ Building docs from pull requests is also enabled.
298 302 ## ZLIB COMPATIBILITY
299 303  
300 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 308 * `build-scripts/test-alt-zlib` runs in CI and runs the test suite
305 309 with a non-default zlib. Please refer to that code for an example of
... ...
README.md
... ... @@ -66,6 +66,16 @@ below.
66 66  
67 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 79 # Licensing terms of embedded software
70 80  
71 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 24 #define PL_FLATE_HH
25 25  
26 26 #include <qpdf/Pipeline.hh>
  27 +#include <qpdf/QPDFLogger.hh>
27 28 #include <functional>
28 29 #include <memory>
  30 +#include <string>
29 31  
30 32 class QPDF_DLL_CLASS Pl_Flate: public Pipeline
31 33 {
... ... @@ -65,6 +67,23 @@ class QPDF_DLL_CLASS Pl_Flate: public Pipeline
65 67 QPDF_DLL
66 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 87 private:
69 88 QPDF_DLL_PRIVATE
70 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 91 void checkError(char const* prefix, int error_code);
73 92 QPDF_DLL_PRIVATE
74 93 void warn(char const*, int error_code);
  94 + QPDF_DLL_PRIVATE
  95 + void finish_zopfli();
75 96  
76 97 QPDF_DLL_PRIVATE
77 98 static int compression_level;
... ... @@ -95,6 +116,7 @@ class QPDF_DLL_CLASS Pl_Flate: public Pipeline
95 116 void* zdata;
96 117 unsigned long long written{0};
97 118 std::function<void(char const*, int)> callback;
  119 + std::unique_ptr<std::string> zopfli_buf;
98 120 };
99 121  
100 122 std::shared_ptr<Members> m;
... ...
job.sums
1 1 # Generated by generate_auto_job
2   -CMakeLists.txt 4aaa3d5df1713d9e3b9c6778101c6af3efa2131a2f4c069095abee269d5eaccc
  2 +CMakeLists.txt af74c05aea88512ef9a37a0708c2a03747a4ff23dc7919075c8d3b62b73aad79
3 3 generate_auto_job f64733b79dcee5a0e3e8ccc6976448e8ddf0e8b6529987a66a7d3ab2ebc10a86
4 4 include/qpdf/auto_job_c_att.hh 4c2b171ea00531db54720bf49a43f8b34481586ae7fb6cbf225099ee42bc5bb4
5 5 include/qpdf/auto_job_c_copy_att.hh 50609012bff14fd82f0649185940d617d05d530cdc522185c7f3920a561ccb42
... ... @@ -7,14 +7,14 @@ include/qpdf/auto_job_c_enc.hh 28446f3c32153a52afa239ea40503e6cc8ac2c026813526a3
7 7 include/qpdf/auto_job_c_main.hh 84f463237235b2c095b747a4f5dd00f109ee596a1c207b944efb296c0c568cae
8 8 include/qpdf/auto_job_c_pages.hh 09ca15649cc94fdaf6d9bdae28a20723f2a66616bf15aa86d83df31051d82506
9 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 14 libqpdf/qpdf/auto_job_json_decl.hh 843892c8e8652a86b7eb573893ef24050b7f36fe313f7251874be5cd4cdbe3fd
15 15 libqpdf/qpdf/auto_job_json_init.hh 344c2fb473f88fe829c93b1efe6c70a0e4796537b8eb35e421d955fff481ba7d
16 16 libqpdf/qpdf/auto_job_schema.hh 6d3eef5137b8828eaa301a1b3cf75cb7bb812aa6e2d8301de865b42d238d7a7c
17 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 20 manual/qpdf.1.in 436ecc85d45c4c9e2dbd1725fb7f0177fb627179469f114561adf3cb6cbb677b
... ...
... ... @@ -81,6 +81,7 @@ options:
81 81 - copyright
82 82 - show-crypto
83 83 - job-json-help
  84 + - zopfli
84 85 optional_choices:
85 86 json-help: json_version
86 87 - table: main
... ...
libqpdf/CMakeLists.txt
... ... @@ -190,6 +190,18 @@ if(NOT EXTERNAL_LIBS)
190 190 endif()
191 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 205 # Update JPEG_INCLUDE in PARENT_SCOPE after we have finished setting it.
194 206 set(JPEG_INCLUDE ${JPEG_INCLUDE} PARENT_SCOPE)
195 207  
... ...
libqpdf/Pl_Flate.cc
... ... @@ -6,6 +6,11 @@
6 6  
7 7 #include <qpdf/QIntC.hh>
8 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 15 namespace
11 16 {
... ... @@ -39,6 +44,10 @@ Pl_Flate::Members::Members(size_t out_bufsize, action_e action) :
39 44 zstream.avail_in = 0;
40 45 zstream.next_out = this->outbuf.get();
41 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 53 Pl_Flate::Members::~Members()
... ... @@ -59,7 +68,7 @@ Pl_Flate::Members::~Members()
59 68 Pl_Flate::Pl_Flate(
60 69 char const* identifier, Pipeline* next, action_e action, unsigned int out_bufsize_int) :
61 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 73 if (!next) {
65 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 107 throw std::logic_error(
99 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 115 // Write in chunks in case len is too big to fit in an int. Assume int is at least 32 bits.
103 116 static size_t const max_bytes = 1 << 30;
... ... @@ -211,7 +224,9 @@ Pl_Flate::finish()
211 224 throw std::runtime_error("PL_Flate memory limit exceeded");
212 225 }
213 226 try {
214   - if (m->outbuf.get()) {
  227 + if (m->zopfli_buf) {
  228 + finish_zopfli();
  229 + } else if (m->outbuf.get()) {
215 230 if (m->initialized) {
216 231 z_stream& zstream = *(static_cast<z_stream*>(m->zdata));
217 232 unsigned char buf[1];
... ... @@ -291,3 +306,76 @@ Pl_Flate::checkError(char const* prefix, int error_code)
291 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 498 void
499 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 506 if (!createsOutput()) {
502 507 doInspection(pdf);
503 508 } else if (m->split_pages) {
... ...
libqpdf/QPDFJob_argv.cc
... ... @@ -6,6 +6,7 @@
6 6 #include <iostream>
7 7 #include <memory>
8 8  
  9 +#include <qpdf/Pl_Flate.hh>
9 10 #include <qpdf/QPDFArgParser.hh>
10 11 #include <qpdf/QPDFCryptoProvider.hh>
11 12 #include <qpdf/QPDFLogger.hh>
... ... @@ -105,6 +106,27 @@ ArgParser::argVersion()
105 106 }
106 107  
107 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 130 ArgParser::argCopyright()
109 131 {
110 132 // clang-format off
... ...
libqpdf/qpdf/auto_job_decl.hh
... ... @@ -19,6 +19,7 @@ void argVersion();
19 19 void argCopyright();
20 20 void argShowCrypto();
21 21 void argJobJsonHelp();
  22 +void argZopfli();
22 23 void argJsonHelp(std::string const&);
23 24 void argPositional(std::string const&);
24 25 void argAddAttachment();
... ...
libqpdf/qpdf/auto_job_help.hh
... ... @@ -73,6 +73,11 @@ default provider is shown first.
73 73 ap.addOptionHelp("--job-json-help", "help", "show format of job JSON", R"(Describe the format of the QPDFJob JSON input used by
74 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 81 ap.addHelpTopic("general", "general options", R"(General options control qpdf's behavior in ways that are not
77 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 91 The first line of the specified file is used as the password.
87 92 This is used in place of the --password option.
88 93 )");
  94 +}
  95 +static void add_help_2(QPDFArgParser& ap)
  96 +{
89 97 ap.addOptionHelp("--verbose", "general", "print additional information", R"(Output additional information about various things qpdf is
90 98 doing, including information about files created and operations
91 99 performed.
92 100 )");
93   -}
94   -static void add_help_2(QPDFArgParser& ap)
95   -{
96 101 ap.addOptionHelp("--progress", "general", "show progress when writing", R"(Indicate progress when writing files.
97 102 )");
98 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 173 preserving the input file's encryption. Use --encryption-file-password
169 174 to specify the encryption file's password.
170 175 )");
  176 +}
  177 +static void add_help_3(QPDFArgParser& ap)
  178 +{
171 179 ap.addOptionHelp("--encryption-file-password", "transformation", "supply password for --copy-encryption", R"(--encryption-file-password=password
172 180  
173 181 If the file named in --copy-encryption requires a password, use
174 182 this option to supply the password.
175 183 )");
176   -}
177   -static void add_help_3(QPDFArgParser& ap)
178   -{
179 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 185 editing. This is for editing the PDF code, not the page contents.
181 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 290 value, even if the file uses features that may not be available
286 291 in that version.
287 292 )");
  293 +}
  294 +static void add_help_4(QPDFArgParser& ap)
  295 +{
288 296 ap.addHelpTopic("page-ranges", "page range syntax", R"(A full description of the page range syntax, with examples, can be
289 297 found in the manual. In summary, a range is a comma-separated list
290 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 313 :even starts with the second page. These are odd and even pages
306 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 316 ap.addHelpTopic("modification", "change parts of the PDF", R"(Modification options make systematic changes to certain parts of
312 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 421 )");
417 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 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 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 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 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 647 qpdf does this automatically when needed.
643 648 )");
  649 +}
  650 +static void add_help_6(QPDFArgParser& ap)
  651 +{
644 652 ap.addOptionHelp("--force-R5", "encryption", "use unsupported R=5 encryption", R"(Use an undocumented, unsupported, deprecated encryption
645 653 algorithm that existed only in Acrobat version IX. This option
646 654 should not be used except for compatibility testing.
647 655 )");
648   -}
649   -static void add_help_6(QPDFArgParser& ap)
650   -{
651 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 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 832 be replaced by the new attachment. Otherwise, qpdf gives an
828 833 error if an attachment with that key is already present.
829 834 )");
  835 +}
  836 +static void add_help_7(QPDFArgParser& ap)
  837 +{
830 838 ap.addHelpTopic("copy-attachments", "copy attachments from another file", R"(The options listed below appear between --copy-attachments-from and
831 839 its terminating "--".
832 840  
833 841 To copy attachments from a password-protected file, use
834 842 the --password option after the file name.
835 843 )");
836   -}
837   -static void add_help_7(QPDFArgParser& ap)
838   -{
839 844 ap.addOptionHelp("--prefix", "copy-attachments", "key prefix for copying attachments", R"(--prefix=prefix
840 845  
841 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 925 Write the contents of the specified attachment to standard
921 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 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 934 ap.addOptionHelp("--json", "json", "show file in JSON format", R"(--json[=version]
930 935  
931 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 32 this->ap.addBare("copyright", b(&ArgParser::argCopyright));
33 33 this->ap.addBare("show-crypto", b(&ArgParser::argShowCrypto));
34 34 this->ap.addBare("job-json-help", b(&ArgParser::argJobJsonHelp));
  35 +this->ap.addBare("zopfli", b(&ArgParser::argZopfli));
35 36 this->ap.addChoices("json-help", p(&ArgParser::argJsonHelp), false, json_version_choices);
36 37 this->ap.selectMainOptionTable();
37 38 this->ap.addPositional(p(&ArgParser::argPositional));
... ...
libqpdf/qpdf/qpdf-config.h.in
... ... @@ -6,6 +6,7 @@
6 6 #cmakedefine USE_CRYPTO_OPENSSL 1
7 7 #cmakedefine USE_INSECURE_RANDOM 1
8 8 #cmakedefine SKIP_OS_SECURE_RANDOM 1
  9 +#cmakedefine ZOPFLI 1
9 10  
10 11 /* large file support -- may be needed for 32-bit systems */
11 12 #cmakedefine _FILE_OFFSET_BITS ${_FILE_OFFSET_BITS}
... ...
manual/cli.rst
... ... @@ -354,6 +354,21 @@ Related Options
354 354 :qpdf:ref:`--job-json-file`. For more information about QPDFJob,
355 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 372 .. _general-options:
358 373  
359 374 General Options
... ... @@ -936,7 +951,7 @@ Related Options
936 951  
937 952 As a special case, streams already compressed with ``/FlateDecode``
938 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 956 .. qpdf:option:: --stream-data=parameter
942 957  
... ... @@ -986,7 +1001,8 @@ Related Options
986 1001 tells :command:`qpdf` to uncompress and recompress streams
987 1002 compressed with flate. This can be useful when combined with
988 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 1007 .. qpdf:option:: --compression-level=level
992 1008  
... ... @@ -1009,7 +1025,8 @@ Related Options
1009 1025 :qpdf:ref:`--recompress-flate`. If your goal is to shrink the size
1010 1026 of PDF files, you should also use
1011 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 1031 .. qpdf:option:: --normalize-content=[y|n]
1015 1032  
... ... @@ -1734,7 +1751,7 @@ Related Options
1734 1751 and :qpdf:ref:`--oi-min-area` options. By default, inline images
1735 1752 are converted to regular images and optimized as well. Use
1736 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 1756 .. qpdf:option:: --oi-min-width=width
1740 1757  
... ... @@ -3941,3 +3958,86 @@ from password to encryption key entirely, allowing the raw
3941 3958 encryption key to be specified directly. That behavior is useful for
3942 3959 forensic purposes or for brute-force recovery of files with unknown
3943 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 30 <https://openssl.org/>`__ to be able to use the openssl crypto
31 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 36 The qpdf source tree includes a few automatically generated files. The
34 37 code generator uses Python 3. Automatic code generation is off by
35 38 default. For a discussion, refer to :ref:`build-options`.
... ... @@ -291,6 +294,10 @@ QTEST_COLOR
291 294 Turn this on or off to control whether qtest uses color in its
292 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 301 Options for Working on qpdf
295 302 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
296 303  
... ... @@ -648,6 +655,16 @@ Implementing the registration functions and internal storage of
648 655 registered providers was also easier using C++-11's functional
649 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 668 .. _autoconf-to-cmake:
652 669  
653 670 Converting From autoconf to cmake
... ...
manual/packaging.rst
... ... @@ -44,6 +44,15 @@ particularly useful to packagers.
44 44 11, this was a recommendation for packagers but was not done
45 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 56 .. _package-tests:
48 57  
49 58 Package Tests
... ...
manual/qpdf.1
... ... @@ -115,6 +115,12 @@ default provider is shown first.
115 115 .B --job-json-help \-\- show format of job JSON
116 116 Describe the format of the QPDFJob JSON input used by
117 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 124 .SH GENERAL (general options)
119 125 General options control qpdf's behavior in ways that are not
120 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 46 environments in which writing a binary file to standard output
47 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 54 11.9.1: June 7, 2024
50 55 - Bug Fixes
51 56  
... ...
qpdf/qtest/copy-foreign-objects.test
... ... @@ -51,8 +51,9 @@ $td-&gt;runtest(&quot;indirect filters&quot;,
51 51 foreach my $i (0, 1)
52 52 {
53 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 58 $td->runtest("issue 449",
58 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 21 {$td->STRING => "test 70 done\n", $td->EXIT_STATUS => 0},
22 22 $td->NORMALIZE_NEWLINES);
23 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 27 cleanup();
28 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 1 use File::Spec;
2 2  
3   -my $devNull = File::Spec->devnull();
  3 +my $dev_null = File::Spec->devnull();
4 4  
5 5 my $compare_images = 0;
6 6 if ((exists $ENV{'QPDF_TEST_COMPARE_IMAGES'}) &&
... ... @@ -95,7 +95,7 @@ sub compare_pdfs
95 95 $td->runtest("convert original file to image",
96 96 {$td->COMMAND =>
97 97 "(cd tif1;" .
98   - " gs 2>$devNull $x_gs_args" .
  98 + " gs 2>$dev_null $x_gs_args" .
99 99 " -q -dNOPAUSE -sDEVICE=tiff24nc" .
100 100 " -sOutputFile=a.tif - < ../$f1)"},
101 101 {$td->STRING => "",
... ... @@ -115,7 +115,7 @@ sub compare_pdfs
115 115 $td->runtest("convert new file to image",
116 116 {$td->COMMAND =>
117 117 "(cd tif2;" .
118   - " gs 2>$devNull $x_gs_args" .
  118 + " gs 2>$dev_null $x_gs_args" .
119 119 " -q -dNOPAUSE -sDEVICE=tiff24nc" .
120 120 " -sOutputFile=a.tif - < ../$f2)"},
121 121 {$td->STRING => "",
... ...
qpdf/qtest/split-pages.test
... ... @@ -172,8 +172,8 @@ $td-&gt;runtest(&quot;merge for compare&quot;,
172 172 " split-out-shared-form*.pdf -- a.pdf"},
173 173 {$td->STRING => "", $td->EXIT_STATUS => 0});
174 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 177 compare_pdfs($td, "shared-form-images.pdf", "a.pdf");
178 178  
179 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 28 {$td->FILE => "test8.out", $td->EXIT_STATUS => 0},
29 29 $td->NORMALIZE_NEWLINES);
30 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 33 $td->runtest("new streams",
34 34 {$td->COMMAND => "test_driver 9 minimal.pdf"},
35 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 21 }
22 22 close(F);
23 23  
  24 +my $dev_null = File::Spec->devnull();
  25 +my $n_tests = 9;
  26 +
24 27 foreach my $level ('', '=1', '=9')
25 28 {
26 29 my $f = $level;
... ... @@ -35,11 +38,18 @@ foreach my $level (&#39;&#39;, &#39;=1&#39;, &#39;=9&#39;)
35 38 {$td->FILE => "a.uncompressed", $td->EXIT_STATUS => 0});
36 39 }
37 40  
  41 +chomp(my $zopfli = `zlib-flate --_zopfli`);
38 42 my $size1 = (stat("a.=1"))[7];
39 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 54 $td->runtest("error",
45 55 {$td->COMMAND => "zlib-flate -uncompress < 1.uncompressed"},
... ... @@ -54,7 +64,40 @@ $td-&gt;runtest(&quot;corrupted input&quot;,
54 64 $td->EXIT_STATUS => 3},
55 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 102 cleanup();
60 103  
... ...
zlib-flate/zlib-flate.cc
... ... @@ -6,7 +6,6 @@
6 6 #include <cstdio>
7 7 #include <cstdlib>
8 8 #include <cstring>
9   -#include <fcntl.h>
10 9 #include <iostream>
11 10  
12 11 static char const* whoami = nullptr;
... ... @@ -50,21 +49,29 @@ main(int argc, char* argv[])
50 49 action = Pl_Flate::a_deflate;
51 50 int level = QUtil::string_to_int(argv[1] + 10);
52 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 58 } else {
54 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 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 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 75 unsigned char buf[10000];
69 76 bool done = false;
70 77 while (!done) {
... ...