image_optim 0.17.1 → 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +8 -8
  2. data/.gitignore +1 -0
  3. data/.travis.yml +5 -18
  4. data/CHANGELOG.markdown +10 -0
  5. data/README.markdown +31 -1
  6. data/bin/image_optim +3 -137
  7. data/image_optim.gemspec +6 -3
  8. data/lib/image_optim.rb +20 -3
  9. data/lib/image_optim/bin_resolver.rb +28 -1
  10. data/lib/image_optim/bin_resolver/bin.rb +17 -7
  11. data/lib/image_optim/cmd.rb +49 -0
  12. data/lib/image_optim/config.rb +64 -4
  13. data/lib/image_optim/image_path.rb +5 -0
  14. data/lib/image_optim/option_definition.rb +5 -3
  15. data/lib/image_optim/runner.rb +1 -2
  16. data/lib/image_optim/runner/option_parser.rb +216 -0
  17. data/lib/image_optim/worker.rb +32 -17
  18. data/lib/image_optim/worker/advpng.rb +7 -1
  19. data/lib/image_optim/worker/gifsicle.rb +16 -3
  20. data/lib/image_optim/worker/jhead.rb +15 -8
  21. data/lib/image_optim/worker/jpegoptim.rb +6 -2
  22. data/lib/image_optim/worker/jpegtran.rb +10 -3
  23. data/lib/image_optim/worker/optipng.rb +6 -1
  24. data/lib/image_optim/worker/pngcrush.rb +8 -1
  25. data/lib/image_optim/worker/pngout.rb +8 -1
  26. data/lib/image_optim/worker/svgo.rb +4 -1
  27. data/script/worker_analysis +523 -0
  28. data/script/worker_analysis.haml +153 -0
  29. data/spec/image_optim/bin_resolver/comparable_condition_spec.rb +4 -5
  30. data/spec/image_optim/bin_resolver/simple_version_spec.rb +44 -21
  31. data/spec/image_optim/bin_resolver_spec.rb +63 -29
  32. data/spec/image_optim/cmd_spec.rb +66 -0
  33. data/spec/image_optim/config_spec.rb +38 -38
  34. data/spec/image_optim/handler_spec.rb +15 -12
  35. data/spec/image_optim/hash_helpers_spec.rb +14 -13
  36. data/spec/image_optim/image_path_spec.rb +22 -7
  37. data/spec/image_optim/runner/glob_helpers_spec.rb +6 -5
  38. data/spec/image_optim/runner/option_parser_spec.rb +99 -0
  39. data/spec/image_optim/space_spec.rb +5 -4
  40. data/spec/image_optim/worker_spec.rb +6 -5
  41. data/spec/image_optim_spec.rb +209 -237
  42. data/spec/spec_helper.rb +3 -0
  43. metadata +43 -11
@@ -1,6 +1,8 @@
1
1
  require 'image_optim/bin_resolver/error'
2
2
  require 'image_optim/bin_resolver/simple_version'
3
3
  require 'image_optim/bin_resolver/comparable_condition'
4
+ require 'image_optim/cmd'
5
+ require 'shellwords'
4
6
 
5
7
  class ImageOptim
6
8
  class BinResolver
@@ -11,7 +13,7 @@ class ImageOptim
11
13
  attr_reader :name, :path, :version
12
14
  def initialize(name, path)
13
15
  @name = name.to_sym
14
- @path = path
16
+ @path = path.to_s
15
17
  @version = detect_version
16
18
  end
17
19
 
@@ -59,18 +61,18 @@ class ImageOptim
59
61
  def version_string
60
62
  case name
61
63
  when :advpng, :gifsicle, :jpegoptim, :optipng, :pngquant
62
- `#{path.shellescape} --version 2> /dev/null`[/\d+(\.\d+){1,}/]
64
+ capture("#{escaped_path} --version 2> /dev/null")[/\d+(\.\d+){1,}/]
63
65
  when :svgo
64
- `#{path.shellescape} --version 2>&1`[/\d+(\.\d+){1,}/]
66
+ capture("#{escaped_path} --version 2>&1")[/\d+(\.\d+){1,}/]
65
67
  when :jhead
66
- `#{path.shellescape} -V 2> /dev/null`[/\d+(\.\d+){1,}/]
68
+ capture("#{escaped_path} -V 2> /dev/null")[/\d+(\.\d+){1,}/]
67
69
  when :jpegtran
68
- `#{path.shellescape} -v - 2>&1`[/version (\d+\S*)/, 1]
70
+ capture("#{escaped_path} -v - 2>&1")[/version (\d+\S*)/, 1]
69
71
  when :pngcrush
70
- `#{path.shellescape} -version 2>&1`[/\d+(\.\d+){1,}/]
72
+ capture("#{escaped_path} -version 2>&1")[/\d+(\.\d+){1,}/]
71
73
  when :pngout
72
74
  date_regexp = /[A-Z][a-z]{2} (?: |\d)\d \d{4}/
73
- date_str = `#{path.shellescape} 2>&1`[date_regexp]
75
+ date_str = capture("#{escaped_path} 2>&1")[date_regexp]
74
76
  Date.parse(date_str).strftime('%Y%m%d') if date_str
75
77
  when :jpegrescan
76
78
  # jpegrescan has no version so just check presence
@@ -79,6 +81,14 @@ class ImageOptim
79
81
  fail "getting `#{name}` version is not defined"
80
82
  end
81
83
  end
84
+
85
+ def capture(cmd)
86
+ Cmd.capture(cmd)
87
+ end
88
+
89
+ def escaped_path
90
+ path.shellescape
91
+ end
82
92
  end
83
93
  end
84
94
  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
@@ -2,6 +2,7 @@ require 'image_optim/option_helpers'
2
2
  require 'image_optim/configuration_error'
3
3
  require 'image_optim/hash_helpers'
4
4
  require 'image_optim/worker'
5
+ require 'image_optim/cmd'
5
6
  require 'set'
6
7
  require 'yaml'
7
8
 
@@ -10,9 +11,13 @@ class ImageOptim
10
11
  class Config
11
12
  include OptionHelpers
12
13
 
14
+ # Global config path at `$XDG_CONFIG_HOME/image_optim.yml` (by default
15
+ # `~/.config/image_optim.yml`)
13
16
  GLOBAL_PATH = begin
14
17
  File.join(ENV['XDG_CONFIG_HOME'] || '~/.config', 'image_optim.yml')
15
18
  end
19
+
20
+ # Local config path at `./.image_optim.yml`
16
21
  LOCAL_PATH = './.image_optim.yml'
17
22
 
18
23
  class << self
@@ -37,6 +42,9 @@ class ImageOptim
37
42
  end
38
43
  end
39
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`
40
48
  def initialize(options)
41
49
  config_paths = options.delete(:config_paths) || [GLOBAL_PATH, LOCAL_PATH]
42
50
  config_paths = Array(config_paths)
@@ -50,18 +58,31 @@ class ImageOptim
50
58
  @used = Set.new
51
59
  end
52
60
 
61
+ # Gets value for key converted to symbol and mark option as used
53
62
  def get!(key)
54
63
  key = key.to_sym
55
64
  @used << key
56
65
  @options[key]
57
66
  end
58
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!`)
59
76
  def assert_no_unused_options!
60
77
  unknown_options = @options.reject{ |key, _value| @used.include?(key) }
61
78
  return if unknown_options.empty?
62
79
  fail ConfigurationError, "unknown options #{unknown_options.inspect}"
63
80
  end
64
81
 
82
+ # Nice level:
83
+ # * `10` by default and for `nil` or `true`
84
+ # * `0` for `false`
85
+ # * otherwise convert to integer
65
86
  def nice
66
87
  nice = get!(:nice)
67
88
 
@@ -75,6 +96,10 @@ class ImageOptim
75
96
  end
76
97
  end
77
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
78
103
  def threads
79
104
  threads = get!(:threads)
80
105
 
@@ -88,10 +113,40 @@ class ImageOptim
88
113
  end
89
114
  end
90
115
 
116
+ # Verbose mode, converted to boolean
91
117
  def verbose
92
118
  !!get!(:verbose)
93
119
  end
94
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
+ # Options for worker class by its `bin_sym`:
146
+ # * `Hash` passed as is
147
+ # * `{}` for `true` or `nil`
148
+ # * `false` for `false`
149
+ # * otherwise fail with `ConfigurationError`
95
150
  def for_worker(klass)
96
151
  worker_options = get!(klass.bin_sym)
97
152
 
@@ -108,6 +163,7 @@ class ImageOptim
108
163
  end
109
164
  end
110
165
 
166
+ # yaml dump without document beginning prefix `---`
111
167
  def to_s
112
168
  YAML.dump(HashHelpers.deep_stringify_keys(@options)).sub(/\A---\n/, '')
113
169
  end
@@ -118,13 +174,17 @@ class ImageOptim
118
174
  def processor_count
119
175
  @processor_count ||= case host_os = RbConfig::CONFIG['host_os']
120
176
  when /darwin9/
121
- `hwprefs cpu_count`
177
+ Cmd.capture 'hwprefs cpu_count'
122
178
  when /darwin/
123
- (`which hwprefs` != '') ? `hwprefs thread_count` : `sysctl -n hw.ncpu`
179
+ if (Cmd.capture 'which hwprefs') != ''
180
+ Cmd.capture 'hwprefs thread_count'
181
+ else
182
+ Cmd.capture 'sysctl -n hw.ncpu'
183
+ end
124
184
  when /linux/
125
- `grep -c processor /proc/cpuinfo`
185
+ Cmd.capture 'grep -c processor /proc/cpuinfo'
126
186
  when /freebsd/
127
- `sysctl -n hw.ncpu`
187
+ Cmd.capture 'sysctl -n hw.ncpu'
128
188
  when /mswin|mingw/
129
189
  require 'win32ole'
130
190
  WIN32OLE.
@@ -54,6 +54,11 @@ class ImageOptim
54
54
  image_meta && image_meta.format
55
55
  end
56
56
 
57
+ # Read binary data
58
+ def binread
59
+ open('rb', &:read)
60
+ end
61
+
57
62
  # Returns path if it is already an instance of this class otherwise new
58
63
  # instance
59
64
  def self.convert(path)
@@ -3,9 +3,11 @@ class ImageOptim
3
3
  class OptionDefinition
4
4
  attr_reader :name, :default, :type, :description, :proc
5
5
 
6
- def initialize(name, default, type, description, &proc)
7
- if type.is_a?(String)
8
- type, description = default.class, type
6
+ def initialize(name, default, type_or_description, description = nil, &proc)
7
+ if type_or_description.is_a?(Class)
8
+ type = type_or_description
9
+ else
10
+ type, description = default.class, type_or_description
9
11
  end
10
12
 
11
13
  @name = name.to_sym
@@ -3,7 +3,6 @@ require 'image_optim/hash_helpers'
3
3
  require 'image_optim/runner/glob_helpers'
4
4
  require 'image_optim/space'
5
5
  require 'progress'
6
- require 'optparse'
7
6
  require 'find'
8
7
  require 'yaml'
9
8
 
@@ -127,7 +126,7 @@ class ImageOptim
127
126
  basename = File.basename(path)
128
127
  globs.any? do |glob|
129
128
  File.fnmatch(glob, relative_path, File::FNM_PATHNAME) ||
130
- File.fnmatch(glob, basename, File::FNM_PATHNAME)
129
+ File.fnmatch(glob, basename, File::FNM_PATHNAME)
131
130
  end
132
131
  end
133
132
 
@@ -0,0 +1,216 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'image_optim'
4
+ require 'image_optim/true_false_nil'
5
+ require 'image_optim/non_negative_integer_range'
6
+ require 'optparse'
7
+
8
+ class ImageOptim
9
+ class Runner
10
+ # Parse options from arguments to image_optim binary
11
+ class OptionParser < ::OptionParser
12
+ # Parse and remove options from args, return options Hash
13
+ # Calls abort in case of parse error
14
+ def self.parse!(args)
15
+ # assume -v to be a request to print version if it is the only argument
16
+ args = %w[--version] if args == %w[-v]
17
+
18
+ options = {}
19
+ parser = new(options)
20
+ parser.parse!(args)
21
+ options
22
+ rescue OptionParser::ParseError => e
23
+ abort "#{e}\n\n#{parser.help}"
24
+ end
25
+
26
+ # After initialization passes self and options to DEFINE
27
+ def initialize(options)
28
+ super
29
+ DEFINE.call(self, options)
30
+ end
31
+
32
+ # Wraps and indents lines of overriden method
33
+ def help
34
+ text = super
35
+
36
+ # reserve one column
37
+ columns = terminal_columns - 1
38
+ # 1 for distance between summary and description
39
+ # 2 for additional indent
40
+ wrapped_indent = summary_indent + ' ' * (summary_width + 1 + 2)
41
+ wrapped_width = columns - wrapped_indent.length
42
+ # don't try to wrap if there is too little space for description
43
+ return text if wrapped_width < 20
44
+
45
+ wrapped = ''
46
+ text.split("\n").each do |line|
47
+ if line.length <= columns
48
+ wrapped << line << "\n"
49
+ else
50
+ indented = line =~ /^\s/
51
+ wrapped << line.slice!(wrap_regex(columns)) << "\n"
52
+ line.scan(wrap_regex(wrapped_width)) do |part|
53
+ wrapped << wrapped_indent if indented
54
+ wrapped << part << "\n"
55
+ end
56
+ end
57
+ end
58
+ wrapped
59
+ end
60
+
61
+ private
62
+
63
+ def terminal_columns
64
+ stty_columns = `stty size 2> /dev/null`[/^\d+ (\d+)$/, 1]
65
+ stty_columns ? stty_columns.to_i : `tput cols`.to_i
66
+ end
67
+
68
+ def wrap_regex(width)
69
+ /.*?.{1,#{width}}(?:\s|\z)/
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ ImageOptim::Runner::OptionParser::DEFINE = proc do |op, options|
76
+ unless op.is_a?(OptionParser)
77
+ fail ArgumentError, "expected instance of OptionParser, got #{op.inspect}"
78
+ end
79
+ unless options.is_a?(Hash)
80
+ fail ArgumentError, "expected instance of Hash, got #{options.inspect}"
81
+ end
82
+
83
+ ImageOptim::TrueFalseNil.add_to_option_parser(op)
84
+ ImageOptim::NonNegativeIntegerRange.add_to_option_parser(op)
85
+
86
+ op.banner = <<-TEXT.gsub(/^\s*\|/, '')
87
+ |#{ImageOptim.full_version}
88
+ |
89
+ |Usege:
90
+ | #{op.program_name} [options] image_path …
91
+ |
92
+ |Configuration will be read and prepanded to options from two paths:
93
+ | #{ImageOptim::Config::GLOBAL_PATH}
94
+ | #{ImageOptim::Config::LOCAL_PATH}
95
+ |
96
+ TEXT
97
+
98
+ op.on('--config-paths PATH1,PATH2', Array, 'Config paths to use instead of '\
99
+ 'default ones') do |paths|
100
+ options[:config_paths] = paths
101
+ end
102
+
103
+ op.separator nil
104
+
105
+ op.on('-r', '-R', '--recursive', 'Recursively scan directories '\
106
+ 'for images') do |recursive|
107
+ options[:recursive] = recursive
108
+ end
109
+
110
+ op.on("--exclude-dir 'GLOB'", 'Glob for excluding directories '\
111
+ '(defaults to .*)') do |glob|
112
+ options[:exclude_dir_glob] = glob
113
+ end
114
+
115
+ op.on("--exclude-file 'GLOB'", 'Glob for excluding files '\
116
+ '(defaults to .*)') do |glob|
117
+ options[:exclude_file_glob] = glob
118
+ end
119
+
120
+ op.on("--exclude 'GLOB'", 'Set glob for excluding both directories and '\
121
+ 'files') do |glob|
122
+ options[:exclude_file_glob] = options[:exclude_dir_glob] = glob
123
+ end
124
+
125
+ op.separator nil
126
+
127
+ op.on('--[no-]threads N', Integer, 'Number of threads or disable '\
128
+ '(defaults to number of processors)') do |threads|
129
+ options[:threads] = threads
130
+ end
131
+
132
+ op.on('--[no-]nice N', Integer, 'Nice level (defaults to 10)') do |nice|
133
+ options[:nice] = nice
134
+ end
135
+
136
+ op.on('--[no-]pack', 'Require image_optim_pack or disable it, '\
137
+ 'by default image_optim_pack will be used if available, '\
138
+ 'will turn on skip-missing-workers unless explicitly disabled') do |pack|
139
+ options[:pack] = pack
140
+ end
141
+
142
+ op.separator nil
143
+ op.separator ' Disabling workers:'
144
+
145
+ op.on('--[no-]skip-missing-workers', 'Skip workers with missing or '\
146
+ 'problematic binaries') do |skip|
147
+ options[:skip_missing_workers] = skip
148
+ end
149
+
150
+ ImageOptim::Worker.klasses.each do |klass|
151
+ bin = klass.bin_sym
152
+ op.on("--no-#{bin}", "disable #{bin} worker") do |enable|
153
+ options[bin] = enable
154
+ end
155
+ end
156
+
157
+ op.separator nil
158
+ op.separator ' Worker options:'
159
+
160
+ ImageOptim::Worker.klasses.each_with_index do |klass, i|
161
+ next if klass.option_definitions.empty?
162
+ op.separator nil unless i.zero?
163
+
164
+ bin = klass.bin_sym
165
+ klass.option_definitions.each do |option_definition|
166
+ name = option_definition.name.to_s.gsub('_', '-')
167
+ default = option_definition.default
168
+ type = option_definition.type
169
+
170
+ type, marking = case
171
+ when [TrueClass, FalseClass, ImageOptim::TrueFalseNil].include?(type)
172
+ [type, 'B']
173
+ when Integer >= type
174
+ [Integer, 'N']
175
+ when Array >= type
176
+ [Array, 'a,b,c']
177
+ when ImageOptim::NonNegativeIntegerRange == type
178
+ [type, 'M-N']
179
+ else
180
+ fail "Unknown type #{type}"
181
+ end
182
+
183
+ description_lines = %W[
184
+ #{option_definition.description.gsub(' - ', ' - ')}
185
+ (defaults to #{default})
186
+ ].join(' ')
187
+
188
+ op.on("--#{bin}-#{name} #{marking}", type, *description_lines) do |value|
189
+ options[bin] = {} unless options[bin].is_a?(Hash)
190
+ options[bin][option_definition.name.to_sym] = value
191
+ end
192
+ end
193
+ end
194
+
195
+ op.separator nil
196
+ op.separator ' Common options:'
197
+
198
+ op.on_tail('-v', '--verbose', 'Verbose output') do
199
+ options[:verbose] = true
200
+ end
201
+
202
+ op.on_tail('-h', '--help', 'Show help and exit') do
203
+ puts op.help
204
+ exit
205
+ end
206
+
207
+ op.on_tail('--version', 'Show version and exit') do
208
+ puts ImageOptim.version
209
+ exit
210
+ end
211
+
212
+ op.on_tail('--info', 'Show environment info and exit') do
213
+ options[:verbose] = true
214
+ options[:only_info] = true
215
+ end
216
+ end