image_optim 0.26.5 → 0.30.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.appveyor.yml +2 -0
  3. data/.pre-commit-hooks.yaml +9 -0
  4. data/.rubocop.yml +33 -14
  5. data/.travis.yml +17 -15
  6. data/CHANGELOG.markdown +26 -0
  7. data/CONTRIBUTING.markdown +4 -1
  8. data/LICENSE.txt +1 -1
  9. data/README.markdown +9 -3
  10. data/Vagrantfile +1 -1
  11. data/image_optim.gemspec +7 -4
  12. data/lib/image_optim.rb +15 -9
  13. data/lib/image_optim/bin_resolver/bin.rb +10 -8
  14. data/lib/image_optim/bin_resolver/comparable_condition.rb +1 -0
  15. data/lib/image_optim/cache.rb +6 -0
  16. data/lib/image_optim/cache_path.rb +19 -2
  17. data/lib/image_optim/cmd.rb +45 -6
  18. data/lib/image_optim/config.rb +13 -7
  19. data/lib/image_optim/elapsed_time.rb +26 -0
  20. data/lib/image_optim/errors.rb +9 -0
  21. data/lib/image_optim/optimized_path.rb +1 -1
  22. data/lib/image_optim/path.rb +29 -6
  23. data/lib/image_optim/runner/option_parser.rb +6 -0
  24. data/lib/image_optim/timer.rb +25 -0
  25. data/lib/image_optim/worker.rb +29 -26
  26. data/lib/image_optim/worker/advpng.rb +2 -2
  27. data/lib/image_optim/worker/class_methods.rb +2 -0
  28. data/lib/image_optim/worker/gifsicle.rb +3 -3
  29. data/lib/image_optim/worker/jhead.rb +2 -2
  30. data/lib/image_optim/worker/jpegoptim.rb +8 -6
  31. data/lib/image_optim/worker/jpegrecompress.rb +17 -2
  32. data/lib/image_optim/worker/jpegtran.rb +3 -3
  33. data/lib/image_optim/worker/optipng.rb +4 -4
  34. data/lib/image_optim/worker/pngcrush.rb +4 -4
  35. data/lib/image_optim/worker/pngout.rb +2 -2
  36. data/lib/image_optim/worker/pngquant.rb +3 -2
  37. data/lib/image_optim/worker/svgo.rb +2 -2
  38. data/script/update_worker_options_in_readme +1 -1
  39. data/script/worker_analysis +20 -19
  40. data/spec/image_optim/bin_resolver_spec.rb +5 -5
  41. data/spec/image_optim/cache_path_spec.rb +67 -28
  42. data/spec/image_optim/cache_spec.rb +10 -8
  43. data/spec/image_optim/cmd_spec.rb +58 -6
  44. data/spec/image_optim/config_spec.rb +36 -20
  45. data/spec/image_optim/elapsed_time_spec.rb +14 -0
  46. data/spec/image_optim/hash_helpers_spec.rb +18 -18
  47. data/spec/image_optim/option_definition_spec.rb +6 -6
  48. data/spec/image_optim/path_spec.rb +61 -26
  49. data/spec/image_optim/runner/option_parser_spec.rb +4 -4
  50. data/spec/image_optim/timer_spec.rb +32 -0
  51. data/spec/image_optim/worker/jpegrecompress_spec.rb +32 -0
  52. data/spec/image_optim/worker/optipng_spec.rb +11 -11
  53. data/spec/image_optim/worker/pngquant_spec.rb +5 -5
  54. data/spec/image_optim/worker_spec.rb +17 -17
  55. data/spec/image_optim_spec.rb +50 -12
  56. data/spec/images/quant/generate +2 -2
  57. data/spec/spec_helper.rb +16 -15
  58. metadata +36 -12
@@ -26,6 +26,7 @@ class ImageOptim
26
26
  end
27
27
 
28
28
  attr_reader :method, :args
29
+
29
30
  def initialize(method, *args)
30
31
  @method, @args = method.to_sym, args
31
32
 
@@ -21,6 +21,11 @@ class ImageOptim
21
21
  "#{bin.name}[#{bin.digest}]"
22
22
  end.sort!.uniq.join(', ')]
23
23
  end]
24
+ @global_options = begin
25
+ options = {}
26
+ options[:timeout] = image_optim.timeout if image_optim.timeout
27
+ options.empty? ? '' : options.inspect
28
+ end
24
29
  end
25
30
 
26
31
  def fetch(original)
@@ -68,6 +73,7 @@ class ImageOptim
68
73
  digest = Digest::SHA1.file(path)
69
74
  digest.update options_by_format(format)
70
75
  digest.update bins_by_format(format) if @cache_worker_digests
76
+ digest.update @global_options
71
77
  s = digest.hexdigest
72
78
  "#{s[0..1]}/#{s[2..-1]}"
73
79
  end
@@ -7,8 +7,25 @@ class ImageOptim
7
7
  class CachePath < Path
8
8
  # Atomic replace dst with self
9
9
  def replace(dst)
10
- dst = self.class.new(dst)
11
- dst.temp_path(dst.dirname) do |temp|
10
+ dst = self.class.convert(dst)
11
+ tmpdir = [dirname, Path.new(Dir.tmpdir)].find do |dir|
12
+ dir.same_dev?(dst.dirname)
13
+ end
14
+ if tmpdir
15
+ begin
16
+ replace_using_tmp_file(dst, tmpdir)
17
+ rescue Errno::EXDEV
18
+ replace_using_tmp_file(dst, dst.dirname)
19
+ end
20
+ else
21
+ replace_using_tmp_file(dst, dst.dirname)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def replace_using_tmp_file(dst, tmpdir)
28
+ dst.temp_path_with_tmp_ext(tmpdir) do |temp|
12
29
  copy(temp)
13
30
  dst.copy_metadata(temp)
14
31
  temp.rename(dst.to_s)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'image_optim/errors'
3
4
  require 'English'
4
5
 
5
6
  class ImageOptim
@@ -10,11 +11,30 @@ class ImageOptim
10
11
  # Return success status
11
12
  # Will raise SignalException if process was interrupted
12
13
  def run(*args)
13
- success = system(*args)
14
+ if args.last.is_a?(Hash) && (timeout = args.last.delete(:timeout))
15
+ args.last[Gem.win_platform? ? :new_pgroup : :pgroup] = true
14
16
 
15
- check_status!
17
+ pid = Process.spawn(*args)
18
+
19
+ waiter = Process.detach(pid)
20
+ if waiter.join(timeout)
21
+ status = waiter.value
22
+
23
+ check_status!(status)
24
+
25
+ status.success?
26
+ else
27
+ cleanup(pid, waiter)
16
28
 
17
- success
29
+ fail Errors::TimeoutExceeded
30
+ end
31
+ else
32
+ success = system(*args)
33
+
34
+ check_status!
35
+
36
+ success
37
+ end
18
38
  end
19
39
 
20
40
  # Run using backtick
@@ -30,9 +50,7 @@ class ImageOptim
30
50
 
31
51
  private
32
52
 
33
- def check_status!
34
- status = $CHILD_STATUS
35
-
53
+ def check_status!(status = $CHILD_STATUS)
36
54
  return unless status.signaled?
37
55
 
38
56
  # jruby incorrectly returns true for `signaled?` if process exits with
@@ -46,6 +64,27 @@ class ImageOptim
46
64
 
47
65
  fail SignalException, status.termsig
48
66
  end
67
+
68
+ def cleanup(pid, waiter)
69
+ if Gem.win_platform?
70
+ kill('KILL', pid)
71
+ else
72
+ kill('-TERM', pid)
73
+
74
+ # Allow 10 seconds for the process to exit
75
+ waiter.join(10)
76
+
77
+ kill('-KILL', pid)
78
+ end
79
+
80
+ waiter.join
81
+ end
82
+
83
+ def kill(signal, pid)
84
+ Process.kill(signal, pid)
85
+ rescue Errno::ESRCH, Errno::EPERM
86
+ # expected
87
+ end
49
88
  end
50
89
  end
51
90
  end
@@ -15,9 +15,7 @@ class ImageOptim
15
15
 
16
16
  # Global config path at `$XDG_CONFIG_HOME/image_optim.yml` (by default
17
17
  # `~/.config/image_optim.yml`)
18
- GLOBAL_PATH = begin
19
- File.join(ENV['XDG_CONFIG_HOME'] || '~/.config', 'image_optim.yml')
20
- end
18
+ GLOBAL_PATH = File.join(ENV['XDG_CONFIG_HOME'] || '~/.config', 'image_optim.yml')
21
19
 
22
20
  # Local config path at `./.image_optim.yml`
23
21
  LOCAL_PATH = './.image_optim.yml'
@@ -119,6 +117,14 @@ class ImageOptim
119
117
  end
120
118
  end
121
119
 
120
+ # Timeout in seconds for each image:
121
+ # * not set by default and for `nil`
122
+ # * otherwise converted to float
123
+ def timeout
124
+ timeout = get!(:timeout)
125
+ timeout ? timeout.to_f : nil
126
+ end
127
+
122
128
  # Verbose mode, converted to boolean
123
129
  def verbose
124
130
  !!get!(:verbose)
@@ -177,7 +183,7 @@ class ImageOptim
177
183
  when true, nil
178
184
  {}
179
185
  when false
180
- {:disable => true}
186
+ {disable: true}
181
187
  else
182
188
  fail ConfigurationError, "Got #{worker_options.inspect} for "\
183
189
  "#{klass.name} options"
@@ -197,10 +203,10 @@ class ImageOptim
197
203
  when /darwin9/
198
204
  Cmd.capture 'hwprefs cpu_count'
199
205
  when /darwin/
200
- if (Cmd.capture 'which hwprefs') != ''
201
- Cmd.capture 'hwprefs thread_count'
202
- else
206
+ if (Cmd.capture 'which hwprefs') == ''
203
207
  Cmd.capture 'sysctl -n hw.ncpu'
208
+ else
209
+ Cmd.capture 'hwprefs thread_count'
204
210
  end
205
211
  when /linux/
206
212
  Cmd.capture 'grep -c processor /proc/cpuinfo'
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ImageOptim
4
+ # Use Process.clock_gettime if available to get time more fitting to calculate elapsed time
5
+ module ElapsedTime
6
+ CLOCK_NAME = %w[
7
+ CLOCK_UPTIME_RAW
8
+ CLOCK_UPTIME
9
+ CLOCK_MONOTONIC_RAW
10
+ CLOCK_MONOTONIC
11
+ CLOCK_REALTIME
12
+ ].find{ |name| Process.const_defined?(name) }
13
+
14
+ CLOCK_ID = CLOCK_NAME && Process.const_get(CLOCK_NAME)
15
+
16
+ module_function
17
+
18
+ def now
19
+ if CLOCK_ID
20
+ Process.clock_gettime(CLOCK_ID)
21
+ else
22
+ Time.now.to_f
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ImageOptim
4
+ class Error < StandardError; end
5
+
6
+ module Errors
7
+ class TimeoutExceeded < Error; end
8
+ end
9
+ end
@@ -7,7 +7,7 @@ class ImageOptim
7
7
  class OptimizedPath < DelegateClass(Path)
8
8
  def initialize(path, original_or_size = nil)
9
9
  path = Path.convert(path)
10
- __setobj__(path)
10
+ super(path)
11
11
  if original_or_size.is_a?(Integer)
12
12
  @original = path
13
13
  @original_size = original_or_size
@@ -41,7 +41,7 @@ class ImageOptim
41
41
  dst.utime(stat.atime, stat.mtime) if time
42
42
  begin
43
43
  dst.chown(stat.uid, stat.gid)
44
- rescue Errno::EPERM
44
+ rescue Errno::EPERM, Errno::EACCES
45
45
  dst.chmod(stat.mode & 0o1777)
46
46
  else
47
47
  dst.chmod(stat.mode)
@@ -50,11 +50,16 @@ class ImageOptim
50
50
 
51
51
  # Atomic replace dst with self
52
52
  def replace(dst)
53
- dst = self.class.new(dst)
54
- dst.temp_path(dst.dirname) do |temp|
55
- move(temp)
56
- dst.copy_metadata(temp)
57
- temp.rename(dst.to_s)
53
+ dst = self.class.convert(dst)
54
+ if same_dev?(dst.dirname)
55
+ dst.copy_metadata(self)
56
+ begin
57
+ rename(dst.to_s)
58
+ rescue Errno::EXDEV
59
+ replace_using_tmp_file(dst)
60
+ end
61
+ else
62
+ replace_using_tmp_file(dst)
58
63
  end
59
64
  end
60
65
 
@@ -68,5 +73,23 @@ class ImageOptim
68
73
  def self.convert(path)
69
74
  path.is_a?(self) ? path : new(path)
70
75
  end
76
+
77
+ protected
78
+
79
+ def same_dev?(other)
80
+ stat.dev == other.stat.dev
81
+ end
82
+
83
+ def replace_using_tmp_file(dst)
84
+ dst.temp_path_with_tmp_ext(dst.dirname) do |temp|
85
+ move(temp)
86
+ dst.copy_metadata(temp)
87
+ temp.rename(dst.to_s)
88
+ end
89
+ end
90
+
91
+ def temp_path_with_tmp_ext(*args, &block)
92
+ self.class.temp_file_path([basename.to_s, '.tmp'], *args, &block)
93
+ end
71
94
  end
72
95
  end
@@ -182,6 +182,10 @@ ImageOptim::Runner::OptionParser::DEFINE = proc do |op, options|
182
182
  options[:allow_lossy] = allow_lossy
183
183
  end
184
184
 
185
+ op.on('--timeout N', Float, 'Maximum time in seconds to spend on one image') do |timeout|
186
+ options[:timeout] = timeout
187
+ end
188
+
185
189
  op.separator nil
186
190
 
187
191
  ImageOptim::Worker.klasses.each_with_index do |klass, i|
@@ -202,6 +206,8 @@ ImageOptim::Runner::OptionParser::DEFINE = proc do |op, options|
202
206
  [Integer, 'N']
203
207
  when Array >= type
204
208
  [Array, 'a,b,c']
209
+ when String >= type
210
+ [String, 'S']
205
211
  when ImageOptim::NonNegativeIntegerRange == type
206
212
  [type, 'M-N']
207
213
  else
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'image_optim/elapsed_time'
4
+
5
+ class ImageOptim
6
+ # Hold start time and timeout
7
+ class Timer
8
+ include ElapsedTime
9
+
10
+ def initialize(seconds)
11
+ @start = now
12
+ @seconds = seconds
13
+ end
14
+
15
+ def elapsed
16
+ now - @start
17
+ end
18
+
19
+ def left
20
+ @seconds - elapsed
21
+ end
22
+
23
+ alias_method :to_f, :left
24
+ end
25
+ end
@@ -3,6 +3,7 @@
3
3
 
4
4
  require 'image_optim/cmd'
5
5
  require 'image_optim/configuration_error'
6
+ require 'image_optim/elapsed_time'
6
7
  require 'image_optim/path'
7
8
  require 'image_optim/worker/class_methods'
8
9
  require 'shellwords'
@@ -41,7 +42,7 @@ class ImageOptim
41
42
 
42
43
  # Optimize image at src, output at dst, must be overriden in subclass
43
44
  # return true on success
44
- def optimize(_src, _dst)
45
+ def optimize(_src, _dst, options = {})
45
46
  fail NotImplementedError, "implement method optimize in #{self.class}"
46
47
  end
47
48
 
@@ -122,42 +123,44 @@ class ImageOptim
122
123
  end
123
124
 
124
125
  # Run command setting priority and hiding output
125
- def execute(bin, *arguments)
126
+ def execute(bin, arguments, options)
126
127
  resolve_bin!(bin)
127
128
 
128
129
  cmd_args = [bin, *arguments].map(&:to_s)
129
130
 
130
- start = Time.now
131
-
132
- success = run_command(cmd_args)
133
-
134
131
  if @image_optim.verbose
135
- seconds = Time.now - start
136
- $stderr << "#{success ? '✓' : '✗'} #{seconds}s #{cmd_args.shelljoin}\n"
132
+ run_command_verbose(cmd_args, options)
133
+ else
134
+ run_command(cmd_args, options)
137
135
  end
138
-
139
- success
140
136
  end
141
137
 
142
138
  # Run command defining environment, setting nice level, removing output and
143
139
  # reraising signal exception
144
- def run_command(cmd_args)
145
- args = if RUBY_VERSION < '1.9' || defined?(JRUBY_VERSION)
146
- %W[
147
- env PATH=#{@image_optim.env_path.shellescape}
148
- nice -n #{@image_optim.nice}
149
- #{cmd_args.shelljoin}
150
- > #{Path::NULL} 2>&1
151
- ].join(' ')
152
- else
153
- [
154
- {'PATH' => @image_optim.env_path},
155
- %W[nice -n #{@image_optim.nice}],
156
- cmd_args,
157
- {:out => Path::NULL, :err => Path::NULL},
158
- ].flatten
159
- end
140
+ def run_command(cmd_args, options)
141
+ args = [
142
+ {'PATH' => @image_optim.env_path},
143
+ *%W[nice -n #{@image_optim.nice}],
144
+ *cmd_args,
145
+ options.merge(out: Path::NULL, err: Path::NULL),
146
+ ]
160
147
  Cmd.run(*args)
161
148
  end
149
+
150
+ # Wrap run_command and output status, elapsed time and command
151
+ def run_command_verbose(cmd_args, options)
152
+ start = ElapsedTime.now
153
+
154
+ begin
155
+ success = run_command(cmd_args, options)
156
+ status = success ? '✓' : '✗'
157
+ success
158
+ rescue Errors::TimeoutExceeded
159
+ status = 'timeout'
160
+ raise
161
+ ensure
162
+ $stderr << format("%s %.1fs %s\n", status, ElapsedTime.now - start, cmd_args.shelljoin)
163
+ end
164
+ end
162
165
  end
163
166
  end
@@ -21,7 +21,7 @@ class ImageOptim
21
21
  4
22
22
  end
23
23
 
24
- def optimize(src, dst)
24
+ def optimize(src, dst, options = {})
25
25
  src.copy(dst)
26
26
  args = %W[
27
27
  --recompress
@@ -30,7 +30,7 @@ class ImageOptim
30
30
  --
31
31
  #{dst}
32
32
  ]
33
- execute(:advpng, *args) && optimized?(src, dst)
33
+ execute(:advpng, args, options) && optimized?(src, dst)
34
34
  end
35
35
  end
36
36
  end
@@ -18,6 +18,7 @@ class ImageOptim
18
18
 
19
19
  # Remember all classes inheriting from this one
20
20
  def inherited(base)
21
+ super
21
22
  @klasses << base
22
23
  end
23
24
 
@@ -36,6 +37,7 @@ class ImageOptim
36
37
 
37
38
  def option(name, default, type, description = nil, &proc)
38
39
  attr_reader name
40
+
39
41
  OptionDefinition.new(name, default, type, description, &proc).
40
42
  tap{ |option_definition| option_definitions << option_definition }
41
43
  end