discourse_image_optim 0.24.4

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