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.
- checksums.yaml +8 -8
- data/.gitignore +1 -0
- data/.rubocop.yml +3 -0
- data/.travis.yml +10 -3
- data/CHANGELOG.markdown +11 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +1 -1
- data/README.markdown +3 -2
- data/Vagrantfile +33 -0
- data/image_optim.gemspec +3 -4
- data/lib/image_optim/config.rb +1 -1
- data/lib/image_optim/runner/option_parser.rb +2 -2
- data/lib/image_optim/worker.rb +22 -17
- data/lib/image_optim/worker/advpng.rb +4 -0
- data/lib/image_optim/worker/class_methods.rb +2 -1
- data/lib/image_optim/worker/gifsicle.rb +4 -4
- data/lib/image_optim/worker/jhead.rb +1 -1
- data/lib/image_optim/worker/jpegoptim.rb +1 -1
- data/lib/image_optim/worker/jpegrecompress.rb +1 -1
- data/lib/image_optim/worker/optipng.rb +8 -0
- data/lib/image_optim/worker/pngcrush.rb +1 -2
- data/lib/image_optim/worker/pngout.rb +1 -2
- data/lib/image_optim/worker/pngquant.rb +1 -2
- data/script/template/jquery-2.1.3.min.js +4 -0
- data/script/template/sortable-0.6.0.min.js +2 -0
- data/script/template/worker_analysis.erb +254 -0
- data/script/update_worker_options_in_readme +1 -1
- data/script/worker_analysis +108 -53
- data/spec/image_optim/config_spec.rb +2 -2
- data/spec/image_optim/railtie_spec.rb +121 -0
- data/spec/image_optim/worker_spec.rb +3 -1
- data/spec/image_optim_spec.rb +0 -52
- data/spec/images/rails.png +0 -0
- data/spec/spec_helper.rb +52 -0
- metadata +20 -21
- data/script/worker_analysis.haml +0 -174
@@ -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} =>"
|
data/script/worker_analysis
CHANGED
@@ -9,7 +9,8 @@ require 'progress'
|
|
9
9
|
require 'shellwords'
|
10
10
|
require 'gdbm'
|
11
11
|
require 'digest'
|
12
|
-
require '
|
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
|
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
|
-
|
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!(
|
110
|
+
get!(full_key, etag) || set!(full_key, etag, &block)
|
109
111
|
else
|
110
|
-
get!(
|
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 =
|
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
|
-
|
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 :
|
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
|
-
@
|
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
|
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,
|
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
|
-
|
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
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
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
|
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
|
-
|
285
|
-
|
286
|
-
|
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] ||=
|
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
|
-
|
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 =
|
470
|
-
WorkerRunner.new(path, workers_for_image(path)).results
|
471
|
-
end
|
500
|
+
results = collect_results(paths)
|
472
501
|
|
473
|
-
template =
|
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.
|
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
|
94
|
+
it 'returns {:disable => true} for false' do
|
95
95
|
config = IOConfig.new(:abc => false)
|
96
|
-
expect(config.for_worker(Abc)).to eq(
|
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']
|