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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -3
- data/CHANGELOG.markdown +4 -0
- data/README.markdown +2 -1
- data/Vagrantfile +1 -1
- data/image_optim.gemspec +1 -1
- data/lib/image_optim.rb +15 -3
- data/lib/image_optim/bin_resolver/bin.rb +7 -7
- data/lib/image_optim/cache.rb +6 -0
- data/lib/image_optim/cmd.rb +45 -6
- data/lib/image_optim/config.rb +9 -1
- data/lib/image_optim/elapsed_time.rb +26 -0
- data/lib/image_optim/errors.rb +9 -0
- data/lib/image_optim/runner/option_parser.rb +4 -0
- data/lib/image_optim/timer.rb +25 -0
- data/lib/image_optim/worker.rb +24 -12
- data/lib/image_optim/worker/advpng.rb +2 -2
- data/lib/image_optim/worker/gifsicle.rb +3 -3
- data/lib/image_optim/worker/jhead.rb +2 -2
- data/lib/image_optim/worker/jpegoptim.rb +2 -2
- data/lib/image_optim/worker/jpegrecompress.rb +2 -2
- data/lib/image_optim/worker/jpegtran.rb +3 -3
- data/lib/image_optim/worker/optipng.rb +2 -2
- data/lib/image_optim/worker/pngcrush.rb +2 -2
- data/lib/image_optim/worker/pngout.rb +2 -2
- data/lib/image_optim/worker/pngquant.rb +2 -2
- data/lib/image_optim/worker/svgo.rb +2 -2
- data/script/worker_analysis +4 -4
- data/spec/image_optim/bin_resolver_spec.rb +5 -5
- data/spec/image_optim/cache_path_spec.rb +3 -7
- data/spec/image_optim/cache_spec.rb +7 -7
- data/spec/image_optim/cmd_spec.rb +58 -6
- data/spec/image_optim/config_spec.rb +36 -20
- data/spec/image_optim/elapsed_time_spec.rb +14 -0
- data/spec/image_optim/hash_helpers_spec.rb +18 -18
- data/spec/image_optim/option_definition_spec.rb +6 -6
- data/spec/image_optim/path_spec.rb +4 -8
- data/spec/image_optim/runner/option_parser_spec.rb +4 -4
- data/spec/image_optim/timer_spec.rb +32 -0
- data/spec/image_optim/worker/jpegrecompress_spec.rb +2 -2
- data/spec/image_optim/worker/optipng_spec.rb +11 -11
- data/spec/image_optim/worker/pngquant_spec.rb +5 -5
- data/spec/image_optim/worker_spec.rb +17 -17
- data/spec/image_optim_spec.rb +46 -9
- data/spec/spec_helper.rb +16 -15
- metadata +11 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0fb8179d2e5c5236278611842ad5dbb3ab39fc1ff92bbd50ed9dae6ccd931bf5
|
4
|
+
data.tar.gz: 83ab00c10f205a164ea66f51b6964d3fb4fa7d019c30e472496a0e21879242c0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
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.
|
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
|
-
|
115
|
-
|
116
|
-
|
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
|
-
:
|
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
|
-
:
|
45
|
+
pngquant: [
|
46
46
|
[is < '2.0', 'is not supported'],
|
47
47
|
],
|
48
48
|
}.freeze
|
49
49
|
|
50
50
|
WARN_CHECKS = {
|
51
|
-
:
|
51
|
+
advpng: [
|
52
52
|
[is == 'none', 'is of unknown version'],
|
53
53
|
[is < '1.17', 'does not use zopfli'],
|
54
54
|
],
|
55
|
-
:
|
55
|
+
gifsicle: [
|
56
56
|
[is < '1.85', 'does not support removing extension blocks'],
|
57
57
|
],
|
58
|
-
:
|
58
|
+
pngcrush: [
|
59
59
|
[is < '1.7.38', 'does not have blacken flag'],
|
60
60
|
],
|
61
|
-
:
|
61
|
+
pngquant: [
|
62
62
|
[is < '2.1', 'may be lossy even with quality `100-`'],
|
63
63
|
],
|
64
|
-
:
|
64
|
+
optipng: [
|
65
65
|
[is < '0.7', 'does not support -strip option'],
|
66
66
|
],
|
67
67
|
}.freeze
|
data/lib/image_optim/cache.rb
CHANGED
@@ -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
|
data/lib/image_optim/cmd.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/image_optim/config.rb
CHANGED
@@ -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
|
-
{:
|
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
|
@@ -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
|
data/lib/image_optim/worker.rb
CHANGED
@@ -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,
|
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
|
-
|
136
|
-
|
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
|
-
|
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
|