image_optim 0.13.3 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|