openstreetmap-image_optim 0.21.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rubocop.yml +65 -0
  4. data/.travis.yml +42 -0
  5. data/CHANGELOG.markdown +272 -0
  6. data/CONTRIBUTING.markdown +10 -0
  7. data/Gemfile +9 -0
  8. data/LICENSE.txt +20 -0
  9. data/README.markdown +344 -0
  10. data/Vagrantfile +33 -0
  11. data/bin/image_optim +28 -0
  12. data/image_optim.gemspec +29 -0
  13. data/lib/image_optim.rb +228 -0
  14. data/lib/image_optim/bin_resolver.rb +144 -0
  15. data/lib/image_optim/bin_resolver/bin.rb +105 -0
  16. data/lib/image_optim/bin_resolver/comparable_condition.rb +60 -0
  17. data/lib/image_optim/bin_resolver/error.rb +6 -0
  18. data/lib/image_optim/bin_resolver/simple_version.rb +31 -0
  19. data/lib/image_optim/cmd.rb +49 -0
  20. data/lib/image_optim/config.rb +205 -0
  21. data/lib/image_optim/configuration_error.rb +3 -0
  22. data/lib/image_optim/handler.rb +57 -0
  23. data/lib/image_optim/hash_helpers.rb +45 -0
  24. data/lib/image_optim/image_meta.rb +25 -0
  25. data/lib/image_optim/image_path.rb +68 -0
  26. data/lib/image_optim/non_negative_integer_range.rb +11 -0
  27. data/lib/image_optim/option_definition.rb +32 -0
  28. data/lib/image_optim/option_helpers.rb +17 -0
  29. data/lib/image_optim/railtie.rb +38 -0
  30. data/lib/image_optim/runner.rb +139 -0
  31. data/lib/image_optim/runner/glob_helpers.rb +45 -0
  32. data/lib/image_optim/runner/option_parser.rb +227 -0
  33. data/lib/image_optim/space.rb +29 -0
  34. data/lib/image_optim/true_false_nil.rb +16 -0
  35. data/lib/image_optim/worker.rb +159 -0
  36. data/lib/image_optim/worker/advpng.rb +35 -0
  37. data/lib/image_optim/worker/class_methods.rb +91 -0
  38. data/lib/image_optim/worker/gifsicle.rb +63 -0
  39. data/lib/image_optim/worker/jhead.rb +43 -0
  40. data/lib/image_optim/worker/jpegoptim.rb +58 -0
  41. data/lib/image_optim/worker/jpegrecompress.rb +44 -0
  42. data/lib/image_optim/worker/jpegtran.rb +46 -0
  43. data/lib/image_optim/worker/optipng.rb +45 -0
  44. data/lib/image_optim/worker/pngcrush.rb +54 -0
  45. data/lib/image_optim/worker/pngout.rb +38 -0
  46. data/lib/image_optim/worker/pngquant.rb +51 -0
  47. data/lib/image_optim/worker/svgo.rb +32 -0
  48. data/script/template/jquery-2.1.3.min.js +4 -0
  49. data/script/template/sortable-0.6.0.min.js +2 -0
  50. data/script/template/worker_analysis.erb +254 -0
  51. data/script/update_worker_options_in_readme +60 -0
  52. data/script/worker_analysis +599 -0
  53. data/spec/image_optim/bin_resolver/comparable_condition_spec.rb +37 -0
  54. data/spec/image_optim/bin_resolver/simple_version_spec.rb +57 -0
  55. data/spec/image_optim/bin_resolver_spec.rb +272 -0
  56. data/spec/image_optim/cmd_spec.rb +66 -0
  57. data/spec/image_optim/config_spec.rb +217 -0
  58. data/spec/image_optim/handler_spec.rb +95 -0
  59. data/spec/image_optim/hash_helpers_spec.rb +76 -0
  60. data/spec/image_optim/image_path_spec.rb +54 -0
  61. data/spec/image_optim/railtie_spec.rb +121 -0
  62. data/spec/image_optim/runner/glob_helpers_spec.rb +25 -0
  63. data/spec/image_optim/runner/option_parser_spec.rb +99 -0
  64. data/spec/image_optim/space_spec.rb +25 -0
  65. data/spec/image_optim/worker_spec.rb +192 -0
  66. data/spec/image_optim_spec.rb +242 -0
  67. data/spec/images/comparison.png +0 -0
  68. data/spec/images/decompressed.jpeg +0 -0
  69. data/spec/images/icecream.gif +0 -0
  70. data/spec/images/image.jpg +0 -0
  71. data/spec/images/invisiblepixels/generate +24 -0
  72. data/spec/images/invisiblepixels/image.png +0 -0
  73. data/spec/images/lena.jpg +0 -0
  74. data/spec/images/orient/0.jpg +0 -0
  75. data/spec/images/orient/1.jpg +0 -0
  76. data/spec/images/orient/2.jpg +0 -0
  77. data/spec/images/orient/3.jpg +0 -0
  78. data/spec/images/orient/4.jpg +0 -0
  79. data/spec/images/orient/5.jpg +0 -0
  80. data/spec/images/orient/6.jpg +0 -0
  81. data/spec/images/orient/7.jpg +0 -0
  82. data/spec/images/orient/8.jpg +0 -0
  83. data/spec/images/orient/generate +23 -0
  84. data/spec/images/orient/original.jpg +0 -0
  85. data/spec/images/quant/64.png +0 -0
  86. data/spec/images/quant/generate +25 -0
  87. data/spec/images/rails.png +0 -0
  88. data/spec/images/test.svg +3 -0
  89. data/spec/images/transparency1.png +0 -0
  90. data/spec/images/transparency2.png +0 -0
  91. data/spec/images/vergroessert.jpg +0 -0
  92. data/spec/spec_helper.rb +64 -0
  93. data/vendor/jpegrescan +143 -0
  94. metadata +308 -0
@@ -0,0 +1,599 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ require 'bundler/setup'
5
+
6
+ require 'image_optim'
7
+ require 'image_optim/cmd'
8
+ require 'progress'
9
+ require 'shellwords'
10
+ require 'gdbm'
11
+ require 'digest'
12
+ require 'erb'
13
+ require 'ostruct'
14
+
15
+ DIR = 'tmp'
16
+ Pathname(DIR).mkpath
17
+
18
+ Array.class_eval do
19
+ # For an array of arrays with possible values yields arrays with all
20
+ # combinations of values
21
+ #
22
+ # [[1, 2], 3, [4, 5]].variants{ |v| p v }
23
+ # # [1, 3, 4]
24
+ # # [1, 3, 5]
25
+ # # [2, 3, 4]
26
+ # # [2, 3, 5]
27
+ def variants(&block)
28
+ if block
29
+ if empty?
30
+ yield([])
31
+ else
32
+ head, *tail = map(&method(:Array))
33
+ head.product(*tail, &block)
34
+ end
35
+ self
36
+ else
37
+ enum_for(:variants)
38
+ end
39
+ end
40
+
41
+ # Sum elements or results of running block on elements
42
+ def sum(initial = 0, &block)
43
+ if block
44
+ reduce(initial){ |memo, item| memo + block[item] }
45
+ else
46
+ reduce(initial, :+)
47
+ end
48
+ end
49
+ end
50
+
51
+ Hash.class_eval do
52
+ # For a hash with arrays of possible values yields hashes with all
53
+ # combinations of keys mapped to value
54
+ #
55
+ # {:a => [1, 2], :b => 3, :c => [4, 5]}.variants{ |v| p v }
56
+ # # {:a=>1, :b=>3, :c=>4}
57
+ # # {:a=>1, :b=>3, :c=>5}
58
+ # # {:a=>2, :b=>3, :c=>4}
59
+ # # {:a=>2, :b=>3, :c=>5}
60
+ def variants
61
+ if block_given?
62
+ if empty?
63
+ yield({})
64
+ else
65
+ keys, values = to_a.transpose
66
+ values.variants do |variant|
67
+ yield Hash[keys.zip(variant)]
68
+ end
69
+ end
70
+ self
71
+ else
72
+ enum_for(:variants)
73
+ end
74
+ end
75
+ end
76
+
77
+ Process.times.class.class_eval do
78
+ def sum
79
+ utime + stime + cutime + cstime
80
+ end
81
+ end
82
+
83
+ ImageOptim::ImagePath.class_eval do
84
+ def shellescape
85
+ to_s.shellescape
86
+ end
87
+
88
+ def digest
89
+ @digest ||= Digest::SHA256.file(to_s).hexdigest
90
+ end
91
+
92
+ def etag
93
+ [mtime, digest]
94
+ end
95
+ end
96
+
97
+ # Analyse efficency of workers
98
+ class Analyser
99
+ Cmd = ImageOptim::Cmd
100
+ HashHelpers = ImageOptim::HashHelpers
101
+
102
+ # Caching entries using GDBM
103
+ class Cache
104
+ DB = GDBM.new("#{DIR}/worker-analysis.db")
105
+
106
+ class << self
107
+ def get(context, key, etag, &block)
108
+ full_key = [context, key]
109
+ if block
110
+ get!(full_key, etag) || set!(full_key, etag, &block)
111
+ else
112
+ get!(full_key, etag)
113
+ end
114
+ end
115
+
116
+ def set(context, key, etag, &block)
117
+ set!([context, key], etag, &block)
118
+ end
119
+
120
+ private
121
+
122
+ def get!(key, etag)
123
+ raw = DB[Marshal.dump(key)]
124
+ return unless raw
125
+ entry = Marshal.load(raw)
126
+ return unless entry[1] == etag
127
+ entry[0]
128
+ end
129
+
130
+ def set!(key, etag, &block)
131
+ value = block.call
132
+ DB[Marshal.dump(key)] = Marshal.dump([value, etag])
133
+ value
134
+ end
135
+ end
136
+ end
137
+
138
+ # Delegate to worker with short id
139
+ class WorkerVariant < DelegateClass(ImageOptim::Worker)
140
+ attr_reader :name, :id, :cons_id
141
+ def initialize(klass, image_optim, options)
142
+ allow_consecutive_on = Array(options.delete(:allow_consecutive_on))
143
+ @image_optim = image_optim
144
+ @name = klass.bin_sym.to_s + options_string(options)
145
+ __setobj__(klass.new(image_optim, options))
146
+ @id = klass.bin_sym.to_s + options_string(self.options)
147
+ @cons_id = [klass, allow_consecutive_on.map{ |key| [key, send(key)] }]
148
+ end
149
+
150
+ def etag
151
+ [
152
+ id,
153
+ bin_versions,
154
+ source_digest,
155
+ ]
156
+ end
157
+
158
+ private
159
+
160
+ def bin_versions
161
+ @bin_versions ||= used_bins.map do |name|
162
+ @image_optim.resolve_bin!(name).to_s
163
+ end
164
+ end
165
+
166
+ def source_digest
167
+ @digest ||= begin
168
+ source_path = __getobj__.method(:optimize).source_location[0]
169
+ Digest::SHA256.file(source_path).hexdigest
170
+ end
171
+ end
172
+
173
+ def options_string(options)
174
+ return '' if options.empty?
175
+ "(#{options.sort.map{ |k, v| "#{k}:#{v.inspect}" }.join(', ')})"
176
+ end
177
+ end
178
+
179
+ # One worker result
180
+ StepResult = Struct.new(*[
181
+ :worker_id,
182
+ :success,
183
+ :time,
184
+ :src_size,
185
+ :dst_size,
186
+ :cache,
187
+ ]) do
188
+ def self.run(src, worker)
189
+ dst = src.temp_path
190
+ start = Process.times.sum
191
+ success = worker.optimize(src, dst)
192
+ time = Process.times.sum - start
193
+
194
+ dst_size = success ? dst.size : nil
195
+ cache = (success ? dst : src).digest.sub(/../, '\0/') + ".#{src.format}"
196
+ result = new(worker.id, success, time, src.size, dst_size, cache)
197
+ if success
198
+ path = result.path
199
+ unless path.exist?
200
+ path.dirname.mkpath
201
+ dst.rename(path)
202
+ end
203
+ end
204
+ result
205
+ end
206
+
207
+ def size
208
+ success ? dst_size : src_size
209
+ end
210
+
211
+ def path
212
+ ImageOptim::ImagePath.convert("#{DIR}/worker-analysis/#{cache}")
213
+ end
214
+
215
+ def inspect
216
+ "<S:#{worker_id} #{success ? '✓' : '✗'} #{time}s #{src_size}→#{dst_size}>"
217
+ end
218
+ end
219
+
220
+ # Chain of workers result
221
+ ChainResult = Struct.new(*[
222
+ :format,
223
+ :steps,
224
+ :difference,
225
+ ]) do
226
+ def worker_ids
227
+ steps.map(&:worker_id)
228
+ end
229
+
230
+ def time
231
+ steps.sum(&:time)
232
+ end
233
+
234
+ def src_size
235
+ steps.first.src_size
236
+ end
237
+
238
+ def dst_size
239
+ steps.last.size
240
+ end
241
+
242
+ def ratio
243
+ dst_size.to_f / src_size
244
+ end
245
+
246
+ def inspect
247
+ "<C #{src_size}→#{dst_size} %:#{difference} #{steps.inspect}>"
248
+ end
249
+ end
250
+
251
+ # Run all possible worker chains
252
+ class WorkerRunner
253
+ def initialize(path, workers)
254
+ @path = ImageOptim::ImagePath.convert(path)
255
+ @workers = workers
256
+ end
257
+
258
+ def results
259
+ results = []
260
+ run_workers(@path, @workers){ |result| results << result }
261
+ run_cache.clear
262
+ results
263
+ end
264
+
265
+ private
266
+
267
+ def run_cache
268
+ @run_cache ||= Hash.new{ |h, k| h[k] = {} }
269
+ end
270
+
271
+ def with_progress(workers, last_result, &block)
272
+ if !last_result || last_result.steps.length < 3
273
+ workers.with_progress(&block)
274
+ else
275
+ workers.each(&block)
276
+ end
277
+ end
278
+
279
+ def run_workers(src, workers, last_result = nil, &block)
280
+ with_progress(workers, last_result) do |worker|
281
+ worker_result, result_image = run_worker(src, worker)
282
+
283
+ steps = (last_result ? last_result.steps : []) + [worker_result]
284
+ chain_result = ChainResult.new(src.format, steps)
285
+ chain_result.difference = difference_with(result_image)
286
+
287
+ block.call(chain_result)
288
+
289
+ workers_left = workers.reject do |w|
290
+ w.cons_id == worker.cons_id || w.run_order < worker.run_order
291
+ end
292
+ run_workers(result_image, workers_left, chain_result, &block)
293
+ end
294
+ end
295
+
296
+ def run_worker(src, worker)
297
+ run_cache[:run][[src.digest, worker.id]] ||= begin
298
+ cache_args = [:result, [src.digest, worker.id], worker.etag]
299
+ result = Cache.get(*cache_args)
300
+ if !result || (result.success && !result.path.exist?)
301
+ result = Cache.set(*cache_args) do
302
+ StepResult.run(src, worker)
303
+ end
304
+ end
305
+ [result, result.success ? result.path : src]
306
+ end
307
+ end
308
+
309
+ def difference_with(other)
310
+ run_cache[:difference][other.digest] ||=
311
+ Cache.get(:difference, [@path.digest, other.digest].sort, nil) do
312
+ images = [flatten_animation(@path), flatten_animation(other)]
313
+
314
+ alpha_presence = images.map do |image|
315
+ Cmd.capture("identify -format '%A' #{image.shellescape}")
316
+ end
317
+ if alpha_presence.uniq.length == 2
318
+ images.map!{ |image| underlay_noise(image) }
319
+ end
320
+
321
+ nrmse_command = %W[
322
+ convert
323
+ #{images[0]} -auto-orient
324
+ #{images[1]} -auto-orient
325
+ -metric RMSE
326
+ -compare
327
+ -format %[distortion]
328
+ info:
329
+ ].shelljoin
330
+ nrmse = Cmd.capture(nrmse_command).to_f
331
+ unless $CHILD_STATUS.success?
332
+ fail "failed comparison of #{@path} with #{other}"
333
+ end
334
+ nrmse
335
+ end
336
+ end
337
+
338
+ def flatten_animation(image)
339
+ run_cache[:flatten][image.digest] ||= begin
340
+ if image.format == :gif
341
+ flattened = image.temp_path
342
+ Cmd.run(*%W[
343
+ convert
344
+ #{image.shellescape}
345
+ -coalesce
346
+ -append
347
+ #{flattened.shellescape}
348
+ ])
349
+ unless $CHILD_STATUS.success?
350
+ fail "failed flattening of #{image}"
351
+ end
352
+ flattened
353
+ else
354
+ image
355
+ end
356
+ end
357
+ end
358
+
359
+ def underlay_noise(image)
360
+ run_cache[:noise][image.digest] ||= begin
361
+ with_noise = image.temp_path
362
+ Cmd.run(*%W[
363
+ convert
364
+ #{image.shellescape}
365
+ +noise Random
366
+ #{image.shellescape}
367
+ -flatten
368
+ -alpha off
369
+ #{with_noise.shellescape}
370
+ ])
371
+ unless $CHILD_STATUS.success?
372
+ fail "failed underlaying noise to #{image}"
373
+ end
374
+ with_noise
375
+ end
376
+ end
377
+ end
378
+
379
+ # Helper for producing statistics
380
+ class Stats
381
+ # Calculate statistics for chain
382
+ class Chain
383
+ attr_reader :worker_stats
384
+ attr_reader :unused_workers
385
+ attr_reader :entry_count
386
+ attr_reader :original_size, :optimized_size, :ratio, :avg_ratio
387
+ attr_reader :avg_difference, :max_difference, :warn_level
388
+ attr_reader :time, :avg_time, :speed
389
+
390
+ def initialize(worker_ids, results, ids2names)
391
+ @worker_stats = build_worker_stats(worker_ids, results, ids2names)
392
+ @unused_workers = worker_stats.any?(&:unused?)
393
+
394
+ @entry_count = results.count
395
+ @original_size = results.sum(&:src_size)
396
+ @optimized_size = results.sum(&:dst_size)
397
+ @ratio = optimized_size.to_f / original_size
398
+ @avg_ratio = results.sum(&:ratio) / results.length
399
+ @avg_difference = results.sum(&:difference) / results.length
400
+ @max_difference = results.map(&:difference).max
401
+ @time = results.sum(&:time)
402
+ @avg_time = time / results.length
403
+
404
+ @warn_level = calculate_warn_level
405
+ @speed = calculate_speed
406
+ end
407
+
408
+ private
409
+
410
+ def build_worker_stats(worker_ids, results, ids2names)
411
+ steps_by_worker_id = results.flat_map(&:steps).group_by(&:worker_id)
412
+ worker_ids.map do |worker_id|
413
+ worker_name = ids2names[worker_id] || worker_id
414
+ Worker.new(worker_name, steps_by_worker_id[worker_id])
415
+ end
416
+ end
417
+
418
+ def calculate_warn_level
419
+ case
420
+ when max_difference >= 0.1 then 'high'
421
+ when max_difference >= 0.01 then 'medium'
422
+ when max_difference >= 0.001 then 'low'
423
+ end
424
+ end
425
+
426
+ def calculate_speed
427
+ case
428
+ when time > 0 then (original_size - optimized_size) / time
429
+ when original_size == optimized_size then 0
430
+ else 1.0 / 0.0
431
+ end
432
+ end
433
+ end
434
+
435
+ # Worker usage
436
+ class Worker
437
+ attr_reader :name
438
+ attr_reader :success_count
439
+ attr_reader :time, :avg_time
440
+ def initialize(name, steps)
441
+ @name = name
442
+ @success_count = steps.count(&:success)
443
+ @time = steps.sum(&:time)
444
+ @avg_time = time / steps.length
445
+ end
446
+
447
+ def unused?
448
+ success_count.zero?
449
+ end
450
+ end
451
+
452
+ attr_reader :name, :results, :ids2names
453
+ def initialize(name, results, ids2names)
454
+ @name = name.to_s
455
+ @results = results
456
+ @ids2names = ids2names
457
+ end
458
+
459
+ def each_chain(&block)
460
+ chains = results.group_by(&:worker_ids).map do |worker_ids, results|
461
+ Chain.new(worker_ids, results, ids2names)
462
+ end
463
+ chains.sort_by!{ |chain| [chain.optimized_size, chain.time] }
464
+ chains.each(&block)
465
+ end
466
+ end
467
+
468
+ def initialize(option_variants)
469
+ option_variants = HashHelpers.deep_symbolise_keys(option_variants)
470
+ image_optim = ImageOptim.new
471
+
472
+ @workers_by_format = Hash.new{ |h, k| h[k] = [] }
473
+ ImageOptim::Worker.klasses.each do |klass|
474
+ worker_options_config = option_variants.delete(klass.bin_sym) || {}
475
+ allow_consecutive_on = worker_options_config.delete(:allow_consecutive_on)
476
+ worker_option_variants = case worker_options_config
477
+ when Array
478
+ worker_options_config
479
+ when Hash
480
+ worker_options_config.variants
481
+ else
482
+ fail "Array or Hash expected, got #{worker_options_config}"
483
+ end
484
+ worker_option_variants.each do |options|
485
+ options = HashHelpers.deep_symbolise_keys(options)
486
+ options[:allow_consecutive_on] = allow_consecutive_on
487
+ worker = WorkerVariant.new(klass, image_optim, options)
488
+ worker.image_formats.each do |format|
489
+ @workers_by_format[format] << worker
490
+ end
491
+ end
492
+ end
493
+
494
+ log_workers_by_format
495
+
496
+ fail "unknown variants: #{option_variants}" unless option_variants.empty?
497
+ end
498
+
499
+ def analyse(paths)
500
+ results = collect_results(paths)
501
+
502
+ template = ERB.new(template_path.read, nil, '>')
503
+ by_format = results.group_by(&:format)
504
+ formats = by_format.keys.sort
505
+ basenames = Hash[formats.map do |format|
506
+ [format, "worker-analysis-#{format}.html"]
507
+ end]
508
+ formats.each do |format|
509
+ stats = Stats.new('all', by_format[format], worker_ids2names)
510
+ path = FSPath("#{DIR}/#{basenames[format]}")
511
+ model = {
512
+ :stats_format => format,
513
+ :stats => stats,
514
+ :format_links => basenames,
515
+ :template_dir => template_path.dirname.relative_path_from(path.dirname),
516
+ }
517
+ html = template.result(OpenStruct.new(model).instance_eval{ binding })
518
+ path.write(html)
519
+ puts "Created #{path}"
520
+ end
521
+ end
522
+
523
+ private
524
+
525
+ def worker_ids2names
526
+ Hash[@workers_by_format.values.flatten.map do |worker|
527
+ [worker.id, worker.name]
528
+ end]
529
+ end
530
+
531
+ def collect_results(paths)
532
+ process_paths(paths).shuffle.with_progress.flat_map do |path|
533
+ WorkerRunner.new(path, workers_for_image(path)).results
534
+ end
535
+ end
536
+
537
+ def process_paths(paths)
538
+ paths = paths.map{ |path| ImageOptim::ImagePath.convert(path) }
539
+ paths.select!{ |path| path.exist? || warn("#{path} doesn't exits") }
540
+ paths.select!{ |path| path.file? || warn("#{path} is not a file") }
541
+ paths.select!{ |path| path.format || warn("#{path} is not an image") }
542
+ paths.select! do |path|
543
+ workers_for_image(path) || warn("#{path} can't be handled by any worker")
544
+ end
545
+ paths
546
+ end
547
+
548
+ def workers_for_image(path)
549
+ @workers_by_format[ImageOptim::ImagePath.convert(path).format]
550
+ end
551
+
552
+ def log_workers_by_format
553
+ @workers_by_format.each do |format, workers|
554
+ puts "#{format}:"
555
+ workers.sort_by.with_index{ |w, i| [w.run_order, i] }.each do |worker|
556
+ puts " #{worker.name} [#{worker.run_order}]"
557
+ end
558
+ end
559
+ end
560
+
561
+ def template_path
562
+ FSPath("#{File.dirname(__FILE__)}/template/#{File.basename(__FILE__)}.erb")
563
+ end
564
+ end
565
+
566
+ def option_variants
567
+ path = '.analysis_variants.yml'
568
+ case h = YAML.load_file(path)
569
+ when Hash then h
570
+ when false then {}
571
+ else abort "expected a hash in #{path}"
572
+ end
573
+ rescue Errno::ENOENT => e
574
+ warn e
575
+ {}
576
+ end
577
+
578
+ analyser = Analyser.new(option_variants)
579
+
580
+ if ARGV.empty?
581
+ abort <<-HELP
582
+ Specify paths for analysis.
583
+
584
+ Example of `.analysis_variants.yml`:
585
+ jpegtran: # 3 worker variants
586
+ - jpegrescan: true
587
+ - progressive: true
588
+ - progressive: false
589
+ optipng: # 6 worker variants by combining options
590
+ level: [6, 7]
591
+ interlace: [true, false, nil]
592
+ gifsicle: # allow variants with different interlace to run consecutively
593
+ allow_consecutive_on: interlace
594
+ interlace: [true, false]
595
+ careful: [true, false]
596
+ # other workers will be used with default options
597
+ HELP
598
+ end
599
+ analyser.analyse(ARGV)