image_optim 0.17.1 → 0.18.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/.travis.yml +5 -18
- data/CHANGELOG.markdown +10 -0
- data/README.markdown +31 -1
- data/bin/image_optim +3 -137
- data/image_optim.gemspec +6 -3
- data/lib/image_optim.rb +20 -3
- data/lib/image_optim/bin_resolver.rb +28 -1
- data/lib/image_optim/bin_resolver/bin.rb +17 -7
- data/lib/image_optim/cmd.rb +49 -0
- data/lib/image_optim/config.rb +64 -4
- data/lib/image_optim/image_path.rb +5 -0
- data/lib/image_optim/option_definition.rb +5 -3
- data/lib/image_optim/runner.rb +1 -2
- data/lib/image_optim/runner/option_parser.rb +216 -0
- data/lib/image_optim/worker.rb +32 -17
- data/lib/image_optim/worker/advpng.rb +7 -1
- data/lib/image_optim/worker/gifsicle.rb +16 -3
- data/lib/image_optim/worker/jhead.rb +15 -8
- data/lib/image_optim/worker/jpegoptim.rb +6 -2
- data/lib/image_optim/worker/jpegtran.rb +10 -3
- data/lib/image_optim/worker/optipng.rb +6 -1
- data/lib/image_optim/worker/pngcrush.rb +8 -1
- data/lib/image_optim/worker/pngout.rb +8 -1
- data/lib/image_optim/worker/svgo.rb +4 -1
- data/script/worker_analysis +523 -0
- data/script/worker_analysis.haml +153 -0
- data/spec/image_optim/bin_resolver/comparable_condition_spec.rb +4 -5
- data/spec/image_optim/bin_resolver/simple_version_spec.rb +44 -21
- data/spec/image_optim/bin_resolver_spec.rb +63 -29
- data/spec/image_optim/cmd_spec.rb +66 -0
- data/spec/image_optim/config_spec.rb +38 -38
- data/spec/image_optim/handler_spec.rb +15 -12
- data/spec/image_optim/hash_helpers_spec.rb +14 -13
- data/spec/image_optim/image_path_spec.rb +22 -7
- data/spec/image_optim/runner/glob_helpers_spec.rb +6 -5
- data/spec/image_optim/runner/option_parser_spec.rb +99 -0
- data/spec/image_optim/space_spec.rb +5 -4
- data/spec/image_optim/worker_spec.rb +6 -5
- data/spec/image_optim_spec.rb +209 -237
- data/spec/spec_helper.rb +3 -0
- metadata +43 -11
data/lib/image_optim/worker.rb
CHANGED
@@ -4,21 +4,24 @@ require 'image_optim/bin_resolver/error'
|
|
4
4
|
require 'image_optim/configuration_error'
|
5
5
|
require 'image_optim/option_definition'
|
6
6
|
require 'image_optim/option_helpers'
|
7
|
+
require 'image_optim/cmd'
|
7
8
|
require 'shellwords'
|
8
9
|
require 'English'
|
9
10
|
|
10
11
|
class ImageOptim
|
11
12
|
# Base class for all workers
|
12
13
|
class Worker
|
14
|
+
@klasses = []
|
15
|
+
|
13
16
|
class << self
|
14
17
|
# List of available workers
|
15
18
|
def klasses
|
16
|
-
@klasses
|
19
|
+
@klasses.to_enum
|
17
20
|
end
|
18
21
|
|
19
22
|
# Remember all classes inheriting from this one
|
20
23
|
def inherited(base)
|
21
|
-
klasses << base
|
24
|
+
@klasses << base
|
22
25
|
end
|
23
26
|
|
24
27
|
# Underscored class name symbol
|
@@ -36,7 +39,7 @@ class ImageOptim
|
|
36
39
|
def option(name, default, type, description = nil, &proc)
|
37
40
|
attr_reader name
|
38
41
|
option_definitions <<
|
39
|
-
|
42
|
+
OptionDefinition.new(name, default, type, description, &proc)
|
40
43
|
end
|
41
44
|
|
42
45
|
# Initialize all workers using options from calling options_proc with
|
@@ -56,6 +59,18 @@ class ImageOptim
|
|
56
59
|
return if errors.empty?
|
57
60
|
fail BinResolver::Error, ['Bin resolving errors:', *errors].join("\n")
|
58
61
|
end
|
62
|
+
|
63
|
+
# Resolve all bins of all workers showing warning for missing ones and
|
64
|
+
# returning others
|
65
|
+
def reject_missing(workers)
|
66
|
+
resolved = []
|
67
|
+
errors = BinResolver.collect_errors(workers) do |worker|
|
68
|
+
worker.resolve_used_bins!
|
69
|
+
resolved << worker
|
70
|
+
end
|
71
|
+
errors.each{ |error| warn error }
|
72
|
+
resolved
|
73
|
+
end
|
59
74
|
end
|
60
75
|
|
61
76
|
# Configure (raises on extra options)
|
@@ -81,9 +96,11 @@ class ImageOptim
|
|
81
96
|
|
82
97
|
# Return hash with worker options
|
83
98
|
def options
|
84
|
-
|
85
|
-
|
99
|
+
hash = {}
|
100
|
+
self.class.option_definitions.each do |option|
|
101
|
+
hash[option.name] = send(option.name)
|
86
102
|
end
|
103
|
+
hash
|
87
104
|
end
|
88
105
|
|
89
106
|
# Optimize image at src, output at dst, must be overriden in subclass
|
@@ -111,7 +128,7 @@ class ImageOptim
|
|
111
128
|
[self.class.bin_sym]
|
112
129
|
end
|
113
130
|
|
114
|
-
# Resolve used bins, raise exception
|
131
|
+
# Resolve used bins, raise exception concatenating all messages
|
115
132
|
def resolve_used_bins!
|
116
133
|
errors = BinResolver.collect_errors(used_bins) do |bin|
|
117
134
|
@image_optim.resolve_bin!(bin)
|
@@ -125,6 +142,14 @@ class ImageOptim
|
|
125
142
|
dst.size? && dst.size < src.size
|
126
143
|
end
|
127
144
|
|
145
|
+
# Short inspect
|
146
|
+
def inspect
|
147
|
+
options_string = options.map do |name, value|
|
148
|
+
" @#{name}=#{value.inspect}"
|
149
|
+
end.join(',')
|
150
|
+
"#<#{self.class}#{options_string}>"
|
151
|
+
end
|
152
|
+
|
128
153
|
private
|
129
154
|
|
130
155
|
def assert_no_unknown_options!(options)
|
@@ -179,17 +204,7 @@ class ImageOptim
|
|
179
204
|
nice -n #{@image_optim.nice}
|
180
205
|
#{command} > /dev/null 2>&1
|
181
206
|
].join(' ')
|
182
|
-
|
183
|
-
|
184
|
-
status = $CHILD_STATUS
|
185
|
-
if status.signaled?
|
186
|
-
# jruby does not differ non zero exit status and signal number
|
187
|
-
unless defined?(JRUBY_VERSION) && status.exitstatus == status.termsig
|
188
|
-
fail SignalException, status.termsig
|
189
|
-
end
|
190
|
-
end
|
191
|
-
|
192
|
-
success
|
207
|
+
Cmd.run full_command
|
193
208
|
end
|
194
209
|
end
|
195
210
|
end
|
@@ -7,10 +7,20 @@ class ImageOptim
|
|
7
7
|
INTERLACE_OPTION =
|
8
8
|
option(:interlace, false, 'Turn interlacing on'){ |v| !!v }
|
9
9
|
|
10
|
+
LEVEL_OPTION =
|
11
|
+
option(:level, 3, 'Compression level: '\
|
12
|
+
'`1` - light and fast, '\
|
13
|
+
'`2` - normal, '\
|
14
|
+
'`3` - heavy (slower)') do |v|
|
15
|
+
OptionHelpers.limit_with_range(v.to_i, 1..3)
|
16
|
+
end
|
17
|
+
|
18
|
+
CAREFUL_OPTION =
|
19
|
+
option(:careful, false, 'Avoid bugs with some software'){ |v| !!v }
|
20
|
+
|
10
21
|
def optimize(src, dst)
|
11
22
|
args = %W[
|
12
|
-
|
13
|
-
-O3
|
23
|
+
--output=#{dst}
|
14
24
|
--no-comments
|
15
25
|
--no-names
|
16
26
|
--same-delay
|
@@ -19,7 +29,10 @@ class ImageOptim
|
|
19
29
|
--
|
20
30
|
#{src}
|
21
31
|
]
|
22
|
-
|
32
|
+
|
33
|
+
args.unshift('--interlace') if interlace
|
34
|
+
args.unshift('--careful') if careful
|
35
|
+
args.unshift("--optimize=#{level}") if level
|
23
36
|
execute(:gifsicle, *args) && optimized?(src, dst)
|
24
37
|
end
|
25
38
|
end
|
@@ -22,14 +22,21 @@ class ImageOptim
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def optimize(src, dst)
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
25
|
+
return false unless oriented?(src)
|
26
|
+
src.copy(dst)
|
27
|
+
args = %W[
|
28
|
+
-autorot
|
29
|
+
#{dst}
|
30
|
+
]
|
31
|
+
resolve_bin!(:jpegtran)
|
32
|
+
execute(:jhead, *args) && dst.size?
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def oriented?(image)
|
38
|
+
exif = EXIFR::JPEG.new(image.to_s)
|
39
|
+
(2..8).include?(exif.orientation.to_i)
|
33
40
|
end
|
34
41
|
end
|
35
42
|
end
|
@@ -34,11 +34,15 @@ class ImageOptim
|
|
34
34
|
|
35
35
|
def optimize(src, dst)
|
36
36
|
src.copy(dst)
|
37
|
-
args = %W[
|
37
|
+
args = %W[
|
38
|
+
--quiet
|
39
|
+
--
|
40
|
+
#{dst}
|
41
|
+
]
|
38
42
|
strip.each do |strip_marker|
|
39
43
|
args.unshift "--strip-#{strip_marker}"
|
40
44
|
end
|
41
|
-
args.unshift "
|
45
|
+
args.unshift "--max=#{max_quality}" if max_quality < 100
|
42
46
|
execute(:jpegoptim, *args) && optimized?(src, dst)
|
43
47
|
end
|
44
48
|
end
|
@@ -23,13 +23,20 @@ class ImageOptim
|
|
23
23
|
|
24
24
|
def optimize(src, dst)
|
25
25
|
if jpegrescan
|
26
|
-
args = %W[
|
26
|
+
args = %W[
|
27
|
+
#{src}
|
28
|
+
#{dst}
|
29
|
+
]
|
27
30
|
args.unshift '-s' unless copy_chunks
|
28
31
|
resolve_bin!(:jpegtran)
|
29
32
|
execute(:jpegrescan, *args) && optimized?(src, dst)
|
30
33
|
else
|
31
|
-
args = %W[
|
32
|
-
|
34
|
+
args = %W[
|
35
|
+
-optimize
|
36
|
+
-outfile #{dst}
|
37
|
+
#{src}
|
38
|
+
]
|
39
|
+
args.unshift '-copy', (copy_chunks ? 'all' : 'none')
|
33
40
|
args.unshift '-progressive' if progressive
|
34
41
|
execute(:jpegtran, *args) && optimized?(src, dst)
|
35
42
|
end
|
@@ -23,7 +23,12 @@ class ImageOptim
|
|
23
23
|
|
24
24
|
def optimize(src, dst)
|
25
25
|
src.copy(dst)
|
26
|
-
args = %W[
|
26
|
+
args = %W[
|
27
|
+
-o #{level}
|
28
|
+
-quiet
|
29
|
+
--
|
30
|
+
#{dst}
|
31
|
+
]
|
27
32
|
args.unshift "-i#{interlace ? 1 : 0}" unless interlace.nil?
|
28
33
|
execute(:optipng, *args) && optimized?(src, dst)
|
29
34
|
end
|
@@ -24,7 +24,14 @@ class ImageOptim
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def optimize(src, dst)
|
27
|
-
args = %W[
|
27
|
+
args = %W[
|
28
|
+
-k#{copy_chunks ? 1 : 0}
|
29
|
+
-s#{strategy}
|
30
|
+
-q
|
31
|
+
-y
|
32
|
+
#{src}
|
33
|
+
#{dst}
|
34
|
+
]
|
28
35
|
execute(:pngout, *args) && optimized?(src, dst)
|
29
36
|
end
|
30
37
|
end
|
@@ -0,0 +1,523 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: UTF-8
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
5
|
+
|
6
|
+
require 'image_optim'
|
7
|
+
require 'image_optim/cmd'
|
8
|
+
require 'progress'
|
9
|
+
require 'shellwords'
|
10
|
+
require 'gdbm'
|
11
|
+
require 'digest'
|
12
|
+
require 'haml'
|
13
|
+
|
14
|
+
DIR = 'tmp'
|
15
|
+
Pathname(DIR).mkpath
|
16
|
+
|
17
|
+
Array.class_eval do
|
18
|
+
# For an array of arrays with possible values yields arrays with all
|
19
|
+
# combinations of values
|
20
|
+
#
|
21
|
+
# [[1, 2], 3, [4, 5]].variants{ |v| p v }
|
22
|
+
# # [1, 3, 4]
|
23
|
+
# # [1, 3, 5]
|
24
|
+
# # [2, 3, 4]
|
25
|
+
# # [2, 3, 5]
|
26
|
+
def variants(&block)
|
27
|
+
if block
|
28
|
+
if empty?
|
29
|
+
yield([])
|
30
|
+
else
|
31
|
+
head, *tail = map(&method(:Array))
|
32
|
+
head.product(*tail, &block)
|
33
|
+
end
|
34
|
+
self
|
35
|
+
else
|
36
|
+
enum_for(:variants)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Sum elements or results of running block on elements
|
41
|
+
def sum(initial = 0, &block)
|
42
|
+
if block
|
43
|
+
reduce(initial){ |memo, item| memo + block[item] }
|
44
|
+
else
|
45
|
+
reduce(initial, :+)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
Hash.class_eval do
|
51
|
+
# For a hash with arrays of possible values yields hashes with all
|
52
|
+
# combinations of keys mapped to value
|
53
|
+
#
|
54
|
+
# {:a => [1, 2], :b => 3, :c => [4, 5]}.variants{ |v| p v }
|
55
|
+
# # {:a=>1, :b=>3, :c=>4}
|
56
|
+
# # {:a=>1, :b=>3, :c=>5}
|
57
|
+
# # {:a=>2, :b=>3, :c=>4}
|
58
|
+
# # {:a=>2, :b=>3, :c=>5}
|
59
|
+
def variants
|
60
|
+
if block_given?
|
61
|
+
if empty?
|
62
|
+
yield({})
|
63
|
+
else
|
64
|
+
keys, values = to_a.transpose
|
65
|
+
values.variants do |variant|
|
66
|
+
yield Hash[keys.zip(variant)]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
self
|
70
|
+
else
|
71
|
+
enum_for(:variants)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
Process.times.class.class_eval do
|
77
|
+
def sum
|
78
|
+
utime + stime + cutime + cstime
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
ImageOptim::ImagePath.class_eval do
|
83
|
+
def shellescape
|
84
|
+
to_s.shellescape
|
85
|
+
end
|
86
|
+
|
87
|
+
def digest
|
88
|
+
@digest ||= Digest::SHA256.file(to_s).hexdigest
|
89
|
+
end
|
90
|
+
|
91
|
+
def cache_etag
|
92
|
+
[mtime, digest]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Analyse efficency of workers
|
97
|
+
class Analyser
|
98
|
+
Cmd = ImageOptim::Cmd
|
99
|
+
HashHelpers = ImageOptim::HashHelpers
|
100
|
+
|
101
|
+
# Caching entries using GDBM
|
102
|
+
class Cache
|
103
|
+
PATH = "#{DIR}/worker-analysis.db"
|
104
|
+
|
105
|
+
class << self
|
106
|
+
def get(key, etag, &block)
|
107
|
+
if block
|
108
|
+
get!(key, etag) || set!(key, etag, &block)
|
109
|
+
else
|
110
|
+
get!(key, etag)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def set(key, etag, &block)
|
115
|
+
set!(key, etag, &block)
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def open
|
121
|
+
GDBM.open(PATH) do |db|
|
122
|
+
yield db
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def get!(key, etag)
|
127
|
+
raw = open{ |db| db[Marshal.dump(key)] }
|
128
|
+
return unless raw
|
129
|
+
entry = Marshal.load(raw)
|
130
|
+
return unless entry[1] == etag
|
131
|
+
entry[0]
|
132
|
+
end
|
133
|
+
|
134
|
+
def set!(key, etag, &block)
|
135
|
+
value = block.call
|
136
|
+
open{ |db| db[Marshal.dump(key)] = Marshal.dump([value, etag]) }
|
137
|
+
value
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Delegate to worker with short id
|
143
|
+
class WorkerVariant < DelegateClass(ImageOptim::Worker)
|
144
|
+
attr_reader :klass, :id
|
145
|
+
def initialize(klass, image_optim, options)
|
146
|
+
@klass = klass
|
147
|
+
@image_optim = image_optim
|
148
|
+
@id = "#{klass.bin_sym}#{options unless options.empty?}"
|
149
|
+
__setobj__(klass.new(image_optim, options))
|
150
|
+
end
|
151
|
+
|
152
|
+
def cache_etag
|
153
|
+
[
|
154
|
+
id,
|
155
|
+
bin_versions,
|
156
|
+
source_digest,
|
157
|
+
]
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
def bin_versions
|
163
|
+
@bin_versions ||= used_bins.map do |name|
|
164
|
+
@image_optim.resolve_bin!(name).to_s
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def source_digest
|
169
|
+
@digest ||= begin
|
170
|
+
source_path = __getobj__.method(:optimize).source_location[0]
|
171
|
+
Digest::SHA256.file(source_path).hexdigest
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# One worker result
|
177
|
+
StepResult = Struct.new(*[
|
178
|
+
:worker_id,
|
179
|
+
:success,
|
180
|
+
:time,
|
181
|
+
:src_size,
|
182
|
+
:dst_size,
|
183
|
+
]) do
|
184
|
+
def self.run(src, dst, worker)
|
185
|
+
start = Process.times.sum
|
186
|
+
success = worker.optimize(src, dst)
|
187
|
+
time = Process.times.sum - start
|
188
|
+
|
189
|
+
new(worker.id, success, time, src.size, success ? dst.size : nil)
|
190
|
+
end
|
191
|
+
|
192
|
+
def size
|
193
|
+
success ? dst_size : src_size
|
194
|
+
end
|
195
|
+
|
196
|
+
def inspect
|
197
|
+
"<S:#{worker_id} #{success ? '✓' : '✗'} #{time}s #{src_size}→#{dst_size}>"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Chain of workers result
|
202
|
+
ChainResult = Struct.new(*[
|
203
|
+
:format,
|
204
|
+
:steps,
|
205
|
+
:difference,
|
206
|
+
]) do
|
207
|
+
def worker_ids
|
208
|
+
steps.map(&:worker_id)
|
209
|
+
end
|
210
|
+
|
211
|
+
def time
|
212
|
+
steps.sum(&:time)
|
213
|
+
end
|
214
|
+
|
215
|
+
def src_size
|
216
|
+
steps.first.src_size
|
217
|
+
end
|
218
|
+
|
219
|
+
def dst_size
|
220
|
+
steps.last.size
|
221
|
+
end
|
222
|
+
|
223
|
+
def ratio
|
224
|
+
dst_size.to_f / src_size
|
225
|
+
end
|
226
|
+
|
227
|
+
def inspect
|
228
|
+
"<C #{src_size}→#{dst_size} %:#{difference} #{steps.inspect}>"
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# Run all possible worker chains
|
233
|
+
class WorkerRunner
|
234
|
+
def initialize(path, workers)
|
235
|
+
@path = ImageOptim::ImagePath.convert(path)
|
236
|
+
@workers = workers
|
237
|
+
end
|
238
|
+
|
239
|
+
def results
|
240
|
+
cache_etag = [@path.cache_etag, @workers.map(&:cache_etag).sort]
|
241
|
+
Cache.get(@path.to_s, cache_etag) do
|
242
|
+
results = []
|
243
|
+
run_workers(@path, @workers){ |result| results << result }
|
244
|
+
run_cache.clear
|
245
|
+
results
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
private
|
250
|
+
|
251
|
+
def run_cache
|
252
|
+
@run_cache ||= Hash.new{ |h, k| h[k] = {} }
|
253
|
+
end
|
254
|
+
|
255
|
+
def with_progress(workers, last_result, &block)
|
256
|
+
if !last_result || last_result.steps.length < 3
|
257
|
+
workers.with_progress(&block)
|
258
|
+
else
|
259
|
+
workers.each(&block)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def run_workers(src, workers, last_result = nil, &block)
|
264
|
+
with_progress(workers, last_result) do |worker|
|
265
|
+
worker_result, result_image = run_worker(src, worker)
|
266
|
+
|
267
|
+
steps = (last_result ? last_result.steps : []) + [worker_result]
|
268
|
+
chain_result = ChainResult.new(src.format, steps)
|
269
|
+
chain_result.difference = difference_with(result_image)
|
270
|
+
|
271
|
+
block.call(chain_result)
|
272
|
+
|
273
|
+
workers_left = workers.reject{ |w| w.klass == worker.klass }
|
274
|
+
run_workers(result_image, workers_left, chain_result, &block)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def run_worker(src, worker)
|
279
|
+
run_cache[:run][[src.digest, worker.id]] ||= begin
|
280
|
+
dst = src.temp_path
|
281
|
+
worker_result = StepResult.run(src, dst, worker)
|
282
|
+
[worker_result, worker_result.success ? dst : src]
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def difference_with(other)
|
287
|
+
run_cache[:difference][other.digest] ||= begin
|
288
|
+
images = [flatten_animation(@path), flatten_animation(other)]
|
289
|
+
|
290
|
+
alpha_presence = images.map do |image|
|
291
|
+
Cmd.capture("identify -format '%A' #{image.shellescape}")
|
292
|
+
end
|
293
|
+
if alpha_presence.uniq.length == 2
|
294
|
+
images.map!{ |image| underlay_noise(image) }
|
295
|
+
end
|
296
|
+
|
297
|
+
nrmse_command = %W[
|
298
|
+
convert
|
299
|
+
#{images[0]} -auto-orient
|
300
|
+
#{images[1]} -auto-orient
|
301
|
+
-metric RMSE
|
302
|
+
-compare
|
303
|
+
-format %[distortion]
|
304
|
+
info:
|
305
|
+
].shelljoin
|
306
|
+
nrmse = Cmd.capture(nrmse_command).to_f
|
307
|
+
unless $CHILD_STATUS.success?
|
308
|
+
fail "failed comparison of #{@path} with #{other}"
|
309
|
+
end
|
310
|
+
nrmse
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
def flatten_animation(image)
|
315
|
+
run_cache[:flatten][image.digest] ||= begin
|
316
|
+
if image.format == :gif
|
317
|
+
flattened = image.temp_path
|
318
|
+
Cmd.run(*%W[
|
319
|
+
convert
|
320
|
+
#{image.shellescape}
|
321
|
+
-coalesce
|
322
|
+
-append
|
323
|
+
#{flattened.shellescape}
|
324
|
+
])
|
325
|
+
unless $CHILD_STATUS.success?
|
326
|
+
fail "failed flattening of #{image}"
|
327
|
+
end
|
328
|
+
flattened
|
329
|
+
else
|
330
|
+
image
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
def underlay_noise(image)
|
336
|
+
run_cache[:noise][image.digest] ||= begin
|
337
|
+
with_noise = image.temp_path
|
338
|
+
Cmd.run(*%W[
|
339
|
+
convert
|
340
|
+
#{image.shellescape}
|
341
|
+
+noise Random
|
342
|
+
#{image.shellescape}
|
343
|
+
-flatten
|
344
|
+
-alpha off
|
345
|
+
#{with_noise.shellescape}
|
346
|
+
])
|
347
|
+
unless $CHILD_STATUS.success?
|
348
|
+
fail "failed underlaying noise to #{image}"
|
349
|
+
end
|
350
|
+
with_noise
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
# Helper for producing statistics
|
356
|
+
class Stats
|
357
|
+
# Calculate statistics for chain
|
358
|
+
class Chain
|
359
|
+
attr_reader :worker_stats
|
360
|
+
attr_reader :unused_workers
|
361
|
+
attr_reader :entry_count
|
362
|
+
attr_reader :original_size, :optimized_size, :ratio, :avg_ratio
|
363
|
+
attr_reader :avg_difference, :max_difference, :warn_level
|
364
|
+
attr_reader :time, :speed
|
365
|
+
|
366
|
+
def initialize(worker_ids, results)
|
367
|
+
steps_by_worker_id = results.flat_map(&:steps).group_by(&:worker_id)
|
368
|
+
@worker_stats = worker_ids.map do |worker_id|
|
369
|
+
Worker.new(worker_id, steps_by_worker_id[worker_id])
|
370
|
+
end
|
371
|
+
@unused_workers = worker_stats.any?(&:unused?)
|
372
|
+
|
373
|
+
@entry_count = results.count
|
374
|
+
@original_size = results.sum(&:src_size)
|
375
|
+
@optimized_size = results.sum(&:dst_size)
|
376
|
+
@ratio = optimized_size.to_f / original_size
|
377
|
+
@avg_ratio = results.sum(&:ratio) / results.length
|
378
|
+
@avg_difference = results.sum(&:difference) / results.length
|
379
|
+
@max_difference = results.map(&:difference).max
|
380
|
+
@warn_level = case
|
381
|
+
when max_difference >= 0.1 then 'high'
|
382
|
+
when max_difference >= 0.01 then 'medium'
|
383
|
+
when max_difference >= 0.001 then 'low'
|
384
|
+
end
|
385
|
+
@time = results.sum(&:time)
|
386
|
+
@speed = case
|
387
|
+
when time > 0 then (original_size - optimized_size) / time
|
388
|
+
when original_size == optimized_size then 0
|
389
|
+
else 1.0 / 0.0
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
# Worker usage
|
395
|
+
class Worker
|
396
|
+
attr_reader :name
|
397
|
+
attr_reader :success_count
|
398
|
+
def initialize(name, steps)
|
399
|
+
@name = name
|
400
|
+
@success_count = steps.count(&:success)
|
401
|
+
end
|
402
|
+
|
403
|
+
def unused?
|
404
|
+
success_count.zero?
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
attr_reader :name, :results
|
409
|
+
def initialize(name, results)
|
410
|
+
@name = name.to_s
|
411
|
+
@results = results
|
412
|
+
end
|
413
|
+
|
414
|
+
def each_chain(&block)
|
415
|
+
chains = results.group_by(&:worker_ids).map do |worker_ids, results|
|
416
|
+
Chain.new(worker_ids, results)
|
417
|
+
end
|
418
|
+
chains.sort_by!{ |chain| [chain.optimized_size, chain.time] }
|
419
|
+
chains.each(&block)
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
def initialize(option_variants)
|
424
|
+
option_variants = HashHelpers.deep_symbolise_keys(option_variants)
|
425
|
+
image_optim = ImageOptim.new
|
426
|
+
|
427
|
+
@workers_by_format = Hash.new{ |h, k| h[k] = [] }
|
428
|
+
ImageOptim::Worker.klasses.each do |klass|
|
429
|
+
worker_options_config = option_variants.delete(klass.bin_sym) || {}
|
430
|
+
worker_option_variants = case worker_options_config
|
431
|
+
when Array
|
432
|
+
worker_options_config
|
433
|
+
when Hash
|
434
|
+
worker_options_config.variants
|
435
|
+
else
|
436
|
+
fail "Array or Hash expected, got #{worker_options_config}"
|
437
|
+
end
|
438
|
+
worker_option_variants.each do |options|
|
439
|
+
options = HashHelpers.deep_symbolise_keys(options)
|
440
|
+
worker = WorkerVariant.new(klass, image_optim, options)
|
441
|
+
puts worker.id
|
442
|
+
worker.image_formats.each do |format|
|
443
|
+
@workers_by_format[format] << worker
|
444
|
+
end
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
fail "unknown variants: #{option_variants}" unless option_variants.empty?
|
449
|
+
end
|
450
|
+
|
451
|
+
def analyse(paths)
|
452
|
+
results = process_paths(paths).shuffle.with_progress.flat_map do |path|
|
453
|
+
WorkerRunner.new(path, workers_for_image(path)).results
|
454
|
+
end
|
455
|
+
|
456
|
+
template = Haml::Engine.new(File.read("#{__FILE__}.haml"))
|
457
|
+
by_format = results.group_by(&:format)
|
458
|
+
formats = by_format.keys.sort
|
459
|
+
basenames = Hash[formats.map do |format|
|
460
|
+
[format, "worker-analysis-#{format}.html"]
|
461
|
+
end]
|
462
|
+
formats.each do |format|
|
463
|
+
stats = Stats.new('all', by_format[format])
|
464
|
+
model = {
|
465
|
+
:stats_format => format,
|
466
|
+
:stats => stats,
|
467
|
+
:format_links => basenames,
|
468
|
+
}
|
469
|
+
html = template.render(nil, model)
|
470
|
+
path = FSPath("#{DIR}/#{basenames[format]}")
|
471
|
+
path.write(html)
|
472
|
+
puts "Created #{path}"
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
private
|
477
|
+
|
478
|
+
def process_paths(paths)
|
479
|
+
paths = paths.map{ |path| ImageOptim::ImagePath.convert(path) }
|
480
|
+
paths.select!{ |path| path.exist? || warn("#{path} doesn't exits") }
|
481
|
+
paths.select!{ |path| path.file? || warn("#{path} is not a file") }
|
482
|
+
paths.select!{ |path| path.format || warn("#{path} is not an image") }
|
483
|
+
paths.select! do |path|
|
484
|
+
workers_for_image(path) || warn("#{path} can't be handled by any worker")
|
485
|
+
end
|
486
|
+
paths
|
487
|
+
end
|
488
|
+
|
489
|
+
def workers_for_image(path)
|
490
|
+
@workers_by_format[ImageOptim::ImagePath.convert(path).format]
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
def option_variants
|
495
|
+
path = '.analysis_variants.yml'
|
496
|
+
case h = YAML.load_file(path)
|
497
|
+
when Hash then h
|
498
|
+
when false then {}
|
499
|
+
else abort "expected a hash in #{path}"
|
500
|
+
end
|
501
|
+
rescue Errno::ENOENT => e
|
502
|
+
warn e
|
503
|
+
{}
|
504
|
+
end
|
505
|
+
|
506
|
+
analyser = Analyser.new(option_variants)
|
507
|
+
|
508
|
+
if ARGV.empty?
|
509
|
+
abort <<-HELP
|
510
|
+
Specify paths for analysis.
|
511
|
+
|
512
|
+
Example of `.analysis_variants.yml`:
|
513
|
+
jpegtran: # 3 worker variants
|
514
|
+
- jpegrescan: true
|
515
|
+
- progressive: true
|
516
|
+
- progressive: false
|
517
|
+
optipng: # 6 worker variants by combining options
|
518
|
+
level: [6, 7]
|
519
|
+
interlace: [true, false, nil]
|
520
|
+
# other workers will be used with default options
|
521
|
+
HELP
|
522
|
+
end
|
523
|
+
analyser.analyse(ARGV)
|