image_optim 0.13.3 → 0.14.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 +8 -8
- data/.rubocop.yml +56 -0
- data/.travis.yml +3 -1
- data/README.markdown +23 -10
- data/bin/image_optim +25 -15
- data/image_optim.gemspec +5 -2
- data/lib/image_optim.rb +47 -37
- data/lib/image_optim/bin_resolver.rb +17 -12
- data/lib/image_optim/bin_resolver/comparable_condition.rb +23 -7
- data/lib/image_optim/bin_resolver/simple_version.rb +2 -0
- data/lib/image_optim/config.rb +21 -13
- data/lib/image_optim/handler.rb +18 -12
- data/lib/image_optim/hash_helpers.rb +23 -13
- data/lib/image_optim/image_meta.rb +1 -0
- data/lib/image_optim/image_path.rb +14 -13
- data/lib/image_optim/option_definition.rb +11 -9
- data/lib/image_optim/option_helpers.rb +1 -2
- data/lib/image_optim/railtie.rb +18 -15
- data/lib/image_optim/runner.rb +67 -61
- data/lib/image_optim/space.rb +29 -0
- data/lib/image_optim/true_false_nil.rb +9 -1
- data/lib/image_optim/worker.rb +40 -16
- data/lib/image_optim/worker/advpng.rb +8 -1
- data/lib/image_optim/worker/gifsicle.rb +13 -1
- data/lib/image_optim/worker/jhead.rb +5 -0
- data/lib/image_optim/worker/jpegoptim.rb +17 -4
- data/lib/image_optim/worker/jpegtran.rb +9 -1
- data/lib/image_optim/worker/optipng.rb +13 -2
- data/lib/image_optim/worker/pngcrush.rb +14 -5
- data/lib/image_optim/worker/pngout.rb +10 -2
- data/lib/image_optim/worker/svgo.rb +1 -0
- data/script/update_worker_options_in_readme +42 -27
- data/spec/image_optim/bin_resolver/comparable_condition_spec.rb +13 -13
- data/spec/image_optim/bin_resolver/simple_version_spec.rb +4 -4
- data/spec/image_optim/bin_resolver_spec.rb +65 -37
- data/spec/image_optim/config_spec.rb +121 -110
- data/spec/image_optim/handler_spec.rb +29 -18
- data/spec/image_optim/hash_helpers_spec.rb +29 -27
- data/spec/image_optim/image_path_spec.rb +17 -17
- data/spec/image_optim/space_spec.rb +24 -0
- data/spec/image_optim/worker_spec.rb +18 -0
- data/spec/image_optim_spec.rb +134 -74
- metadata +27 -7
- data/script/update_instructions_in_readme +0 -44
@@ -0,0 +1,29 @@
|
|
1
|
+
class ImageOptim
|
2
|
+
# Present size in readable form as fixed length string
|
3
|
+
module Space
|
4
|
+
SIZE_SYMBOLS = %w[B K M G T P E].freeze
|
5
|
+
BASE = 1024.0
|
6
|
+
PRECISION = 1
|
7
|
+
LENGTH = 4 + PRECISION + 1
|
8
|
+
|
9
|
+
EMPTY_SPACE = ' ' * LENGTH
|
10
|
+
|
11
|
+
def self.space(size)
|
12
|
+
case size
|
13
|
+
when 0, nil
|
14
|
+
EMPTY_SPACE
|
15
|
+
else
|
16
|
+
log_denominator = Math.log(size) / Math.log(BASE)
|
17
|
+
degree = [log_denominator.floor, SIZE_SYMBOLS.length - 1].min
|
18
|
+
number_string = if degree == 0
|
19
|
+
size.to_s
|
20
|
+
else
|
21
|
+
denominator = BASE**degree
|
22
|
+
number = size / denominator
|
23
|
+
format("%.#{PRECISION}f", number)
|
24
|
+
end
|
25
|
+
"#{number_string}#{SIZE_SYMBOLS[degree]}".rjust(LENGTH)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -1,3 +1,11 @@
|
|
1
1
|
class ImageOptim
|
2
|
-
|
2
|
+
# Denote ternary value (`true`/`false`/`nil`) for worker option
|
3
|
+
class TrueFalseNil
|
4
|
+
# Add handling of ternary value in OptionParser instance, maps `nil` and
|
5
|
+
# `'nil'` to `nil`
|
6
|
+
def self.add_to_option_parser(option_parser)
|
7
|
+
completing = OptionParser.top.atype[TrueClass][0].merge('nil' => nil)
|
8
|
+
option_parser.accept(self, completing){ |_arg, val| val }
|
9
|
+
end
|
10
|
+
end
|
3
11
|
end
|
data/lib/image_optim/worker.rb
CHANGED
@@ -3,8 +3,10 @@
|
|
3
3
|
require 'image_optim/option_definition'
|
4
4
|
require 'image_optim/option_helpers'
|
5
5
|
require 'shellwords'
|
6
|
+
require 'English'
|
6
7
|
|
7
8
|
class ImageOptim
|
9
|
+
# Base class for all workers
|
8
10
|
class Worker
|
9
11
|
class << self
|
10
12
|
# List of available workers
|
@@ -19,7 +21,10 @@ class ImageOptim
|
|
19
21
|
|
20
22
|
# Underscored class name symbol
|
21
23
|
def bin_sym
|
22
|
-
@underscored_name ||= name.
|
24
|
+
@underscored_name ||= name.
|
25
|
+
split('::').last. # get last part
|
26
|
+
gsub(/([a-z])([A-Z])/, '\1_\2').downcase. # convert AbcDef to abc_def
|
27
|
+
to_sym
|
23
28
|
end
|
24
29
|
|
25
30
|
def option_definitions
|
@@ -28,7 +33,8 @@ class ImageOptim
|
|
28
33
|
|
29
34
|
def option(name, default, type, description = nil, &proc)
|
30
35
|
attr_reader name
|
31
|
-
option_definitions <<
|
36
|
+
option_definitions <<
|
37
|
+
OptionDefinition.new(name, default, type, description, &proc)
|
32
38
|
end
|
33
39
|
end
|
34
40
|
|
@@ -36,7 +42,7 @@ class ImageOptim
|
|
36
42
|
def initialize(image_optim, options = {})
|
37
43
|
@image_optim = image_optim
|
38
44
|
self.class.option_definitions.each do |option_definition|
|
39
|
-
value = if options.
|
45
|
+
value = if options.key?(option_definition.name)
|
40
46
|
options[option_definition.name]
|
41
47
|
else
|
42
48
|
option_definition.default
|
@@ -50,14 +56,22 @@ class ImageOptim
|
|
50
56
|
assert_no_unknown_options!(options)
|
51
57
|
end
|
52
58
|
|
59
|
+
# Optimize image at src, output at dst, must be overriden in subclass
|
60
|
+
# return true on success
|
61
|
+
def optimize(_src, _dst)
|
62
|
+
fail NotImplementedError, "implement method optimize in #{self.class}"
|
63
|
+
end
|
64
|
+
|
53
65
|
# List of formats which worker can optimize
|
54
66
|
def image_formats
|
55
67
|
format_from_name = self.class.name.downcase[/gif|jpeg|png|svg/]
|
56
|
-
|
68
|
+
unless format_from_name
|
69
|
+
fail "#{self.class}: can't guess applicable format from worker name"
|
70
|
+
end
|
57
71
|
[format_from_name.to_sym]
|
58
72
|
end
|
59
73
|
|
60
|
-
# Ordering in list of workers
|
74
|
+
# Ordering in list of workers, 0 by default
|
61
75
|
def run_order
|
62
76
|
0
|
63
77
|
end
|
@@ -75,10 +89,10 @@ class ImageOptim
|
|
75
89
|
|
76
90
|
def assert_no_unknown_options!(options)
|
77
91
|
known_keys = self.class.option_definitions.map(&:name)
|
78
|
-
unknown_options = options.reject{ |key,
|
79
|
-
|
80
|
-
|
81
|
-
|
92
|
+
unknown_options = options.reject{ |key, _value| known_keys.include?(key) }
|
93
|
+
return if unknown_options.empty?
|
94
|
+
fail ConfigurationError, "unknown options #{unknown_options.inspect} "\
|
95
|
+
"for #{self}"
|
82
96
|
end
|
83
97
|
|
84
98
|
# Forward bin resolving to image_optim
|
@@ -94,7 +108,9 @@ class ImageOptim
|
|
94
108
|
|
95
109
|
success = run_command(command)
|
96
110
|
|
97
|
-
|
111
|
+
if @image_optim.verbose
|
112
|
+
$stderr << "#{success ? '✓' : '✗'} #{Time.now - start}s #{command}\n"
|
113
|
+
end
|
98
114
|
|
99
115
|
success
|
100
116
|
end
|
@@ -106,13 +122,21 @@ class ImageOptim
|
|
106
122
|
[bin, *arguments].map(&:to_s).shelljoin
|
107
123
|
end
|
108
124
|
|
109
|
-
# Run command defining environment, setting nice level, removing output and
|
125
|
+
# Run command defining environment, setting nice level, removing output and
|
126
|
+
# reraising signal exception
|
110
127
|
def run_command(command)
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
128
|
+
full_command = %W[
|
129
|
+
env PATH=#{@image_optim.env_path.shellescape}
|
130
|
+
nice -n #{@image_optim.nice}
|
131
|
+
#{command} > /dev/null 2>&1
|
132
|
+
].join(' ')
|
133
|
+
success = system full_command
|
134
|
+
|
135
|
+
status = $CHILD_STATUS
|
136
|
+
if status.signaled?
|
137
|
+
# jruby does not differ non zero exit status and signal number
|
138
|
+
unless defined?(JRUBY_VERSION) && status.exitstatus == status.termsig
|
139
|
+
fail SignalException, status.termsig
|
116
140
|
end
|
117
141
|
end
|
118
142
|
|
@@ -3,8 +3,15 @@ require 'image_optim/option_helpers'
|
|
3
3
|
|
4
4
|
class ImageOptim
|
5
5
|
class Worker
|
6
|
+
# http://advancemame.sourceforge.net/doc-advpng.html
|
6
7
|
class Advpng < Worker
|
7
|
-
|
8
|
+
LEVEL_OPTION =
|
9
|
+
option(:level, 4, 'Compression level: '\
|
10
|
+
'`0` - don\'t compress, '\
|
11
|
+
'`1` - fast, '\
|
12
|
+
'`2` - normal, '\
|
13
|
+
'`3` - extra, '\
|
14
|
+
'`4` - extreme') do |v|
|
8
15
|
OptionHelpers.limit_with_range(v.to_i, 0..4)
|
9
16
|
end
|
10
17
|
|
@@ -2,11 +2,23 @@ require 'image_optim/worker'
|
|
2
2
|
|
3
3
|
class ImageOptim
|
4
4
|
class Worker
|
5
|
+
# http://www.lcdf.org/gifsicle/
|
5
6
|
class Gifsicle < Worker
|
7
|
+
INTERLACE_OPTION =
|
6
8
|
option(:interlace, false, 'Turn interlacing on'){ |v| !!v }
|
7
9
|
|
8
10
|
def optimize(src, dst)
|
9
|
-
args = %W[
|
11
|
+
args = %W[
|
12
|
+
-o #{dst}
|
13
|
+
-O3
|
14
|
+
--no-comments
|
15
|
+
--no-names
|
16
|
+
--same-delay
|
17
|
+
--same-loopcount
|
18
|
+
--no-warnings
|
19
|
+
--
|
20
|
+
#{src}
|
21
|
+
]
|
10
22
|
args.unshift('-i') if interlace
|
11
23
|
execute(:gifsicle, *args) && optimized?(src, dst)
|
12
24
|
end
|
@@ -3,11 +3,16 @@ require 'exifr'
|
|
3
3
|
|
4
4
|
class ImageOptim
|
5
5
|
class Worker
|
6
|
+
# http://www.sentex.net/~mwandel/jhead/
|
7
|
+
#
|
8
|
+
# Jhead internally uses jpegtran which should be on path
|
6
9
|
class Jhead < Worker
|
10
|
+
# Works on jpegs
|
7
11
|
def image_formats
|
8
12
|
[:jpeg]
|
9
13
|
end
|
10
14
|
|
15
|
+
# Run first [-10]
|
11
16
|
def run_order
|
12
17
|
-10
|
13
18
|
end
|
@@ -3,18 +3,31 @@ require 'image_optim/option_helpers'
|
|
3
3
|
|
4
4
|
class ImageOptim
|
5
5
|
class Worker
|
6
|
+
# http://www.kokkonen.net/tjko/projects.html
|
6
7
|
class Jpegoptim < Worker
|
7
|
-
|
8
|
+
STRIP_OPTION =
|
9
|
+
option(:strip, :all, Array, 'List of extra markers to strip: '\
|
10
|
+
'`:comments`, '\
|
11
|
+
'`:exif`, '\
|
12
|
+
'`:iptc`, '\
|
13
|
+
'`:icc` or '\
|
14
|
+
'`:all`') do |v|
|
8
15
|
values = Array(v).map(&:to_s)
|
9
16
|
known_values = %w[all comments exif iptc icc]
|
10
17
|
unknown_values = values - known_values
|
11
|
-
|
18
|
+
unless unknown_values.empty?
|
19
|
+
warn "Unknown markers for jpegoptim: #{unknown_values.join(', ')}"
|
20
|
+
end
|
12
21
|
values & known_values
|
13
22
|
end
|
14
23
|
|
15
|
-
|
24
|
+
MAX_QUALITY_OPTION =
|
25
|
+
option(:max_quality, 100, 'Maximum image quality factor '\
|
26
|
+
'`0`..`100`') do |v|
|
27
|
+
OptionHelpers.limit_with_range(v.to_i, 0..100)
|
28
|
+
end
|
16
29
|
|
17
|
-
# Run first if max_quality < 100
|
30
|
+
# Run first [-1] if max_quality < 100 otherwise with normal priority
|
18
31
|
def run_order
|
19
32
|
max_quality < 100 ? -1 : 0
|
20
33
|
end
|
@@ -2,12 +2,20 @@ require 'image_optim/worker'
|
|
2
2
|
|
3
3
|
class ImageOptim
|
4
4
|
class Worker
|
5
|
+
# http://www.ijg.org/
|
6
|
+
#
|
7
|
+
# Uses jpegtran through jpegrescan if enabled, jpegrescan is vendored with
|
8
|
+
# this gem
|
5
9
|
class Jpegtran < Worker
|
10
|
+
COPY_CHUNKS_OPTION =
|
6
11
|
option(:copy_chunks, false, 'Copy all chunks'){ |v| !!v }
|
7
12
|
|
13
|
+
PROGRESSIVE_OPTION =
|
8
14
|
option(:progressive, true, 'Create progressive JPEG file'){ |v| !!v }
|
9
15
|
|
10
|
-
|
16
|
+
JPEGRESCAN_OPTION =
|
17
|
+
option(:jpegrescan, false, 'Use jpegtran through jpegrescan, '\
|
18
|
+
'ignore progressive option'){ |v| !!v }
|
11
19
|
|
12
20
|
def optimize(src, dst)
|
13
21
|
if jpegrescan
|
@@ -4,10 +4,21 @@ require 'image_optim/true_false_nil'
|
|
4
4
|
|
5
5
|
class ImageOptim
|
6
6
|
class Worker
|
7
|
+
# http://optipng.sourceforge.net/
|
7
8
|
class Optipng < Worker
|
8
|
-
|
9
|
+
LEVEL_OPTION =
|
10
|
+
option(:level, 6, 'Optimization level preset: '\
|
11
|
+
'`0` is least, '\
|
12
|
+
'`7` is best') do |v|
|
13
|
+
OptionHelpers.limit_with_range(v.to_i, 0..7)
|
14
|
+
end
|
9
15
|
|
10
|
-
|
16
|
+
INTERLACE_OPTION =
|
17
|
+
option(:interlace, false, TrueFalseNil, 'Interlace, '\
|
18
|
+
'`true` - interlace on, '\
|
19
|
+
'`false` - interlace off, '\
|
20
|
+
'`nil` - as is in original image') do |v|
|
21
|
+
# convert everything truthy to `true`, leave `false` and `nil` as is
|
11
22
|
v && true
|
12
23
|
end
|
13
24
|
|
@@ -2,15 +2,24 @@ require 'image_optim/worker'
|
|
2
2
|
|
3
3
|
class ImageOptim
|
4
4
|
class Worker
|
5
|
+
# http://pmt.sourceforge.net/pngcrush/
|
5
6
|
class Pngcrush < Worker
|
6
|
-
|
7
|
-
|
7
|
+
CHUNKS_OPTION =
|
8
|
+
option(:chunks, :alla, Array, 'List of chunks to remove or '\
|
9
|
+
'`:alla` - all except tRNS/transparency or '\
|
10
|
+
'`:allb` - all except tRNS and gAMA/gamma') do |v|
|
11
|
+
Array(v).map(&:to_s)
|
12
|
+
end
|
8
13
|
|
9
|
-
|
14
|
+
FIX_OPTION =
|
15
|
+
option(:fix, false, 'Fix otherwise fatal conditions '\
|
16
|
+
'such as bad CRCs'){ |v| !!v }
|
10
17
|
|
11
|
-
|
18
|
+
BRUTE_OPTION =
|
19
|
+
option(:brute, false, 'Brute force try all methods, '\
|
20
|
+
'very time-consuming and generally not worthwhile'){ |v| !!v }
|
12
21
|
|
13
|
-
# Always run first
|
22
|
+
# Always run first [-1]
|
14
23
|
def run_order
|
15
24
|
-1
|
16
25
|
end
|
@@ -3,14 +3,22 @@ require 'image_optim/option_helpers'
|
|
3
3
|
|
4
4
|
class ImageOptim
|
5
5
|
class Worker
|
6
|
+
# http://www.advsys.net/ken/util/pngout.htm
|
6
7
|
class Pngout < Worker
|
8
|
+
COPY_CHUNKS_OPTION =
|
7
9
|
option(:copy_chunks, false, 'Copy optional chunks'){ |v| !!v }
|
8
10
|
|
9
|
-
|
11
|
+
STRATEGY_OPTION =
|
12
|
+
option(:strategy, 0, 'Strategy: '\
|
13
|
+
'`0` - xtreme, '\
|
14
|
+
'`1` - intense, '\
|
15
|
+
'`2` - longest Match, '\
|
16
|
+
'`3` - huffman Only, '\
|
17
|
+
'`4` - uncompressed') do |v|
|
10
18
|
OptionHelpers.limit_with_range(v.to_i, 0..4)
|
11
19
|
end
|
12
20
|
|
13
|
-
# Always run first
|
21
|
+
# Always run first [-1]
|
14
22
|
def run_order
|
15
23
|
-1
|
16
24
|
end
|
@@ -1,46 +1,61 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# encoding: UTF-8
|
3
3
|
|
4
|
-
|
4
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
5
5
|
|
6
6
|
require 'image_optim'
|
7
7
|
|
8
8
|
README_FILE = File.expand_path('../../README.markdown', __FILE__)
|
9
9
|
BEGIN_MARKER = '<!---<worker-options>-->'
|
10
10
|
END_MARKER = '<!---</worker-options>-->'
|
11
|
-
GENERATED_NOTE =
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
11
|
+
GENERATED_NOTE = '<!-- markdown for worker options is generated by '\
|
12
|
+
"`#{$PROGRAM_NAME}` -->"
|
13
|
+
|
14
|
+
def write_worker_options(io, klass)
|
15
|
+
io.puts "### :#{klass.bin_sym} =>"
|
16
|
+
if klass.option_definitions.empty?
|
17
|
+
io.puts 'Worker has no options'
|
18
|
+
else
|
19
|
+
klass.option_definitions.each do |option_definition|
|
20
|
+
io.puts %W[
|
21
|
+
*
|
22
|
+
`:#{option_definition.name}`
|
23
|
+
— #{option_definition.description}
|
24
|
+
*(defaults to `#{option_definition.default.inspect}`)*
|
25
|
+
].join(' ')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
io.puts
|
29
|
+
end
|
30
|
+
|
31
|
+
def write_marked(io)
|
32
|
+
io.puts BEGIN_MARKER
|
33
|
+
io.puts GENERATED_NOTE
|
34
|
+
io.puts
|
35
|
+
|
36
|
+
ImageOptim::Worker.klasses.each do |klass|
|
37
|
+
write_worker_options(io, klass)
|
38
|
+
end
|
39
|
+
|
40
|
+
io.puts END_MARKER
|
41
|
+
end
|
42
|
+
|
43
|
+
def update_readme(text)
|
44
|
+
marked_reg = /#{Regexp.escape(BEGIN_MARKER)}.*#{Regexp.escape(END_MARKER)}/m
|
45
|
+
text.clone.sub!(marked_reg) do
|
46
|
+
StringIO.open do |io|
|
47
|
+
write_marked(io)
|
48
|
+
|
49
|
+
io.string.strip
|
35
50
|
end
|
36
51
|
end
|
37
52
|
end
|
38
53
|
|
39
54
|
readme = File.read(README_FILE)
|
40
|
-
if readme =
|
55
|
+
if (readme = update_readme(readme))
|
41
56
|
File.open(README_FILE, 'w') do |f|
|
42
57
|
f.write readme
|
43
58
|
end
|
44
59
|
else
|
45
|
-
abort
|
60
|
+
abort 'Did not update worker options'
|
46
61
|
end
|