openstreetmap-image_optim 0.21.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rubocop.yml +65 -0
- data/.travis.yml +42 -0
- data/CHANGELOG.markdown +272 -0
- data/CONTRIBUTING.markdown +10 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +344 -0
- data/Vagrantfile +33 -0
- data/bin/image_optim +28 -0
- data/image_optim.gemspec +29 -0
- data/lib/image_optim.rb +228 -0
- data/lib/image_optim/bin_resolver.rb +144 -0
- data/lib/image_optim/bin_resolver/bin.rb +105 -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/cmd.rb +49 -0
- data/lib/image_optim/config.rb +205 -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 +25 -0
- data/lib/image_optim/image_path.rb +68 -0
- data/lib/image_optim/non_negative_integer_range.rb +11 -0
- data/lib/image_optim/option_definition.rb +32 -0
- data/lib/image_optim/option_helpers.rb +17 -0
- data/lib/image_optim/railtie.rb +38 -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 +227 -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 +159 -0
- data/lib/image_optim/worker/advpng.rb +35 -0
- data/lib/image_optim/worker/class_methods.rb +91 -0
- data/lib/image_optim/worker/gifsicle.rb +63 -0
- data/lib/image_optim/worker/jhead.rb +43 -0
- data/lib/image_optim/worker/jpegoptim.rb +58 -0
- data/lib/image_optim/worker/jpegrecompress.rb +44 -0
- data/lib/image_optim/worker/jpegtran.rb +46 -0
- data/lib/image_optim/worker/optipng.rb +45 -0
- data/lib/image_optim/worker/pngcrush.rb +54 -0
- data/lib/image_optim/worker/pngout.rb +38 -0
- data/lib/image_optim/worker/pngquant.rb +51 -0
- data/lib/image_optim/worker/svgo.rb +32 -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 +60 -0
- data/script/worker_analysis +599 -0
- data/spec/image_optim/bin_resolver/comparable_condition_spec.rb +37 -0
- data/spec/image_optim/bin_resolver/simple_version_spec.rb +57 -0
- data/spec/image_optim/bin_resolver_spec.rb +272 -0
- data/spec/image_optim/cmd_spec.rb +66 -0
- data/spec/image_optim/config_spec.rb +217 -0
- data/spec/image_optim/handler_spec.rb +95 -0
- data/spec/image_optim/hash_helpers_spec.rb +76 -0
- data/spec/image_optim/image_path_spec.rb +54 -0
- data/spec/image_optim/railtie_spec.rb +121 -0
- data/spec/image_optim/runner/glob_helpers_spec.rb +25 -0
- data/spec/image_optim/runner/option_parser_spec.rb +99 -0
- data/spec/image_optim/space_spec.rb +25 -0
- data/spec/image_optim/worker_spec.rb +192 -0
- data/spec/image_optim_spec.rb +242 -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 +64 -0
- data/vendor/jpegrescan +143 -0
- metadata +308 -0
@@ -0,0 +1,144 @@
|
|
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
|
+
if @bins[name]
|
48
|
+
@bins[name].check_fail!
|
49
|
+
else
|
50
|
+
fail BinNotFound, "`#{name}` not found"
|
51
|
+
end
|
52
|
+
|
53
|
+
@bins[name]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Path to vendor at root of image_optim
|
57
|
+
VENDOR_PATH = File.expand_path('../../../vendor', __FILE__)
|
58
|
+
|
59
|
+
# Prepand `dir` and append `VENDOR_PATH` to `PATH` from environment
|
60
|
+
def env_path
|
61
|
+
[
|
62
|
+
dir,
|
63
|
+
pack_path,
|
64
|
+
ENV['PATH'],
|
65
|
+
VENDOR_PATH,
|
66
|
+
].compact.join(File::PATH_SEPARATOR)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Collect resolving errors when running block over items of enumerable
|
70
|
+
def self.collect_errors(enumerable)
|
71
|
+
errors = []
|
72
|
+
enumerable.each do |item|
|
73
|
+
begin
|
74
|
+
yield item
|
75
|
+
rescue Error => e
|
76
|
+
errors << e
|
77
|
+
end
|
78
|
+
end
|
79
|
+
errors
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def init_pack
|
85
|
+
return unless @image_optim.pack
|
86
|
+
|
87
|
+
@pack_path = if @image_optim.verbose
|
88
|
+
Pack.path do |message|
|
89
|
+
$stderr << "#{message}\n"
|
90
|
+
end
|
91
|
+
else
|
92
|
+
Pack.path
|
93
|
+
end
|
94
|
+
return if @pack_path
|
95
|
+
|
96
|
+
warn 'No pack for this OS and/or ARCH, check verbose output'
|
97
|
+
end
|
98
|
+
|
99
|
+
# Double-checked locking
|
100
|
+
def resolving(name)
|
101
|
+
return if @bins.include?(name)
|
102
|
+
@lock.synchronize do
|
103
|
+
yield unless @bins.include?(name)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Check path in XXX_BIN to exist, be a file and be executable and symlink to
|
108
|
+
# dir as name
|
109
|
+
def symlink_custom_bin!(name)
|
110
|
+
env_name = "#{name}_bin".upcase
|
111
|
+
path = ENV[env_name]
|
112
|
+
return unless path
|
113
|
+
path = File.expand_path(path)
|
114
|
+
desc = "`#{path}` specified in #{env_name}"
|
115
|
+
fail "#{desc} doesn\'t exist" unless File.exist?(path)
|
116
|
+
fail "#{desc} is not a file" unless File.file?(path)
|
117
|
+
fail "#{desc} is not executable" unless File.executable?(path)
|
118
|
+
if @image_optim.verbose
|
119
|
+
$stderr << "Custom path for #{name} specified in #{env_name}: #{path}\n"
|
120
|
+
end
|
121
|
+
unless @dir
|
122
|
+
@dir = FSPath.temp_dir
|
123
|
+
at_exit{ FileUtils.remove_entry_secure @dir }
|
124
|
+
end
|
125
|
+
symlink = @dir / name
|
126
|
+
symlink.make_symlink(path)
|
127
|
+
path
|
128
|
+
end
|
129
|
+
|
130
|
+
# Return full path to bin or null
|
131
|
+
# based on http://stackoverflow.com/a/5471032/96823
|
132
|
+
def full_path(name)
|
133
|
+
# PATHEXT is needed only for windows
|
134
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
135
|
+
env_path.split(File::PATH_SEPARATOR).each do |dir|
|
136
|
+
exts.each do |ext|
|
137
|
+
path = File.expand_path("#{name}#{ext}", dir)
|
138
|
+
return path if File.file?(path) && File.executable?(path)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
nil
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,105 @@
|
|
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 'shellwords'
|
6
|
+
|
7
|
+
class ImageOptim
|
8
|
+
class BinResolver
|
9
|
+
# Holds bin name and path, gets version
|
10
|
+
class Bin
|
11
|
+
class UnknownVersion < Error; end
|
12
|
+
class BadVersion < Error; end
|
13
|
+
|
14
|
+
attr_reader :name, :path, :version
|
15
|
+
def initialize(name, path)
|
16
|
+
@name = name.to_sym
|
17
|
+
@path = path.to_s
|
18
|
+
@version = detect_version
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
"#{name} #{version || '?'} at #{path}"
|
23
|
+
end
|
24
|
+
|
25
|
+
is = ComparableCondition.is
|
26
|
+
|
27
|
+
FAIL_CHECKS = [
|
28
|
+
[:pngcrush, is.between?('1.7.60', '1.7.65'), 'is known to produce '\
|
29
|
+
'broken pngs'],
|
30
|
+
[:pngcrush, is == '1.7.80', 'loses one color in indexed images'],
|
31
|
+
[:pngquant, is < '2.0', 'is not supported'],
|
32
|
+
]
|
33
|
+
|
34
|
+
WARN_CHECKS = [
|
35
|
+
[:advpng, is < '1.17', 'does not use zopfli'],
|
36
|
+
[:gifsicle, is < '1.85', 'does not support removing extension blocks'],
|
37
|
+
[:pngcrush, is < '1.7.38', 'does not have blacken flag'],
|
38
|
+
[:pngquant, is < '2.1', 'may be lossy even with quality `100-`'],
|
39
|
+
]
|
40
|
+
|
41
|
+
# Fail if version will not work properly
|
42
|
+
def check_fail!
|
43
|
+
fail UnknownVersion, "didn't get version of #{self}" unless version
|
44
|
+
|
45
|
+
FAIL_CHECKS.each do |bin_name, matcher, message|
|
46
|
+
next unless bin_name == name
|
47
|
+
next unless matcher.match(version)
|
48
|
+
fail BadVersion, "#{self} (#{matcher}) #{message}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Run check_fail!, otherwise warn if version is known to misbehave
|
53
|
+
def check!
|
54
|
+
check_fail!
|
55
|
+
|
56
|
+
WARN_CHECKS.each do |bin_name, matcher, message|
|
57
|
+
next unless bin_name == name
|
58
|
+
next unless matcher.match(version)
|
59
|
+
warn "WARN: #{self} (#{matcher}) #{message}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
# Wrap version_string with SimpleVersion
|
66
|
+
def detect_version
|
67
|
+
str = version_string
|
68
|
+
str && SimpleVersion.new(str)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Getting version of bin, will fail for an unknown name
|
72
|
+
def version_string
|
73
|
+
case name
|
74
|
+
when :advpng, :gifsicle, :jpegoptim, :optipng, :pngquant
|
75
|
+
capture("#{escaped_path} --version 2> /dev/null")[/\d+(\.\d+){1,}/]
|
76
|
+
when :svgo
|
77
|
+
capture("#{escaped_path} --version 2>&1")[/\d+(\.\d+){1,}/]
|
78
|
+
when :jhead, :'jpeg-recompress'
|
79
|
+
capture("#{escaped_path} -V 2> /dev/null")[/\d+(\.\d+){1,}/]
|
80
|
+
when :jpegtran
|
81
|
+
capture("#{escaped_path} -v - 2>&1")[/version (\d+\S*)/, 1]
|
82
|
+
when :pngcrush
|
83
|
+
capture("#{escaped_path} -version 2>&1")[/\d+(\.\d+){1,}/]
|
84
|
+
when :pngout
|
85
|
+
date_regexp = /[A-Z][a-z]{2} (?: |\d)\d \d{4}/
|
86
|
+
date_str = capture("#{escaped_path} 2>&1")[date_regexp]
|
87
|
+
Date.parse(date_str).strftime('%Y%m%d') if date_str
|
88
|
+
when :jpegrescan
|
89
|
+
# jpegrescan has no version so just check presence
|
90
|
+
path && '-'
|
91
|
+
else
|
92
|
+
fail "getting `#{name}` version is not defined"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def capture(cmd)
|
97
|
+
Cmd.capture(cmd)
|
98
|
+
end
|
99
|
+
|
100
|
+
def escaped_path
|
101
|
+
path.shellescape
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
class ImageOptim
|
2
|
+
class BinResolver
|
3
|
+
# Allows to externalize conditions for an instance of Comparable to use in
|
4
|
+
# case statemens
|
5
|
+
#
|
6
|
+
# is = ComparableCondition.is
|
7
|
+
# case rand(100)
|
8
|
+
# when is < 10 then # ...
|
9
|
+
# when is.between?(13, 23) then # ...
|
10
|
+
# when is >= 90 then # ...
|
11
|
+
# end
|
12
|
+
class ComparableCondition
|
13
|
+
# Helper class for creating conditions using ComparableCondition.is
|
14
|
+
class Builder
|
15
|
+
Comparable.instance_methods.each do |method|
|
16
|
+
define_method method do |*args|
|
17
|
+
ComparableCondition.new(method, *args)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.is
|
23
|
+
Builder.new
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :method, :args
|
27
|
+
def initialize(method, *args)
|
28
|
+
@method, @args = method.to_sym, args
|
29
|
+
|
30
|
+
case @method
|
31
|
+
when :between?
|
32
|
+
@args.length == 2 || argument_error!("`between?' expects 2 arguments")
|
33
|
+
when :<, :<=, :==, :>, :>=
|
34
|
+
@args.length == 1 || argument_error!("`#{method}' expects 1 argument")
|
35
|
+
else
|
36
|
+
argument_error! "Unknown method `#{method}'"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def ===(other)
|
41
|
+
other.send(@method, *@args)
|
42
|
+
end
|
43
|
+
alias_method :match, :===
|
44
|
+
|
45
|
+
def to_s
|
46
|
+
if @method == :between?
|
47
|
+
@args.join('..')
|
48
|
+
else
|
49
|
+
"#{@method} #{@args.first}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def argument_error!(message)
|
56
|
+
fail ArgumentError, message
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class ImageOptim
|
2
|
+
class BinResolver
|
3
|
+
# Allows comparision of simple versions, only numbers separated by dots are
|
4
|
+
# taken into account
|
5
|
+
class SimpleVersion
|
6
|
+
include Comparable
|
7
|
+
|
8
|
+
# Numbers extracted from version string
|
9
|
+
attr_reader :parts
|
10
|
+
|
11
|
+
# Initialize with a string or an object convertible to string
|
12
|
+
#
|
13
|
+
# SimpleVersion.new('2.0.1') <=> SimpleVersion.new(2)
|
14
|
+
def initialize(str)
|
15
|
+
@str = String(str)
|
16
|
+
@parts = @str.split('.').map(&:to_i).reverse.drop_while(&:zero?).reverse
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns original version string
|
20
|
+
def to_s
|
21
|
+
@str
|
22
|
+
end
|
23
|
+
|
24
|
+
# Compare version parts of self with other
|
25
|
+
def <=>(other)
|
26
|
+
other = self.class.new(other) unless other.is_a?(self.class)
|
27
|
+
parts <=> other.parts
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'English'
|
2
|
+
|
3
|
+
class ImageOptim
|
4
|
+
# Helper for running commands
|
5
|
+
module Cmd
|
6
|
+
class << self
|
7
|
+
# Run using `system`
|
8
|
+
# Return success status
|
9
|
+
# Will raise SignalException if process was interrupted
|
10
|
+
def run(*args)
|
11
|
+
success = system(*args)
|
12
|
+
|
13
|
+
check_status!
|
14
|
+
|
15
|
+
success
|
16
|
+
end
|
17
|
+
|
18
|
+
# Run using backtick
|
19
|
+
# Return captured output
|
20
|
+
# Will raise SignalException if process was interrupted
|
21
|
+
def capture(cmd)
|
22
|
+
output = `#{cmd}`
|
23
|
+
|
24
|
+
check_status!
|
25
|
+
|
26
|
+
output
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def check_status!
|
32
|
+
status = $CHILD_STATUS
|
33
|
+
|
34
|
+
return unless status.signaled?
|
35
|
+
|
36
|
+
# jruby incorrectly returns true for `signaled?` if process exits with
|
37
|
+
# non zero status. For following code
|
38
|
+
#
|
39
|
+
# `sh -c 'exit 66'`
|
40
|
+
# p [$?.signaled?, $?.exitstatus, $?.termsig]
|
41
|
+
#
|
42
|
+
# jruby outputs `[true, 66, 66]` instead of expected `[false, 66, nil]`
|
43
|
+
return if defined?(JRUBY_VERSION) && status.exitstatus == status.termsig
|
44
|
+
|
45
|
+
fail SignalException, status.termsig
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
require 'image_optim/option_helpers'
|
2
|
+
require 'image_optim/configuration_error'
|
3
|
+
require 'image_optim/hash_helpers'
|
4
|
+
require 'image_optim/worker'
|
5
|
+
require 'image_optim/cmd'
|
6
|
+
require 'set'
|
7
|
+
require 'yaml'
|
8
|
+
|
9
|
+
class ImageOptim
|
10
|
+
# Read, merge and parse configuration
|
11
|
+
class Config
|
12
|
+
include OptionHelpers
|
13
|
+
|
14
|
+
# Global config path at `$XDG_CONFIG_HOME/image_optim.yml` (by default
|
15
|
+
# `~/.config/image_optim.yml`)
|
16
|
+
GLOBAL_PATH = begin
|
17
|
+
File.join(ENV['XDG_CONFIG_HOME'] || '~/.config', 'image_optim.yml')
|
18
|
+
end
|
19
|
+
|
20
|
+
# Local config path at `./.image_optim.yml`
|
21
|
+
LOCAL_PATH = './.image_optim.yml'
|
22
|
+
|
23
|
+
class << self
|
24
|
+
# Read options at path: expand path (warn on failure), return {} if file
|
25
|
+
# does not exist, read yaml, check if it is a Hash, deep symbolise keys
|
26
|
+
def read_options(path)
|
27
|
+
begin
|
28
|
+
full_path = File.expand_path(path)
|
29
|
+
rescue ArgumentError => e
|
30
|
+
warn "Can't expand path #{path}: #{e}"
|
31
|
+
return {}
|
32
|
+
end
|
33
|
+
return {} unless File.file?(full_path)
|
34
|
+
config = YAML.load_file(full_path)
|
35
|
+
unless config.is_a?(Hash)
|
36
|
+
fail "expected hash, got #{config.inspect}"
|
37
|
+
end
|
38
|
+
HashHelpers.deep_symbolise_keys(config)
|
39
|
+
rescue => e
|
40
|
+
warn "exception when reading #{full_path}: #{e}"
|
41
|
+
{}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Merge config from files with passed options
|
46
|
+
# Config files are checked at `GLOBAL_PATH` and `LOCAL_PATH` unless
|
47
|
+
# overriden using `:config_paths`
|
48
|
+
def initialize(options)
|
49
|
+
config_paths = options.delete(:config_paths) || [GLOBAL_PATH, LOCAL_PATH]
|
50
|
+
config_paths = Array(config_paths)
|
51
|
+
|
52
|
+
to_merge = config_paths.map{ |path| self.class.read_options(path) }
|
53
|
+
to_merge << HashHelpers.deep_symbolise_keys(options)
|
54
|
+
|
55
|
+
@options = to_merge.reduce do |memo, hash|
|
56
|
+
HashHelpers.deep_merge(memo, hash)
|
57
|
+
end
|
58
|
+
@used = Set.new
|
59
|
+
end
|
60
|
+
|
61
|
+
# Gets value for key converted to symbol and mark option as used
|
62
|
+
def get!(key)
|
63
|
+
key = key.to_sym
|
64
|
+
@used << key
|
65
|
+
@options[key]
|
66
|
+
end
|
67
|
+
|
68
|
+
# Check if key is present
|
69
|
+
def key?(key)
|
70
|
+
key = key.to_sym
|
71
|
+
@options.key?(key)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Fail unless all options were marked as used (directly or indirectly
|
75
|
+
# accessed using `get!`)
|
76
|
+
def assert_no_unused_options!
|
77
|
+
unknown_options = @options.reject{ |key, _value| @used.include?(key) }
|
78
|
+
return if unknown_options.empty?
|
79
|
+
fail ConfigurationError, "unknown options #{unknown_options.inspect}"
|
80
|
+
end
|
81
|
+
|
82
|
+
# Nice level:
|
83
|
+
# * `10` by default and for `nil` or `true`
|
84
|
+
# * `0` for `false`
|
85
|
+
# * otherwise convert to integer
|
86
|
+
def nice
|
87
|
+
nice = get!(:nice)
|
88
|
+
|
89
|
+
case nice
|
90
|
+
when true, nil
|
91
|
+
10
|
92
|
+
when false
|
93
|
+
0
|
94
|
+
else
|
95
|
+
nice.to_i
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Number of parallel threads:
|
100
|
+
# * `processor_count` by default and for `nil` or `true`
|
101
|
+
# * `1` for `false`
|
102
|
+
# * otherwise convert to integer
|
103
|
+
def threads
|
104
|
+
threads = get!(:threads)
|
105
|
+
|
106
|
+
case threads
|
107
|
+
when true, nil
|
108
|
+
processor_count
|
109
|
+
when false
|
110
|
+
1
|
111
|
+
else
|
112
|
+
threads.to_i
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Verbose mode, converted to boolean
|
117
|
+
def verbose
|
118
|
+
!!get!(:verbose)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Using image_optim_pack:
|
122
|
+
# * `false` to disable
|
123
|
+
# * `nil` to use if available
|
124
|
+
# * everything else to require
|
125
|
+
def pack
|
126
|
+
pack = get!(:pack)
|
127
|
+
return false if pack == false
|
128
|
+
|
129
|
+
require 'image_optim/pack'
|
130
|
+
true
|
131
|
+
rescue LoadError => e
|
132
|
+
raise "Cannot load image_optim_pack: #{e}" if pack
|
133
|
+
false
|
134
|
+
end
|
135
|
+
|
136
|
+
# Skip missing workers, converted to boolean
|
137
|
+
def skip_missing_workers
|
138
|
+
if key?(:skip_missing_workers)
|
139
|
+
!!get!(:skip_missing_workers)
|
140
|
+
else
|
141
|
+
pack
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Allow lossy workers and optimizations, converted to boolean
|
146
|
+
def allow_lossy
|
147
|
+
!!get!(:allow_lossy)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Options for worker class by its `bin_sym`:
|
151
|
+
# * `Hash` passed as is
|
152
|
+
# * `{}` for `true` or `nil`
|
153
|
+
# * `false` for `false`
|
154
|
+
# * otherwise fail with `ConfigurationError`
|
155
|
+
def for_worker(klass)
|
156
|
+
worker_options = get!(klass.bin_sym)
|
157
|
+
|
158
|
+
case worker_options
|
159
|
+
when Hash
|
160
|
+
worker_options
|
161
|
+
when true, nil
|
162
|
+
{}
|
163
|
+
when false
|
164
|
+
{:disable => true}
|
165
|
+
else
|
166
|
+
fail ConfigurationError, "Got #{worker_options.inspect} for "\
|
167
|
+
"#{klass.name} options"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# yaml dump without document beginning prefix `---`
|
172
|
+
def to_s
|
173
|
+
YAML.dump(HashHelpers.deep_stringify_keys(@options)).sub(/\A---\n/, '')
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
|
178
|
+
# http://stackoverflow.com/a/6420817
|
179
|
+
def processor_count
|
180
|
+
@processor_count ||= case host_os = RbConfig::CONFIG['host_os']
|
181
|
+
when /darwin9/
|
182
|
+
Cmd.capture 'hwprefs cpu_count'
|
183
|
+
when /darwin/
|
184
|
+
if (Cmd.capture 'which hwprefs') != ''
|
185
|
+
Cmd.capture 'hwprefs thread_count'
|
186
|
+
else
|
187
|
+
Cmd.capture 'sysctl -n hw.ncpu'
|
188
|
+
end
|
189
|
+
when /linux/
|
190
|
+
Cmd.capture 'grep -c processor /proc/cpuinfo'
|
191
|
+
when /freebsd/
|
192
|
+
Cmd.capture 'sysctl -n hw.ncpu'
|
193
|
+
when /mswin|mingw/
|
194
|
+
require 'win32ole'
|
195
|
+
WIN32OLE.
|
196
|
+
connect('winmgmts://').
|
197
|
+
ExecQuery('select NumberOfLogicalProcessors from Win32_Processor').
|
198
|
+
to_enum.first.NumberOfLogicalProcessors
|
199
|
+
else
|
200
|
+
warn "Unknown architecture (#{host_os}) assuming one processor."
|
201
|
+
1
|
202
|
+
end.to_i
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|