diff --git a/include/qpdf/Pl_DCT.hh b/include/qpdf/Pl_DCT.hh index 09a7277..9f232a2 100644 --- a/include/qpdf/Pl_DCT.hh +++ b/include/qpdf/Pl_DCT.hh @@ -64,7 +64,7 @@ class QPDF_DLL_CLASS Pl_DCT: public Pipeline }; QPDF_DLL - static std::shared_ptr + static std::unique_ptr make_compress_config(std::function); // Constructor for compressing image data diff --git a/include/qpdf/QPDFJob.hh b/include/qpdf/QPDFJob.hh index 4f373c9..0874ac7 100644 --- a/include/qpdf/QPDFJob.hh +++ b/include/qpdf/QPDFJob.hh @@ -634,6 +634,7 @@ class QPDFJob bool recompress_flate{false}; bool recompress_flate_set{false}; int compression_level{-1}; + int jpeg_quality{-1}; qpdf_stream_decode_level_e decode_level{qpdf_dl_generalized}; bool decode_level_set{false}; bool normalize_set{false}; diff --git a/include/qpdf/auto_job_c_main.hh b/include/qpdf/auto_job_c_main.hh index 8778631..eb19a8f 100644 --- a/include/qpdf/auto_job_c_main.hh +++ b/include/qpdf/auto_job_c_main.hh @@ -54,6 +54,7 @@ QPDF_DLL Config* verbose(); QPDF_DLL Config* warningExit0(); QPDF_DLL Config* withImages(); QPDF_DLL Config* compressionLevel(std::string const& parameter); +QPDF_DLL Config* jpegQuality(std::string const& parameter); QPDF_DLL Config* copyEncryption(std::string const& parameter); QPDF_DLL Config* encryptionFilePassword(std::string const& parameter); QPDF_DLL Config* forceVersion(std::string const& parameter); diff --git a/job.sums b/job.sums index 3b06a5e..23d05b3 100644 --- a/job.sums +++ b/job.sums @@ -4,17 +4,17 @@ generate_auto_job f64733b79dcee5a0e3e8ccc6976448e8ddf0e8b6529987a66a7d3ab2ebc10a include/qpdf/auto_job_c_att.hh 4c2b171ea00531db54720bf49a43f8b34481586ae7fb6cbf225099ee42bc5bb4 include/qpdf/auto_job_c_copy_att.hh 50609012bff14fd82f0649185940d617d05d530cdc522185c7f3920a561ccb42 include/qpdf/auto_job_c_enc.hh 28446f3c32153a52afa239ea40503e6cc8ac2c026813526a349e0cd4ae17ddd5 -include/qpdf/auto_job_c_main.hh 48e8ea475e8a8f4c96de86bdad10dff83a263deccc3798c8bed7f5e0e070a037 +include/qpdf/auto_job_c_main.hh b865eb827356554763bb8349eadfcbc5cb260f80e025a5e229467c525007356d include/qpdf/auto_job_c_pages.hh 09ca15649cc94fdaf6d9bdae28a20723f2a66616bf15aa86d83df31051d82506 include/qpdf/auto_job_c_uo.hh 9c2f98a355858dd54d0bba444b73177a59c9e56833e02fa6406f429c07f39e62 -job.yml 9245e70c233dc2067827593403bd9e9feafc5aa0be6b12bb7b99a4b2cab84584 +job.yml e136726a6c7e43736b1f75f4de347fa50baf5f38ed1da58647ce2e980751fb29 libqpdf/qpdf/auto_job_decl.hh 34ba07d3891c3e5cdd8712f991e508a0652c9db314c5d5bcdf4421b76e6f6e01 -libqpdf/qpdf/auto_job_help.hh 03bdaab05f84b16bfb15ad7993a4655b7dc14af070fa97fe3035943726d4b258 -libqpdf/qpdf/auto_job_init.hh 029d929f930f60b4055796c8c4ce2ed625f861316ac738ab638579eca46b2472 +libqpdf/qpdf/auto_job_help.hh bcfe600cc01447a7fae8661a8d101c7b15ce144bb06ba0beab656f3a655371d1 +libqpdf/qpdf/auto_job_init.hh 02c526c37ad4051cac956ac7c12ae1d020517264f3f3d3beabb066ae2529e4bf libqpdf/qpdf/auto_job_json_decl.hh 04965f6321e54b8b3b1dd2ca101d763a22ab44fa81c69e4b6fc0fd6bb7f50f92 -libqpdf/qpdf/auto_job_json_init.hh 42b402305b52fc217453206c0a372303d0b59d4d4227bb564b4fa639257d4411 -libqpdf/qpdf/auto_job_schema.hh 2d3c163c74498b638a13931eed71c2a4dc6b155a9d3e2c1b740070fac4293737 +libqpdf/qpdf/auto_job_json_init.hh b49378f00d521a9f3e0ce9086e30b082bc6ef8e43c845e2a3c99857b72448307 +libqpdf/qpdf/auto_job_schema.hh f6a3e8b663714bba50b594f5e31437bbcb96ca4609d2c150c3bbc172e3b000fa manual/_ext/qpdf.py 6add6321666031d55ed4aedf7c00e5662bba856dfcd66ccb526563bffefbb580 -manual/cli.rst 1094662a10db21528fd151739a9779a4504ebac75b483a11a53d42ab0430ee42 -manual/qpdf.1 c7d03b8b544b0c3b2a74149d746596d4564aefff50a53980e435aa5c841f7bed +manual/cli.rst 6fae28c9589bfde5b55260c95a7c64ad48688875f14f195129606405b32a04c6 +manual/qpdf.1 95feb3b18b5ca3f868628591e2e91c3535125e9eedaedf4e3b160f32f1ff9f8f manual/qpdf.1.in 436ecc85d45c4c9e2dbd1725fb7f0177fb627179469f114561adf3cb6cbb677b diff --git a/job.yml b/job.yml index 5db4b8d..6cdcaf3 100644 --- a/job.yml +++ b/job.yml @@ -160,6 +160,7 @@ options: - split-pages required_parameter: compression-level: level + jpeg-quality: level copy-encryption: file encryption-file-password: password force-version: version @@ -413,6 +414,7 @@ json: suppress-recovery: coalesce-contents: compression-level: + jpeg-quality: externalize-inline-images: ii-min-bytes: remove-unreferenced-resources: diff --git a/libqpdf/Pl_DCT.cc b/libqpdf/Pl_DCT.cc index 2e480ec..7770cc0 100644 --- a/libqpdf/Pl_DCT.cc +++ b/libqpdf/Pl_DCT.cc @@ -424,8 +424,8 @@ Pl_DCT::decompress(void* cinfo_p, Buffer* b) next()->finish(); } -std::shared_ptr +std::unique_ptr Pl_DCT::make_compress_config(std::function f) { - return std::make_shared(f); + return std::make_unique(f); } diff --git a/libqpdf/QPDFJob.cc b/libqpdf/QPDFJob.cc index 3077200..bc47e34 100644 --- a/libqpdf/QPDFJob.cc +++ b/libqpdf/QPDFJob.cc @@ -45,6 +45,7 @@ namespace size_t oi_min_width, size_t oi_min_height, size_t oi_min_area, + int quality, QPDFObjectHandle& image); ~ImageOptimizer() override = default; void provideStreamData(QPDFObjGen const&, Pipeline* pipeline) override; @@ -56,6 +57,7 @@ namespace size_t oi_min_width; size_t oi_min_height; size_t oi_min_area; + qpdf_stream_decode_level_e decode_level{qpdf_dl_specialized}; QPDFObjectHandle image; std::shared_ptr config; }; @@ -109,6 +111,7 @@ ImageOptimizer::ImageOptimizer( size_t oi_min_width, size_t oi_min_height, size_t oi_min_area, + int quality, QPDFObjectHandle& image) : o(o), oi_min_width(oi_min_width), @@ -116,7 +119,12 @@ ImageOptimizer::ImageOptimizer( oi_min_area(oi_min_area), image(image) { - config = Pl_DCT::make_compress_config([](jpeg_compress_struct* config) {}); + if (quality >= 0) { + // Recompress existing jpeg. + decode_level = qpdf_dl_all; + config = Pl_DCT::make_compress_config( + [quality](jpeg_compress_struct* cinfo) { jpeg_set_quality(cinfo, quality, false); }); + } } std::shared_ptr @@ -204,7 +212,8 @@ ImageOptimizer::makePipeline(std::string const& description, Pipeline* next) bool ImageOptimizer::evaluate(std::string const& description) { - if (!image.pipeStreamData(nullptr, 0, qpdf_dl_specialized, true)) { + // Note: passing nullptr as pipeline (first argument) just tests whether we can filter. + if (!image.pipeStreamData(nullptr, 0, decode_level, true)) { QTC::TC("qpdf", "QPDFJob image optimize no pipeline"); o.doIfVerbose([&](Pipeline& v, std::string const& prefix) { v << prefix << ": " << description @@ -219,7 +228,7 @@ ImageOptimizer::evaluate(std::string const& description) // message issued by makePipeline return false; } - if (!image.pipeStreamData(p.get(), 0, qpdf_dl_specialized)) { + if (!image.pipeStreamData(p.get(), 0, decode_level)) { return false; } long long orig_length = image.getDict().getKey("/Length").getIntValue(); @@ -249,7 +258,7 @@ ImageOptimizer::provideStreamData(QPDFObjGen const&, Pipeline* pipeline) pipeline->finish(); return; } - image.pipeStreamData(p.get(), 0, qpdf_dl_specialized, false, false); + image.pipeStreamData(p.get(), 0, decode_level, false, false); } QPDFJob::PageSpec::PageSpec( @@ -2196,7 +2205,12 @@ QPDFJob::handleTransformations(QPDF& pdf) [this, pageno, &pdf]( QPDFObjectHandle& obj, QPDFObjectHandle& xobj_dict, std::string const& key) { auto io = std::make_unique( - *this, m->oi_min_width, m->oi_min_height, m->oi_min_area, obj); + *this, + m->oi_min_width, + m->oi_min_height, + m->oi_min_area, + m->jpeg_quality, + obj); if (io->evaluate("image " + key + " on page " + std::to_string(pageno))) { QPDFObjectHandle new_image = pdf.newStream(); new_image.replaceDict(obj.getDict().shallowCopy()); diff --git a/libqpdf/QPDFJob_config.cc b/libqpdf/QPDFJob_config.cc index 32eec68..2aff98c 100644 --- a/libqpdf/QPDFJob_config.cc +++ b/libqpdf/QPDFJob_config.cc @@ -140,6 +140,13 @@ QPDFJob::Config::compressionLevel(std::string const& parameter) } QPDFJob::Config* +QPDFJob::Config::jpegQuality(std::string const& parameter) +{ + o.m->jpeg_quality = QUtil::string_to_int(parameter.c_str()); + return this; +} + +QPDFJob::Config* QPDFJob::Config::copyEncryption(std::string const& parameter) { o.m->encryption_file = parameter; diff --git a/libqpdf/qpdf/auto_job_help.hh b/libqpdf/qpdf/auto_job_help.hh index 78799c3..fed33ef 100644 --- a/libqpdf/qpdf/auto_job_help.hh +++ b/libqpdf/qpdf/auto_job_help.hh @@ -239,6 +239,14 @@ gzip), which is the default compression for most PDF files. You need --recompress-flate with this option if you want to change already compressed streams. )"); +ap.addOptionHelp("--jpeg-quality", "transformation", "set jpeg quality level for jpeg", R"(--jpeg-quality=level + +When rewriting images with --optimize-images, set a quality +level from 0 (lowest) to 100 (highest) for writing new images. +Higher quality results in larger images, and lower quality +results in smaller images. This option is only effective when +combined with --optimize-images. +)"); ap.addOptionHelp("--normalize-content", "transformation", "fix newlines in content streams", R"(--normalize-content=[y|n] Normalize newlines to UNIX-style newlines in PDF content @@ -284,15 +292,15 @@ version. The version number format is to "major.minor" and the extension level, if specified, to "extension-level". )"); +} +static void add_help_4(QPDFArgParser& ap) +{ ap.addOptionHelp("--force-version", "transformation", "set output PDF version", R"(--force-version=version 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 @@ -419,11 +427,11 @@ Don't optimize images whose area in pixels is below the specified value. )"); ap.addOptionHelp("--keep-inline-images", "modification", "exclude inline images from optimization", R"(Prevent inline images from being considered by --optimize-images. )"); -ap.addOptionHelp("--remove-info", "modification", "remove file information", R"(Exclude file information (except modification date) from the output file. -)"); } static void add_help_5(QPDFArgParser& ap) { +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. )"); ap.addOptionHelp("--remove-page-labels", "modification", "remove explicit page numbers", R"(Exclude page labels (explicit page numbers) from the output file. @@ -642,12 +650,12 @@ version to be at least 1.6. This option is only available with 128-bit encryption. The default is "n" for compatibility reasons. Use 256-bit encryption instead. )"); -ap.addOptionHelp("--allow-insecure", "encryption", "allow empty owner passwords", R"(Allow creation of PDF files with empty owner passwords and -non-empty user passwords when using 256-bit encryption. -)"); } static void add_help_6(QPDFArgParser& ap) { +ap.addOptionHelp("--allow-insecure", "encryption", "allow empty owner passwords", R"(Allow creation of PDF files with empty owner passwords and +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. )"); @@ -825,14 +833,14 @@ ap.addOptionHelp("--mimetype", "add-attachment", "attachment mime type, e.g. app Specify the mime type for the attachment, such as text/plain, application/pdf, image/png, etc. )"); +} +static void add_help_7(QPDFArgParser& ap) +{ ap.addOptionHelp("--description", "add-attachment", "set attachment's description", R"(--description="text" Supply descriptive text for the attachment, displayed by some PDF viewers. )"); -} -static void add_help_7(QPDFArgParser& ap) -{ ap.addOptionHelp("--replace", "add-attachment", "replace attachment with same key", R"(Indicate that any existing attachment with the same key should be replaced by the new attachment. Otherwise, qpdf gives an error if an attachment with that key is already present. @@ -919,12 +927,12 @@ object and for each content stream associated with the page. ap.addOptionHelp("--with-images", "inspection", "include image details with --show-pages", R"(When used with --show-pages, also shows the object and generation numbers for the image objects on each page. )"); -ap.addOptionHelp("--list-attachments", "inspection", "list embedded files", R"(Show the key and stream number for each embedded file. Combine -with --verbose for more detailed information. -)"); } static void add_help_8(QPDFArgParser& ap) { +ap.addOptionHelp("--list-attachments", "inspection", "list embedded files", R"(Show the key and stream number for each embedded file. Combine +with --verbose for more detailed information. +)"); ap.addOptionHelp("--show-attachment", "inspection", "export an embedded file", R"(--show-attachment=key Write the contents of the specified attachment to standard diff --git a/libqpdf/qpdf/auto_job_init.hh b/libqpdf/qpdf/auto_job_init.hh index 97ccc77..7b6c99f 100644 --- a/libqpdf/qpdf/auto_job_init.hh +++ b/libqpdf/qpdf/auto_job_init.hh @@ -94,6 +94,7 @@ this->ap.addBare("verbose", [this](){c_main->verbose();}); this->ap.addBare("warning-exit-0", [this](){c_main->warningExit0();}); this->ap.addBare("with-images", [this](){c_main->withImages();}); this->ap.addRequiredParameter("compression-level", [this](std::string const& x){c_main->compressionLevel(x);}, "level"); +this->ap.addRequiredParameter("jpeg-quality", [this](std::string const& x){c_main->jpegQuality(x);}, "level"); this->ap.addRequiredParameter("copy-encryption", [this](std::string const& x){c_main->copyEncryption(x);}, "file"); this->ap.addRequiredParameter("encryption-file-password", [this](std::string const& x){c_main->encryptionFilePassword(x);}, "password"); this->ap.addRequiredParameter("force-version", [this](std::string const& x){c_main->forceVersion(x);}, "version"); diff --git a/libqpdf/qpdf/auto_job_json_init.hh b/libqpdf/qpdf/auto_job_json_init.hh index 6457f26..8612ca9 100644 --- a/libqpdf/qpdf/auto_job_json_init.hh +++ b/libqpdf/qpdf/auto_job_json_init.hh @@ -314,6 +314,9 @@ popHandler(); // key: coalesceContents pushKey("compressionLevel"); addParameter([this](std::string const& p) { c_main->compressionLevel(p); }); popHandler(); // key: compressionLevel +pushKey("jpegQuality"); +addParameter([this](std::string const& p) { c_main->jpegQuality(p); }); +popHandler(); // key: jpegQuality pushKey("externalizeInlineImages"); addBare([this]() { c_main->externalizeInlineImages(); }); popHandler(); // key: externalizeInlineImages diff --git a/libqpdf/qpdf/auto_job_schema.hh b/libqpdf/qpdf/auto_job_schema.hh index 23ba4de..7acabc4 100644 --- a/libqpdf/qpdf/auto_job_schema.hh +++ b/libqpdf/qpdf/auto_job_schema.hh @@ -104,6 +104,7 @@ static constexpr char const* JOB_SCHEMA_DATA = R"({ "suppressRecovery": "suppress error recovery", "coalesceContents": "combine content streams", "compressionLevel": "set compression level for flate", + "jpegQuality": "set jpeg quality level for jpeg", "externalizeInlineImages": "convert inline to regular images", "iiMinBytes": "set minimum size for externalizeInlineImages", "removeUnreferencedResources": "remove unreferenced page resources", diff --git a/libtests/dct_compress.cc b/libtests/dct_compress.cc index c5f8396..93979bd 100644 --- a/libtests/dct_compress.cc +++ b/libtests/dct_compress.cc @@ -1,7 +1,8 @@ +#include + #include #include #include -#include #include #include diff --git a/manual/cli.rst b/manual/cli.rst index b8f5d74..088c43b 100644 --- a/manual/cli.rst +++ b/manual/cli.rst @@ -1028,6 +1028,26 @@ Related Options defers to the compression library's default behavior. See also :ref:`small-files`. +.. qpdf:option:: --jpeg-quality=level + + .. help: set jpeg quality level for jpeg + + When rewriting images with --optimize-images, set a quality + level from 0 (lowest) to 100 (highest) for writing new images. + Higher quality results in larger images, and lower quality + results in smaller images. This option is only effective when + combined with --optimize-images. + + When rewriting images with :qpdf:ref:`--optimize-images`, set a + quality level from 0 (lowest) to 100 (highest) for writing new + images. Higher quality results in larger images, and lower quality + results in smaller images. Be sure to check your output to see if + the quality is acceptable. This option is only effective when + combined with :qpdf:ref:`--optimize-images`. This option also + causes files that are already compressed with JPEG compression to + be uncompressed and recompressed, potentially introducing + additional loss of image quality. See also :ref:`small-files`. + .. qpdf:option:: --normalize-content=[y|n] .. help: fix newlines in content streams @@ -4005,6 +4025,13 @@ generate: lossy, so images may have artifacts. These are not usually noticeable to the casual observer. +- ``--jpeg-quality=n``: set the JPEG quality used by :qpdf:ref:`--optimize-images` + when writing JPEG files. Use a lower number for ``n`` for smaller, + lower-quality images. The default of most JPEG libraries is 75. + Smaller numbers result in lower-quality but smaller images. The + :qpdf:ref:`--jpeg-quality` option was added in qpdf 12.1. This only + works in combination with :qpdf:ref:`--optimize-images`. + - ``--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`) diff --git a/manual/qpdf.1 b/manual/qpdf.1 index 5f33a49..d3cc76f 100644 --- a/manual/qpdf.1 +++ b/manual/qpdf.1 @@ -317,6 +317,15 @@ gzip), which is the default compression for most PDF files. You need --recompress-flate with this option if you want to change already compressed streams. .TP +.B --jpeg-quality \-\- set jpeg quality level for jpeg +--jpeg-quality=level + +When rewriting images with --optimize-images, set a quality +level from 0 (lowest) to 100 (highest) for writing new images. +Higher quality results in larger images, and lower quality +results in smaller images. This option is only effective when +combined with --optimize-images. +.TP .B --normalize-content \-\- fix newlines in content streams --normalize-content=[y|n] diff --git a/manual/release-notes.rst b/manual/release-notes.rst index cb22412..39ebcd7 100644 --- a/manual/release-notes.rst +++ b/manual/release-notes.rst @@ -41,14 +41,18 @@ more detail. - Library Enhancements - Add function ``Pl_DCT::make_compress_config`` to return a - ``Pl_DCT::CompressConfig`` shared pointer from a - ``std::function`` for a more modern configuration option. + ``Pl_DCT::CompressConfig`` unique pointer to a + ``CompressConfig`` from a ``std::function`` for a more modern + configuration option. - CLI Enhancements - New :qpdf:ref:`--remove-structure` option to exclude the document structure tree from the output PDF. + - New :qpdf:ref:`--jpeg-quality` option to set jpeg quality used + with :qpdf:ref:`--optimize-images`. + - Other enhancements - There have been further enhancements to how files with damaged xref diff --git a/qpdf/qtest/image-optimization.test b/qpdf/qtest/image-optimization.test index 10ffd52..5126dbc 100644 --- a/qpdf/qtest/image-optimization.test +++ b/qpdf/qtest/image-optimization.test @@ -37,7 +37,7 @@ my @image_opt = ( '--oi-min-width=0 --oi-min-height=0 --oi-min-area=0'] ); -my $n_tests = 2 * scalar(@image_opt); +my $n_tests = 2 * scalar(@image_opt) + 5; foreach my $d (@image_opt) { @@ -58,5 +58,34 @@ foreach my $d (@image_opt) $td->NORMALIZE_NEWLINES); } +$td->runtest("quality = 100", + {$td->COMMAND => + "qpdf --static-id --optimize-images --jpeg-quality=100" . + " large-inline-image.pdf a.pdf"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}); +$td->runtest("quality = 50", + {$td->COMMAND => + "qpdf --static-id --optimize-images --jpeg-quality=50" . + " large-inline-image.pdf b.pdf"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}); +$td->runtest("quality = 50 from DCT", + {$td->COMMAND => + "qpdf --static-id --optimize-images --jpeg-quality=50" . + " a.pdf c.pdf"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}); +my $size100 = (stat("a.pdf"))[7]; +my $size50 = (stat("b.pdf"))[7]; +my $size50b = (stat("c.pdf"))[7]; +my $result = $size50 < $size100 ? "ok\n" : "failed\n"; +$td->runtest("quality 50 < quality 100", + {$td->STRING => $result}, + {$td->STRING => "ok\n"}, + $td->NORMALIZE_NEWLINES); +$result = $size50b < $size100 ? "ok\n" : "failed\n"; +$td->runtest("quality 50 from DCT < quality 100", + {$td->STRING => $result}, + {$td->STRING => "ok\n"}, + $td->NORMALIZE_NEWLINES); + cleanup(); $td->report($n_tests);