diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 87ccd61..8fe8acd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -123,6 +123,13 @@ jobs: - uses: actions/checkout@v4 - name: 'Sanitizer Tests' run: build-scripts/test-sanitizers + Zopfli: + runs-on: ubuntu-latest + needs: Prebuild + steps: + - uses: actions/checkout@v4 + - name: 'Zopfli Tests' + run: build-scripts/test-zopfli CodeCov: runs-on: ubuntu-latest needs: Prebuild diff --git a/CMakeLists.txt b/CMakeLists.txt index 42468ff..975c8f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,6 +98,8 @@ set(DEFAULT_CRYPTO CACHE STRING "") option(DEFAULT_CRYPTO "Specify default crypto; otherwise chosen automatically" "") +option(ZOPFLI, "Use zopfli for zlib-compatible compression") + # INSTALL_MANUAL is not dependent on building docs. When creating some # distributions, we build the doc in one run, copy doc-dist in, and # install it elsewhere. diff --git a/ChangeLog b/ChangeLog index 3a7b5a7..76d66af 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,8 @@ 2025-02-02 Jay Berkenbilt + * Add support for the zopfli compression library. See manual for + details. Fixes #1323. + * Have fix-qdf accept a second argument, interpreted as the output file. Fixes #1330. diff --git a/README-maintainer.md b/README-maintainer.md index 2d4fb50..c1657d0 100644 --- a/README-maintainer.md +++ b/README-maintainer.md @@ -67,6 +67,10 @@ Note that, in early 2024, branch coverage information is not very accurate with Memory checks: +Note: if clang++ fails to create output, it may be necessary to install a specific version of +libstdc++-dev. For example, with clang++ version 20 on Ubuntu 24.04, `clang++ -v` indicates the +selected GCC installation is 14, so it is necessary to install `libstdc++-14-dev`. + ``` CFLAGS="-fsanitize=address -fsanitize=undefined" \ CXXFLAGS="-fsanitize=address -fsanitize=undefined" \ @@ -298,8 +302,8 @@ Building docs from pull requests is also enabled. ## ZLIB COMPATIBILITY The qpdf test suite is designed to be independent of the output of any -particular version of zlib. There are several strategies to make this -work: +particular version of zlib. (See also `ZOPFLI` in README.md.) There +are several strategies to make this work: * `build-scripts/test-alt-zlib` runs in CI and runs the test suite with a non-default zlib. Please refer to that code for an example of diff --git a/README.md b/README.md index deda9f6..71f5a32 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,16 @@ below. Detailed information appears in the [manual](https://qpdf.readthedocs.io/en/latest/installation.html). +## Zopfli + +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. + +The environment variable `QPDF_ZOPFLI` can be set to the following values: +* `disabled` (or unset): do not use zopfli +* `force`: use zopfli; fail if zopfli is not compiled in +* `silent`: use zopfli if available; otherwise silently fall back to zlib +* any other value: use zopfli if available, and warn if not + # Licensing terms of embedded software qpdf makes use of zlib and jpeg libraries for its functionality. These packages can be downloaded separately from their diff --git a/build-scripts/test-zopfli b/build-scripts/test-zopfli new file mode 100755 index 0000000..9e00608 --- /dev/null +++ b/build-scripts/test-zopfli @@ -0,0 +1,27 @@ +#!/bin/bash +set -eo pipefail +sudo apt-get update +sudo apt-get -y install \ + build-essential cmake \ + zlib1g-dev libjpeg-dev libgnutls28-dev libssl-dev \ + libzopfli-dev + +cmake -S . -B build \ + -DCI_MODE=1 -DBUILD_STATIC_LIBS=0 -DCMAKE_BUILD_TYPE=Release \ + -DREQUIRE_CRYPTO_OPENSSL=1 -DREQUIRE_CRYPTO_GNUTLS=1 \ + -DENABLE_QTC=1 -DZOPFLI=1 +cmake --build build --verbose -j$(nproc) -- -k + +# Make sure we are using zopfli +export QPDF_ZOPFLI=force +zopfli="$(./build/zlib-flate/zlib-flate --_zopfli)" +if [ "$zopfli" != "11" ]; then + echo "zopfli is not working" + exit 2 +fi + +# If this fails, please see ZLIB COMPATIBILITY in README-maintainer.md. +# The tests are very slow with this option. Just run essential tests. +# If zlib-flate and qpdf tests all pass, we can be pretty sure it works. +(cd build; ctest --verbose -R zlib-flate) +(cd build; ctest --verbose -R qpdf) diff --git a/include/qpdf/Pl_Flate.hh b/include/qpdf/Pl_Flate.hh index c29f461..322d461 100644 --- a/include/qpdf/Pl_Flate.hh +++ b/include/qpdf/Pl_Flate.hh @@ -24,8 +24,10 @@ #define PL_FLATE_HH #include +#include #include #include +#include class QPDF_DLL_CLASS Pl_Flate: public Pipeline { @@ -65,6 +67,23 @@ class QPDF_DLL_CLASS Pl_Flate: public Pipeline QPDF_DLL void setWarnCallback(std::function callback); + // Returns true if qpdf was built with zopfli support. + QPDF_DLL + static bool zopfli_supported(); + + // Returns true if zopfli is enabled. Zopfli is enabled if QPDF_ZOPFLI is set to a value other + // than "disabled" and zopfli support is compiled in. + QPDF_DLL + static bool zopfli_enabled(); + + // If zopfli is supported, returns true. Otherwise, check the QPDF_ZOPFLI + // environment variable as follows: + // - "disabled" or "silent": return true + // - "force": qpdf_exit_error, throw an exception + // - Any other value: issue a warning, and return false + QPDF_DLL + static bool zopfli_check_env(QPDFLogger* logger = nullptr); + private: QPDF_DLL_PRIVATE void handleData(unsigned char const* data, size_t len, int flush); @@ -72,6 +91,8 @@ class QPDF_DLL_CLASS Pl_Flate: public Pipeline void checkError(char const* prefix, int error_code); QPDF_DLL_PRIVATE void warn(char const*, int error_code); + QPDF_DLL_PRIVATE + void finish_zopfli(); QPDF_DLL_PRIVATE static int compression_level; @@ -95,6 +116,7 @@ class QPDF_DLL_CLASS Pl_Flate: public Pipeline void* zdata; unsigned long long written{0}; std::function callback; + std::unique_ptr zopfli_buf; }; std::shared_ptr m; diff --git a/job.sums b/job.sums index 1a9b422..d2a6dd5 100644 --- a/job.sums +++ b/job.sums @@ -1,5 +1,5 @@ # Generated by generate_auto_job -CMakeLists.txt 4aaa3d5df1713d9e3b9c6778101c6af3efa2131a2f4c069095abee269d5eaccc +CMakeLists.txt af74c05aea88512ef9a37a0708c2a03747a4ff23dc7919075c8d3b62b73aad79 generate_auto_job f64733b79dcee5a0e3e8ccc6976448e8ddf0e8b6529987a66a7d3ab2ebc10a86 include/qpdf/auto_job_c_att.hh 4c2b171ea00531db54720bf49a43f8b34481586ae7fb6cbf225099ee42bc5bb4 include/qpdf/auto_job_c_copy_att.hh 50609012bff14fd82f0649185940d617d05d530cdc522185c7f3920a561ccb42 @@ -7,14 +7,14 @@ include/qpdf/auto_job_c_enc.hh 28446f3c32153a52afa239ea40503e6cc8ac2c026813526a3 include/qpdf/auto_job_c_main.hh 84f463237235b2c095b747a4f5dd00f109ee596a1c207b944efb296c0c568cae include/qpdf/auto_job_c_pages.hh 09ca15649cc94fdaf6d9bdae28a20723f2a66616bf15aa86d83df31051d82506 include/qpdf/auto_job_c_uo.hh 9c2f98a355858dd54d0bba444b73177a59c9e56833e02fa6406f429c07f39e62 -job.yml 31935064eca625af7657b23f2f12c614d14751ec0b12702482b1768a04905d22 -libqpdf/qpdf/auto_job_decl.hh 20d6affe1e260f5a1af4f1d82a820b933835440ff03020e877382da2e8dac6c6 -libqpdf/qpdf/auto_job_help.hh 9628a4b3f57ed8ecda3c7b8761b4daa48eaf9da0a6a0fc68bf417c467bd737eb -libqpdf/qpdf/auto_job_init.hh e2a6bb87870c5522a01b15461c9fe909e360f5c7fed06e41acf13a125bd1d03e +job.yml 2c424c7be0c02545191969e849e1d8f7fdb4ab65bbf799b9a190e21343899751 +libqpdf/qpdf/auto_job_decl.hh 34ba07d3891c3e5cdd8712f991e508a0652c9db314c5d5bcdf4421b76e6f6e01 +libqpdf/qpdf/auto_job_help.hh a36476d0c823033b2af0e4170651e1fa31173887c310f2f208e9ed7e6e36a2ce +libqpdf/qpdf/auto_job_init.hh f89e7f9950a185372732d2ff7f113161f275f45ee7937dd7fd37e38013bf22e7 libqpdf/qpdf/auto_job_json_decl.hh 843892c8e8652a86b7eb573893ef24050b7f36fe313f7251874be5cd4cdbe3fd libqpdf/qpdf/auto_job_json_init.hh 344c2fb473f88fe829c93b1efe6c70a0e4796537b8eb35e421d955fff481ba7d libqpdf/qpdf/auto_job_schema.hh 6d3eef5137b8828eaa301a1b3cf75cb7bb812aa6e2d8301de865b42d238d7a7c manual/_ext/qpdf.py 6add6321666031d55ed4aedf7c00e5662bba856dfcd66ccb526563bffefbb580 -manual/cli.rst 45629c81bb407e7a1d2302ff1a9ef87f706565904aa5c21e64adefb34eee575c -manual/qpdf.1 e058bd97a2bbc1e39282c709fb3beb1c3f8d3e2372b2974e11a849fd0bfb3505 +manual/cli.rst 67357688f9a52fafa9a4f231fe4ce74c3cd8977130da7501efe54439a1ee22d4 +manual/qpdf.1 cf5fc00789744c619f2af285fd715e5f85ced53f0126f8df5f97e27f920a9a7a manual/qpdf.1.in 436ecc85d45c4c9e2dbd1725fb7f0177fb627179469f114561adf3cb6cbb677b diff --git a/job.yml b/job.yml index 13b61a4..b172086 100644 --- a/job.yml +++ b/job.yml @@ -81,6 +81,7 @@ options: - copyright - show-crypto - job-json-help + - zopfli optional_choices: json-help: json_version - table: main diff --git a/libqpdf/CMakeLists.txt b/libqpdf/CMakeLists.txt index 50ada14..2cfeb65 100644 --- a/libqpdf/CMakeLists.txt +++ b/libqpdf/CMakeLists.txt @@ -190,6 +190,18 @@ if(NOT EXTERNAL_LIBS) endif() endif() +if(ZOPFLI) + find_path(ZOPFLI_H_PATH zopfli/zopfli.h) + find_library(ZOPFLI_LIB_PATH NAMES zopfli) + if(ZOPFLI_H_PATH AND ZOPFLI_LIB_PATH) + list(APPEND dep_include_directories ${ZOPFLI_H_PATH}) + list(APPEND dep_link_libraries ${ZOPFLI_LIB_PATH}) + else() + message(SEND_ERROR "zopfli not found") + set(ANYTHING_MISSING 1) + endif() +endif() + # Update JPEG_INCLUDE in PARENT_SCOPE after we have finished setting it. set(JPEG_INCLUDE ${JPEG_INCLUDE} PARENT_SCOPE) diff --git a/libqpdf/Pl_Flate.cc b/libqpdf/Pl_Flate.cc index 78a9b47..6e690cb 100644 --- a/libqpdf/Pl_Flate.cc +++ b/libqpdf/Pl_Flate.cc @@ -6,6 +6,11 @@ #include #include +#include + +#ifdef ZOPFLI +# include +#endif namespace { @@ -39,6 +44,10 @@ Pl_Flate::Members::Members(size_t out_bufsize, action_e action) : zstream.avail_in = 0; zstream.next_out = this->outbuf.get(); zstream.avail_out = QIntC::to_uint(out_bufsize); + + if (action == a_deflate && Pl_Flate::zopfli_enabled()) { + zopfli_buf = std::make_unique(); + } } Pl_Flate::Members::~Members() @@ -59,7 +68,7 @@ Pl_Flate::Members::~Members() Pl_Flate::Pl_Flate( char const* identifier, Pipeline* next, action_e action, unsigned int out_bufsize_int) : Pipeline(identifier, next), - m(new Members(QIntC::to_size(out_bufsize_int), action)) + m(std::shared_ptr(new Members(QIntC::to_size(out_bufsize_int), action))) { if (!next) { 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) throw std::logic_error( this->identifier + ": Pl_Flate: write() called after finish() called"); } + if (m->zopfli_buf) { + m->zopfli_buf->append(reinterpret_cast(data), len); + return; + } // Write in chunks in case len is too big to fit in an int. Assume int is at least 32 bits. static size_t const max_bytes = 1 << 30; @@ -211,7 +224,9 @@ Pl_Flate::finish() throw std::runtime_error("PL_Flate memory limit exceeded"); } try { - if (m->outbuf.get()) { + if (m->zopfli_buf) { + finish_zopfli(); + } else if (m->outbuf.get()) { if (m->initialized) { z_stream& zstream = *(static_cast(m->zdata)); unsigned char buf[1]; @@ -291,3 +306,76 @@ Pl_Flate::checkError(char const* prefix, int error_code) throw std::runtime_error(msg); } } + +void +Pl_Flate::finish_zopfli() +{ +#ifdef ZOPFLI + if (!m->zopfli_buf) { + return; + } + auto buf = std::move(*m->zopfli_buf.release()); + ZopfliOptions z_opt; + ZopfliInitOptions(&z_opt); + unsigned char* out{nullptr}; + size_t out_size{0}; + ZopfliCompress( + &z_opt, + ZOPFLI_FORMAT_ZLIB, + reinterpret_cast(buf.c_str()), + buf.size(), + &out, + &out_size); + std::unique_ptr p(out, &free); + next()->write(out, out_size); + // next()->finish is called by finish() +#endif +} + +bool +Pl_Flate::zopfli_supported() +{ +#ifdef ZOPFLI + return true; +#else + return false; +#endif +} + +bool +Pl_Flate::zopfli_enabled() +{ + if (zopfli_supported()) { + std::string value; + static bool enabled = QUtil::get_env("QPDF_ZOPFLI", &value) && value != "disabled"; + return enabled; + } else { + return false; + } +} + +bool +Pl_Flate::zopfli_check_env(QPDFLogger* logger) +{ + if (Pl_Flate::zopfli_supported()) { + return true; + } + std::string value; + auto is_set = QUtil::get_env("QPDF_ZOPFLI", &value); + if (!is_set || value == "disabled" || value == "silent") { + return true; + } + if (!logger) { + logger = QPDFLogger::defaultLogger().get(); + } + + // This behavior is known in QPDFJob (for the --zopfli argument), Pl_Flate.hh, README.md, + // and the manual. Do a case-insensitive search for zopfli if changing the behavior. + if (value == "force") { + throw std::runtime_error("QPDF_ZOPFLI=force, and zopfli support is not enabled"); + } + logger->warn("QPDF_ZOPFLI is set, but libqpdf was not built with zopfli support\n"); + logger->warn( + "Set QPDF_ZOPFLI=silent to suppress this warning and use zopfli when available.\n"); + return false; +} diff --git a/libqpdf/QPDFJob.cc b/libqpdf/QPDFJob.cc index 9ed1685..a8d6541 100644 --- a/libqpdf/QPDFJob.cc +++ b/libqpdf/QPDFJob.cc @@ -498,6 +498,11 @@ QPDFJob::createQPDF() void QPDFJob::writeQPDF(QPDF& pdf) { + if (createsOutput()) { + if (!Pl_Flate::zopfli_check_env(pdf.getLogger().get())) { + m->warnings = true; + } + } if (!createsOutput()) { doInspection(pdf); } else if (m->split_pages) { diff --git a/libqpdf/QPDFJob_argv.cc b/libqpdf/QPDFJob_argv.cc index 829e7aa..9aeabc0 100644 --- a/libqpdf/QPDFJob_argv.cc +++ b/libqpdf/QPDFJob_argv.cc @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -105,6 +106,27 @@ ArgParser::argVersion() } void +ArgParser::argZopfli() +{ + auto logger = QPDFLogger::defaultLogger(); + if (Pl_Flate::zopfli_supported()) { + if (Pl_Flate::zopfli_enabled()) { + logger->info("zopfli support is enabled, and zopfli is active\n"); + } else { + logger->info("zopfli support is enabled but not active\n"); + logger->info("Set the environment variable QPDF_ZOPFLI to activate.\n"); + logger->info("* QPDF_ZOPFLI=disabled or QPDF_ZOPFLI not set: don't use zopfli.\n"); + logger->info("* QPDF_ZOPFLI=force: use zopfli, and fail if not available.\n"); + logger->info("* QPDF_ZOPFLI=silent: use zopfli if available and silently fall back if not.\n"); + logger->info("* QPDF_ZOPFLI= any other value: use zopfli if available, and warn if not.\n"); + } + } else { + logger->error("zopfli support is not enabled\n"); + std::exit(qpdf_exit_error); + } +} + +void ArgParser::argCopyright() { // clang-format off diff --git a/libqpdf/qpdf/auto_job_decl.hh b/libqpdf/qpdf/auto_job_decl.hh index d70af25..6016509 100644 --- a/libqpdf/qpdf/auto_job_decl.hh +++ b/libqpdf/qpdf/auto_job_decl.hh @@ -19,6 +19,7 @@ void argVersion(); void argCopyright(); void argShowCrypto(); void argJobJsonHelp(); +void argZopfli(); void argJsonHelp(std::string const&); void argPositional(std::string const&); void argAddAttachment(); diff --git a/libqpdf/qpdf/auto_job_help.hh b/libqpdf/qpdf/auto_job_help.hh index f345ce7..8d91db8 100644 --- a/libqpdf/qpdf/auto_job_help.hh +++ b/libqpdf/qpdf/auto_job_help.hh @@ -73,6 +73,11 @@ default provider is shown first. ap.addOptionHelp("--job-json-help", "help", "show format of job JSON", R"(Describe the format of the QPDFJob JSON input used by --job-json-file. )"); +ap.addOptionHelp("--zopfli", "help", "indicate whether zopfli is enabled and active", R"(If zopfli support is compiled in, indicate whether it is active, +and exit normally. Otherwise, indicate that it is not compiled +in, and exit with an error code. If zopfli is compiled in, +activate it by setting the ``QPDF_ZOPFLI`` environment variable. +)"); ap.addHelpTopic("general", "general options", R"(General options control qpdf's behavior in ways that are not directly related to the operation it is performing. )"); @@ -86,13 +91,13 @@ ap.addOptionHelp("--password-file", "general", "read password from a file", R"(- The first line of the specified file is used as the password. This is used in place of the --password option. )"); +} +static void add_help_2(QPDFArgParser& ap) +{ ap.addOptionHelp("--verbose", "general", "print additional information", R"(Output additional information about various things qpdf is doing, including information about files created and operations performed. )"); -} -static void add_help_2(QPDFArgParser& ap) -{ ap.addOptionHelp("--progress", "general", "show progress when writing", R"(Indicate progress when writing files. )"); 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 preserving the input file's encryption. Use --encryption-file-password to specify the encryption file's password. )"); +} +static void add_help_3(QPDFArgParser& ap) +{ ap.addOptionHelp("--encryption-file-password", "transformation", "supply password for --copy-encryption", R"(--encryption-file-password=password If the file named in --copy-encryption requires a password, use this option to supply the password. )"); -} -static void add_help_3(QPDFArgParser& ap) -{ 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 editing. This is for editing the PDF code, not the page contents. All streams that can be uncompressed are uncompressed, and @@ -285,6 +290,9 @@ Force the output PDF file's PDF version header to be the specified value, even if the file uses features that may not be available in that version. )"); +} +static void add_help_4(QPDFArgParser& ap) +{ ap.addHelpTopic("page-ranges", "page range syntax", R"(A full description of the page range syntax, with examples, can be found in the manual. In summary, a range is a comma-separated list 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 :even starts with the second page. These are odd and even pages from the resulting set, not based on the original page numbers. )"); -} -static void add_help_4(QPDFArgParser& ap) -{ ap.addHelpTopic("modification", "change parts of the PDF", R"(Modification options make systematic changes to certain parts of the PDF, causing the PDF to render differently from the original. )"); @@ -416,11 +421,11 @@ ap.addOptionHelp("--keep-inline-images", "modification", "exclude inline images )"); ap.addOptionHelp("--remove-info", "modification", "remove file information", R"(Exclude file information (except modification date) from the output file. )"); -ap.addOptionHelp("--remove-metadata", "modification", "remove metadata", R"(Exclude metadata from the output file. -)"); } static void add_help_5(QPDFArgParser& ap) { +ap.addOptionHelp("--remove-metadata", "modification", "remove metadata", R"(Exclude metadata from the output file. +)"); ap.addOptionHelp("--remove-page-labels", "modification", "remove explicit page numbers", R"(Exclude page labels (explicit page numbers) from the output file. )"); 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. ap.addOptionHelp("--force-V4", "encryption", "force V=4 in encryption dictionary", R"(This option is for testing and is never needed in practice since qpdf does this automatically when needed. )"); +} +static void add_help_6(QPDFArgParser& ap) +{ ap.addOptionHelp("--force-R5", "encryption", "use unsupported R=5 encryption", R"(Use an undocumented, unsupported, deprecated encryption algorithm that existed only in Acrobat version IX. This option should not be used except for compatibility testing. )"); -} -static void add_help_6(QPDFArgParser& ap) -{ ap.addHelpTopic("page-selection", "select pages from one or more files", R"(Use the --pages option to select pages from multiple files. Usage: qpdf in.pdf --pages --file=input-file \ @@ -827,15 +832,15 @@ ap.addOptionHelp("--replace", "add-attachment", "replace attachment with same ke be replaced by the new attachment. Otherwise, qpdf gives an error if an attachment with that key is already present. )"); +} +static void add_help_7(QPDFArgParser& ap) +{ ap.addHelpTopic("copy-attachments", "copy attachments from another file", R"(The options listed below appear between --copy-attachments-from and its terminating "--". To copy attachments from a password-protected file, use the --password option after the file name. )"); -} -static void add_help_7(QPDFArgParser& ap) -{ ap.addOptionHelp("--prefix", "copy-attachments", "key prefix for copying attachments", R"(--prefix=prefix Prepend a prefix to each key; may be needed if there are @@ -920,12 +925,12 @@ ap.addOptionHelp("--show-attachment", "inspection", "export an embedded file", R Write the contents of the specified attachment to standard output as binary data. Get the key with --list-attachments. )"); -ap.addHelpTopic("json", "JSON output for PDF information", R"(Show information about the PDF file in JSON format. Please see the -JSON chapter in the qpdf manual for details. -)"); } static void add_help_8(QPDFArgParser& ap) { +ap.addHelpTopic("json", "JSON output for PDF information", R"(Show information about the PDF file in JSON format. Please see the +JSON chapter in the qpdf manual for details. +)"); ap.addOptionHelp("--json", "json", "show file in JSON format", R"(--json[=version] Generate a JSON representation of the file. This is described in diff --git a/libqpdf/qpdf/auto_job_init.hh b/libqpdf/qpdf/auto_job_init.hh index 5db1131..8307e46 100644 --- a/libqpdf/qpdf/auto_job_init.hh +++ b/libqpdf/qpdf/auto_job_init.hh @@ -32,6 +32,7 @@ this->ap.addBare("version", b(&ArgParser::argVersion)); this->ap.addBare("copyright", b(&ArgParser::argCopyright)); this->ap.addBare("show-crypto", b(&ArgParser::argShowCrypto)); this->ap.addBare("job-json-help", b(&ArgParser::argJobJsonHelp)); +this->ap.addBare("zopfli", b(&ArgParser::argZopfli)); this->ap.addChoices("json-help", p(&ArgParser::argJsonHelp), false, json_version_choices); this->ap.selectMainOptionTable(); this->ap.addPositional(p(&ArgParser::argPositional)); diff --git a/libqpdf/qpdf/qpdf-config.h.in b/libqpdf/qpdf/qpdf-config.h.in index ff05478..7e779f0 100644 --- a/libqpdf/qpdf/qpdf-config.h.in +++ b/libqpdf/qpdf/qpdf-config.h.in @@ -6,6 +6,7 @@ #cmakedefine USE_CRYPTO_OPENSSL 1 #cmakedefine USE_INSECURE_RANDOM 1 #cmakedefine SKIP_OS_SECURE_RANDOM 1 +#cmakedefine ZOPFLI 1 /* large file support -- may be needed for 32-bit systems */ #cmakedefine _FILE_OFFSET_BITS ${_FILE_OFFSET_BITS} diff --git a/manual/cli.rst b/manual/cli.rst index 44c0953..c1c20b2 100644 --- a/manual/cli.rst +++ b/manual/cli.rst @@ -354,6 +354,21 @@ Related Options :qpdf:ref:`--job-json-file`. For more information about QPDFJob, see :ref:`qpdf-job`. +.. qpdf:option:: --zopfli + + .. help: indicate whether zopfli is enabled and active + + If zopfli support is compiled in, indicate whether it is active, + and exit normally. Otherwise, indicate that it is not compiled + in, and exit with an error code. If zopfli is compiled in, + activate it by setting the ``QPDF_ZOPFLI`` environment variable. + + If zopfli support is compiled in, indicate whether it is active, + and exit normally. Otherwise, indicate that it is not compiled in, + and exit with an error code. If zopfli is compiled in, activate it + by setting the ``QPDF_ZOPFLI`` environment variable. See + :ref:`zopfli`. + .. _general-options: General Options @@ -936,7 +951,7 @@ Related Options As a special case, streams already compressed with ``/FlateDecode`` are not uncompressed and recompressed. You can change this behavior - with :qpdf:ref:`--recompress-flate`. + with :qpdf:ref:`--recompress-flate`. See also :ref:`small-files`. .. qpdf:option:: --stream-data=parameter @@ -986,7 +1001,8 @@ Related Options tells :command:`qpdf` to uncompress and recompress streams compressed with flate. This can be useful when combined with :qpdf:ref:`--compression-level`. Using this option may make - :command:`qpdf` much slower when writing output files. + :command:`qpdf` much slower when writing output files. See also + :ref:`small-files`. .. qpdf:option:: --compression-level=level @@ -1009,7 +1025,8 @@ Related Options :qpdf:ref:`--recompress-flate`. If your goal is to shrink the size of PDF files, you should also use :samp:`--object-streams=generate`. If you omit this option, qpdf - defers to the compression library's default behavior. + defers to the compression library's default behavior. See also + :ref:`small-files`. .. qpdf:option:: --normalize-content=[y|n] @@ -1734,7 +1751,7 @@ Related Options and :qpdf:ref:`--oi-min-area` options. By default, inline images are converted to regular images and optimized as well. Use :qpdf:ref:`--keep-inline-images` to prevent inline images from - being included. + being included. See also :ref:`small-files`. .. qpdf:option:: --oi-min-width=width @@ -3941,3 +3958,86 @@ from password to encryption key entirely, allowing the raw encryption key to be specified directly. That behavior is useful for forensic purposes or for brute-force recovery of files with unknown passwords and has nothing to do with the document's actual passwords. + +.. _small-files: + +Optimizing File Size +-------------------- + +While qpdf's primary function is not to optimize the size of PDF +files, there are a number of things you can do to make files smaller. +Note that qpdf will not resample images or make optimizations that +modify content with the exception of possibly recompressing images +using DCT (JPEG) compression. + +The following options will give you the smallest files that qpdf can +generate: + +- ``--compress-streams=y``: make sure streams are compressed (see + :qpdf:ref:`--compress-streams`) + +- ``--decode-level=generalized``: apply any non-specialized filters + (see :qpdf:ref:`--decode-level`) + +- :qpdf:ref:`--recompress-flate`: uncompress and recompress streams + that are already compressed with zlib (flate) compression + +- ``--compression-level=9``: use the highest possible compression + level (see :ref:`zopfli` and :qpdf:ref:`--compression-level`) + +- :qpdf:ref:`--optimize-images`: replace non-JPEG images with JPEG if + doing so reduces their size. Not all types of images are supported, + but qpdf will only keep the result if it is supported and reduces + the size. Images are not resampled, but bear in mind that JPEG is + lossy, so images may have artifacts. These are not usually + noticeable to the casual observer. + +- ``--object-streams=generate``: generate object streams, which means + that more of the PDF file's structural content will be compressed + (see :qpdf:ref:`--object-streams`) + +.. _zopfli: + +Zopfli Compression Algorithm +---------------------------- + +If qpdf is built with `zopfli `__ +support (see :ref:`build-zopfli`), you can have qpdf use the Zopfli +Compression Algorithm in place of zlib. In this mode, qpdf is *much +slower*, but produces slightly smaller compressed output. (According +to their documentation, zopfli is about 100 times slower than zlib and +produces output that's about 5% better than the best compression +available with other libraries.) For this to be useful, you should run +qpdf with options to recompress compressed streams. See +:ref:`small-files` for tips. In order to use zopfli, in addition to +building with zopfli support, you must set the ``QPDF_ZOPFLI`` +environment variable to some value other than ``disabled``. Note that +:qpdf:ref:`--compression-level` has no effect when zopfli is in use, +since zopfli always optimizes for size over everything else. + +Here are the supported values for ``QPDF_ZOPFLI``: + +.. list-table:: ``QPDF_ZOPFLI`` values + :widths: 20 60 + :header-rows: 0 + + - - ``disabled`` or unset + - do not use zopfli even if available + + - - ``silent`` + - use zopfli if available; otherwise silently fall back to zlib + + - - ``force`` + - use zopfli if available; fail with an error if not available + + - - any other value + - use zopfli if available; otherwise issue a warning and fall + back to zlib + +Note that the warning and error behavior are managed in ``QPDFJob`` +and affect the ``qpdf`` executable. For code that directly uses the +qpdf library, the behavior is that zopfli is enabled with any value +other than ``disabled`` but silently falls back to zlib. If you want +your application to behave the same as the ``qpdf`` executable with +respect to zopfli, you can call ``Pl_Flate::zopfli_check_env()``. See +its documentation in the ``qpdf/Pl_Flate.hh`` header file. diff --git a/manual/installation.rst b/manual/installation.rst index fefed72..a19b210 100644 --- a/manual/installation.rst +++ b/manual/installation.rst @@ -30,6 +30,9 @@ Basic Dependencies `__ to be able to use the openssl crypto provider +- If the ``ZOPFLI`` build option is specified (off by default), the + `zopfli `__ library. + The qpdf source tree includes a few automatically generated files. The code generator uses Python 3. Automatic code generation is off by default. For a discussion, refer to :ref:`build-options`. @@ -291,6 +294,10 @@ QTEST_COLOR Turn this on or off to control whether qtest uses color in its output. +ZOPFLI + Use the `zopfli `__ library for + zlib-compatible compression. See :ref:`zopfli`. + Options for Working on qpdf ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -648,6 +655,16 @@ Implementing the registration functions and internal storage of registered providers was also easier using C++-11's functional interfaces, which was another reason to require C++-11 at this time. +.. _build-zopfli: + +Building with zopfli support +---------------------------- + +If you compile with ``-DZOPFLI-ON`` and have the `zopfli +`__ development files available, +qpdf will be built with zopfli support. See :ref:`zopfli` for +information about using zopfli with qpdf. + .. _autoconf-to-cmake: Converting From autoconf to cmake diff --git a/manual/packaging.rst b/manual/packaging.rst index ca32554..740be24 100644 --- a/manual/packaging.rst +++ b/manual/packaging.rst @@ -44,6 +44,15 @@ particularly useful to packagers. 11, this was a recommendation for packagers but was not done automatically. +- Starting with qpdf 11.10, qpdf can be built with zopfli support (see + :ref:`build-zopfli`). It is recommended not to build qpdf with zopfli + for distributions since it adds zopfli as a dependency, and this + library is less widely used that qpdf's other dependencies. Users + who want that probably know they want it, and they can compile from + source. Note that, per zopfli's own documentation, zopfli is about + 100 times slower than zlib and produces compression output about 5% + smaller. + .. _package-tests: Package Tests diff --git a/manual/qpdf.1 b/manual/qpdf.1 index 70f9f55..e6f2d7e 100644 --- a/manual/qpdf.1 +++ b/manual/qpdf.1 @@ -115,6 +115,12 @@ default provider is shown first. .B --job-json-help \-\- show format of job JSON Describe the format of the QPDFJob JSON input used by --job-json-file. +.TP +.B --zopfli \-\- indicate whether zopfli is enabled and active +If zopfli support is compiled in, indicate whether it is active, +and exit normally. Otherwise, indicate that it is not compiled +in, and exit with an error code. If zopfli is compiled in, +activate it by setting the ``QPDF_ZOPFLI`` environment variable. .SH GENERAL (general options) General options control qpdf's behavior in ways that are not directly related to the operation it is performing. diff --git a/manual/release-notes.rst b/manual/release-notes.rst index 26fc535..d011403 100644 --- a/manual/release-notes.rst +++ b/manual/release-notes.rst @@ -46,6 +46,11 @@ Planned changes for future 12.x (subject to change): environments in which writing a binary file to standard output doesn't work (such as PowerShell 5). + - Library Enhancements + + - qpdf can now be built with zopfli support. For details, see + :ref:`zopfli`. + 11.9.1: June 7, 2024 - Bug Fixes diff --git a/qpdf/qtest/copy-foreign-objects.test b/qpdf/qtest/copy-foreign-objects.test index 73f50e0..1485ea0 100644 --- a/qpdf/qtest/copy-foreign-objects.test +++ b/qpdf/qtest/copy-foreign-objects.test @@ -51,8 +51,9 @@ $td->runtest("indirect filters", foreach my $i (0, 1) { $td->runtest("check output", - {$td->FILE => "auto-$i.pdf"}, - {$td->FILE => "indirect-filter-out-$i.pdf"}); + {$td->COMMAND => + "qpdf-test-compare auto-$i.pdf indirect-filter-out-$i.pdf"}, + {$td->FILE => "indirect-filter-out-$i.pdf", $td->EXIT_STATUS => 0}); } $td->runtest("issue 449", {$td->COMMAND => "test_driver 69 issue-449.pdf"}, diff --git a/qpdf/qtest/disable-filter-on-write.test b/qpdf/qtest/disable-filter-on-write.test index 0192895..552d035 100644 --- a/qpdf/qtest/disable-filter-on-write.test +++ b/qpdf/qtest/disable-filter-on-write.test @@ -21,8 +21,8 @@ $td->runtest("no filter on write", {$td->STRING => "test 70 done\n", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); $td->runtest("check output", - {$td->FILE => "a.pdf"}, - {$td->FILE => "filter-on-write-out.pdf"}); + {$td->COMMAND => "qpdf-test-compare a.pdf filter-on-write-out.pdf"}, + {$td->FILE => "filter-on-write-out.pdf", $td->EXIT_STATUS => 0}); cleanup(); $td->report($n_tests); diff --git a/qpdf/qtest/qpdf/minimal-out.pdf b/qpdf/qtest/qpdf/minimal-out.pdf new file mode 100644 index 0000000..b2c4c2c --- /dev/null +++ b/qpdf/qtest/qpdf/minimal-out.pdf diff --git a/qpdf/qtest/qpdf/zopfli-warning.out b/qpdf/qtest/qpdf/zopfli-warning.out new file mode 100644 index 0000000..c56e18c --- /dev/null +++ b/qpdf/qtest/qpdf/zopfli-warning.out @@ -0,0 +1,3 @@ +QPDF_ZOPFLI is set, but libqpdf was not built with zopfli support +Set QPDF_ZOPFLI=silent to suppress this warning and use zopfli when available. +qpdf: operation succeeded with warnings; resulting file may have some problems diff --git a/qpdf/qtest/qpdf_test_helpers.pm b/qpdf/qtest/qpdf_test_helpers.pm index f1e4a93..946cf49 100644 --- a/qpdf/qtest/qpdf_test_helpers.pm +++ b/qpdf/qtest/qpdf_test_helpers.pm @@ -1,6 +1,6 @@ use File::Spec; -my $devNull = File::Spec->devnull(); +my $dev_null = File::Spec->devnull(); my $compare_images = 0; if ((exists $ENV{'QPDF_TEST_COMPARE_IMAGES'}) && @@ -95,7 +95,7 @@ sub compare_pdfs $td->runtest("convert original file to image", {$td->COMMAND => "(cd tif1;" . - " gs 2>$devNull $x_gs_args" . + " gs 2>$dev_null $x_gs_args" . " -q -dNOPAUSE -sDEVICE=tiff24nc" . " -sOutputFile=a.tif - < ../$f1)"}, {$td->STRING => "", @@ -115,7 +115,7 @@ sub compare_pdfs $td->runtest("convert new file to image", {$td->COMMAND => "(cd tif2;" . - " gs 2>$devNull $x_gs_args" . + " gs 2>$dev_null $x_gs_args" . " -q -dNOPAUSE -sDEVICE=tiff24nc" . " -sOutputFile=a.tif - < ../$f2)"}, {$td->STRING => "", diff --git a/qpdf/qtest/split-pages.test b/qpdf/qtest/split-pages.test index 455a4ed..ac2d777 100644 --- a/qpdf/qtest/split-pages.test +++ b/qpdf/qtest/split-pages.test @@ -172,8 +172,8 @@ $td->runtest("merge for compare", " split-out-shared-form*.pdf -- a.pdf"}, {$td->STRING => "", $td->EXIT_STATUS => 0}); $td->runtest("check output", - {$td->FILE => "a.pdf"}, - {$td->FILE => "shared-form-images-merged.pdf"}); + {$td->COMMAND => "qpdf-test-compare a.pdf shared-form-images-merged.pdf"}, + {$td->FILE => "shared-form-images-merged.pdf", $td->EXIT_STATUS => 0}); compare_pdfs($td, "shared-form-images.pdf", "a.pdf"); $td->runtest("shared form xobject subkey", diff --git a/qpdf/qtest/stream-replacements.test b/qpdf/qtest/stream-replacements.test index 547e928..34811ad 100644 --- a/qpdf/qtest/stream-replacements.test +++ b/qpdf/qtest/stream-replacements.test @@ -28,8 +28,8 @@ $td->runtest("replace stream data compressed", {$td->FILE => "test8.out", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); $td->runtest("check output", - {$td->FILE => "a.pdf"}, - {$td->FILE => "replaced-stream-data-flate.pdf"}); + {$td->COMMAND => "qpdf-test-compare a.pdf replaced-stream-data-flate.pdf"}, + {$td->FILE => "replaced-stream-data-flate.pdf", $td->EXIT_STATUS => 0}); $td->runtest("new streams", {$td->COMMAND => "test_driver 9 minimal.pdf"}, {$td->FILE => "test9.out", $td->EXIT_STATUS => 0}, diff --git a/qpdf/qtest/zopfli.test b/qpdf/qtest/zopfli.test new file mode 100644 index 0000000..38b802c --- /dev/null +++ b/qpdf/qtest/zopfli.test @@ -0,0 +1,85 @@ +#!/usr/bin/env perl +require 5.008; +use warnings; +use strict; + +unshift(@INC, '.'); +require qpdf_test_helpers; + +chdir("qpdf") or die "chdir testdir failed: $!\n"; + +require TestDriver; + +my $dev_null = File::Spec->devnull(); +cleanup(); + +my $td = new TestDriver('zopfli'); + +my $n_tests = 0; + +my $zopfli_enabled = (system("qpdf --zopfli >$dev_null 2>&1") == 0); + +if (! $zopfli_enabled) { + # Variables are not checked + $n_tests = 8; + $td->runtest("zopfli not enabled", + {$td->COMMAND => "QPDF_ZOPFLI=force qpdf --zopfli"}, + {$td->STRING => "zopfli support is not enabled\n", + $td->EXIT_STATUS => 2}, + $td->NORMALIZE_NEWLINES); + + $td->runtest("zopfli disabled", + {$td->COMMAND => "QPDF_ZOPFLI=disabled qpdf minimal.pdf a.pdf"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); + $td->runtest("check output", + {$td->COMMAND => "qpdf-test-compare a.pdf minimal-out.pdf"}, + {$td->FILE => "minimal-out.pdf", $td->EXIT_STATUS => 0}); + $td->runtest("zopfli silent", + {$td->COMMAND => "QPDF_ZOPFLI=silent qpdf minimal.pdf a.pdf"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); + $td->runtest("check output", + {$td->COMMAND => "qpdf-test-compare a.pdf minimal-out.pdf"}, + {$td->FILE => "minimal-out.pdf", $td->EXIT_STATUS => 0}); + + $td->runtest("zopfli warning", + {$td->COMMAND => "QPDF_ZOPFLI=on qpdf minimal.pdf a.pdf"}, + {$td->FILE => "zopfli-warning.out", $td->EXIT_STATUS => 3}, + $td->NORMALIZE_NEWLINES); + $td->runtest("check output", + {$td->COMMAND => "qpdf-test-compare a.pdf minimal-out.pdf"}, + {$td->FILE => "minimal-out.pdf", $td->EXIT_STATUS => 0}); + + $td->runtest("zopfli error", + {$td->COMMAND => "QPDF_ZOPFLI=force qpdf minimal.pdf a.pdf"}, + {$td->REGEXP => "QPDF_ZOPFLI=force, and zopfli support is not enabled", + $td->EXIT_STATUS => 2}, + $td->NORMALIZE_NEWLINES); + +} else { + # Check variables + $n_tests = 4; + $td->runtest("zopfli supported and enabled", + {$td->COMMAND => "QPDF_ZOPFLI=on qpdf --zopfli"}, + {$td->STRING => "zopfli support is enabled, and zopfli is active\n", + $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); + $td->runtest("zopfli supported and disabled", + {$td->COMMAND => "QPDF_ZOPFLI=disabled qpdf --zopfli"}, + {$td->REGEXP => "(?s)zopfli support is enabled but not active.*QPDF_ZOPFLI.*\n", + $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); + # CI runs the whole test suite with QPDF_ZOPFLI=force, but run one + # for a guarantee. + $td->runtest("run with zopfli", + {$td->COMMAND => "QPDF_ZOPFLI=force qpdf minimal.pdf a.pdf"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); + $td->runtest("check output", + {$td->COMMAND => "qpdf-test-compare a.pdf minimal-out.pdf"}, + {$td->FILE => "minimal-out.pdf", $td->EXIT_STATUS => 0}); +} + +cleanup(); +$td->report($n_tests); diff --git a/zlib-flate/qtest/zf.test b/zlib-flate/qtest/zf.test index 2fa5c1e..7e6ea26 100644 --- a/zlib-flate/qtest/zf.test +++ b/zlib-flate/qtest/zf.test @@ -21,6 +21,9 @@ for (my $i = 0; $i < 100; $i++) } close(F); +my $dev_null = File::Spec->devnull(); +my $n_tests = 9; + foreach my $level ('', '=1', '=9') { my $f = $level; @@ -35,11 +38,18 @@ foreach my $level ('', '=1', '=9') {$td->FILE => "a.uncompressed", $td->EXIT_STATUS => 0}); } +chomp(my $zopfli = `zlib-flate --_zopfli`); my $size1 = (stat("a.=1"))[7]; my $size9 = (stat("a.=9"))[7]; -$td->runtest("higher compression is smaller", - {$td->STRING => ($size9 < $size1 ? "YES\n" : "$size9 $size1\n")}, - {$td->STRING => "YES\n"}); +if ($zopfli =~ m/1$/) { + $td->runtest("compression level is ignored with zopfli", + {$td->STRING => ($size9 == $size1 ? "YES\n" : "$size9 $size1\n")}, + {$td->STRING => "YES\n"}); +} else { + $td->runtest("higher compression is smaller", + {$td->STRING => ($size9 < $size1 ? "YES\n" : "$size9 $size1\n")}, + {$td->STRING => "YES\n"}); +} $td->runtest("error", {$td->COMMAND => "zlib-flate -uncompress < 1.uncompressed"}, @@ -54,7 +64,40 @@ $td->runtest("corrupted input", $td->EXIT_STATUS => 3}, $td->NORMALIZE_NEWLINES); -$td->report(9); +# Exercise different values of the QPDF_ZOPFLI variable +if ($zopfli =~ m/^0/) { + $n_tests += 4; + $td->runtest("disabled", + {$td->COMMAND => "QPDF_ZOPFLI=disabled zlib-flate --_zopfli"}, + {$td->STRING => "00\n", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); + $td->runtest("force", + {$td->COMMAND => "QPDF_ZOPFLI=force zlib-flate -compress < a.uncompressed"}, + {$td->REGEXP => "QPDF_ZOPFLI=force, and zopfli support is not enabled", + $td->EXIT_STATUS => 2}, + $td->NORMALIZE_NEWLINES); + $td->runtest("silent", + {$td->COMMAND => "QPDF_ZOPFLI=silent zlib-flate -compress < a.uncompressed > $dev_null"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); + $td->runtest("other", + {$td->COMMAND => "QPDF_ZOPFLI=other zlib-flate -compress < a.uncompressed > $dev_null"}, + {$td->REGEXP => "QPDF_ZOPFLI is set, but libqpdf was not built with zopfli support", + $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +} else { + $n_tests += 2; + $td->runtest("disabled", + {$td->COMMAND => "QPDF_ZOPFLI=disabled zlib-flate --_zopfli"}, + {$td->STRING => "10\n", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); + $td->runtest("force", + {$td->COMMAND => "QPDF_ZOPFLI=force zlib-flate --_zopfli"}, + {$td->STRING => "11\n", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +} + +$td->report($n_tests); cleanup(); diff --git a/zlib-flate/zlib-flate.cc b/zlib-flate/zlib-flate.cc index 198ef07..02a748e 100644 --- a/zlib-flate/zlib-flate.cc +++ b/zlib-flate/zlib-flate.cc @@ -6,7 +6,6 @@ #include #include #include -#include #include static char const* whoami = nullptr; @@ -50,21 +49,29 @@ main(int argc, char* argv[]) action = Pl_Flate::a_deflate; int level = QUtil::string_to_int(argv[1] + 10); Pl_Flate::setCompressionLevel(level); + } else if (strcmp(argv[1], "--_zopfli") == 0) { + // Undocumented option, but that doesn't mean someone doesn't use it... + // This is primarily here to support the test suite. + std::cout << (Pl_Flate::zopfli_supported() ? "1" : "0") + << (Pl_Flate::zopfli_enabled() ? "1" : "0") << std::endl; + return 0; } else { usage(); } - QUtil::binary_stdout(); - QUtil::binary_stdin(); - auto out = std::make_shared("stdout", stdout); - auto flate = std::make_shared("flate", out.get(), action); bool warn = false; - flate->setWarnCallback([&warn](char const* msg, int code) { - warn = true; - std::cerr << whoami << ": WARNING: zlib code " << code << ", msg = " << msg << std::endl; - }); - try { + QUtil::binary_stdout(); + QUtil::binary_stdin(); + Pl_Flate::zopfli_check_env(); + auto out = std::make_shared("stdout", stdout); + auto flate = std::make_shared("flate", out.get(), action); + flate->setWarnCallback([&warn](char const* msg, int code) { + warn = true; + std::cerr << whoami << ": WARNING: zlib code " << code << ", msg = " << msg + << std::endl; + }); + unsigned char buf[10000]; bool done = false; while (!done) {