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.
- checksums.yaml +7 -0
- data/.appveyor.yml +46 -0
- data/.gitignore +18 -0
- data/.rubocop.yml +110 -0
- data/.travis.yml +42 -0
- data/CHANGELOG.markdown +316 -0
- data/CONTRIBUTING.markdown +11 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +358 -0
- data/Vagrantfile +38 -0
- data/bin/image_optim +28 -0
- data/image_optim.gemspec +34 -0
- data/lib/image_optim.rb +267 -0
- data/lib/image_optim/bin_resolver.rb +142 -0
- data/lib/image_optim/bin_resolver/bin.rb +115 -0
- data/lib/image_optim/bin_resolver/comparable_condition.rb +60 -0
- data/lib/image_optim/bin_resolver/error.rb +6 -0
- data/lib/image_optim/bin_resolver/simple_version.rb +31 -0
- data/lib/image_optim/cache.rb +72 -0
- data/lib/image_optim/cache_path.rb +16 -0
- data/lib/image_optim/cmd.rb +122 -0
- data/lib/image_optim/config.rb +219 -0
- data/lib/image_optim/configuration_error.rb +3 -0
- data/lib/image_optim/handler.rb +57 -0
- data/lib/image_optim/hash_helpers.rb +45 -0
- data/lib/image_optim/image_meta.rb +20 -0
- data/lib/image_optim/non_negative_integer_range.rb +11 -0
- data/lib/image_optim/optimized_path.rb +25 -0
- data/lib/image_optim/option_definition.rb +38 -0
- data/lib/image_optim/option_helpers.rb +17 -0
- data/lib/image_optim/path.rb +70 -0
- data/lib/image_optim/runner.rb +139 -0
- data/lib/image_optim/runner/glob_helpers.rb +45 -0
- data/lib/image_optim/runner/option_parser.rb +246 -0
- data/lib/image_optim/space.rb +29 -0
- data/lib/image_optim/true_false_nil.rb +16 -0
- data/lib/image_optim/worker.rb +170 -0
- data/lib/image_optim/worker/advpng.rb +37 -0
- data/lib/image_optim/worker/class_methods.rb +107 -0
- data/lib/image_optim/worker/gifsicle.rb +65 -0
- data/lib/image_optim/worker/jhead.rb +47 -0
- data/lib/image_optim/worker/jpegoptim.rb +63 -0
- data/lib/image_optim/worker/jpegrecompress.rb +49 -0
- data/lib/image_optim/worker/jpegtran.rb +48 -0
- data/lib/image_optim/worker/optipng.rb +53 -0
- data/lib/image_optim/worker/pngcrush.rb +56 -0
- data/lib/image_optim/worker/pngout.rb +40 -0
- data/lib/image_optim/worker/pngquant.rb +61 -0
- data/lib/image_optim/worker/svgo.rb +34 -0
- 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 +59 -0
- data/script/worker_analysis +589 -0
- data/spec/image_optim/bin_resolver/comparable_condition_spec.rb +37 -0
- data/spec/image_optim/bin_resolver/simple_version_spec.rb +65 -0
- data/spec/image_optim/bin_resolver_spec.rb +290 -0
- data/spec/image_optim/cache_path_spec.rb +57 -0
- data/spec/image_optim/cache_spec.rb +162 -0
- data/spec/image_optim/cmd_spec.rb +93 -0
- data/spec/image_optim/config_spec.rb +254 -0
- data/spec/image_optim/handler_spec.rb +90 -0
- data/spec/image_optim/hash_helpers_spec.rb +74 -0
- data/spec/image_optim/image_meta_spec.rb +61 -0
- data/spec/image_optim/optimized_path_spec.rb +58 -0
- data/spec/image_optim/option_definition_spec.rb +138 -0
- data/spec/image_optim/option_helpers_spec.rb +25 -0
- data/spec/image_optim/path_spec.rb +103 -0
- data/spec/image_optim/runner/glob_helpers_spec.rb +21 -0
- data/spec/image_optim/runner/option_parser_spec.rb +105 -0
- data/spec/image_optim/space_spec.rb +23 -0
- data/spec/image_optim/worker/optipng_spec.rb +102 -0
- data/spec/image_optim/worker/pngquant_spec.rb +67 -0
- data/spec/image_optim/worker_spec.rb +303 -0
- data/spec/image_optim_spec.rb +259 -0
- data/spec/images/broken_jpeg +1 -0
- data/spec/images/comparison.png +0 -0
- data/spec/images/decompressed.jpeg +0 -0
- data/spec/images/icecream.gif +0 -0
- data/spec/images/image.jpg +0 -0
- data/spec/images/invisiblepixels/generate +24 -0
- data/spec/images/invisiblepixels/image.png +0 -0
- data/spec/images/lena.jpg +0 -0
- data/spec/images/orient/0.jpg +0 -0
- data/spec/images/orient/1.jpg +0 -0
- data/spec/images/orient/2.jpg +0 -0
- data/spec/images/orient/3.jpg +0 -0
- data/spec/images/orient/4.jpg +0 -0
- data/spec/images/orient/5.jpg +0 -0
- data/spec/images/orient/6.jpg +0 -0
- data/spec/images/orient/7.jpg +0 -0
- data/spec/images/orient/8.jpg +0 -0
- data/spec/images/orient/generate +23 -0
- data/spec/images/orient/original.jpg +0 -0
- data/spec/images/quant/64.png +0 -0
- data/spec/images/quant/generate +25 -0
- data/spec/images/rails.png +0 -0
- data/spec/images/test.svg +3 -0
- data/spec/images/transparency1.png +0 -0
- data/spec/images/transparency2.png +0 -0
- data/spec/images/vergroessert.jpg +0 -0
- data/spec/spec_helper.rb +93 -0
- 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)
|