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.
- checksums.yaml +4 -4
- data/.appveyor.yml +2 -0
- data/.pre-commit-hooks.yaml +9 -0
- data/.rubocop.yml +33 -14
- data/.travis.yml +17 -15
- data/CHANGELOG.markdown +26 -0
- data/CONTRIBUTING.markdown +4 -1
- data/LICENSE.txt +1 -1
- data/README.markdown +9 -3
- data/Vagrantfile +1 -1
- data/image_optim.gemspec +7 -4
- data/lib/image_optim.rb +15 -9
- data/lib/image_optim/bin_resolver/bin.rb +10 -8
- data/lib/image_optim/bin_resolver/comparable_condition.rb +1 -0
- data/lib/image_optim/cache.rb +6 -0
- data/lib/image_optim/cache_path.rb +19 -2
- data/lib/image_optim/cmd.rb +45 -6
- data/lib/image_optim/config.rb +13 -7
- data/lib/image_optim/elapsed_time.rb +26 -0
- data/lib/image_optim/errors.rb +9 -0
- data/lib/image_optim/optimized_path.rb +1 -1
- data/lib/image_optim/path.rb +29 -6
- data/lib/image_optim/runner/option_parser.rb +6 -0
- data/lib/image_optim/timer.rb +25 -0
- data/lib/image_optim/worker.rb +29 -26
- data/lib/image_optim/worker/advpng.rb +2 -2
- data/lib/image_optim/worker/class_methods.rb +2 -0
- 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 +8 -6
- data/lib/image_optim/worker/jpegrecompress.rb +17 -2
- data/lib/image_optim/worker/jpegtran.rb +3 -3
- data/lib/image_optim/worker/optipng.rb +4 -4
- data/lib/image_optim/worker/pngcrush.rb +4 -4
- data/lib/image_optim/worker/pngout.rb +2 -2
- data/lib/image_optim/worker/pngquant.rb +3 -2
- data/lib/image_optim/worker/svgo.rb +2 -2
- data/script/update_worker_options_in_readme +1 -1
- data/script/worker_analysis +20 -19
- data/spec/image_optim/bin_resolver_spec.rb +5 -5
- data/spec/image_optim/cache_path_spec.rb +67 -28
- data/spec/image_optim/cache_spec.rb +10 -8
- 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 +61 -26
- 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 +32 -0
- 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 +50 -12
- data/spec/images/quant/generate +2 -2
- data/spec/spec_helper.rb +16 -15
- metadata +36 -12
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
|
|
@@ -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.
|
|
11
|
-
|
|
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)
|
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
|
@@ -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 =
|
|
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
|
-
{:
|
|
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
|
|
@@ -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
|
-
|
|
10
|
+
super(path)
|
|
11
11
|
if original_or_size.is_a?(Integer)
|
|
12
12
|
@original = path
|
|
13
13
|
@original_size = original_or_size
|
data/lib/image_optim/path.rb
CHANGED
|
@@ -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.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
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,42 +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)
|
|
145
|
-
args =
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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,
|
|
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
|