openstreetmap-image_optim 0.21.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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)