image_optim 0.31.3 → 0.32.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/.github/dependabot.yml +8 -0
- data/.github/workflows/build-test-containers.yml +60 -0
- data/.github/workflows/check.yml +43 -26
- data/.github/workflows/rubocop.yml +2 -2
- data/.rubocop.yml +5 -0
- data/CHANGELOG.markdown +13 -0
- data/Dockerfile.test +46 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +1 -1
- data/README.markdown +29 -6
- data/image_optim.gemspec +15 -5
- data/lib/image_optim/benchmark_result.rb +24 -0
- data/lib/image_optim/bin_resolver.rb +1 -1
- data/lib/image_optim/config.rb +15 -6
- data/lib/image_optim/runner/option_parser.rb +60 -45
- data/lib/image_optim/runner.rb +69 -6
- data/lib/image_optim/table.rb +64 -0
- data/lib/image_optim/worker/svgo.rb +72 -7
- data/lib/image_optim.rb +22 -0
- data/script/update-rubygems-n-bundler +50 -0
- data/script/update_worker_options_in_readme +16 -3
- data/spec/image_optim/bin_resolver_spec.rb +3 -3
- data/spec/image_optim/cache_spec.rb +3 -1
- data/spec/image_optim/option_definition_spec.rb +1 -3
- data/spec/image_optim/runner/option_parser_spec.rb +10 -1
- data/spec/image_optim/true_false_nil_spec.rb +24 -0
- data/spec/image_optim/worker/svgo_spec.rb +96 -0
- data/spec/image_optim_spec.rb +19 -4
- data/spec/images/invisiblepixels/generate +3 -1
- data/spec/images/invisiblepixels/image.png +0 -0
- data/spec/spec_helper.rb +8 -8
- metadata +14 -9
- data/.github/workflows/codeql.yml +0 -30
|
@@ -34,8 +34,8 @@ class ImageOptim
|
|
|
34
34
|
def help
|
|
35
35
|
text = super
|
|
36
36
|
|
|
37
|
-
# reserve one column
|
|
38
|
-
columns = terminal_columns - 1
|
|
37
|
+
# reserve one column and limit to 120
|
|
38
|
+
columns = [terminal_columns - 1, 120].min
|
|
39
39
|
# 1 for distance between summary and description
|
|
40
40
|
# 2 for additional indent
|
|
41
41
|
wrapped_indent = summary_indent + (' ' * (summary_width + 1 + 2))
|
|
@@ -43,20 +43,7 @@ class ImageOptim
|
|
|
43
43
|
# don't try to wrap if there is too little space for description
|
|
44
44
|
return text if wrapped_width < 20
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
text.split("\n").each do |line|
|
|
48
|
-
if line.length <= columns
|
|
49
|
-
wrapped << line << "\n"
|
|
50
|
-
else
|
|
51
|
-
indented = line =~ /^\s/
|
|
52
|
-
wrapped << line.slice!(wrap_regex(columns)) << "\n"
|
|
53
|
-
line.scan(wrap_regex(wrapped_width)) do |part|
|
|
54
|
-
wrapped << wrapped_indent if indented
|
|
55
|
-
wrapped << part << "\n"
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
wrapped
|
|
46
|
+
wrapped_text(text, wrapped_width, wrapped_indent, columns)
|
|
60
47
|
end
|
|
61
48
|
|
|
62
49
|
private
|
|
@@ -66,6 +53,27 @@ class ImageOptim
|
|
|
66
53
|
stty_columns ? stty_columns.to_i : `tput cols`.to_i
|
|
67
54
|
end
|
|
68
55
|
|
|
56
|
+
def wrapped_text(text, wrapped_width, wrapped_indent, columns)
|
|
57
|
+
wrapped = []
|
|
58
|
+
text.split("\n").each do |line|
|
|
59
|
+
if line.length <= columns
|
|
60
|
+
wrapped << line << "\n"
|
|
61
|
+
else
|
|
62
|
+
wrapped << line.slice!(wrap_regex(columns)).rstrip << "\n"
|
|
63
|
+
if line =~ /^\s/
|
|
64
|
+
line.scan(wrap_regex(wrapped_width)) do |part|
|
|
65
|
+
wrapped << wrapped_indent << part.rstrip << "\n"
|
|
66
|
+
end
|
|
67
|
+
else
|
|
68
|
+
line.scan(wrap_regex(columns)) do |part|
|
|
69
|
+
wrapped << part.rstrip << "\n"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
wrapped.join(nil)
|
|
75
|
+
end
|
|
76
|
+
|
|
69
77
|
def wrap_regex(width)
|
|
70
78
|
/.*?.{1,#{width}}(?:\s|\z)/
|
|
71
79
|
end
|
|
@@ -96,30 +104,25 @@ ImageOptim::Runner::OptionParser::DEFINE = proc do |op, options|
|
|
|
96
104
|
|
|
|
97
105
|
TEXT
|
|
98
106
|
|
|
99
|
-
op.on('--config-paths PATH1,PATH2', Array, 'Config paths to use instead of '
|
|
100
|
-
'default ones') do |paths|
|
|
107
|
+
op.on('--config-paths PATH1,PATH2', Array, 'Config paths to use instead of default ones') do |paths|
|
|
101
108
|
options[:config_paths] = paths
|
|
102
109
|
end
|
|
103
110
|
|
|
104
111
|
op.separator nil
|
|
105
112
|
|
|
106
|
-
op.on('-r', '-R', '--recursive', 'Recursively scan directories '
|
|
107
|
-
'for images') do |recursive|
|
|
113
|
+
op.on('-r', '-R', '--recursive', 'Recursively scan directories for images') do |recursive|
|
|
108
114
|
options[:recursive] = recursive
|
|
109
115
|
end
|
|
110
116
|
|
|
111
|
-
op.on("--exclude-dir 'GLOB'", 'Glob for excluding directories '
|
|
112
|
-
'(defaults to .*)') do |glob|
|
|
117
|
+
op.on("--exclude-dir 'GLOB'", 'Glob for excluding directories (defaults to .*)') do |glob|
|
|
113
118
|
options[:exclude_dir_glob] = glob
|
|
114
119
|
end
|
|
115
120
|
|
|
116
|
-
op.on("--exclude-file 'GLOB'", 'Glob for excluding files '
|
|
117
|
-
'(defaults to .*)') do |glob|
|
|
121
|
+
op.on("--exclude-file 'GLOB'", 'Glob for excluding files (defaults to .*)') do |glob|
|
|
118
122
|
options[:exclude_file_glob] = glob
|
|
119
123
|
end
|
|
120
124
|
|
|
121
|
-
op.on("--exclude 'GLOB'", 'Set glob for excluding both directories and '
|
|
122
|
-
'files') do |glob|
|
|
125
|
+
op.on("--exclude 'GLOB'", 'Set glob for excluding both directories and files') do |glob|
|
|
123
126
|
options[:exclude_file_glob] = options[:exclude_dir_glob] = glob
|
|
124
127
|
end
|
|
125
128
|
|
|
@@ -129,41 +132,51 @@ ImageOptim::Runner::OptionParser::DEFINE = proc do |op, options|
|
|
|
129
132
|
options[:show_progress] = show_progress
|
|
130
133
|
end
|
|
131
134
|
|
|
132
|
-
op.on('--[no-]threads N', Integer, 'Number of threads or disable '
|
|
133
|
-
'(defaults to number of processors)') do |threads|
|
|
135
|
+
op.on('--[no-]threads N', Integer, 'Number of threads or disable (defaults to number of processors)') do |threads|
|
|
134
136
|
options[:threads] = threads
|
|
135
137
|
end
|
|
136
138
|
|
|
137
|
-
op.on(
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
op.on(
|
|
140
|
+
'--[no-]nice N',
|
|
141
|
+
Integer,
|
|
142
|
+
'Nice level, priority of all used tools with higher value meaning lower priority, in range -20..19, negative ' \
|
|
143
|
+
'values can be set only if run by root user (defaults to 10)'
|
|
144
|
+
) do |nice|
|
|
140
145
|
options[:nice] = nice
|
|
141
146
|
end
|
|
142
147
|
|
|
143
|
-
op.on(
|
|
144
|
-
|
|
145
|
-
|
|
148
|
+
op.on(
|
|
149
|
+
'--[no-]pack',
|
|
150
|
+
'Require image_optim_pack or disable it, by default image_optim_pack will be used if available, will turn on ' \
|
|
151
|
+
'skip-missing-workers unless explicitly disabled'
|
|
152
|
+
) do |pack|
|
|
146
153
|
options[:pack] = pack
|
|
147
154
|
end
|
|
148
155
|
|
|
156
|
+
op.separator nil
|
|
157
|
+
op.on(
|
|
158
|
+
'--benchmark TYPE',
|
|
159
|
+
[:isolated],
|
|
160
|
+
'Run benchmarks, to compare tools without modifying images. `isolated` is the only supported type so far.'
|
|
161
|
+
) do |benchmark|
|
|
162
|
+
options[:benchmark] = benchmark
|
|
163
|
+
end
|
|
164
|
+
|
|
149
165
|
op.separator nil
|
|
150
166
|
op.separator ' Caching:'
|
|
151
167
|
|
|
152
|
-
op.on('--cache-dir DIR', 'Cache optimized images '
|
|
153
|
-
'into the specified directory') do |cache_dir|
|
|
168
|
+
op.on('--cache-dir DIR', 'Cache optimized images into the specified directory') do |cache_dir|
|
|
154
169
|
options[:cache_dir] = cache_dir
|
|
155
170
|
end
|
|
156
171
|
|
|
157
|
-
op.on('--cache-worker-digests', 'Cache worker digests '
|
|
158
|
-
'(updating workers invalidates cache)') do |cache_worker_digests|
|
|
172
|
+
op.on('--cache-worker-digests', 'Cache worker digests (updating workers invalidates cache)') do |cache_worker_digests|
|
|
159
173
|
options[:cache_worker_digests] = cache_worker_digests
|
|
160
174
|
end
|
|
161
175
|
|
|
162
176
|
op.separator nil
|
|
163
177
|
op.separator ' Disabling workers:'
|
|
164
178
|
|
|
165
|
-
op.on('--[no-]skip-missing-workers', 'Skip workers with missing or '
|
|
166
|
-
'problematic binaries') do |skip|
|
|
179
|
+
op.on('--[no-]skip-missing-workers', 'Skip workers with missing or problematic binaries') do |skip|
|
|
167
180
|
options[:skip_missing_workers] = skip
|
|
168
181
|
end
|
|
169
182
|
|
|
@@ -177,8 +190,7 @@ ImageOptim::Runner::OptionParser::DEFINE = proc do |op, options|
|
|
|
177
190
|
op.separator nil
|
|
178
191
|
op.separator ' Worker options:'
|
|
179
192
|
|
|
180
|
-
op.on('--allow-lossy', 'Allow lossy workers and '
|
|
181
|
-
'optimizations') do |allow_lossy|
|
|
193
|
+
op.on('--allow-lossy', 'Allow lossy workers and optimizations') do |allow_lossy|
|
|
182
194
|
options[:allow_lossy] = allow_lossy
|
|
183
195
|
end
|
|
184
196
|
|
|
@@ -229,9 +241,12 @@ ImageOptim::Runner::OptionParser::DEFINE = proc do |op, options|
|
|
|
229
241
|
op.separator nil
|
|
230
242
|
op.separator ' Common options:'
|
|
231
243
|
|
|
232
|
-
op.on_tail(
|
|
233
|
-
|
|
234
|
-
|
|
244
|
+
op.on_tail(
|
|
245
|
+
'-v',
|
|
246
|
+
'--verbose',
|
|
247
|
+
'Verbose output (show global and worker config, binary resolution log, information about each tool invocation, ' \
|
|
248
|
+
'backtrace of exception)'
|
|
249
|
+
) do
|
|
235
250
|
options[:verbose] = true
|
|
236
251
|
end
|
|
237
252
|
|
data/lib/image_optim/runner.rb
CHANGED
|
@@ -45,6 +45,43 @@ class ImageOptim
|
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# files, elapsed, kb saved, kb/s
|
|
49
|
+
class BenchmarkResults
|
|
50
|
+
def initialize
|
|
51
|
+
@all = []
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def add(rows)
|
|
55
|
+
@all.concat(rows)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def print
|
|
59
|
+
if @all.empty?
|
|
60
|
+
puts 'nothing to report'
|
|
61
|
+
return
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
report = @all.group_by(&:worker).map do |name, results|
|
|
65
|
+
kb = (results.sum(&:bytes) / 1024.0)
|
|
66
|
+
elapsed = results.sum(&:elapsed)
|
|
67
|
+
{
|
|
68
|
+
'name' => name,
|
|
69
|
+
'files' => results.length,
|
|
70
|
+
'elapsed' => elapsed,
|
|
71
|
+
'kb saved' => kb,
|
|
72
|
+
'kb/s' => (kb / elapsed),
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
report = report.sort_by do |row|
|
|
77
|
+
[-row['kb/s'], row['name']]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
puts "\nBENCHMARK RESULTS\n\n"
|
|
81
|
+
Table.new(report).write($stdout)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
48
85
|
def initialize(options)
|
|
49
86
|
options = HashHelpers.deep_symbolise_keys(options)
|
|
50
87
|
@recursive = options.delete(:recursive)
|
|
@@ -53,19 +90,40 @@ class ImageOptim
|
|
|
53
90
|
glob = options.delete(:"exclude_#{type}_glob") || '.*'
|
|
54
91
|
GlobHelpers.expand_braces(glob)
|
|
55
92
|
end
|
|
93
|
+
|
|
94
|
+
# --benchmark
|
|
95
|
+
@benchmark = options.delete(:benchmark)
|
|
96
|
+
if @benchmark
|
|
97
|
+
unless options[:threads].nil?
|
|
98
|
+
warning '--benchmark ignores --threads'
|
|
99
|
+
options[:threads] = 1 # for consistency
|
|
100
|
+
end
|
|
101
|
+
if options[:timeout]
|
|
102
|
+
warning '--benchmark ignores --timeout'
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
56
106
|
@image_optim = ImageOptim.new(options)
|
|
57
107
|
end
|
|
58
108
|
|
|
59
|
-
def run!(args)
|
|
109
|
+
def run!(args) # rubocop:disable Naming/PredicateMethod
|
|
60
110
|
to_optimize = find_to_optimize(args)
|
|
61
111
|
unless to_optimize.empty?
|
|
62
|
-
|
|
112
|
+
if @benchmark
|
|
113
|
+
benchmark_results = BenchmarkResults.new
|
|
114
|
+
benchmark_images(to_optimize).each do |_original, rows| # rubocop:disable Style/HashEachMethods
|
|
115
|
+
benchmark_results.add(rows)
|
|
116
|
+
end
|
|
117
|
+
benchmark_results.print
|
|
118
|
+
else
|
|
119
|
+
results = Results.new
|
|
63
120
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
121
|
+
optimize_images!(to_optimize).each do |original, optimized|
|
|
122
|
+
results.add(original, optimized)
|
|
123
|
+
end
|
|
67
124
|
|
|
68
|
-
|
|
125
|
+
results.print
|
|
126
|
+
end
|
|
69
127
|
end
|
|
70
128
|
|
|
71
129
|
!@warnings
|
|
@@ -73,6 +131,11 @@ class ImageOptim
|
|
|
73
131
|
|
|
74
132
|
private
|
|
75
133
|
|
|
134
|
+
def benchmark_images(to_optimize, &block)
|
|
135
|
+
to_optimize = to_optimize.with_progress('benchmarking') if @progress
|
|
136
|
+
@image_optim.benchmark_images(to_optimize, &block)
|
|
137
|
+
end
|
|
138
|
+
|
|
76
139
|
def optimize_images!(to_optimize, &block)
|
|
77
140
|
to_optimize = to_optimize.with_progress('optimizing') if @progress
|
|
78
141
|
@image_optim.optimize_images!(to_optimize, &block)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ImageOptim
|
|
4
|
+
# Handy class for pretty printing a table in the terminal. This is very simple, switch to Terminal
|
|
5
|
+
# Table, Table Tennis or similar if we need more.
|
|
6
|
+
class Table
|
|
7
|
+
attr_reader :rows
|
|
8
|
+
|
|
9
|
+
def initialize(rows)
|
|
10
|
+
@rows = rows
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def write(io)
|
|
14
|
+
io.puts render_row(columns)
|
|
15
|
+
io.puts render_sep
|
|
16
|
+
rows.each do |row|
|
|
17
|
+
io.puts render_row(row.values)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
protected
|
|
22
|
+
|
|
23
|
+
# array of column names
|
|
24
|
+
def columns
|
|
25
|
+
@columns ||= rows.first.keys
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# should columns be justified left or right?
|
|
29
|
+
def justs
|
|
30
|
+
@justs ||= columns.map do |col|
|
|
31
|
+
rows.first[col].is_a?(Numeric) ? :rjust : :ljust
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# max width of each column
|
|
36
|
+
def widths
|
|
37
|
+
@widths ||= columns.map do |col|
|
|
38
|
+
values = rows.map{ |row| fmt(row[col]) }
|
|
39
|
+
([col] + values).map(&:length).max
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# render an array of row values
|
|
44
|
+
def render_row(values)
|
|
45
|
+
values.zip(justs, widths).map do |value, just, width|
|
|
46
|
+
fmt(value).send(just, width)
|
|
47
|
+
end.join(' ')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# render a separator line
|
|
51
|
+
def render_sep
|
|
52
|
+
render_row(widths.map{ |width| '-' * width })
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# format one cell value
|
|
56
|
+
def fmt(value)
|
|
57
|
+
if value.is_a?(Float)
|
|
58
|
+
format('%0.3f', value)
|
|
59
|
+
else
|
|
60
|
+
value.to_s
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'image_optim/option_helpers'
|
|
3
4
|
require 'image_optim/worker'
|
|
5
|
+
require 'fspath'
|
|
4
6
|
|
|
5
7
|
class ImageOptim
|
|
6
8
|
class Worker
|
|
7
9
|
# https://github.com/svg/svgo
|
|
8
10
|
class Svgo < Worker
|
|
11
|
+
PLUGIN_NAME_R = /\A[a-zA-Z]+\z/.freeze
|
|
12
|
+
|
|
9
13
|
DISABLE_PLUGINS_OPTION =
|
|
10
14
|
option(:disable_plugins, [], 'List of plugins to disable') do |v|
|
|
11
|
-
|
|
15
|
+
parse_plugin_names(v)
|
|
12
16
|
end
|
|
13
17
|
|
|
14
18
|
ENABLE_PLUGINS_OPTION =
|
|
15
19
|
option(:enable_plugins, [], 'List of plugins to enable') do |v|
|
|
16
|
-
|
|
20
|
+
parse_plugin_names(v)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
ALLOW_LOSSY_OPTION =
|
|
24
|
+
option(:allow_lossy, false, 'Allow precision option'){ |v| !!v }
|
|
25
|
+
|
|
26
|
+
PRECISION_OPTION =
|
|
27
|
+
option(:precision, 3, 'Number of digits in the fractional part ' \
|
|
28
|
+
'`0`..`20`, ignored in default/lossless mode') \
|
|
29
|
+
do |v, opt_def|
|
|
30
|
+
if allow_lossy
|
|
31
|
+
OptionHelpers.limit_with_range(v.to_i, 0..20)
|
|
32
|
+
else
|
|
33
|
+
if v != opt_def.default
|
|
34
|
+
warn "#{self.class.bin_sym} #{opt_def.name} #{v} ignored " \
|
|
35
|
+
'in default/lossless mode'
|
|
36
|
+
end
|
|
37
|
+
opt_def.default
|
|
38
|
+
end
|
|
17
39
|
end
|
|
18
40
|
|
|
19
41
|
def optimize(src, dst, options = {})
|
|
@@ -21,14 +43,57 @@ class ImageOptim
|
|
|
21
43
|
--input #{src}
|
|
22
44
|
--output #{dst}
|
|
23
45
|
]
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
46
|
+
if resolve_bin!(:svgo).version >= '2.0.0'
|
|
47
|
+
unless disable_plugins.empty? && enable_plugins.empty?
|
|
48
|
+
config_file = plugins_config_file
|
|
49
|
+
args.unshift "--config=#{config_file.path}"
|
|
50
|
+
end
|
|
51
|
+
else
|
|
52
|
+
disable_plugins.each do |plugin_name|
|
|
53
|
+
args.unshift "--disable=#{plugin_name}"
|
|
54
|
+
end
|
|
55
|
+
enable_plugins.each do |plugin_name|
|
|
56
|
+
args.unshift "--enable=#{plugin_name}"
|
|
57
|
+
end
|
|
29
58
|
end
|
|
59
|
+
args.unshift "--precision=#{precision}" if allow_lossy
|
|
30
60
|
execute(:svgo, args, options) && optimized?(src, dst)
|
|
31
61
|
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def parse_plugin_names(value)
|
|
66
|
+
Array(value).map(&:to_s).select do |name|
|
|
67
|
+
if name =~ PLUGIN_NAME_R
|
|
68
|
+
true
|
|
69
|
+
else
|
|
70
|
+
warn "Doesn't look like svgo plugin name: #{name}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def plugins_config_file
|
|
76
|
+
@plugins_config_file ||= FSPath.temp_file(%w[image_optim .js]).tap do |config_file|
|
|
77
|
+
config_file.puts 'export default {'
|
|
78
|
+
config_file.puts ' plugins: ['
|
|
79
|
+
config_file.puts ' {'
|
|
80
|
+
config_file.puts ' name: \'preset-default\','
|
|
81
|
+
config_file.puts ' params: {'
|
|
82
|
+
config_file.puts ' overrides: {'
|
|
83
|
+
disable_plugins.each do |plugin_name|
|
|
84
|
+
config_file.puts " #{plugin_name}: false,"
|
|
85
|
+
end
|
|
86
|
+
config_file.puts ' }'
|
|
87
|
+
config_file.puts ' }'
|
|
88
|
+
config_file.puts ' },'
|
|
89
|
+
enable_plugins.each do |plugin_name|
|
|
90
|
+
config_file.puts " '#{plugin_name}',"
|
|
91
|
+
end
|
|
92
|
+
config_file.puts ' ]'
|
|
93
|
+
config_file.puts '};'
|
|
94
|
+
config_file.close
|
|
95
|
+
end
|
|
96
|
+
end
|
|
32
97
|
end
|
|
33
98
|
end
|
|
34
99
|
end
|
data/lib/image_optim.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'image_optim/benchmark_result'
|
|
3
4
|
require 'image_optim/bin_resolver'
|
|
4
5
|
require 'image_optim/cache'
|
|
5
6
|
require 'image_optim/config'
|
|
@@ -8,6 +9,7 @@ require 'image_optim/handler'
|
|
|
8
9
|
require 'image_optim/image_meta'
|
|
9
10
|
require 'image_optim/optimized_path'
|
|
10
11
|
require 'image_optim/path'
|
|
12
|
+
require 'image_optim/table'
|
|
11
13
|
require 'image_optim/timer'
|
|
12
14
|
require 'image_optim/worker'
|
|
13
15
|
require 'in_threads'
|
|
@@ -162,6 +164,22 @@ class ImageOptim
|
|
|
162
164
|
end
|
|
163
165
|
end
|
|
164
166
|
|
|
167
|
+
def benchmark_image(original)
|
|
168
|
+
src = Path.convert(original)
|
|
169
|
+
return unless (workers = workers_for_image(src))
|
|
170
|
+
|
|
171
|
+
dst = src.temp_path
|
|
172
|
+
begin
|
|
173
|
+
workers.map do |worker|
|
|
174
|
+
start = ElapsedTime.now
|
|
175
|
+
worker.optimize(src, dst)
|
|
176
|
+
BenchmarkResult.new(src, dst, ElapsedTime.now - start, worker)
|
|
177
|
+
end
|
|
178
|
+
ensure
|
|
179
|
+
dst.unlink
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
165
183
|
# Optimize multiple images
|
|
166
184
|
# if block given yields path and result for each image and returns array of
|
|
167
185
|
# yield results
|
|
@@ -186,6 +204,10 @@ class ImageOptim
|
|
|
186
204
|
run_method_for(datas, :optimize_image_data, &block)
|
|
187
205
|
end
|
|
188
206
|
|
|
207
|
+
def benchmark_images(paths, &block)
|
|
208
|
+
run_method_for(paths, :benchmark_image, &block)
|
|
209
|
+
end
|
|
210
|
+
|
|
189
211
|
class << self
|
|
190
212
|
# Optimization methods with default options
|
|
191
213
|
def method_missing(method, *args, &block)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euxo pipefail
|
|
4
|
+
|
|
5
|
+
ruby <<'RUBY'
|
|
6
|
+
short_version = RUBY_VERSION.to_f
|
|
7
|
+
|
|
8
|
+
gemrc_path = File.expand_path('~/.gemrc')
|
|
9
|
+
unless File.exist?(gemrc_path)
|
|
10
|
+
File.open(gemrc_path, 'w') do |f|
|
|
11
|
+
if short_version < 2.0
|
|
12
|
+
f.puts 'gem: --no-ri --no-rdoc'
|
|
13
|
+
else
|
|
14
|
+
f.puts 'gem: --no-document'
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def sh(*args)
|
|
20
|
+
abort unless system(*args)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
case
|
|
24
|
+
when short_version < 2.3
|
|
25
|
+
sh 'curl --output rubygems-update.gem https://rubygems.org/downloads/rubygems-update-2.7.11.gem'
|
|
26
|
+
sh 'gem install --local rubygems-update.gem'
|
|
27
|
+
sh 'update_rubygems'
|
|
28
|
+
File.unlink(`which bundle`.strip)
|
|
29
|
+
sh 'curl --output bundler.gem https://rubygems.org/downloads/bundler-1.17.3.gem'
|
|
30
|
+
sh 'gem install --local bundler.gem'
|
|
31
|
+
when short_version < 2.6
|
|
32
|
+
sh 'gem update --system 3.3.27'
|
|
33
|
+
sh 'gem install bundler --version 2.3.27'
|
|
34
|
+
when short_version < 3.0
|
|
35
|
+
sh 'gem update --system 3.4.22'
|
|
36
|
+
sh 'gem install bundler --version 2.4.22'
|
|
37
|
+
when short_version < 3.1
|
|
38
|
+
sh 'gem update --system 3.5.23'
|
|
39
|
+
sh 'gem install bundler --version 2.5.23'
|
|
40
|
+
when short_version < 3.2
|
|
41
|
+
sh 'gem update --system 3.6.9'
|
|
42
|
+
sh 'gem install bundler --version 2.6.9'
|
|
43
|
+
else
|
|
44
|
+
sh 'gem update --system'
|
|
45
|
+
sh 'gem install bundler'
|
|
46
|
+
end
|
|
47
|
+
RUBY
|
|
48
|
+
|
|
49
|
+
gem -v
|
|
50
|
+
bundle -v
|
|
@@ -51,8 +51,21 @@ def update_readme(text)
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
readme = File.read(README_FILE)
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
program_name = File.basename($PROGRAM_NAME, '.*')
|
|
55
|
+
case ARGV
|
|
56
|
+
when []
|
|
57
|
+
if (readme = update_readme(readme))
|
|
58
|
+
File.write(README_FILE, readme)
|
|
59
|
+
else
|
|
60
|
+
abort 'Did not update worker options'
|
|
61
|
+
end
|
|
62
|
+
when %w[-n]
|
|
63
|
+
updated = update_readme(readme)
|
|
64
|
+
unless updated == readme
|
|
65
|
+
puts "Run #{program_name} to update work options in readme"
|
|
66
|
+
IO.popen('diff -u README.markdown -', 'w'){ |f| f << updated }
|
|
67
|
+
exit 1
|
|
68
|
+
end
|
|
56
69
|
else
|
|
57
|
-
abort
|
|
70
|
+
abort "#{program_name} [-n] (-n to change exit code instead of updating)"
|
|
58
71
|
end
|
|
@@ -143,14 +143,14 @@ describe ImageOptim::BinResolver do
|
|
|
143
143
|
expect(FSPath).to receive(:temp_dir).
|
|
144
144
|
once.and_return(tmpdir)
|
|
145
145
|
expect(tmpdir).to receive(:/).
|
|
146
|
-
with(:
|
|
146
|
+
with(:'the-optimizer').once.and_return(symlink)
|
|
147
147
|
expect(symlink).to receive(:make_symlink).
|
|
148
148
|
with(File.expand_path(path)).once
|
|
149
149
|
|
|
150
150
|
expect(resolver).not_to receive(:full_path)
|
|
151
151
|
bin = double
|
|
152
152
|
expect(Bin).to receive(:new).
|
|
153
|
-
with(:
|
|
153
|
+
with(:'the-optimizer', File.expand_path(path)).and_return(bin)
|
|
154
154
|
expect(bin).to receive(:check!).once
|
|
155
155
|
expect(bin).to receive(:check_fail!).exactly(5).times
|
|
156
156
|
|
|
@@ -160,7 +160,7 @@ describe ImageOptim::BinResolver do
|
|
|
160
160
|
end
|
|
161
161
|
|
|
162
162
|
5.times do
|
|
163
|
-
resolver.resolve!(:
|
|
163
|
+
resolver.resolve!(:'the-optimizer')
|
|
164
164
|
end
|
|
165
165
|
expect(resolver.env_path).to eq([
|
|
166
166
|
tmpdir,
|
|
@@ -15,7 +15,9 @@ describe ImageOptim::Cache do
|
|
|
15
15
|
|
|
16
16
|
let(:cache_dir) do
|
|
17
17
|
dir = '/somewhere/cache'
|
|
18
|
-
allow(
|
|
18
|
+
allow(Dir).to receive(:mkdir).with(File.dirname(dir))
|
|
19
|
+
allow(Dir).to receive(:mkdir).with(dir)
|
|
20
|
+
allow(Dir).to receive(:mkdir).with(%r{\A#{Regexp.escape(dir)}/[^/]+\z})
|
|
19
21
|
allow(FileUtils).to receive(:touch)
|
|
20
22
|
allow(FSPath).to receive(:temp_file_path) do
|
|
21
23
|
tmp_file
|
|
@@ -71,9 +71,7 @@ describe ImageOptim::OptionDefinition do
|
|
|
71
71
|
|
|
72
72
|
context 'when proc given' do
|
|
73
73
|
subject do
|
|
74
|
-
|
|
75
|
-
# to_s is just to calm rubocop
|
|
76
|
-
described_class.new('abc', :def, 'desc'){ |o| o.inspect.to_s }
|
|
74
|
+
described_class.new('abc', :def, 'desc', &:inspect)
|
|
77
75
|
end
|
|
78
76
|
|
|
79
77
|
context 'when option not provided' do
|
|
@@ -101,7 +101,16 @@ describe ImageOptim::Runner::OptionParser do
|
|
|
101
101
|
allow(parser).to receive(:terminal_columns).and_return(80)
|
|
102
102
|
|
|
103
103
|
expect(parser.help.split("\n")).
|
|
104
|
-
to all(satisfy{ |line| line.length
|
|
104
|
+
to all(satisfy{ |line| line.length < 80 })
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'wraps texts even for too wide terminals' do
|
|
108
|
+
parser = OptionParser.new({})
|
|
109
|
+
|
|
110
|
+
allow(parser).to receive(:terminal_columns).and_return(1000)
|
|
111
|
+
|
|
112
|
+
expect(parser.help.split("\n")).
|
|
113
|
+
to all(satisfy{ |line| line.length <= 120 })
|
|
105
114
|
end
|
|
106
115
|
end
|
|
107
116
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'image_optim/true_false_nil'
|
|
5
|
+
|
|
6
|
+
describe ImageOptim::TrueFalseNil do
|
|
7
|
+
describe '.convert' do
|
|
8
|
+
it 'keeps true' do
|
|
9
|
+
expect(described_class.convert(true)).to eq(true)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'keeps false' do
|
|
13
|
+
expect(described_class.convert(false)).to eq(false)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'keeps nil' do
|
|
17
|
+
expect(described_class.convert(nil)).to eq(nil)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'converts truthy to true' do
|
|
21
|
+
expect(described_class.convert(1)).to eq(true)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|