Commit 021edd02d7625324352c70babe36ba50e2bc314a

Authored by Jay Berkenbilt
1 parent e62f1e4e

Add --jpeg-quality-level flag (fixes #488)

Thanks to github user @cdosborn for the basic enhancement.
include/qpdf/Pl_DCT.hh
@@ -64,7 +64,7 @@ class QPDF_DLL_CLASS Pl_DCT: public Pipeline @@ -64,7 +64,7 @@ class QPDF_DLL_CLASS Pl_DCT: public Pipeline
64 }; 64 };
65 65
66 QPDF_DLL 66 QPDF_DLL
67 - static std::shared_ptr<CompressConfig> 67 + static std::unique_ptr<CompressConfig>
68 make_compress_config(std::function<void(jpeg_compress_struct*)>); 68 make_compress_config(std::function<void(jpeg_compress_struct*)>);
69 69
70 // Constructor for compressing image data 70 // Constructor for compressing image data
include/qpdf/QPDFJob.hh
@@ -634,6 +634,7 @@ class QPDFJob @@ -634,6 +634,7 @@ class QPDFJob
634 bool recompress_flate{false}; 634 bool recompress_flate{false};
635 bool recompress_flate_set{false}; 635 bool recompress_flate_set{false};
636 int compression_level{-1}; 636 int compression_level{-1};
  637 + int jpeg_quality{-1};
637 qpdf_stream_decode_level_e decode_level{qpdf_dl_generalized}; 638 qpdf_stream_decode_level_e decode_level{qpdf_dl_generalized};
638 bool decode_level_set{false}; 639 bool decode_level_set{false};
639 bool normalize_set{false}; 640 bool normalize_set{false};
include/qpdf/auto_job_c_main.hh
@@ -54,6 +54,7 @@ QPDF_DLL Config* verbose(); @@ -54,6 +54,7 @@ QPDF_DLL Config* verbose();
54 QPDF_DLL Config* warningExit0(); 54 QPDF_DLL Config* warningExit0();
55 QPDF_DLL Config* withImages(); 55 QPDF_DLL Config* withImages();
56 QPDF_DLL Config* compressionLevel(std::string const& parameter); 56 QPDF_DLL Config* compressionLevel(std::string const& parameter);
  57 +QPDF_DLL Config* jpegQuality(std::string const& parameter);
57 QPDF_DLL Config* copyEncryption(std::string const& parameter); 58 QPDF_DLL Config* copyEncryption(std::string const& parameter);
58 QPDF_DLL Config* encryptionFilePassword(std::string const& parameter); 59 QPDF_DLL Config* encryptionFilePassword(std::string const& parameter);
59 QPDF_DLL Config* forceVersion(std::string const& parameter); 60 QPDF_DLL Config* forceVersion(std::string const& parameter);
job.sums
@@ -4,17 +4,17 @@ generate_auto_job f64733b79dcee5a0e3e8ccc6976448e8ddf0e8b6529987a66a7d3ab2ebc10a @@ -4,17 +4,17 @@ generate_auto_job f64733b79dcee5a0e3e8ccc6976448e8ddf0e8b6529987a66a7d3ab2ebc10a
4 include/qpdf/auto_job_c_att.hh 4c2b171ea00531db54720bf49a43f8b34481586ae7fb6cbf225099ee42bc5bb4 4 include/qpdf/auto_job_c_att.hh 4c2b171ea00531db54720bf49a43f8b34481586ae7fb6cbf225099ee42bc5bb4
5 include/qpdf/auto_job_c_copy_att.hh 50609012bff14fd82f0649185940d617d05d530cdc522185c7f3920a561ccb42 5 include/qpdf/auto_job_c_copy_att.hh 50609012bff14fd82f0649185940d617d05d530cdc522185c7f3920a561ccb42
6 include/qpdf/auto_job_c_enc.hh 28446f3c32153a52afa239ea40503e6cc8ac2c026813526a349e0cd4ae17ddd5 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 include/qpdf/auto_job_c_pages.hh 09ca15649cc94fdaf6d9bdae28a20723f2a66616bf15aa86d83df31051d82506 8 include/qpdf/auto_job_c_pages.hh 09ca15649cc94fdaf6d9bdae28a20723f2a66616bf15aa86d83df31051d82506
9 include/qpdf/auto_job_c_uo.hh 9c2f98a355858dd54d0bba444b73177a59c9e56833e02fa6406f429c07f39e62 9 include/qpdf/auto_job_c_uo.hh 9c2f98a355858dd54d0bba444b73177a59c9e56833e02fa6406f429c07f39e62
10 -job.yml 9245e70c233dc2067827593403bd9e9feafc5aa0be6b12bb7b99a4b2cab84584 10 +job.yml e136726a6c7e43736b1f75f4de347fa50baf5f38ed1da58647ce2e980751fb29
11 libqpdf/qpdf/auto_job_decl.hh 34ba07d3891c3e5cdd8712f991e508a0652c9db314c5d5bcdf4421b76e6f6e01 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 libqpdf/qpdf/auto_job_json_decl.hh 04965f6321e54b8b3b1dd2ca101d763a22ab44fa81c69e4b6fc0fd6bb7f50f92 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 manual/_ext/qpdf.py 6add6321666031d55ed4aedf7c00e5662bba856dfcd66ccb526563bffefbb580 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 manual/qpdf.1.in 436ecc85d45c4c9e2dbd1725fb7f0177fb627179469f114561adf3cb6cbb677b 20 manual/qpdf.1.in 436ecc85d45c4c9e2dbd1725fb7f0177fb627179469f114561adf3cb6cbb677b
@@ -160,6 +160,7 @@ options: @@ -160,6 +160,7 @@ options:
160 - split-pages 160 - split-pages
161 required_parameter: 161 required_parameter:
162 compression-level: level 162 compression-level: level
  163 + jpeg-quality: level
163 copy-encryption: file 164 copy-encryption: file
164 encryption-file-password: password 165 encryption-file-password: password
165 force-version: version 166 force-version: version
@@ -413,6 +414,7 @@ json: @@ -413,6 +414,7 @@ json:
413 suppress-recovery: 414 suppress-recovery:
414 coalesce-contents: 415 coalesce-contents:
415 compression-level: 416 compression-level:
  417 + jpeg-quality:
416 externalize-inline-images: 418 externalize-inline-images:
417 ii-min-bytes: 419 ii-min-bytes:
418 remove-unreferenced-resources: 420 remove-unreferenced-resources:
libqpdf/Pl_DCT.cc
@@ -424,8 +424,8 @@ Pl_DCT::decompress(void* cinfo_p, Buffer* b) @@ -424,8 +424,8 @@ Pl_DCT::decompress(void* cinfo_p, Buffer* b)
424 next()->finish(); 424 next()->finish();
425 } 425 }
426 426
427 -std::shared_ptr<Pl_DCT::CompressConfig> 427 +std::unique_ptr<Pl_DCT::CompressConfig>
428 Pl_DCT::make_compress_config(std::function<void(jpeg_compress_struct*)> f) 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,6 +45,7 @@ namespace
45 size_t oi_min_width, 45 size_t oi_min_width,
46 size_t oi_min_height, 46 size_t oi_min_height,
47 size_t oi_min_area, 47 size_t oi_min_area,
  48 + int quality,
48 QPDFObjectHandle& image); 49 QPDFObjectHandle& image);
49 ~ImageOptimizer() override = default; 50 ~ImageOptimizer() override = default;
50 void provideStreamData(QPDFObjGen const&, Pipeline* pipeline) override; 51 void provideStreamData(QPDFObjGen const&, Pipeline* pipeline) override;
@@ -56,6 +57,7 @@ namespace @@ -56,6 +57,7 @@ namespace
56 size_t oi_min_width; 57 size_t oi_min_width;
57 size_t oi_min_height; 58 size_t oi_min_height;
58 size_t oi_min_area; 59 size_t oi_min_area;
  60 + qpdf_stream_decode_level_e decode_level{qpdf_dl_specialized};
59 QPDFObjectHandle image; 61 QPDFObjectHandle image;
60 std::shared_ptr<Pl_DCT::CompressConfig> config; 62 std::shared_ptr<Pl_DCT::CompressConfig> config;
61 }; 63 };
@@ -109,6 +111,7 @@ ImageOptimizer::ImageOptimizer( @@ -109,6 +111,7 @@ ImageOptimizer::ImageOptimizer(
109 size_t oi_min_width, 111 size_t oi_min_width,
110 size_t oi_min_height, 112 size_t oi_min_height,
111 size_t oi_min_area, 113 size_t oi_min_area,
  114 + int quality,
112 QPDFObjectHandle& image) : 115 QPDFObjectHandle& image) :
113 o(o), 116 o(o),
114 oi_min_width(oi_min_width), 117 oi_min_width(oi_min_width),
@@ -116,7 +119,12 @@ ImageOptimizer::ImageOptimizer( @@ -116,7 +119,12 @@ ImageOptimizer::ImageOptimizer(
116 oi_min_area(oi_min_area), 119 oi_min_area(oi_min_area),
117 image(image) 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 std::shared_ptr<Pipeline> 130 std::shared_ptr<Pipeline>
@@ -204,7 +212,8 @@ ImageOptimizer::makePipeline(std::string const&amp; description, Pipeline* next) @@ -204,7 +212,8 @@ ImageOptimizer::makePipeline(std::string const&amp; description, Pipeline* next)
204 bool 212 bool
205 ImageOptimizer::evaluate(std::string const& description) 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 QTC::TC("qpdf", "QPDFJob image optimize no pipeline"); 217 QTC::TC("qpdf", "QPDFJob image optimize no pipeline");
209 o.doIfVerbose([&](Pipeline& v, std::string const& prefix) { 218 o.doIfVerbose([&](Pipeline& v, std::string const& prefix) {
210 v << prefix << ": " << description 219 v << prefix << ": " << description
@@ -219,7 +228,7 @@ ImageOptimizer::evaluate(std::string const&amp; description) @@ -219,7 +228,7 @@ ImageOptimizer::evaluate(std::string const&amp; description)
219 // message issued by makePipeline 228 // message issued by makePipeline
220 return false; 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 return false; 232 return false;
224 } 233 }
225 long long orig_length = image.getDict().getKey("/Length").getIntValue(); 234 long long orig_length = image.getDict().getKey("/Length").getIntValue();
@@ -249,7 +258,7 @@ ImageOptimizer::provideStreamData(QPDFObjGen const&amp;, Pipeline* pipeline) @@ -249,7 +258,7 @@ ImageOptimizer::provideStreamData(QPDFObjGen const&amp;, Pipeline* pipeline)
249 pipeline->finish(); 258 pipeline->finish();
250 return; 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 QPDFJob::PageSpec::PageSpec( 264 QPDFJob::PageSpec::PageSpec(
@@ -2196,7 +2205,12 @@ QPDFJob::handleTransformations(QPDF&amp; pdf) @@ -2196,7 +2205,12 @@ QPDFJob::handleTransformations(QPDF&amp; pdf)
2196 [this, pageno, &pdf]( 2205 [this, pageno, &pdf](
2197 QPDFObjectHandle& obj, QPDFObjectHandle& xobj_dict, std::string const& key) { 2206 QPDFObjectHandle& obj, QPDFObjectHandle& xobj_dict, std::string const& key) {
2198 auto io = std::make_unique<ImageOptimizer>( 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 if (io->evaluate("image " + key + " on page " + std::to_string(pageno))) { 2214 if (io->evaluate("image " + key + " on page " + std::to_string(pageno))) {
2201 QPDFObjectHandle new_image = pdf.newStream(); 2215 QPDFObjectHandle new_image = pdf.newStream();
2202 new_image.replaceDict(obj.getDict().shallowCopy()); 2216 new_image.replaceDict(obj.getDict().shallowCopy());
libqpdf/QPDFJob_config.cc
@@ -140,6 +140,13 @@ QPDFJob::Config::compressionLevel(std::string const&amp; parameter) @@ -140,6 +140,13 @@ QPDFJob::Config::compressionLevel(std::string const&amp; parameter)
140 } 140 }
141 141
142 QPDFJob::Config* 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 QPDFJob::Config::copyEncryption(std::string const& parameter) 150 QPDFJob::Config::copyEncryption(std::string const& parameter)
144 { 151 {
145 o.m->encryption_file = parameter; 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,6 +239,14 @@ gzip), which is the default compression for most PDF files.
239 You need --recompress-flate with this option if you want to 239 You need --recompress-flate with this option if you want to
240 change already compressed streams. 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 ap.addOptionHelp("--normalize-content", "transformation", "fix newlines in content streams", R"(--normalize-content=[y|n] 250 ap.addOptionHelp("--normalize-content", "transformation", "fix newlines in content streams", R"(--normalize-content=[y|n]
243 251
244 Normalize newlines to UNIX-style newlines in PDF content 252 Normalize newlines to UNIX-style newlines in PDF content
@@ -284,15 +292,15 @@ version. The version number format is @@ -284,15 +292,15 @@ version. The version number format is
284 to "major.minor" and the extension level, if specified, to 292 to "major.minor" and the extension level, if specified, to
285 "extension-level". 293 "extension-level".
286 )"); 294 )");
  295 +}
  296 +static void add_help_4(QPDFArgParser& ap)
  297 +{
287 ap.addOptionHelp("--force-version", "transformation", "set output PDF version", R"(--force-version=version 298 ap.addOptionHelp("--force-version", "transformation", "set output PDF version", R"(--force-version=version
288 299
289 Force the output PDF file's PDF version header to be the specified 300 Force the output PDF file's PDF version header to be the specified
290 value, even if the file uses features that may not be available 301 value, even if the file uses features that may not be available
291 in that version. 302 in that version.
292 )"); 303 )");
293 -}  
294 -static void add_help_4(QPDFArgParser& ap)  
295 -{  
296 ap.addHelpTopic("page-ranges", "page range syntax", R"(A full description of the page range syntax, with examples, can be 304 ap.addHelpTopic("page-ranges", "page range syntax", R"(A full description of the page range syntax, with examples, can be
297 found in the manual. In summary, a range is a comma-separated list 305 found in the manual. In summary, a range is a comma-separated list
298 of groups. A group is a number or a range of numbers separated by a 306 of groups. A group is a number or a range of numbers separated by a
@@ -419,11 +427,11 @@ Don&#39;t optimize images whose area in pixels is below the specified value. @@ -419,11 +427,11 @@ Don&#39;t optimize images whose area in pixels is below the specified value.
419 )"); 427 )");
420 ap.addOptionHelp("--keep-inline-images", "modification", "exclude inline images from optimization", R"(Prevent inline images from being considered by --optimize-images. 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 static void add_help_5(QPDFArgParser& ap) 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 ap.addOptionHelp("--remove-metadata", "modification", "remove metadata", R"(Exclude metadata from the output file. 435 ap.addOptionHelp("--remove-metadata", "modification", "remove metadata", R"(Exclude metadata from the output file.
428 )"); 436 )");
429 ap.addOptionHelp("--remove-page-labels", "modification", "remove explicit page numbers", R"(Exclude page labels (explicit page numbers) from the output file. 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,12 +650,12 @@ version to be at least 1.6. This option is only available with
642 128-bit encryption. The default is "n" for compatibility 650 128-bit encryption. The default is "n" for compatibility
643 reasons. Use 256-bit encryption instead. 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 static void add_help_6(QPDFArgParser& ap) 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 ap.addOptionHelp("--force-V4", "encryption", "force V=4 in encryption dictionary", R"(This option is for testing and is never needed in practice since 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 qpdf does this automatically when needed. 660 qpdf does this automatically when needed.
653 )"); 661 )");
@@ -825,14 +833,14 @@ ap.addOptionHelp(&quot;--mimetype&quot;, &quot;add-attachment&quot;, &quot;attachment mime type, e.g. app @@ -825,14 +833,14 @@ ap.addOptionHelp(&quot;--mimetype&quot;, &quot;add-attachment&quot;, &quot;attachment mime type, e.g. app
825 Specify the mime type for the attachment, such as text/plain, 833 Specify the mime type for the attachment, such as text/plain,
826 application/pdf, image/png, etc. 834 application/pdf, image/png, etc.
827 )"); 835 )");
  836 +}
  837 +static void add_help_7(QPDFArgParser& ap)
  838 +{
828 ap.addOptionHelp("--description", "add-attachment", "set attachment's description", R"(--description="text" 839 ap.addOptionHelp("--description", "add-attachment", "set attachment's description", R"(--description="text"
829 840
830 Supply descriptive text for the attachment, displayed by some 841 Supply descriptive text for the attachment, displayed by some
831 PDF viewers. 842 PDF viewers.
832 )"); 843 )");
833 -}  
834 -static void add_help_7(QPDFArgParser& ap)  
835 -{  
836 ap.addOptionHelp("--replace", "add-attachment", "replace attachment with same key", R"(Indicate that any existing attachment with the same key should 844 ap.addOptionHelp("--replace", "add-attachment", "replace attachment with same key", R"(Indicate that any existing attachment with the same key should
837 be replaced by the new attachment. Otherwise, qpdf gives an 845 be replaced by the new attachment. Otherwise, qpdf gives an
838 error if an attachment with that key is already present. 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,12 +927,12 @@ object and for each content stream associated with the page.
919 ap.addOptionHelp("--with-images", "inspection", "include image details with --show-pages", R"(When used with --show-pages, also shows the object and 927 ap.addOptionHelp("--with-images", "inspection", "include image details with --show-pages", R"(When used with --show-pages, also shows the object and
920 generation numbers for the image objects on each page. 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 static void add_help_8(QPDFArgParser& ap) 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 ap.addOptionHelp("--show-attachment", "inspection", "export an embedded file", R"(--show-attachment=key 936 ap.addOptionHelp("--show-attachment", "inspection", "export an embedded file", R"(--show-attachment=key
929 937
930 Write the contents of the specified attachment to standard 938 Write the contents of the specified attachment to standard
libqpdf/qpdf/auto_job_init.hh
@@ -94,6 +94,7 @@ this-&gt;ap.addBare(&quot;verbose&quot;, [this](){c_main-&gt;verbose();}); @@ -94,6 +94,7 @@ this-&gt;ap.addBare(&quot;verbose&quot;, [this](){c_main-&gt;verbose();});
94 this->ap.addBare("warning-exit-0", [this](){c_main->warningExit0();}); 94 this->ap.addBare("warning-exit-0", [this](){c_main->warningExit0();});
95 this->ap.addBare("with-images", [this](){c_main->withImages();}); 95 this->ap.addBare("with-images", [this](){c_main->withImages();});
96 this->ap.addRequiredParameter("compression-level", [this](std::string const& x){c_main->compressionLevel(x);}, "level"); 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 this->ap.addRequiredParameter("copy-encryption", [this](std::string const& x){c_main->copyEncryption(x);}, "file"); 98 this->ap.addRequiredParameter("copy-encryption", [this](std::string const& x){c_main->copyEncryption(x);}, "file");
98 this->ap.addRequiredParameter("encryption-file-password", [this](std::string const& x){c_main->encryptionFilePassword(x);}, "password"); 99 this->ap.addRequiredParameter("encryption-file-password", [this](std::string const& x){c_main->encryptionFilePassword(x);}, "password");
99 this->ap.addRequiredParameter("force-version", [this](std::string const& x){c_main->forceVersion(x);}, "version"); 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,6 +314,9 @@ popHandler(); // key: coalesceContents
314 pushKey("compressionLevel"); 314 pushKey("compressionLevel");
315 addParameter([this](std::string const& p) { c_main->compressionLevel(p); }); 315 addParameter([this](std::string const& p) { c_main->compressionLevel(p); });
316 popHandler(); // key: compressionLevel 316 popHandler(); // key: compressionLevel
  317 +pushKey("jpegQuality");
  318 +addParameter([this](std::string const& p) { c_main->jpegQuality(p); });
  319 +popHandler(); // key: jpegQuality
317 pushKey("externalizeInlineImages"); 320 pushKey("externalizeInlineImages");
318 addBare([this]() { c_main->externalizeInlineImages(); }); 321 addBare([this]() { c_main->externalizeInlineImages(); });
319 popHandler(); // key: externalizeInlineImages 322 popHandler(); // key: externalizeInlineImages
libqpdf/qpdf/auto_job_schema.hh
@@ -104,6 +104,7 @@ static constexpr char const* JOB_SCHEMA_DATA = R&quot;({ @@ -104,6 +104,7 @@ static constexpr char const* JOB_SCHEMA_DATA = R&quot;({
104 "suppressRecovery": "suppress error recovery", 104 "suppressRecovery": "suppress error recovery",
105 "coalesceContents": "combine content streams", 105 "coalesceContents": "combine content streams",
106 "compressionLevel": "set compression level for flate", 106 "compressionLevel": "set compression level for flate",
  107 + "jpegQuality": "set jpeg quality level for jpeg",
107 "externalizeInlineImages": "convert inline to regular images", 108 "externalizeInlineImages": "convert inline to regular images",
108 "iiMinBytes": "set minimum size for externalizeInlineImages", 109 "iiMinBytes": "set minimum size for externalizeInlineImages",
109 "removeUnreferencedResources": "remove unreferenced page resources", 110 "removeUnreferencedResources": "remove unreferenced page resources",
libtests/dct_compress.cc
  1 +#include <qpdf/assert_test.h>
  2 +
1 #include <qpdf/Pl_DCT.hh> 3 #include <qpdf/Pl_DCT.hh>
2 #include <qpdf/Pl_StdioFile.hh> 4 #include <qpdf/Pl_StdioFile.hh>
3 #include <qpdf/QUtil.hh> 5 #include <qpdf/QUtil.hh>
4 -#include <qpdf/assert_test.h>  
5 6
6 #include <cstdio> 7 #include <cstdio>
7 #include <cstdlib> 8 #include <cstdlib>
manual/cli.rst
@@ -1028,6 +1028,26 @@ Related Options @@ -1028,6 +1028,26 @@ Related Options
1028 defers to the compression library's default behavior. See also 1028 defers to the compression library's default behavior. See also
1029 :ref:`small-files`. 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 .. qpdf:option:: --normalize-content=[y|n] 1051 .. qpdf:option:: --normalize-content=[y|n]
1032 1052
1033 .. help: fix newlines in content streams 1053 .. help: fix newlines in content streams
@@ -4005,6 +4025,13 @@ generate: @@ -4005,6 +4025,13 @@ generate:
4005 lossy, so images may have artifacts. These are not usually 4025 lossy, so images may have artifacts. These are not usually
4006 noticeable to the casual observer. 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 - ``--object-streams=generate``: generate object streams, which means 4035 - ``--object-streams=generate``: generate object streams, which means
4009 that more of the PDF file's structural content will be compressed 4036 that more of the PDF file's structural content will be compressed
4010 (see :qpdf:ref:`--object-streams`) 4037 (see :qpdf:ref:`--object-streams`)
manual/qpdf.1
@@ -317,6 +317,15 @@ gzip), which is the default compression for most PDF files. @@ -317,6 +317,15 @@ gzip), which is the default compression for most PDF files.
317 You need --recompress-flate with this option if you want to 317 You need --recompress-flate with this option if you want to
318 change already compressed streams. 318 change already compressed streams.
319 .TP 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 .B --normalize-content \-\- fix newlines in content streams 329 .B --normalize-content \-\- fix newlines in content streams
321 --normalize-content=[y|n] 330 --normalize-content=[y|n]
322 331
manual/release-notes.rst
@@ -41,14 +41,18 @@ more detail. @@ -41,14 +41,18 @@ more detail.
41 - Library Enhancements 41 - Library Enhancements
42 42
43 - Add function ``Pl_DCT::make_compress_config`` to return a 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 - CLI Enhancements 48 - CLI Enhancements
48 49
49 - New :qpdf:ref:`--remove-structure` option to exclude the document 50 - New :qpdf:ref:`--remove-structure` option to exclude the document
50 structure tree from the output PDF. 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 - Other enhancements 56 - Other enhancements
53 57
54 - There have been further enhancements to how files with damaged xref 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,7 +37,7 @@ my @image_opt = (
37 '--oi-min-width=0 --oi-min-height=0 --oi-min-area=0'] 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 foreach my $d (@image_opt) 42 foreach my $d (@image_opt)
43 { 43 {
@@ -58,5 +58,34 @@ foreach my $d (@image_opt) @@ -58,5 +58,34 @@ foreach my $d (@image_opt)
58 $td->NORMALIZE_NEWLINES); 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 cleanup(); 90 cleanup();
62 $td->report($n_tests); 91 $td->report($n_tests);