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.
Files changed (44) hide show
  1. checksums.yaml +8 -8
  2. data/.rubocop.yml +56 -0
  3. data/.travis.yml +3 -1
  4. data/README.markdown +23 -10
  5. data/bin/image_optim +25 -15
  6. data/image_optim.gemspec +5 -2
  7. data/lib/image_optim.rb +47 -37
  8. data/lib/image_optim/bin_resolver.rb +17 -12
  9. data/lib/image_optim/bin_resolver/comparable_condition.rb +23 -7
  10. data/lib/image_optim/bin_resolver/simple_version.rb +2 -0
  11. data/lib/image_optim/config.rb +21 -13
  12. data/lib/image_optim/handler.rb +18 -12
  13. data/lib/image_optim/hash_helpers.rb +23 -13
  14. data/lib/image_optim/image_meta.rb +1 -0
  15. data/lib/image_optim/image_path.rb +14 -13
  16. data/lib/image_optim/option_definition.rb +11 -9
  17. data/lib/image_optim/option_helpers.rb +1 -2
  18. data/lib/image_optim/railtie.rb +18 -15
  19. data/lib/image_optim/runner.rb +67 -61
  20. data/lib/image_optim/space.rb +29 -0
  21. data/lib/image_optim/true_false_nil.rb +9 -1
  22. data/lib/image_optim/worker.rb +40 -16
  23. data/lib/image_optim/worker/advpng.rb +8 -1
  24. data/lib/image_optim/worker/gifsicle.rb +13 -1
  25. data/lib/image_optim/worker/jhead.rb +5 -0
  26. data/lib/image_optim/worker/jpegoptim.rb +17 -4
  27. data/lib/image_optim/worker/jpegtran.rb +9 -1
  28. data/lib/image_optim/worker/optipng.rb +13 -2
  29. data/lib/image_optim/worker/pngcrush.rb +14 -5
  30. data/lib/image_optim/worker/pngout.rb +10 -2
  31. data/lib/image_optim/worker/svgo.rb +1 -0
  32. data/script/update_worker_options_in_readme +42 -27
  33. data/spec/image_optim/bin_resolver/comparable_condition_spec.rb +13 -13
  34. data/spec/image_optim/bin_resolver/simple_version_spec.rb +4 -4
  35. data/spec/image_optim/bin_resolver_spec.rb +65 -37
  36. data/spec/image_optim/config_spec.rb +121 -110
  37. data/spec/image_optim/handler_spec.rb +29 -18
  38. data/spec/image_optim/hash_helpers_spec.rb +29 -27
  39. data/spec/image_optim/image_path_spec.rb +17 -17
  40. data/spec/image_optim/space_spec.rb +24 -0
  41. data/spec/image_optim/worker_spec.rb +18 -0
  42. data/spec/image_optim_spec.rb +134 -74
  43. metadata +27 -7
  44. 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
- class TrueFalseNil; end
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
@@ -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.split('::').last.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym
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 << OptionDefinition.new(name, default, type, description, &proc)
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.has_key?(option_definition.name)
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
- raise "#{self.class}: can't guess applicable format from worker name" unless format_from_name
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, value| known_keys.include?(key) }
79
- unless unknown_options.empty?
80
- raise ConfigurationError, "unknown options #{unknown_options.inspect} for #{self}"
81
- end
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
- $stderr << "#{success ? '✓' : '✗'} #{Time.now - start}s #{command}\n" if @image_optim.verbose?
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 reraising signal exception
125
+ # Run command defining environment, setting nice level, removing output and
126
+ # reraising signal exception
110
127
  def run_command(command)
111
- success = system "env PATH=#{@image_optim.env_path.shellescape} nice -n #{@image_optim.nice} #{command} > /dev/null 2>&1"
112
-
113
- if $?.signaled?
114
- unless defined?(JRUBY_VERSION) && $?.exitstatus == $?.termsig # jruby does not differ non zero exit status and signal number
115
- raise SignalException.new($?.termsig)
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
- option(:level, 4, 'Compression level: `0` - don\'t compress, `1` - fast, `2` - normal, `3` - extra, `4` - extreme') do |v|
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[-o #{dst} -O3 --no-comments --no-names --same-delay --same-loopcount --no-warnings -- #{src}]
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
- option(:strip, :all, Array, 'List of extra markers to strip: `:comments`, `:exif`, `:iptc`, `:icc` or `:all`') do |v|
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
- warn "Unknown markers for jpegoptim: #{unknown_values.join(', ')}" unless unknown_values.empty?
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
- option(:max_quality, 100, 'Maximum image quality factor `0`..`100`'){ |v| OptionHelpers.limit_with_range(v.to_i, 0..100) }
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
- option(:jpegrescan, false, 'Use jpegtran through jpegrescan, ignore progressive option'){ |v| !!v }
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
- option(:level, 6, 'Optimization level preset: `0` is least, `7` is best'){ |v| OptionHelpers.limit_with_range(v.to_i, 0..7) }
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
- option(:interlace, false, TrueFalseNil, 'Interlace, `true` - interlace on, `false` - interlace off, `nil` - as is in original image') do |v|
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
- option(:chunks, :alla, Array, 'List of chunks to remove or `:alla` - all except tRNS/transparency or '\
7
- '`:allb` - all except tRNS and gAMA/gamma'){ |v| Array(v).map(&:to_s) }
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
- option(:fix, false, 'Fix otherwise fatal conditions such as bad CRCs'){ |v| !!v }
14
+ FIX_OPTION =
15
+ option(:fix, false, 'Fix otherwise fatal conditions '\
16
+ 'such as bad CRCs'){ |v| !!v }
10
17
 
11
- option(:brute, false, 'Brute force try all methods, very time-consuming and generally not worthwhile'){ |v| !!v }
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
- option(:strategy, 0, 'Strategy: `0` - xtreme, `1` - intense, `2` - longest Match, `3` - huffman Only, `4` - uncompressed') do |v|
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
@@ -2,6 +2,7 @@ require 'image_optim/worker'
2
2
 
3
3
  class ImageOptim
4
4
  class Worker
5
+ # https://github.com/svg/svgo
5
6
  class Svgo < Worker
6
7
  def optimize(src, dst)
7
8
  args = %W[-i #{src} -o #{dst}]
@@ -1,46 +1,61 @@
1
1
  #!/usr/bin/env ruby
2
2
  # encoding: UTF-8
3
3
 
4
- $:.unshift File.expand_path('../../lib', __FILE__)
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 = "<!-- markdown for worker options is generated by `#{$0}` -->"
12
-
13
- def update_worker_options(text)
14
- text.clone.sub!(/#{Regexp.escape(BEGIN_MARKER)}.*#{Regexp.escape(END_MARKER)}/m) do
15
- StringIO.open do |md|
16
- md.puts BEGIN_MARKER
17
- md.puts GENERATED_NOTE
18
- md.puts
19
-
20
- ImageOptim::Worker.klasses.each_with_index do |klass, i|
21
- md.puts "### :#{klass.bin_sym} =>"
22
- if klass.option_definitions.empty?
23
- md.puts 'Worker has no options'
24
- else
25
- klass.option_definitions.each do |option_definition|
26
- md.puts "* `:#{option_definition.name}` — #{option_definition.description} *(defaults to `#{option_definition.default.inspect}`)*"
27
- end
28
- end
29
- md.puts
30
- end
31
-
32
- md.puts END_MARKER
33
-
34
- md.string.strip
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 = update_worker_options(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 "Did not update worker options"
60
+ abort 'Did not update worker options'
46
61
  end