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 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
... ...
... ... @@ -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&amp; 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&amp; 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&amp;, 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&amp; 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&amp; 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&#39;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(&quot;--mimetype&quot;, &quot;add-attachment&quot;, &quot;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-&gt;ap.addBare(&quot;verbose&quot;, [this](){c_main-&gt;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&quot;({
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
  1 +#include <qpdf/assert_test.h>
  2 +
1 3 #include <qpdf/Pl_DCT.hh>
2 4 #include <qpdf/Pl_StdioFile.hh>
3 5 #include <qpdf/QUtil.hh>
4   -#include <qpdf/assert_test.h>
5 6  
6 7 #include <cstdio>
7 8 #include <cstdlib>
... ...
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);
... ...