image_optim 0.17.1 → 0.18.0

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