Commit 021edd02d7625324352c70babe36ba50e2bc314a
1 parent
e62f1e4e
Add --jpeg-quality-level flag (fixes #488)
Thanks to github user @cdosborn for the basic enhancement.
Showing
17 changed files
with
142 additions
and
34 deletions
include/qpdf/Pl_DCT.hh
| ... | ... | @@ -64,7 +64,7 @@ class QPDF_DLL_CLASS Pl_DCT: public Pipeline |
| 64 | 64 | }; |
| 65 | 65 | |
| 66 | 66 | QPDF_DLL |
| 67 | - static std::shared_ptr<CompressConfig> | |
| 67 | + static std::unique_ptr<CompressConfig> | |
| 68 | 68 | make_compress_config(std::function<void(jpeg_compress_struct*)>); |
| 69 | 69 | |
| 70 | 70 | // Constructor for compressing image data | ... | ... |
include/qpdf/QPDFJob.hh
| ... | ... | @@ -634,6 +634,7 @@ class QPDFJob |
| 634 | 634 | bool recompress_flate{false}; |
| 635 | 635 | bool recompress_flate_set{false}; |
| 636 | 636 | int compression_level{-1}; |
| 637 | + int jpeg_quality{-1}; | |
| 637 | 638 | qpdf_stream_decode_level_e decode_level{qpdf_dl_generalized}; |
| 638 | 639 | bool decode_level_set{false}; |
| 639 | 640 | bool normalize_set{false}; | ... | ... |
include/qpdf/auto_job_c_main.hh
| ... | ... | @@ -54,6 +54,7 @@ QPDF_DLL Config* verbose(); |
| 54 | 54 | QPDF_DLL Config* warningExit0(); |
| 55 | 55 | QPDF_DLL Config* withImages(); |
| 56 | 56 | QPDF_DLL Config* compressionLevel(std::string const& parameter); |
| 57 | +QPDF_DLL Config* jpegQuality(std::string const& parameter); | |
| 57 | 58 | QPDF_DLL Config* copyEncryption(std::string const& parameter); |
| 58 | 59 | QPDF_DLL Config* encryptionFilePassword(std::string const& parameter); |
| 59 | 60 | QPDF_DLL Config* forceVersion(std::string const& parameter); | ... | ... |
job.sums
| ... | ... | @@ -4,17 +4,17 @@ generate_auto_job f64733b79dcee5a0e3e8ccc6976448e8ddf0e8b6529987a66a7d3ab2ebc10a |
| 4 | 4 | include/qpdf/auto_job_c_att.hh 4c2b171ea00531db54720bf49a43f8b34481586ae7fb6cbf225099ee42bc5bb4 |
| 5 | 5 | include/qpdf/auto_job_c_copy_att.hh 50609012bff14fd82f0649185940d617d05d530cdc522185c7f3920a561ccb42 |
| 6 | 6 | include/qpdf/auto_job_c_enc.hh 28446f3c32153a52afa239ea40503e6cc8ac2c026813526a349e0cd4ae17ddd5 |
| 7 | -include/qpdf/auto_job_c_main.hh 48e8ea475e8a8f4c96de86bdad10dff83a263deccc3798c8bed7f5e0e070a037 | |
| 7 | +include/qpdf/auto_job_c_main.hh b865eb827356554763bb8349eadfcbc5cb260f80e025a5e229467c525007356d | |
| 8 | 8 | include/qpdf/auto_job_c_pages.hh 09ca15649cc94fdaf6d9bdae28a20723f2a66616bf15aa86d83df31051d82506 |
| 9 | 9 | include/qpdf/auto_job_c_uo.hh 9c2f98a355858dd54d0bba444b73177a59c9e56833e02fa6406f429c07f39e62 |
| 10 | -job.yml 9245e70c233dc2067827593403bd9e9feafc5aa0be6b12bb7b99a4b2cab84584 | |
| 10 | +job.yml e136726a6c7e43736b1f75f4de347fa50baf5f38ed1da58647ce2e980751fb29 | |
| 11 | 11 | libqpdf/qpdf/auto_job_decl.hh 34ba07d3891c3e5cdd8712f991e508a0652c9db314c5d5bcdf4421b76e6f6e01 |
| 12 | -libqpdf/qpdf/auto_job_help.hh 03bdaab05f84b16bfb15ad7993a4655b7dc14af070fa97fe3035943726d4b258 | |
| 13 | -libqpdf/qpdf/auto_job_init.hh 029d929f930f60b4055796c8c4ce2ed625f861316ac738ab638579eca46b2472 | |
| 12 | +libqpdf/qpdf/auto_job_help.hh bcfe600cc01447a7fae8661a8d101c7b15ce144bb06ba0beab656f3a655371d1 | |
| 13 | +libqpdf/qpdf/auto_job_init.hh 02c526c37ad4051cac956ac7c12ae1d020517264f3f3d3beabb066ae2529e4bf | |
| 14 | 14 | libqpdf/qpdf/auto_job_json_decl.hh 04965f6321e54b8b3b1dd2ca101d763a22ab44fa81c69e4b6fc0fd6bb7f50f92 |
| 15 | -libqpdf/qpdf/auto_job_json_init.hh 42b402305b52fc217453206c0a372303d0b59d4d4227bb564b4fa639257d4411 | |
| 16 | -libqpdf/qpdf/auto_job_schema.hh 2d3c163c74498b638a13931eed71c2a4dc6b155a9d3e2c1b740070fac4293737 | |
| 15 | +libqpdf/qpdf/auto_job_json_init.hh b49378f00d521a9f3e0ce9086e30b082bc6ef8e43c845e2a3c99857b72448307 | |
| 16 | +libqpdf/qpdf/auto_job_schema.hh f6a3e8b663714bba50b594f5e31437bbcb96ca4609d2c150c3bbc172e3b000fa | |
| 17 | 17 | manual/_ext/qpdf.py 6add6321666031d55ed4aedf7c00e5662bba856dfcd66ccb526563bffefbb580 |
| 18 | -manual/cli.rst 1094662a10db21528fd151739a9779a4504ebac75b483a11a53d42ab0430ee42 | |
| 19 | -manual/qpdf.1 c7d03b8b544b0c3b2a74149d746596d4564aefff50a53980e435aa5c841f7bed | |
| 18 | +manual/cli.rst 6fae28c9589bfde5b55260c95a7c64ad48688875f14f195129606405b32a04c6 | |
| 19 | +manual/qpdf.1 95feb3b18b5ca3f868628591e2e91c3535125e9eedaedf4e3b160f32f1ff9f8f | |
| 20 | 20 | manual/qpdf.1.in 436ecc85d45c4c9e2dbd1725fb7f0177fb627179469f114561adf3cb6cbb677b | ... | ... |
job.yml
| ... | ... | @@ -160,6 +160,7 @@ options: |
| 160 | 160 | - split-pages |
| 161 | 161 | required_parameter: |
| 162 | 162 | compression-level: level |
| 163 | + jpeg-quality: level | |
| 163 | 164 | copy-encryption: file |
| 164 | 165 | encryption-file-password: password |
| 165 | 166 | force-version: version |
| ... | ... | @@ -413,6 +414,7 @@ json: |
| 413 | 414 | suppress-recovery: |
| 414 | 415 | coalesce-contents: |
| 415 | 416 | compression-level: |
| 417 | + jpeg-quality: | |
| 416 | 418 | externalize-inline-images: |
| 417 | 419 | ii-min-bytes: |
| 418 | 420 | remove-unreferenced-resources: | ... | ... |
libqpdf/Pl_DCT.cc
| ... | ... | @@ -424,8 +424,8 @@ Pl_DCT::decompress(void* cinfo_p, Buffer* b) |
| 424 | 424 | next()->finish(); |
| 425 | 425 | } |
| 426 | 426 | |
| 427 | -std::shared_ptr<Pl_DCT::CompressConfig> | |
| 427 | +std::unique_ptr<Pl_DCT::CompressConfig> | |
| 428 | 428 | Pl_DCT::make_compress_config(std::function<void(jpeg_compress_struct*)> f) |
| 429 | 429 | { |
| 430 | - return std::make_shared<FunctionCallbackConfig>(f); | |
| 430 | + return std::make_unique<FunctionCallbackConfig>(f); | |
| 431 | 431 | } | ... | ... |
libqpdf/QPDFJob.cc
| ... | ... | @@ -45,6 +45,7 @@ namespace |
| 45 | 45 | size_t oi_min_width, |
| 46 | 46 | size_t oi_min_height, |
| 47 | 47 | size_t oi_min_area, |
| 48 | + int quality, | |
| 48 | 49 | QPDFObjectHandle& image); |
| 49 | 50 | ~ImageOptimizer() override = default; |
| 50 | 51 | void provideStreamData(QPDFObjGen const&, Pipeline* pipeline) override; |
| ... | ... | @@ -56,6 +57,7 @@ namespace |
| 56 | 57 | size_t oi_min_width; |
| 57 | 58 | size_t oi_min_height; |
| 58 | 59 | size_t oi_min_area; |
| 60 | + qpdf_stream_decode_level_e decode_level{qpdf_dl_specialized}; | |
| 59 | 61 | QPDFObjectHandle image; |
| 60 | 62 | std::shared_ptr<Pl_DCT::CompressConfig> config; |
| 61 | 63 | }; |
| ... | ... | @@ -109,6 +111,7 @@ ImageOptimizer::ImageOptimizer( |
| 109 | 111 | size_t oi_min_width, |
| 110 | 112 | size_t oi_min_height, |
| 111 | 113 | size_t oi_min_area, |
| 114 | + int quality, | |
| 112 | 115 | QPDFObjectHandle& image) : |
| 113 | 116 | o(o), |
| 114 | 117 | oi_min_width(oi_min_width), |
| ... | ... | @@ -116,7 +119,12 @@ ImageOptimizer::ImageOptimizer( |
| 116 | 119 | oi_min_area(oi_min_area), |
| 117 | 120 | image(image) |
| 118 | 121 | { |
| 119 | - config = Pl_DCT::make_compress_config([](jpeg_compress_struct* config) {}); | |
| 122 | + if (quality >= 0) { | |
| 123 | + // Recompress existing jpeg. | |
| 124 | + decode_level = qpdf_dl_all; | |
| 125 | + config = Pl_DCT::make_compress_config( | |
| 126 | + [quality](jpeg_compress_struct* cinfo) { jpeg_set_quality(cinfo, quality, false); }); | |
| 127 | + } | |
| 120 | 128 | } |
| 121 | 129 | |
| 122 | 130 | std::shared_ptr<Pipeline> |
| ... | ... | @@ -204,7 +212,8 @@ ImageOptimizer::makePipeline(std::string const& description, Pipeline* next) |
| 204 | 212 | bool |
| 205 | 213 | ImageOptimizer::evaluate(std::string const& description) |
| 206 | 214 | { |
| 207 | - if (!image.pipeStreamData(nullptr, 0, qpdf_dl_specialized, true)) { | |
| 215 | + // Note: passing nullptr as pipeline (first argument) just tests whether we can filter. | |
| 216 | + if (!image.pipeStreamData(nullptr, 0, decode_level, true)) { | |
| 208 | 217 | QTC::TC("qpdf", "QPDFJob image optimize no pipeline"); |
| 209 | 218 | o.doIfVerbose([&](Pipeline& v, std::string const& prefix) { |
| 210 | 219 | v << prefix << ": " << description |
| ... | ... | @@ -219,7 +228,7 @@ ImageOptimizer::evaluate(std::string const& description) |
| 219 | 228 | // message issued by makePipeline |
| 220 | 229 | return false; |
| 221 | 230 | } |
| 222 | - if (!image.pipeStreamData(p.get(), 0, qpdf_dl_specialized)) { | |
| 231 | + if (!image.pipeStreamData(p.get(), 0, decode_level)) { | |
| 223 | 232 | return false; |
| 224 | 233 | } |
| 225 | 234 | long long orig_length = image.getDict().getKey("/Length").getIntValue(); |
| ... | ... | @@ -249,7 +258,7 @@ ImageOptimizer::provideStreamData(QPDFObjGen const&, Pipeline* pipeline) |
| 249 | 258 | pipeline->finish(); |
| 250 | 259 | return; |
| 251 | 260 | } |
| 252 | - image.pipeStreamData(p.get(), 0, qpdf_dl_specialized, false, false); | |
| 261 | + image.pipeStreamData(p.get(), 0, decode_level, false, false); | |
| 253 | 262 | } |
| 254 | 263 | |
| 255 | 264 | QPDFJob::PageSpec::PageSpec( |
| ... | ... | @@ -2196,7 +2205,12 @@ QPDFJob::handleTransformations(QPDF& pdf) |
| 2196 | 2205 | [this, pageno, &pdf]( |
| 2197 | 2206 | QPDFObjectHandle& obj, QPDFObjectHandle& xobj_dict, std::string const& key) { |
| 2198 | 2207 | auto io = std::make_unique<ImageOptimizer>( |
| 2199 | - *this, m->oi_min_width, m->oi_min_height, m->oi_min_area, obj); | |
| 2208 | + *this, | |
| 2209 | + m->oi_min_width, | |
| 2210 | + m->oi_min_height, | |
| 2211 | + m->oi_min_area, | |
| 2212 | + m->jpeg_quality, | |
| 2213 | + obj); | |
| 2200 | 2214 | if (io->evaluate("image " + key + " on page " + std::to_string(pageno))) { |
| 2201 | 2215 | QPDFObjectHandle new_image = pdf.newStream(); |
| 2202 | 2216 | new_image.replaceDict(obj.getDict().shallowCopy()); | ... | ... |
libqpdf/QPDFJob_config.cc
| ... | ... | @@ -140,6 +140,13 @@ QPDFJob::Config::compressionLevel(std::string const& parameter) |
| 140 | 140 | } |
| 141 | 141 | |
| 142 | 142 | QPDFJob::Config* |
| 143 | +QPDFJob::Config::jpegQuality(std::string const& parameter) | |
| 144 | +{ | |
| 145 | + o.m->jpeg_quality = QUtil::string_to_int(parameter.c_str()); | |
| 146 | + return this; | |
| 147 | +} | |
| 148 | + | |
| 149 | +QPDFJob::Config* | |
| 143 | 150 | QPDFJob::Config::copyEncryption(std::string const& parameter) |
| 144 | 151 | { |
| 145 | 152 | o.m->encryption_file = parameter; | ... | ... |
libqpdf/qpdf/auto_job_help.hh
| ... | ... | @@ -239,6 +239,14 @@ gzip), which is the default compression for most PDF files. |
| 239 | 239 | You need --recompress-flate with this option if you want to |
| 240 | 240 | change already compressed streams. |
| 241 | 241 | )"); |
| 242 | +ap.addOptionHelp("--jpeg-quality", "transformation", "set jpeg quality level for jpeg", R"(--jpeg-quality=level | |
| 243 | + | |
| 244 | +When rewriting images with --optimize-images, set a quality | |
| 245 | +level from 0 (lowest) to 100 (highest) for writing new images. | |
| 246 | +Higher quality results in larger images, and lower quality | |
| 247 | +results in smaller images. This option is only effective when | |
| 248 | +combined with --optimize-images. | |
| 249 | +)"); | |
| 242 | 250 | ap.addOptionHelp("--normalize-content", "transformation", "fix newlines in content streams", R"(--normalize-content=[y|n] |
| 243 | 251 | |
| 244 | 252 | Normalize newlines to UNIX-style newlines in PDF content |
| ... | ... | @@ -284,15 +292,15 @@ version. The version number format is |
| 284 | 292 | to "major.minor" and the extension level, if specified, to |
| 285 | 293 | "extension-level". |
| 286 | 294 | )"); |
| 295 | +} | |
| 296 | +static void add_help_4(QPDFArgParser& ap) | |
| 297 | +{ | |
| 287 | 298 | ap.addOptionHelp("--force-version", "transformation", "set output PDF version", R"(--force-version=version |
| 288 | 299 | |
| 289 | 300 | Force the output PDF file's PDF version header to be the specified |
| 290 | 301 | value, even if the file uses features that may not be available |
| 291 | 302 | in that version. |
| 292 | 303 | )"); |
| 293 | -} | |
| 294 | -static void add_help_4(QPDFArgParser& ap) | |
| 295 | -{ | |
| 296 | 304 | ap.addHelpTopic("page-ranges", "page range syntax", R"(A full description of the page range syntax, with examples, can be |
| 297 | 305 | found in the manual. In summary, a range is a comma-separated list |
| 298 | 306 | 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. |
| 419 | 427 | )"); |
| 420 | 428 | ap.addOptionHelp("--keep-inline-images", "modification", "exclude inline images from optimization", R"(Prevent inline images from being considered by --optimize-images. |
| 421 | 429 | )"); |
| 422 | -ap.addOptionHelp("--remove-info", "modification", "remove file information", R"(Exclude file information (except modification date) from the output file. | |
| 423 | -)"); | |
| 424 | 430 | } |
| 425 | 431 | static void add_help_5(QPDFArgParser& ap) |
| 426 | 432 | { |
| 433 | +ap.addOptionHelp("--remove-info", "modification", "remove file information", R"(Exclude file information (except modification date) from the output file. | |
| 434 | +)"); | |
| 427 | 435 | ap.addOptionHelp("--remove-metadata", "modification", "remove metadata", R"(Exclude metadata from the output file. |
| 428 | 436 | )"); |
| 429 | 437 | 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 |
| 642 | 650 | 128-bit encryption. The default is "n" for compatibility |
| 643 | 651 | reasons. Use 256-bit encryption instead. |
| 644 | 652 | )"); |
| 645 | -ap.addOptionHelp("--allow-insecure", "encryption", "allow empty owner passwords", R"(Allow creation of PDF files with empty owner passwords and | |
| 646 | -non-empty user passwords when using 256-bit encryption. | |
| 647 | -)"); | |
| 648 | 653 | } |
| 649 | 654 | static void add_help_6(QPDFArgParser& ap) |
| 650 | 655 | { |
| 656 | +ap.addOptionHelp("--allow-insecure", "encryption", "allow empty owner passwords", R"(Allow creation of PDF files with empty owner passwords and | |
| 657 | +non-empty user passwords when using 256-bit encryption. | |
| 658 | +)"); | |
| 651 | 659 | ap.addOptionHelp("--force-V4", "encryption", "force V=4 in encryption dictionary", R"(This option is for testing and is never needed in practice since |
| 652 | 660 | qpdf does this automatically when needed. |
| 653 | 661 | )"); |
| ... | ... | @@ -825,14 +833,14 @@ ap.addOptionHelp("--mimetype", "add-attachment", "attachment mime type, e.g. app |
| 825 | 833 | Specify the mime type for the attachment, such as text/plain, |
| 826 | 834 | application/pdf, image/png, etc. |
| 827 | 835 | )"); |
| 836 | +} | |
| 837 | +static void add_help_7(QPDFArgParser& ap) | |
| 838 | +{ | |
| 828 | 839 | ap.addOptionHelp("--description", "add-attachment", "set attachment's description", R"(--description="text" |
| 829 | 840 | |
| 830 | 841 | Supply descriptive text for the attachment, displayed by some |
| 831 | 842 | PDF viewers. |
| 832 | 843 | )"); |
| 833 | -} | |
| 834 | -static void add_help_7(QPDFArgParser& ap) | |
| 835 | -{ | |
| 836 | 844 | ap.addOptionHelp("--replace", "add-attachment", "replace attachment with same key", R"(Indicate that any existing attachment with the same key should |
| 837 | 845 | be replaced by the new attachment. Otherwise, qpdf gives an |
| 838 | 846 | error if an attachment with that key is already present. |
| ... | ... | @@ -919,12 +927,12 @@ object and for each content stream associated with the page. |
| 919 | 927 | ap.addOptionHelp("--with-images", "inspection", "include image details with --show-pages", R"(When used with --show-pages, also shows the object and |
| 920 | 928 | generation numbers for the image objects on each page. |
| 921 | 929 | )"); |
| 922 | -ap.addOptionHelp("--list-attachments", "inspection", "list embedded files", R"(Show the key and stream number for each embedded file. Combine | |
| 923 | -with --verbose for more detailed information. | |
| 924 | -)"); | |
| 925 | 930 | } |
| 926 | 931 | static void add_help_8(QPDFArgParser& ap) |
| 927 | 932 | { |
| 933 | +ap.addOptionHelp("--list-attachments", "inspection", "list embedded files", R"(Show the key and stream number for each embedded file. Combine | |
| 934 | +with --verbose for more detailed information. | |
| 935 | +)"); | |
| 928 | 936 | ap.addOptionHelp("--show-attachment", "inspection", "export an embedded file", R"(--show-attachment=key |
| 929 | 937 | |
| 930 | 938 | Write the contents of the specified attachment to standard | ... | ... |
libqpdf/qpdf/auto_job_init.hh
| ... | ... | @@ -94,6 +94,7 @@ this->ap.addBare("verbose", [this](){c_main->verbose();}); |
| 94 | 94 | this->ap.addBare("warning-exit-0", [this](){c_main->warningExit0();}); |
| 95 | 95 | this->ap.addBare("with-images", [this](){c_main->withImages();}); |
| 96 | 96 | this->ap.addRequiredParameter("compression-level", [this](std::string const& x){c_main->compressionLevel(x);}, "level"); |
| 97 | +this->ap.addRequiredParameter("jpeg-quality", [this](std::string const& x){c_main->jpegQuality(x);}, "level"); | |
| 97 | 98 | this->ap.addRequiredParameter("copy-encryption", [this](std::string const& x){c_main->copyEncryption(x);}, "file"); |
| 98 | 99 | this->ap.addRequiredParameter("encryption-file-password", [this](std::string const& x){c_main->encryptionFilePassword(x);}, "password"); |
| 99 | 100 | this->ap.addRequiredParameter("force-version", [this](std::string const& x){c_main->forceVersion(x);}, "version"); | ... | ... |
libqpdf/qpdf/auto_job_json_init.hh
| ... | ... | @@ -314,6 +314,9 @@ popHandler(); // key: coalesceContents |
| 314 | 314 | pushKey("compressionLevel"); |
| 315 | 315 | addParameter([this](std::string const& p) { c_main->compressionLevel(p); }); |
| 316 | 316 | popHandler(); // key: compressionLevel |
| 317 | +pushKey("jpegQuality"); | |
| 318 | +addParameter([this](std::string const& p) { c_main->jpegQuality(p); }); | |
| 319 | +popHandler(); // key: jpegQuality | |
| 317 | 320 | pushKey("externalizeInlineImages"); |
| 318 | 321 | addBare([this]() { c_main->externalizeInlineImages(); }); |
| 319 | 322 | popHandler(); // key: externalizeInlineImages | ... | ... |
libqpdf/qpdf/auto_job_schema.hh
| ... | ... | @@ -104,6 +104,7 @@ static constexpr char const* JOB_SCHEMA_DATA = R"({ |
| 104 | 104 | "suppressRecovery": "suppress error recovery", |
| 105 | 105 | "coalesceContents": "combine content streams", |
| 106 | 106 | "compressionLevel": "set compression level for flate", |
| 107 | + "jpegQuality": "set jpeg quality level for jpeg", | |
| 107 | 108 | "externalizeInlineImages": "convert inline to regular images", |
| 108 | 109 | "iiMinBytes": "set minimum size for externalizeInlineImages", |
| 109 | 110 | "removeUnreferencedResources": "remove unreferenced page resources", | ... | ... |
libtests/dct_compress.cc
manual/cli.rst
| ... | ... | @@ -1028,6 +1028,26 @@ Related Options |
| 1028 | 1028 | defers to the compression library's default behavior. See also |
| 1029 | 1029 | :ref:`small-files`. |
| 1030 | 1030 | |
| 1031 | +.. qpdf:option:: --jpeg-quality=level | |
| 1032 | + | |
| 1033 | + .. help: set jpeg quality level for jpeg | |
| 1034 | + | |
| 1035 | + When rewriting images with --optimize-images, set a quality | |
| 1036 | + level from 0 (lowest) to 100 (highest) for writing new images. | |
| 1037 | + Higher quality results in larger images, and lower quality | |
| 1038 | + results in smaller images. This option is only effective when | |
| 1039 | + combined with --optimize-images. | |
| 1040 | + | |
| 1041 | + When rewriting images with :qpdf:ref:`--optimize-images`, set a | |
| 1042 | + quality level from 0 (lowest) to 100 (highest) for writing new | |
| 1043 | + images. Higher quality results in larger images, and lower quality | |
| 1044 | + results in smaller images. Be sure to check your output to see if | |
| 1045 | + the quality is acceptable. This option is only effective when | |
| 1046 | + combined with :qpdf:ref:`--optimize-images`. This option also | |
| 1047 | + causes files that are already compressed with JPEG compression to | |
| 1048 | + be uncompressed and recompressed, potentially introducing | |
| 1049 | + additional loss of image quality. See also :ref:`small-files`. | |
| 1050 | + | |
| 1031 | 1051 | .. qpdf:option:: --normalize-content=[y|n] |
| 1032 | 1052 | |
| 1033 | 1053 | .. help: fix newlines in content streams |
| ... | ... | @@ -4005,6 +4025,13 @@ generate: |
| 4005 | 4025 | lossy, so images may have artifacts. These are not usually |
| 4006 | 4026 | noticeable to the casual observer. |
| 4007 | 4027 | |
| 4028 | +- ``--jpeg-quality=n``: set the JPEG quality used by :qpdf:ref:`--optimize-images` | |
| 4029 | + when writing JPEG files. Use a lower number for ``n`` for smaller, | |
| 4030 | + lower-quality images. The default of most JPEG libraries is 75. | |
| 4031 | + Smaller numbers result in lower-quality but smaller images. The | |
| 4032 | + :qpdf:ref:`--jpeg-quality` option was added in qpdf 12.1. This only | |
| 4033 | + works in combination with :qpdf:ref:`--optimize-images`. | |
| 4034 | + | |
| 4008 | 4035 | - ``--object-streams=generate``: generate object streams, which means |
| 4009 | 4036 | that more of the PDF file's structural content will be compressed |
| 4010 | 4037 | (see :qpdf:ref:`--object-streams`) | ... | ... |
manual/qpdf.1
| ... | ... | @@ -317,6 +317,15 @@ gzip), which is the default compression for most PDF files. |
| 317 | 317 | You need --recompress-flate with this option if you want to |
| 318 | 318 | change already compressed streams. |
| 319 | 319 | .TP |
| 320 | +.B --jpeg-quality \-\- set jpeg quality level for jpeg | |
| 321 | +--jpeg-quality=level | |
| 322 | + | |
| 323 | +When rewriting images with --optimize-images, set a quality | |
| 324 | +level from 0 (lowest) to 100 (highest) for writing new images. | |
| 325 | +Higher quality results in larger images, and lower quality | |
| 326 | +results in smaller images. This option is only effective when | |
| 327 | +combined with --optimize-images. | |
| 328 | +.TP | |
| 320 | 329 | .B --normalize-content \-\- fix newlines in content streams |
| 321 | 330 | --normalize-content=[y|n] |
| 322 | 331 | ... | ... |
manual/release-notes.rst
| ... | ... | @@ -41,14 +41,18 @@ more detail. |
| 41 | 41 | - Library Enhancements |
| 42 | 42 | |
| 43 | 43 | - Add function ``Pl_DCT::make_compress_config`` to return a |
| 44 | - ``Pl_DCT::CompressConfig`` shared pointer from a | |
| 45 | - ``std::function`` for a more modern configuration option. | |
| 44 | + ``Pl_DCT::CompressConfig`` unique pointer to a | |
| 45 | + ``CompressConfig`` from a ``std::function`` for a more modern | |
| 46 | + configuration option. | |
| 46 | 47 | |
| 47 | 48 | - CLI Enhancements |
| 48 | 49 | |
| 49 | 50 | - New :qpdf:ref:`--remove-structure` option to exclude the document |
| 50 | 51 | structure tree from the output PDF. |
| 51 | 52 | |
| 53 | + - New :qpdf:ref:`--jpeg-quality` option to set jpeg quality used | |
| 54 | + with :qpdf:ref:`--optimize-images`. | |
| 55 | + | |
| 52 | 56 | - Other enhancements |
| 53 | 57 | |
| 54 | 58 | - There have been further enhancements to how files with damaged xref | ... | ... |
qpdf/qtest/image-optimization.test
| ... | ... | @@ -37,7 +37,7 @@ my @image_opt = ( |
| 37 | 37 | '--oi-min-width=0 --oi-min-height=0 --oi-min-area=0'] |
| 38 | 38 | ); |
| 39 | 39 | |
| 40 | -my $n_tests = 2 * scalar(@image_opt); | |
| 40 | +my $n_tests = 2 * scalar(@image_opt) + 5; | |
| 41 | 41 | |
| 42 | 42 | foreach my $d (@image_opt) |
| 43 | 43 | { |
| ... | ... | @@ -58,5 +58,34 @@ foreach my $d (@image_opt) |
| 58 | 58 | $td->NORMALIZE_NEWLINES); |
| 59 | 59 | } |
| 60 | 60 | |
| 61 | +$td->runtest("quality = 100", | |
| 62 | + {$td->COMMAND => | |
| 63 | + "qpdf --static-id --optimize-images --jpeg-quality=100" . | |
| 64 | + " large-inline-image.pdf a.pdf"}, | |
| 65 | + {$td->STRING => "", $td->EXIT_STATUS => 0}); | |
| 66 | +$td->runtest("quality = 50", | |
| 67 | + {$td->COMMAND => | |
| 68 | + "qpdf --static-id --optimize-images --jpeg-quality=50" . | |
| 69 | + " large-inline-image.pdf b.pdf"}, | |
| 70 | + {$td->STRING => "", $td->EXIT_STATUS => 0}); | |
| 71 | +$td->runtest("quality = 50 from DCT", | |
| 72 | + {$td->COMMAND => | |
| 73 | + "qpdf --static-id --optimize-images --jpeg-quality=50" . | |
| 74 | + " a.pdf c.pdf"}, | |
| 75 | + {$td->STRING => "", $td->EXIT_STATUS => 0}); | |
| 76 | +my $size100 = (stat("a.pdf"))[7]; | |
| 77 | +my $size50 = (stat("b.pdf"))[7]; | |
| 78 | +my $size50b = (stat("c.pdf"))[7]; | |
| 79 | +my $result = $size50 < $size100 ? "ok\n" : "failed\n"; | |
| 80 | +$td->runtest("quality 50 < quality 100", | |
| 81 | + {$td->STRING => $result}, | |
| 82 | + {$td->STRING => "ok\n"}, | |
| 83 | + $td->NORMALIZE_NEWLINES); | |
| 84 | +$result = $size50b < $size100 ? "ok\n" : "failed\n"; | |
| 85 | +$td->runtest("quality 50 from DCT < quality 100", | |
| 86 | + {$td->STRING => $result}, | |
| 87 | + {$td->STRING => "ok\n"}, | |
| 88 | + $td->NORMALIZE_NEWLINES); | |
| 89 | + | |
| 61 | 90 | cleanup(); |
| 62 | 91 | $td->report($n_tests); | ... | ... |