/
image_optim.rb
276 lines (230 loc) · 6.81 KB
/
image_optim.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# frozen_string_literal: true
require 'image_optim/bin_resolver'
require 'image_optim/cache'
require 'image_optim/config'
require 'image_optim/errors'
require 'image_optim/handler'
require 'image_optim/image_meta'
require 'image_optim/optimized_path'
require 'image_optim/path'
require 'image_optim/timer'
require 'image_optim/worker'
require 'in_threads'
require 'shellwords'
%w[
pngcrush pngout advpng optipng pngquant oxipng
jhead jpegoptim jpegrecompress jpegtran
gifsicle
svgo
].each do |worker|
require "image_optim/worker/#{worker}"
end
# Main interface
class ImageOptim
# Nice level
attr_reader :nice
# Number of threads to run with
attr_reader :threads
# Verbose output?
attr_reader :verbose
# Use image_optim_pack
attr_reader :pack
# Skip workers with missing or problematic binaries
attr_reader :skip_missing_workers
# Allow lossy workers and optimizations
attr_reader :allow_lossy
# Cache directory
attr_reader :cache_dir
# Cache worker digests
attr_reader :cache_worker_digests
# Timeout in seconds for each image
attr_reader :timeout
# Initialize workers, specify options using worker underscored name:
#
# pass false to disable worker
#
# ImageOptim.new(:pngcrush => false)
#
# or hash with options to worker
#
# ImageOptim.new(:advpng => {:level => 3}, :optipng => {:level => 2})
#
# use :threads to set number of parallel optimizers to run (passing true or
# nil determines number of processors, false disables parallel processing)
#
# ImageOptim.new(:threads => 8)
#
# use :nice to specify optimizers nice level (true or nil makes it 10, false
# makes it 0)
#
# ImageOptim.new(:nice => 20)
def initialize(options = {})
config = Config.new(options)
@verbose = config.verbose
$stderr << "config:\n#{config.to_s.gsub(/^/, ' ')}" if verbose
%w[
nice
threads
pack
skip_missing_workers
allow_lossy
cache_dir
cache_worker_digests
timeout
].each do |name|
instance_variable_set(:"@#{name}", config.send(name))
$stderr << "#{name}: #{send(name)}\n" if verbose
end
@bin_resolver = BinResolver.new(self)
$stderr << "PATH: #{@bin_resolver.env_path}\n" if verbose
@workers_by_format = Worker.create_all_by_format(self) do |klass|
config.for_worker(klass)
end
@cache = Cache.new(self, @workers_by_format)
log_workers_by_format if verbose
config.assert_no_unused_options!
end
# Get workers for image
def workers_for_image(path)
@workers_by_format[Path.convert(path).image_format]
end
# Optimize one file, return new path as OptimizedPath or nil if
# optimization failed
def optimize_image(original)
original = Path.convert(original)
return unless (workers = workers_for_image(original))
optimized = @cache.fetch(original) do
timer = timeout && Timer.new(timeout)
Handler.for(original) do |handler|
begin
workers.each do |worker|
handler.process do |src, dst|
worker.optimize(src, dst, timeout: timer)
end
end
rescue Errors::TimeoutExceeded
handler.result
end
end
end
return unless optimized
OptimizedPath.new(optimized, original)
end
# Optimize one file in place, return original as OptimizedPath or nil if
# optimization failed
def optimize_image!(original)
original = Path.convert(original)
return unless (result = optimize_image(original))
result.replace(original)
OptimizedPath.new(original, result.original_size)
end
# Optimize image data, return new data or nil if optimization failed
def optimize_image_data(original_data)
format = ImageMeta.format_for_data(original_data)
return unless format
Path.temp_file %W[image_optim .#{format}] do |temp|
temp.binmode
temp.write(original_data)
temp.close
if (result = optimize_image(temp.path))
result.binread
end
end
end
# Optimize multiple images
# if block given yields path and result for each image and returns array of
# yield results
# else return array of path and result pairs
def optimize_images(paths, &block)
run_method_for(paths, :optimize_image, &block)
end
# Optimize multiple images in place
# if block given yields path and result for each image and returns array of
# yield results
# else return array of path and result pairs
def optimize_images!(paths, &block)
run_method_for(paths, :optimize_image!, &block)
end
# Optimize multiple image datas
# if block given yields original and result for each image data and returns
# array of yield results
# else return array of path and result pairs
def optimize_images_data(datas, &block)
run_method_for(datas, :optimize_image_data, &block)
end
class << self
# Optimization methods with default options
def method_missing(method, *args, &block)
if optimize_image_method?(method)
new.send(method, *args, &block)
else
super
end
end
def respond_to_missing?(method, include_private = false)
optimize_image_method?(method) || super
end
# Version of image_optim gem spec loaded
def version
Gem.loaded_specs['image_optim'].version.to_s
rescue
'DEV'
end
# Full version of image_optim
def full_version
"image_optim v#{version}"
end
private
def optimize_image_method?(method)
method_defined?(method) && method.to_s =~ /^optimize_image/
end
end
# Are there workers for file at path?
def optimizable?(path)
!!workers_for_image(path)
end
# Check existance of binary, create symlink if ENV contains path for key
# XXX_BIN where XXX is upper case bin name
def resolve_bin!(bin)
@bin_resolver.resolve!(bin)
end
# Join resolve_dir, default path and vendor path for PATH environment variable
def env_path
@bin_resolver.env_path
end
private
def log_workers_by_format
$stderr << "Workers by format:\n"
@workers_by_format.each do |format, workers|
$stderr << "#{format}:\n"
workers.each do |worker|
$stderr << " #{worker.class.bin_sym}:\n"
worker.options.each do |name, value|
$stderr << " #{name}: #{value.inspect}\n"
end
end
end
end
# Run method for each item in list
# if block given yields item and result for item and returns array of yield
# results
# else return array of item and result pairs
def run_method_for(list, method_name, &block)
apply_threading(list).map do |item|
result = send(method_name, item)
if block
yield item, result
else
[item, result]
end
end
end
# Apply threading if threading is allowed
def apply_threading(enum)
if threads > 1
enum.in_threads(threads)
else
enum
end
end
end