image_optim 0.20.2 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -9,7 +9,7 @@ README_FILE = File.expand_path('../../README.markdown', __FILE__)
9
9
  BEGIN_MARKER = '<!---<worker-options>-->'
10
10
  END_MARKER = '<!---</worker-options>-->'
11
11
  GENERATED_NOTE = '<!-- markdown for worker options is generated by '\
12
- "`#{$PROGRAM_NAME}` -->"
12
+ "`#{Pathname($PROGRAM_NAME).cleanpath}` -->"
13
13
 
14
14
  def write_worker_options(io, klass)
15
15
  io.puts "### :#{klass.bin_sym} =>"
@@ -9,7 +9,8 @@ require 'progress'
9
9
  require 'shellwords'
10
10
  require 'gdbm'
11
11
  require 'digest'
12
- require 'haml'
12
+ require 'erb'
13
+ require 'ostruct'
13
14
 
14
15
  DIR = 'tmp'
15
16
  Pathname(DIR).mkpath
@@ -88,7 +89,7 @@ ImageOptim::ImagePath.class_eval do
88
89
  @digest ||= Digest::SHA256.file(to_s).hexdigest
89
90
  end
90
91
 
91
- def cache_etag
92
+ def etag
92
93
  [mtime, digest]
93
94
  end
94
95
  end
@@ -100,31 +101,26 @@ class Analyser
100
101
 
101
102
  # Caching entries using GDBM
102
103
  class Cache
103
- PATH = "#{DIR}/worker-analysis.db"
104
+ DB = GDBM.new("#{DIR}/worker-analysis.db")
104
105
 
105
106
  class << self
106
- def get(key, etag, &block)
107
+ def get(context, key, etag, &block)
108
+ full_key = [context, key]
107
109
  if block
108
- get!(key, etag) || set!(key, etag, &block)
110
+ get!(full_key, etag) || set!(full_key, etag, &block)
109
111
  else
110
- get!(key, etag)
112
+ get!(full_key, etag)
111
113
  end
112
114
  end
113
115
 
114
- def set(key, etag, &block)
115
- set!(key, etag, &block)
116
+ def set(context, key, etag, &block)
117
+ set!([context, key], etag, &block)
116
118
  end
117
119
 
118
120
  private
119
121
 
120
- def open
121
- GDBM.open(PATH) do |db|
122
- yield db
123
- end
124
- end
125
-
126
122
  def get!(key, etag)
127
- raw = open{ |db| db[Marshal.dump(key)] }
123
+ raw = DB[Marshal.dump(key)]
128
124
  return unless raw
129
125
  entry = Marshal.load(raw)
130
126
  return unless entry[1] == etag
@@ -133,7 +129,7 @@ class Analyser
133
129
 
134
130
  def set!(key, etag, &block)
135
131
  value = block.call
136
- open{ |db| db[Marshal.dump(key)] = Marshal.dump([value, etag]) }
132
+ DB[Marshal.dump(key)] = Marshal.dump([value, etag])
137
133
  value
138
134
  end
139
135
  end
@@ -141,19 +137,17 @@ class Analyser
141
137
 
142
138
  # Delegate to worker with short id
143
139
  class WorkerVariant < DelegateClass(ImageOptim::Worker)
144
- attr_reader :cons_id, :id
140
+ attr_reader :name, :id, :cons_id
145
141
  def initialize(klass, image_optim, options)
146
142
  allow_consecutive_on = Array(options.delete(:allow_consecutive_on))
147
143
  @image_optim = image_optim
148
- @id = klass.bin_sym.to_s
149
- unless options.empty?
150
- @id << "(#{options.map{ |k, v| "#{k}:#{v.inspect}" }.join(', ')})"
151
- end
144
+ @name = klass.bin_sym.to_s + options_string(options)
152
145
  __setobj__(klass.new(image_optim, options))
146
+ @id = klass.bin_sym.to_s + options_string(self.options)
153
147
  @cons_id = [klass, allow_consecutive_on.map{ |key| [key, send(key)] }]
154
148
  end
155
149
 
156
- def cache_etag
150
+ def etag
157
151
  [
158
152
  id,
159
153
  bin_versions,
@@ -175,6 +169,11 @@ class Analyser
175
169
  Digest::SHA256.file(source_path).hexdigest
176
170
  end
177
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
178
177
  end
179
178
 
180
179
  # One worker result
@@ -184,19 +183,35 @@ class Analyser
184
183
  :time,
185
184
  :src_size,
186
185
  :dst_size,
186
+ :cache,
187
187
  ]) do
188
- def self.run(src, dst, worker)
188
+ def self.run(src, worker)
189
+ dst = src.temp_path
189
190
  start = Process.times.sum
190
191
  success = worker.optimize(src, dst)
191
192
  time = Process.times.sum - start
192
193
 
193
- new(worker.id, success, time, src.size, success ? dst.size : nil)
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
194
205
  end
195
206
 
196
207
  def size
197
208
  success ? dst_size : src_size
198
209
  end
199
210
 
211
+ def path
212
+ ImageOptim::ImagePath.convert("#{DIR}/worker-analysis/#{cache}")
213
+ end
214
+
200
215
  def inspect
201
216
  "<S:#{worker_id} #{success ? '✓' : '✗'} #{time}s #{src_size}→#{dst_size}>"
202
217
  end
@@ -241,13 +256,10 @@ class Analyser
241
256
  end
242
257
 
243
258
  def results
244
- cache_etag = [@path.cache_etag, @workers.map(&:cache_etag).sort]
245
- Cache.get(@path.to_s, cache_etag) do
246
- results = []
247
- run_workers(@path, @workers){ |result| results << result }
248
- run_cache.clear
249
- results
250
- end
259
+ results = []
260
+ run_workers(@path, @workers){ |result| results << result }
261
+ run_cache.clear
262
+ results
251
263
  end
252
264
 
253
265
  private
@@ -274,21 +286,29 @@ class Analyser
274
286
 
275
287
  block.call(chain_result)
276
288
 
277
- workers_left = workers.reject{ |w| w.cons_id == worker.cons_id }
289
+ workers_left = workers.reject do |w|
290
+ w.cons_id == worker.cons_id || w.run_order < worker.run_order
291
+ end
278
292
  run_workers(result_image, workers_left, chain_result, &block)
279
293
  end
280
294
  end
281
295
 
282
296
  def run_worker(src, worker)
283
297
  run_cache[:run][[src.digest, worker.id]] ||= begin
284
- dst = src.temp_path
285
- worker_result = StepResult.run(src, dst, worker)
286
- [worker_result, worker_result.success ? dst : src]
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]
287
306
  end
288
307
  end
289
308
 
290
309
  def difference_with(other)
291
- run_cache[:difference][other.digest] ||= begin
310
+ run_cache[:difference][other.digest] ||=
311
+ Cache.get(:difference, [@path.digest, other.digest].sort, nil) do
292
312
  images = [flatten_animation(@path), flatten_animation(other)]
293
313
 
294
314
  alpha_presence = images.map do |image|
@@ -365,13 +385,10 @@ class Analyser
365
385
  attr_reader :entry_count
366
386
  attr_reader :original_size, :optimized_size, :ratio, :avg_ratio
367
387
  attr_reader :avg_difference, :max_difference, :warn_level
368
- attr_reader :time, :speed
388
+ attr_reader :time, :avg_time, :speed
369
389
 
370
- def initialize(worker_ids, results)
371
- steps_by_worker_id = results.flat_map(&:steps).group_by(&:worker_id)
372
- @worker_stats = worker_ids.map do |worker_id|
373
- Worker.new(worker_id, steps_by_worker_id[worker_id])
374
- end
390
+ def initialize(worker_ids, results, ids2names)
391
+ @worker_stats = build_worker_stats(worker_ids, results, ids2names)
375
392
  @unused_workers = worker_stats.any?(&:unused?)
376
393
 
377
394
  @entry_count = results.count
@@ -382,6 +399,7 @@ class Analyser
382
399
  @avg_difference = results.sum(&:difference) / results.length
383
400
  @max_difference = results.map(&:difference).max
384
401
  @time = results.sum(&:time)
402
+ @avg_time = time / results.length
385
403
 
386
404
  @warn_level = calculate_warn_level
387
405
  @speed = calculate_speed
@@ -389,6 +407,14 @@ class Analyser
389
407
 
390
408
  private
391
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
+
392
418
  def calculate_warn_level
393
419
  case
394
420
  when max_difference >= 0.1 then 'high'
@@ -410,9 +436,12 @@ class Analyser
410
436
  class Worker
411
437
  attr_reader :name
412
438
  attr_reader :success_count
439
+ attr_reader :time, :avg_time
413
440
  def initialize(name, steps)
414
441
  @name = name
415
442
  @success_count = steps.count(&:success)
443
+ @time = steps.sum(&:time)
444
+ @avg_time = time / steps.length
416
445
  end
417
446
 
418
447
  def unused?
@@ -420,15 +449,16 @@ class Analyser
420
449
  end
421
450
  end
422
451
 
423
- attr_reader :name, :results
424
- def initialize(name, results)
452
+ attr_reader :name, :results, :ids2names
453
+ def initialize(name, results, ids2names)
425
454
  @name = name.to_s
426
455
  @results = results
456
+ @ids2names = ids2names
427
457
  end
428
458
 
429
459
  def each_chain(&block)
430
460
  chains = results.group_by(&:worker_ids).map do |worker_ids, results|
431
- Chain.new(worker_ids, results)
461
+ Chain.new(worker_ids, results, ids2names)
432
462
  end
433
463
  chains.sort_by!{ |chain| [chain.optimized_size, chain.time] }
434
464
  chains.each(&block)
@@ -455,36 +485,36 @@ class Analyser
455
485
  options = HashHelpers.deep_symbolise_keys(options)
456
486
  options[:allow_consecutive_on] = allow_consecutive_on
457
487
  worker = WorkerVariant.new(klass, image_optim, options)
458
- puts worker.id
459
488
  worker.image_formats.each do |format|
460
489
  @workers_by_format[format] << worker
461
490
  end
462
491
  end
463
492
  end
464
493
 
494
+ log_workers_by_format
495
+
465
496
  fail "unknown variants: #{option_variants}" unless option_variants.empty?
466
497
  end
467
498
 
468
499
  def analyse(paths)
469
- results = process_paths(paths).shuffle.with_progress.flat_map do |path|
470
- WorkerRunner.new(path, workers_for_image(path)).results
471
- end
500
+ results = collect_results(paths)
472
501
 
473
- template = Haml::Engine.new(File.read("#{__FILE__}.haml"))
502
+ template = ERB.new(template_path.read, nil, '>')
474
503
  by_format = results.group_by(&:format)
475
504
  formats = by_format.keys.sort
476
505
  basenames = Hash[formats.map do |format|
477
506
  [format, "worker-analysis-#{format}.html"]
478
507
  end]
479
508
  formats.each do |format|
480
- stats = Stats.new('all', by_format[format])
509
+ stats = Stats.new('all', by_format[format], worker_ids2names)
510
+ path = FSPath("#{DIR}/#{basenames[format]}")
481
511
  model = {
482
512
  :stats_format => format,
483
513
  :stats => stats,
484
514
  :format_links => basenames,
515
+ :template_dir => template_path.dirname.relative_path_from(path.dirname),
485
516
  }
486
- html = template.render(nil, model)
487
- path = FSPath("#{DIR}/#{basenames[format]}")
517
+ html = template.result(OpenStruct.new(model).instance_eval{ binding })
488
518
  path.write(html)
489
519
  puts "Created #{path}"
490
520
  end
@@ -492,6 +522,18 @@ class Analyser
492
522
 
493
523
  private
494
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
+
495
537
  def process_paths(paths)
496
538
  paths = paths.map{ |path| ImageOptim::ImagePath.convert(path) }
497
539
  paths.select!{ |path| path.exist? || warn("#{path} doesn't exits") }
@@ -506,6 +548,19 @@ private
506
548
  def workers_for_image(path)
507
549
  @workers_by_format[ImageOptim::ImagePath.convert(path).format]
508
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
509
564
  end
510
565
 
511
566
  def option_variants
@@ -91,9 +91,9 @@ describe ImageOptim::Config do
91
91
  expect(config.for_worker(Abc)).to eq(:option => true)
92
92
  end
93
93
 
94
- it 'returns passed false' do
94
+ it 'returns {:disable => true} for false' do
95
95
  config = IOConfig.new(:abc => false)
96
- expect(config.for_worker(Abc)).to eq(false)
96
+ expect(config.for_worker(Abc)).to eq(:disable => true)
97
97
  end
98
98
 
99
99
  it 'raises on unknown option' do
@@ -0,0 +1,121 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'ImageOptim::Railtie' do
4
+ require 'rails/all'
5
+ require 'image_optim/railtie'
6
+
7
+ def init_rails_app
8
+ Class.new(Rails::Application) do
9
+ # Rails 4 requires application class to have name
10
+ def self.name
11
+ 'Dummy'
12
+ end
13
+
14
+ config.active_support.deprecation = :stderr
15
+ config.eager_load = false
16
+
17
+ config.logger = Logger.new('/dev/null')
18
+
19
+ config.assets.tap do |assets|
20
+ assets.enabled = true
21
+ assets.version = '1.0'
22
+ assets.cache_store = :null_store
23
+ assets.paths = %w[spec/images]
24
+
25
+ assets.delete(:compress)
26
+ assets.delete(:image_optim)
27
+ end
28
+
29
+ yield config if block_given?
30
+ end.initialize!
31
+ end
32
+
33
+ after do
34
+ Rails.application = nil
35
+ end
36
+
37
+ describe :initialization do
38
+ it 'initializes by default' do
39
+ expect(ImageOptim).to receive(:new)
40
+ init_rails_app
41
+ end
42
+
43
+ it 'initializes if config.assets.image_optim is nil' do
44
+ expect(ImageOptim).to receive(:new)
45
+ init_rails_app do |config|
46
+ config.assets.image_optim = nil
47
+ end
48
+ end
49
+
50
+ it 'does not initialize if config.assets.image_optim is false' do
51
+ expect(ImageOptim).not_to receive(:new)
52
+ init_rails_app do |config|
53
+ config.assets.image_optim = false
54
+ end
55
+ end
56
+
57
+ it 'does not initialize if config.assets.compress is false' do
58
+ expect(ImageOptim).not_to receive(:new)
59
+ init_rails_app do |config|
60
+ config.assets.compress = false
61
+ end
62
+ end
63
+
64
+ describe 'options' do
65
+ it 'initializes with empty hash by default' do
66
+ expect(ImageOptim).to receive(:new).with({})
67
+ init_rails_app
68
+ end
69
+
70
+ it 'initializes with empty hash if config.assets.image_optim is true' do
71
+ expect(ImageOptim).to receive(:new).with({}).and_call_original
72
+ init_rails_app do |config|
73
+ config.assets.image_optim = true
74
+ end
75
+ end
76
+
77
+ it 'initializes with empty hash if config.assets.image_optim is nil' do
78
+ expect(ImageOptim).to receive(:new).with({}).and_call_original
79
+ init_rails_app do |config|
80
+ config.assets.image_optim = nil
81
+ end
82
+ end
83
+
84
+ it 'initializes with hash assigned to config.assets.image_optim' do
85
+ hash = double
86
+ expect(ImageOptim).to receive(:new).with(hash)
87
+ init_rails_app do |config|
88
+ config.assets.image_optim = hash
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ describe :assets do
95
+ before do
96
+ stub_const('ImagePath', ImageOptim::ImagePath)
97
+ end
98
+
99
+ %w[
100
+ icecream.gif
101
+ lena.jpg
102
+ rails.png
103
+ test.svg
104
+ ].each do |asset_name|
105
+ it "optimizes #{asset_name}" do
106
+ asset = init_rails_app.assets.find_asset(asset_name)
107
+
108
+ asset_data = asset.source
109
+ original = ImagePath.convert(asset.pathname)
110
+
111
+ expect(asset_data).to be_smaller_than(original)
112
+
113
+ ImagePath.temp_file_path %W[spec .#{original.format}] do |temp|
114
+ temp.write(asset_data)
115
+
116
+ expect(temp).to be_similar_to(original, 0)
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end if ENV['RAILS_VERSION']