image_optim 0.20.2 → 0.21.0
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.
- 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']
|