image_optim 0.29.0 → 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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -3
  3. data/CHANGELOG.markdown +4 -0
  4. data/README.markdown +2 -1
  5. data/Vagrantfile +1 -1
  6. data/image_optim.gemspec +1 -1
  7. data/lib/image_optim.rb +15 -3
  8. data/lib/image_optim/bin_resolver/bin.rb +7 -7
  9. data/lib/image_optim/cache.rb +6 -0
  10. data/lib/image_optim/cmd.rb +45 -6
  11. data/lib/image_optim/config.rb +9 -1
  12. data/lib/image_optim/elapsed_time.rb +26 -0
  13. data/lib/image_optim/errors.rb +9 -0
  14. data/lib/image_optim/runner/option_parser.rb +4 -0
  15. data/lib/image_optim/timer.rb +25 -0
  16. data/lib/image_optim/worker.rb +24 -12
  17. data/lib/image_optim/worker/advpng.rb +2 -2
  18. data/lib/image_optim/worker/gifsicle.rb +3 -3
  19. data/lib/image_optim/worker/jhead.rb +2 -2
  20. data/lib/image_optim/worker/jpegoptim.rb +2 -2
  21. data/lib/image_optim/worker/jpegrecompress.rb +2 -2
  22. data/lib/image_optim/worker/jpegtran.rb +3 -3
  23. data/lib/image_optim/worker/optipng.rb +2 -2
  24. data/lib/image_optim/worker/pngcrush.rb +2 -2
  25. data/lib/image_optim/worker/pngout.rb +2 -2
  26. data/lib/image_optim/worker/pngquant.rb +2 -2
  27. data/lib/image_optim/worker/svgo.rb +2 -2
  28. data/script/worker_analysis +4 -4
  29. data/spec/image_optim/bin_resolver_spec.rb +5 -5
  30. data/spec/image_optim/cache_path_spec.rb +3 -7
  31. data/spec/image_optim/cache_spec.rb +7 -7
  32. data/spec/image_optim/cmd_spec.rb +58 -6
  33. data/spec/image_optim/config_spec.rb +36 -20
  34. data/spec/image_optim/elapsed_time_spec.rb +14 -0
  35. data/spec/image_optim/hash_helpers_spec.rb +18 -18
  36. data/spec/image_optim/option_definition_spec.rb +6 -6
  37. data/spec/image_optim/path_spec.rb +4 -8
  38. data/spec/image_optim/runner/option_parser_spec.rb +4 -4
  39. data/spec/image_optim/timer_spec.rb +32 -0
  40. data/spec/image_optim/worker/jpegrecompress_spec.rb +2 -2
  41. data/spec/image_optim/worker/optipng_spec.rb +11 -11
  42. data/spec/image_optim/worker/pngquant_spec.rb +5 -5
  43. data/spec/image_optim/worker_spec.rb +17 -17
  44. data/spec/image_optim_spec.rb +46 -9
  45. data/spec/spec_helper.rb +16 -15
  46. metadata +11 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9035f1f635ab728b4622dc5e99d2f5e9f96eab86ea35038c4037587fd3ca90d7
4
- data.tar.gz: d24f6305c1461483d9a67a9338c4e9385ef9282070aecdc53cf0b79e7bffb5ed
3
+ metadata.gz: 0fb8179d2e5c5236278611842ad5dbb3ab39fc1ff92bbd50ed9dae6ccd931bf5
4
+ data.tar.gz: 83ab00c10f205a164ea66f51b6964d3fb4fa7d019c30e472496a0e21879242c0
5
5
  SHA512:
6
- metadata.gz: 3501154596158ee61209215d7c3a4df1b82d7384d8b54248a16992e5897701eb1e1c10bca9bbbe92312f728e403544dfa6f0dd3c3dfc33918026497550f72ef4
7
- data.tar.gz: 72ef8a7dacc6a7cc8850d996a716a5df2098cbe47e91f58a051e95664a83494346c0daa03f61384f4bd0c6674a65e4c383ac931863ef31ffed2cc6a8d57ef4a3
6
+ metadata.gz: 901e787f11a9829b96dd40f0d8aff14d4f2ac691ff3eaa8486d2915fd020b2f6e0d7425b360cf3bdbc52018afd69508078b69dd4e7846a138d9d5e9ea3b613d7
7
+ data.tar.gz: 8a21b871165184221bf9af9b9647f144a21e5da1ebe03680ac54b09edf955068f0364da1ee0d6d8dd9976bb96c76ff0cdcf49cb005f43fd5c5cdfe3f51cde39f
data/.rubocop.yml CHANGED
@@ -109,9 +109,6 @@ Style/HashConversion:
109
109
  Style/HashEachMethods:
110
110
  Enabled: true
111
111
 
112
- Style/HashSyntax:
113
- EnforcedStyle: hash_rockets
114
-
115
112
  Style/HashTransformKeys:
116
113
  Enabled: false
117
114
 
@@ -130,6 +127,9 @@ Style/OptionalBooleanParameter:
130
127
  Style/ParallelAssignment:
131
128
  Enabled: false
132
129
 
130
+ Style/RedundantBegin:
131
+ Enabled: false
132
+
133
133
  Style/RescueStandardError:
134
134
  EnforcedStyle: implicit
135
135
 
data/CHANGELOG.markdown CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## unreleased
4
4
 
5
+ ## v0.30.0 (2021-05-11)
6
+
7
+ * Add `timeout` option to restrict maximum time spent on every image [#21](https://github.com/toy/image_optim/issues/21) [#148](https://github.com/toy/image_optim/pull/148) [#149](https://github.com/toy/image_optim/pull/149) [#162](https://github.com/toy/image_optim/pull/162) [#184](https://github.com/toy/image_optim/pull/184) [#189](https://github.com/toy/image_optim/pull/189) [@tgxworld](https://github.com/tgxworld) [@oblakeerickson](https://github.com/oblakeerickson) [@toy](https://github.com/toy)
8
+
5
9
  ## v0.29.0 (2021-04-28)
6
10
 
7
11
  * Require at least ruby 1.9.3 [@toy](https://github.com/toy)
data/README.markdown CHANGED
@@ -60,7 +60,7 @@ With version:
60
60
 
61
61
  <!---<update-version>-->
62
62
  ```ruby
63
- gem 'image_optim', '~> 0.29'
63
+ gem 'image_optim', '~> 0.30'
64
64
  ```
65
65
  <!---</update-version>-->
66
66
 
@@ -291,6 +291,7 @@ optipng:
291
291
  * `:allow_lossy` — Allow lossy workers and optimizations *(defaults to `false`)*
292
292
  * `:cache_dir` — Configure cache directory
293
293
  * `:cache_worker_digests` - Also cache worker digests along with original file digest and worker options: updating workers invalidates cache
294
+ * `:timeout` — Maximum time in seconds to spend on one image, note multithreading and cache *(defaults to unlimited)*
294
295
 
295
296
  Worker can be disabled by passing `false` instead of options hash or by setting option `:disable` to `true`.
296
297
 
data/Vagrantfile CHANGED
@@ -3,7 +3,7 @@
3
3
  Vagrant.configure('2') do |config|
4
4
  config.vm.box = 'ubuntu/precise64'
5
5
 
6
- config.vm.provision 'shell', :inline => <<-SH
6
+ config.vm.provision 'shell', inline: <<-SH
7
7
  set -e
8
8
 
9
9
  cd /vagrant
data/image_optim.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'image_optim'
5
- s.version = '0.29.0'
5
+ s.version = '0.30.0'
6
6
  s.summary = %q{Command line tool and ruby interface to optimize (lossless compress, optionally lossy) jpeg, png, gif and svg images using external utilities (advpng, gifsicle, jhead, jpeg-recompress, jpegoptim, jpegrescan, jpegtran, optipng, pngcrush, pngout, pngquant, svgo)}
7
7
  s.homepage = "https://github.com/toy/#{s.name}"
8
8
  s.authors = ['Ivan Kuchin']
data/lib/image_optim.rb CHANGED
@@ -3,10 +3,12 @@
3
3
  require 'image_optim/bin_resolver'
4
4
  require 'image_optim/cache'
5
5
  require 'image_optim/config'
6
+ require 'image_optim/errors'
6
7
  require 'image_optim/handler'
7
8
  require 'image_optim/image_meta'
8
9
  require 'image_optim/optimized_path'
9
10
  require 'image_optim/path'
11
+ require 'image_optim/timer'
10
12
  require 'image_optim/worker'
11
13
  require 'in_threads'
12
14
  require 'shellwords'
@@ -46,6 +48,9 @@ class ImageOptim
46
48
  # Cache worker digests
47
49
  attr_reader :cache_worker_digests
48
50
 
51
+ # Timeout in seconds for each image
52
+ attr_reader :timeout
53
+
49
54
  # Initialize workers, specify options using worker underscored name:
50
55
  #
51
56
  # pass false to disable worker
@@ -78,6 +83,7 @@ class ImageOptim
78
83
  allow_lossy
79
84
  cache_dir
80
85
  cache_worker_digests
86
+ timeout
81
87
  ].each do |name|
82
88
  instance_variable_set(:"@#{name}", config.send(name))
83
89
  $stderr << "#{name}: #{send(name)}\n" if verbose
@@ -110,11 +116,17 @@ class ImageOptim
110
116
  return unless (workers = workers_for_image(original))
111
117
 
112
118
  optimized = @cache.fetch(original) do
119
+ timer = timeout && Timer.new(timeout)
120
+
113
121
  Handler.for(original) do |handler|
114
- workers.each do |worker|
115
- handler.process do |src, dst|
116
- worker.optimize(src, dst)
122
+ begin
123
+ workers.each do |worker|
124
+ handler.process do |src, dst|
125
+ worker.optimize(src, dst, timeout: timer)
126
+ end
117
127
  end
128
+ rescue Errors::TimeoutExceeded
129
+ handler.result
118
130
  end
119
131
  end
120
132
  end
@@ -38,30 +38,30 @@ class ImageOptim
38
38
  is = ComparableCondition.is
39
39
 
40
40
  FAIL_CHECKS = {
41
- :pngcrush => [
41
+ pngcrush: [
42
42
  [is.between?('1.7.60', '1.7.65'), 'is known to produce broken pngs'],
43
43
  [is == '1.7.80', 'loses one color in indexed images'],
44
44
  ],
45
- :pngquant => [
45
+ pngquant: [
46
46
  [is < '2.0', 'is not supported'],
47
47
  ],
48
48
  }.freeze
49
49
 
50
50
  WARN_CHECKS = {
51
- :advpng => [
51
+ advpng: [
52
52
  [is == 'none', 'is of unknown version'],
53
53
  [is < '1.17', 'does not use zopfli'],
54
54
  ],
55
- :gifsicle => [
55
+ gifsicle: [
56
56
  [is < '1.85', 'does not support removing extension blocks'],
57
57
  ],
58
- :pngcrush => [
58
+ pngcrush: [
59
59
  [is < '1.7.38', 'does not have blacken flag'],
60
60
  ],
61
- :pngquant => [
61
+ pngquant: [
62
62
  [is < '2.1', 'may be lossy even with quality `100-`'],
63
63
  ],
64
- :optipng => [
64
+ optipng: [
65
65
  [is < '0.7', 'does not support -strip option'],
66
66
  ],
67
67
  }.freeze
@@ -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
@@ -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
@@ -117,6 +117,14 @@ class ImageOptim
117
117
  end
118
118
  end
119
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
+
120
128
  # Verbose mode, converted to boolean
121
129
  def verbose
122
130
  !!get!(:verbose)
@@ -175,7 +183,7 @@ class ImageOptim
175
183
  when true, nil
176
184
  {}
177
185
  when false
178
- {:disable => true}
186
+ {disable: true}
179
187
  else
180
188
  fail ConfigurationError, "Got #{worker_options.inspect} for "\
181
189
  "#{klass.name} options"
@@ -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
@@ -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|
@@ -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,33 +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)
140
+ def run_command(cmd_args, options)
145
141
  args = [
146
142
  {'PATH' => @image_optim.env_path},
147
143
  *%W[nice -n #{@image_optim.nice}],
148
144
  *cmd_args,
149
- {:out => Path::NULL, :err => Path::NULL},
145
+ options.merge(out: Path::NULL, err: Path::NULL),
150
146
  ]
151
147
  Cmd.run(*args)
152
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
153
165
  end
154
166
  end