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
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
|