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.
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