discourse_image_optim 0.24.4

Sign up to get free protection for your applications and to get access to all the features.
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)