discourse_image_optim 0.24.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/image_optim.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'discourse_image_optim'
|
5
|
+
s.version = '0.24.4'
|
6
|
+
s.summary = %q{Optimize (lossless compress, optionally lossy) images (jpeg, png, gif, svg) using external utilities (advpng, gifsicle, jhead, jpeg-recompress, jpegoptim, jpegrescan, jpegtran, optipng, pngcrush, pngout, pngquant, svgo)}
|
7
|
+
s.homepage = "http://github.com/toy/#{s.name}"
|
8
|
+
s.authors = ['Ivan Kuchin']
|
9
|
+
s.license = 'MIT'
|
10
|
+
|
11
|
+
s.rubyforge_project = s.name
|
12
|
+
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
15
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
16
|
+
s.require_paths = %w[lib]
|
17
|
+
|
18
|
+
s.post_install_message = <<-EOF
|
19
|
+
Rails image assets optimization is extracted into image_optim_rails gem
|
20
|
+
You can safely remove `config.assets.image_optim = false` if you are not going to use that gem
|
21
|
+
EOF
|
22
|
+
|
23
|
+
s.add_dependency 'fspath', '~> 3.0'
|
24
|
+
s.add_dependency 'image_size', '~> 1.5'
|
25
|
+
s.add_dependency 'exifr', '~> 1.2', '>= 1.2.2'
|
26
|
+
s.add_dependency 'progress', '~> 3.0', '>= 3.0.1'
|
27
|
+
s.add_dependency 'in_threads', '~> 1.3'
|
28
|
+
|
29
|
+
s.add_development_dependency 'image_optim_pack', '~> 0.2', '>= 0.2.2'
|
30
|
+
s.add_development_dependency 'rspec', '~> 3.0'
|
31
|
+
if RUBY_VERSION >= '2.0'
|
32
|
+
s.add_development_dependency 'rubocop', '~> 0.47'
|
33
|
+
end
|
34
|
+
end
|
data/lib/image_optim.rb
ADDED
@@ -0,0 +1,267 @@
|
|
1
|
+
require 'image_optim/bin_resolver'
|
2
|
+
require 'image_optim/cache'
|
3
|
+
require 'image_optim/config'
|
4
|
+
require 'image_optim/handler'
|
5
|
+
require 'image_optim/image_meta'
|
6
|
+
require 'image_optim/optimized_path'
|
7
|
+
require 'image_optim/path'
|
8
|
+
require 'image_optim/worker'
|
9
|
+
require 'in_threads'
|
10
|
+
require 'shellwords'
|
11
|
+
|
12
|
+
%w[
|
13
|
+
pngcrush pngout advpng optipng pngquant
|
14
|
+
jhead jpegoptim jpegrecompress jpegtran
|
15
|
+
gifsicle
|
16
|
+
svgo
|
17
|
+
].each do |worker|
|
18
|
+
require "image_optim/worker/#{worker}"
|
19
|
+
end
|
20
|
+
|
21
|
+
# Main interface
|
22
|
+
class ImageOptim
|
23
|
+
# Nice level
|
24
|
+
attr_reader :nice
|
25
|
+
|
26
|
+
# Number of threads to run with
|
27
|
+
attr_reader :threads
|
28
|
+
|
29
|
+
# Verbose output?
|
30
|
+
attr_reader :verbose
|
31
|
+
|
32
|
+
# Use image_optim_pack
|
33
|
+
attr_reader :pack
|
34
|
+
|
35
|
+
# Skip workers with missing or problematic binaries
|
36
|
+
attr_reader :skip_missing_workers
|
37
|
+
|
38
|
+
# Allow lossy workers and optimizations
|
39
|
+
attr_reader :allow_lossy
|
40
|
+
|
41
|
+
# Cache directory
|
42
|
+
attr_reader :cache_dir
|
43
|
+
|
44
|
+
# Cache worker digests
|
45
|
+
attr_reader :cache_worker_digests
|
46
|
+
|
47
|
+
# Timeout
|
48
|
+
attr_reader :timeout
|
49
|
+
|
50
|
+
# Initialize workers, specify options using worker underscored name:
|
51
|
+
#
|
52
|
+
# pass false to disable worker
|
53
|
+
#
|
54
|
+
# ImageOptim.new(:pngcrush => false)
|
55
|
+
#
|
56
|
+
# or hash with options to worker
|
57
|
+
#
|
58
|
+
# ImageOptim.new(:advpng => {:level => 3}, :optipng => {:level => 2})
|
59
|
+
#
|
60
|
+
# use :threads to set number of parallel optimizers to run (passing true or
|
61
|
+
# nil determines number of processors, false disables parallel processing)
|
62
|
+
#
|
63
|
+
# ImageOptim.new(:threads => 8)
|
64
|
+
#
|
65
|
+
# use :nice to specify optimizers nice level (true or nil makes it 10, false
|
66
|
+
# makes it 0)
|
67
|
+
#
|
68
|
+
# ImageOptim.new(:nice => 20)
|
69
|
+
def initialize(options = {})
|
70
|
+
config = Config.new(options)
|
71
|
+
@verbose = config.verbose
|
72
|
+
$stderr << "config:\n#{config.to_s.gsub(/^/, ' ')}" if verbose
|
73
|
+
|
74
|
+
%w[
|
75
|
+
nice
|
76
|
+
threads
|
77
|
+
pack
|
78
|
+
skip_missing_workers
|
79
|
+
allow_lossy
|
80
|
+
cache_dir
|
81
|
+
cache_worker_digests
|
82
|
+
timeout
|
83
|
+
].each do |name|
|
84
|
+
instance_variable_set(:"@#{name}", config.send(name))
|
85
|
+
$stderr << "#{name}: #{send(name)}\n" if verbose
|
86
|
+
end
|
87
|
+
|
88
|
+
@bin_resolver = BinResolver.new(self)
|
89
|
+
|
90
|
+
@workers_by_format = Worker.create_all_by_format(self) do |klass|
|
91
|
+
config.for_worker(klass)
|
92
|
+
end
|
93
|
+
|
94
|
+
@cache = Cache.new(self, @workers_by_format)
|
95
|
+
|
96
|
+
log_workers_by_format if verbose
|
97
|
+
|
98
|
+
config.assert_no_unused_options!
|
99
|
+
end
|
100
|
+
|
101
|
+
# Get workers for image
|
102
|
+
def workers_for_image(path)
|
103
|
+
@workers_by_format[Path.convert(path).image_format]
|
104
|
+
end
|
105
|
+
|
106
|
+
# Optimize one file, return new path as OptimizedPath or nil if
|
107
|
+
# optimization failed
|
108
|
+
def optimize_image(original)
|
109
|
+
original = Path.convert(original)
|
110
|
+
return unless (workers = workers_for_image(original))
|
111
|
+
|
112
|
+
optimized = @cache.fetch(original) do
|
113
|
+
Handler.for(original) do |handler|
|
114
|
+
workers.each do |worker|
|
115
|
+
handler.process do |src, dst|
|
116
|
+
worker.optimize(src, dst)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
return unless optimized
|
123
|
+
OptimizedPath.new(optimized, original)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Optimize one file in place, return original as OptimizedPath or nil if
|
127
|
+
# optimization failed
|
128
|
+
def optimize_image!(original)
|
129
|
+
original = Path.convert(original)
|
130
|
+
return unless (result = optimize_image(original))
|
131
|
+
result.replace(original)
|
132
|
+
OptimizedPath.new(original, result.original_size)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Optimize image data, return new data or nil if optimization failed
|
136
|
+
def optimize_image_data(original_data)
|
137
|
+
format = ImageMeta.format_for_data(original_data)
|
138
|
+
return unless format
|
139
|
+
Path.temp_file %W[image_optim .#{format}] do |temp|
|
140
|
+
temp.binmode
|
141
|
+
temp.write(original_data)
|
142
|
+
temp.close
|
143
|
+
|
144
|
+
if (result = optimize_image(temp.path))
|
145
|
+
result.binread
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Optimize multiple images
|
151
|
+
# if block given yields path and result for each image and returns array of
|
152
|
+
# yield results
|
153
|
+
# else return array of path and result pairs
|
154
|
+
def optimize_images(paths, &block)
|
155
|
+
run_method_for(paths, :optimize_image, &block)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Optimize multiple images in place
|
159
|
+
# if block given yields path and result for each image and returns array of
|
160
|
+
# yield results
|
161
|
+
# else return array of path and result pairs
|
162
|
+
def optimize_images!(paths, &block)
|
163
|
+
run_method_for(paths, :optimize_image!, &block)
|
164
|
+
end
|
165
|
+
|
166
|
+
# Optimize multiple image datas
|
167
|
+
# if block given yields original and result for each image data and returns
|
168
|
+
# array of yield results
|
169
|
+
# else return array of path and result pairs
|
170
|
+
def optimize_images_data(datas, &block)
|
171
|
+
run_method_for(datas, :optimize_image_data, &block)
|
172
|
+
end
|
173
|
+
|
174
|
+
class << self
|
175
|
+
# Optimization methods with default options
|
176
|
+
def method_missing(method, *args, &block)
|
177
|
+
if optimize_image_method?(method)
|
178
|
+
new.send(method, *args, &block)
|
179
|
+
else
|
180
|
+
super
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def respond_to_missing?(method, include_private = false)
|
185
|
+
optimize_image_method?(method) || super
|
186
|
+
end
|
187
|
+
|
188
|
+
if RUBY_VERSION < '1.9'
|
189
|
+
def respond_to?(method, include_private = false)
|
190
|
+
optimize_image_method?(method) || super
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# Version of image_optim gem spec loaded
|
195
|
+
def version
|
196
|
+
Gem.loaded_specs['image_optim'].version.to_s
|
197
|
+
rescue
|
198
|
+
'DEV'
|
199
|
+
end
|
200
|
+
|
201
|
+
# Full version of image_optim
|
202
|
+
def full_version
|
203
|
+
"image_optim v#{version}"
|
204
|
+
end
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
def optimize_image_method?(method)
|
209
|
+
method_defined?(method) && method.to_s =~ /^optimize_image/
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Are there workers for file at path?
|
214
|
+
def optimizable?(path)
|
215
|
+
!!workers_for_image(path)
|
216
|
+
end
|
217
|
+
|
218
|
+
# Check existance of binary, create symlink if ENV contains path for key
|
219
|
+
# XXX_BIN where XXX is upper case bin name
|
220
|
+
def resolve_bin!(bin)
|
221
|
+
@bin_resolver.resolve!(bin)
|
222
|
+
end
|
223
|
+
|
224
|
+
# Join resolve_dir, default path and vendor path for PATH environment variable
|
225
|
+
def env_path
|
226
|
+
@bin_resolver.env_path
|
227
|
+
end
|
228
|
+
|
229
|
+
private
|
230
|
+
|
231
|
+
def log_workers_by_format
|
232
|
+
$stderr << "Workers by format:\n"
|
233
|
+
@workers_by_format.each do |format, workers|
|
234
|
+
$stderr << "#{format}:\n"
|
235
|
+
workers.each do |worker|
|
236
|
+
$stderr << " #{worker.class.bin_sym}:\n"
|
237
|
+
worker.options.each do |name, value|
|
238
|
+
$stderr << " #{name}: #{value.inspect}\n"
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Run method for each item in list
|
245
|
+
# if block given yields item and result for item and returns array of yield
|
246
|
+
# results
|
247
|
+
# else return array of item and result pairs
|
248
|
+
def run_method_for(list, method_name, &block)
|
249
|
+
apply_threading(list).map do |item|
|
250
|
+
result = send(method_name, item)
|
251
|
+
if block
|
252
|
+
yield item, result
|
253
|
+
else
|
254
|
+
[item, result]
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Apply threading if threading is allowed
|
260
|
+
def apply_threading(enum)
|
261
|
+
if threads > 1
|
262
|
+
enum.in_threads(threads)
|
263
|
+
else
|
264
|
+
enum
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'fspath'
|
3
|
+
require 'image_optim/bin_resolver/error'
|
4
|
+
require 'image_optim/bin_resolver/bin'
|
5
|
+
|
6
|
+
class ImageOptim
|
7
|
+
# Handles resolving binaries and checking versions
|
8
|
+
#
|
9
|
+
# If there is an environment variable XXX_BIN when resolving xxx, then a
|
10
|
+
# symlink to binary will be created in a temporary directory which will be
|
11
|
+
# added to PATH
|
12
|
+
class BinResolver
|
13
|
+
class BinNotFound < Error; end
|
14
|
+
|
15
|
+
# Directory for symlinks to bins if XXX_BIN was used
|
16
|
+
attr_reader :dir
|
17
|
+
|
18
|
+
# Path to pack from image_optim_pack if used
|
19
|
+
attr_reader :pack_path
|
20
|
+
|
21
|
+
def initialize(image_optim)
|
22
|
+
@image_optim = image_optim
|
23
|
+
@bins = {}
|
24
|
+
@lock = Mutex.new
|
25
|
+
init_pack
|
26
|
+
end
|
27
|
+
|
28
|
+
# Binary resolving: create symlink if there is XXX_BIN environment variable,
|
29
|
+
# build Bin with full path, check binary version
|
30
|
+
# Return Bin instance
|
31
|
+
def resolve!(name)
|
32
|
+
name = name.to_sym
|
33
|
+
|
34
|
+
resolving(name) do
|
35
|
+
path = symlink_custom_bin!(name) || full_path(name)
|
36
|
+
bin = Bin.new(name, path) if path
|
37
|
+
|
38
|
+
if bin && @image_optim.verbose
|
39
|
+
$stderr << "Resolved #{bin}\n"
|
40
|
+
end
|
41
|
+
|
42
|
+
@bins[name] = bin
|
43
|
+
|
44
|
+
bin.check! if bin
|
45
|
+
end
|
46
|
+
|
47
|
+
fail BinNotFound, "`#{name}` not found" unless @bins[name]
|
48
|
+
|
49
|
+
@bins[name].check_fail!
|
50
|
+
|
51
|
+
@bins[name]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Path to vendor at root of image_optim
|
55
|
+
VENDOR_PATH = File.expand_path('../../../vendor', __FILE__)
|
56
|
+
|
57
|
+
# Prepand `dir` and append `VENDOR_PATH` to `PATH` from environment
|
58
|
+
def env_path
|
59
|
+
[
|
60
|
+
dir,
|
61
|
+
pack_path,
|
62
|
+
ENV['PATH'],
|
63
|
+
VENDOR_PATH,
|
64
|
+
].compact.join(File::PATH_SEPARATOR)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Collect resolving errors when running block over items of enumerable
|
68
|
+
def self.collect_errors(enumerable)
|
69
|
+
errors = []
|
70
|
+
enumerable.each do |item|
|
71
|
+
begin
|
72
|
+
yield item
|
73
|
+
rescue Error => e
|
74
|
+
errors << e
|
75
|
+
end
|
76
|
+
end
|
77
|
+
errors
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def init_pack
|
83
|
+
return unless @image_optim.pack
|
84
|
+
|
85
|
+
@pack_path = if @image_optim.verbose
|
86
|
+
Pack.path do |message|
|
87
|
+
$stderr << "#{message}\n"
|
88
|
+
end
|
89
|
+
else
|
90
|
+
Pack.path
|
91
|
+
end
|
92
|
+
return if @pack_path
|
93
|
+
|
94
|
+
warn 'No pack for this OS and/or ARCH, check verbose output'
|
95
|
+
end
|
96
|
+
|
97
|
+
# Double-checked locking
|
98
|
+
def resolving(name)
|
99
|
+
return if @bins.include?(name)
|
100
|
+
@lock.synchronize do
|
101
|
+
yield unless @bins.include?(name)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Check path in XXX_BIN to exist, be a file and be executable and symlink to
|
106
|
+
# dir as name
|
107
|
+
def symlink_custom_bin!(name)
|
108
|
+
env_name = "#{name}_bin".upcase
|
109
|
+
path = ENV[env_name]
|
110
|
+
return unless path
|
111
|
+
path = File.expand_path(path)
|
112
|
+
desc = "`#{path}` specified in #{env_name}"
|
113
|
+
fail "#{desc} doesn\'t exist" unless File.exist?(path)
|
114
|
+
fail "#{desc} is not a file" unless File.file?(path)
|
115
|
+
fail "#{desc} is not executable" unless File.executable?(path)
|
116
|
+
if @image_optim.verbose
|
117
|
+
$stderr << "Custom path for #{name} specified in #{env_name}: #{path}\n"
|
118
|
+
end
|
119
|
+
unless @dir
|
120
|
+
@dir = FSPath.temp_dir
|
121
|
+
at_exit{ FileUtils.remove_entry_secure @dir }
|
122
|
+
end
|
123
|
+
symlink = @dir / name
|
124
|
+
symlink.make_symlink(path)
|
125
|
+
path
|
126
|
+
end
|
127
|
+
|
128
|
+
# Return full path to bin or null
|
129
|
+
# based on http://stackoverflow.com/a/5471032/96823
|
130
|
+
def full_path(name)
|
131
|
+
# PATHEXT is needed only for windows
|
132
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
133
|
+
env_path.split(File::PATH_SEPARATOR).each do |dir|
|
134
|
+
exts.each do |ext|
|
135
|
+
path = File.expand_path("#{name}#{ext}", dir)
|
136
|
+
return path if File.file?(path) && File.executable?(path)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
nil
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'image_optim/bin_resolver/error'
|
2
|
+
require 'image_optim/bin_resolver/simple_version'
|
3
|
+
require 'image_optim/bin_resolver/comparable_condition'
|
4
|
+
require 'image_optim/cmd'
|
5
|
+
require 'image_optim/path'
|
6
|
+
require 'shellwords'
|
7
|
+
require 'digest/sha1'
|
8
|
+
|
9
|
+
class ImageOptim
|
10
|
+
class BinResolver
|
11
|
+
# Holds bin name and path, gets version
|
12
|
+
class Bin
|
13
|
+
class UnknownVersion < Error; end
|
14
|
+
class BadVersion < Error; end
|
15
|
+
|
16
|
+
attr_reader :name, :path, :version
|
17
|
+
def initialize(name, path)
|
18
|
+
@name = name.to_sym
|
19
|
+
@path = path.to_s
|
20
|
+
@version = detect_version
|
21
|
+
end
|
22
|
+
|
23
|
+
def digest
|
24
|
+
return @digest if defined?(@digest)
|
25
|
+
@digest = File.exist?(@path) && Digest::SHA1.file(@path).hexdigest
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
"#{name} #{version || '?'} at #{path}"
|
30
|
+
end
|
31
|
+
|
32
|
+
is = ComparableCondition.is
|
33
|
+
|
34
|
+
FAIL_CHECKS = [
|
35
|
+
[:pngcrush, is.between?('1.7.60', '1.7.65'), 'is known to produce '\
|
36
|
+
'broken pngs'],
|
37
|
+
[:pngcrush, is == '1.7.80', 'loses one color in indexed images'],
|
38
|
+
[:pngquant, is < '2.0', 'is not supported'],
|
39
|
+
].freeze
|
40
|
+
|
41
|
+
WARN_CHECKS = [
|
42
|
+
[:advpng, is < '1.17', 'does not use zopfli'],
|
43
|
+
[:gifsicle, is < '1.85', 'does not support removing extension blocks'],
|
44
|
+
[:pngcrush, is < '1.7.38', 'does not have blacken flag'],
|
45
|
+
[:pngquant, is < '2.1', 'may be lossy even with quality `100-`'],
|
46
|
+
[:optipng, is < '0.7', 'does not support -strip option'],
|
47
|
+
].freeze
|
48
|
+
|
49
|
+
# Fail if version will not work properly
|
50
|
+
def check_fail!
|
51
|
+
unless version
|
52
|
+
fail UnknownVersion, "could not get version of #{name} at #{path}"
|
53
|
+
end
|
54
|
+
|
55
|
+
FAIL_CHECKS.each do |bin_name, matcher, message|
|
56
|
+
next unless bin_name == name
|
57
|
+
next unless matcher.match(version)
|
58
|
+
fail BadVersion, "#{self} (#{matcher}) #{message}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Run check_fail!, otherwise warn if version is known to misbehave
|
63
|
+
def check!
|
64
|
+
check_fail!
|
65
|
+
|
66
|
+
WARN_CHECKS.each do |bin_name, matcher, message|
|
67
|
+
next unless bin_name == name
|
68
|
+
next unless matcher.match(version)
|
69
|
+
warn "WARN: #{self} (#{matcher}) #{message}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
# Wrap version_string with SimpleVersion
|
76
|
+
def detect_version
|
77
|
+
str = version_string
|
78
|
+
str && SimpleVersion.new(str)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Getting version of bin, will fail for an unknown name
|
82
|
+
def version_string
|
83
|
+
case name
|
84
|
+
when :advpng, :gifsicle, :jpegoptim, :optipng
|
85
|
+
capture("#{escaped_path} --version 2> #{Path::NULL}")[/\d+(\.\d+)+/]
|
86
|
+
when :svgo, :pngquant
|
87
|
+
capture("#{escaped_path} --version 2>&1")[/\d+(\.\d+)+/]
|
88
|
+
when :jhead, :'jpeg-recompress'
|
89
|
+
capture("#{escaped_path} -V 2> #{Path::NULL}")[/\d+(\.\d+)+/]
|
90
|
+
when :jpegtran
|
91
|
+
capture("#{escaped_path} -v - 2>&1")[/version (\d+\S*)/, 1]
|
92
|
+
when :pngcrush
|
93
|
+
capture("#{escaped_path} -version 2>&1")[/pngcrush (\d+(\.\d+)+)/, 1]
|
94
|
+
when :pngout
|
95
|
+
date_regexp = /[A-Z][a-z]{2} (?: |\d)\d \d{4}/
|
96
|
+
date_str = capture("#{escaped_path} 2>&1")[date_regexp]
|
97
|
+
Date.parse(date_str).strftime('%Y%m%d') if date_str
|
98
|
+
when :jpegrescan
|
99
|
+
# jpegrescan has no version so use first 8 characters of sha1 hex
|
100
|
+
Digest::SHA1.file(path).hexdigest[0, 8] if path
|
101
|
+
else
|
102
|
+
fail "getting `#{name}` version is not defined"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def capture(cmd)
|
107
|
+
Cmd.capture(cmd)
|
108
|
+
end
|
109
|
+
|
110
|
+
def escaped_path
|
111
|
+
path.shellescape
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|