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
@@ -1,6 +1,16 @@
1
1
  class ImageOptim
2
2
  class BinResolver
3
+ # Allows to externalize conditions for an instance of Comparable to use in
4
+ # case statemens
5
+ #
6
+ # is = ComparableCondition.is
7
+ # case rand(100)
8
+ # when is < 10 then # ...
9
+ # when is.between?(13, 23) then # ...
10
+ # when is >= 90 then # ...
11
+ # end
3
12
  class ComparableCondition
13
+ # Helper class for creating conditions using ComparableCondition.is
4
14
  class Builder
5
15
  Comparable.instance_methods.each do |method|
6
16
  define_method method do |*args|
@@ -15,22 +25,22 @@ class ImageOptim
15
25
 
16
26
  attr_reader :method, :args
17
27
  def initialize(method, *args)
18
- @method = method.to_sym
19
- @args = args
28
+ @method, @args = method.to_sym, args
20
29
 
21
30
  case @method
22
31
  when :between?
23
- raise ArgumentError, "`between?' expects 2 arguments" unless args.length == 2
32
+ @args.length == 2 || argument_error!("`between?' expects 2 arguments")
24
33
  when :<, :<=, :==, :>, :>=
25
- raise ArgumentError, "`#{method}' expects 1 argument" unless args.length == 1
34
+ @args.length == 1 || argument_error!("`#{method}' expects 1 argument")
26
35
  else
27
- raise ArgumentError, "Unknown method `#{method}'"
36
+ argument_error! "Unknown method `#{method}'"
28
37
  end
29
38
  end
30
39
 
31
- def ===(to_compare)
32
- to_compare.send(@method, *@args)
40
+ def ===(other)
41
+ other.send(@method, *@args)
33
42
  end
43
+ alias_method :match, :===
34
44
 
35
45
  def to_s
36
46
  if @method == :between?
@@ -39,6 +49,12 @@ class ImageOptim
39
49
  "#{@method} #{@args.first}"
40
50
  end
41
51
  end
52
+
53
+ private
54
+
55
+ def argument_error!(message)
56
+ fail ArgumentError, message
57
+ end
42
58
  end
43
59
  end
44
60
  end
@@ -1,5 +1,7 @@
1
1
  class ImageOptim
2
2
  class BinResolver
3
+ # Allows comparision of simple versions, only numbers separated by dots are
4
+ # taken into account
3
5
  class SimpleVersion
4
6
  include Comparable
5
7
 
@@ -6,17 +6,23 @@ require 'set'
6
6
  require 'yaml'
7
7
 
8
8
  class ImageOptim
9
+ # Read, merge and parse configuration
9
10
  class Config
10
11
  include OptionHelpers
11
12
 
12
- GLOBAL_CONFIG_PATH = File.join(File.expand_path(ENV['XDG_CONFIG_HOME'] || '~/.config'), 'image_optim.yml')
13
- LOCAL_CONFIG_PATH = '.image_optim.yml'
13
+ CONFIG_HOME = File.expand_path(ENV['XDG_CONFIG_HOME'] || '~/.config')
14
+ GLOBAL_CONFIG_PATH = File.join(CONFIG_HOME, 'image_optim.yml')
15
+ LOCAL_CONFIG_PATH = './.image_optim.yml'
14
16
 
15
17
  class << self
18
+ # Read config at GLOBAL_CONFIG_PATH if it exists, warn if anything is
19
+ # wrong
16
20
  def global
17
21
  File.file?(GLOBAL_CONFIG_PATH) ? read(GLOBAL_CONFIG_PATH) : {}
18
22
  end
19
23
 
24
+ # Read config at LOCAL_CONFIG_PATH if it exists, warn if anything is
25
+ # wrong
20
26
  def local
21
27
  File.file?(LOCAL_CONFIG_PATH) ? read(LOCAL_CONFIG_PATH) : {}
22
28
  end
@@ -26,7 +32,7 @@ class ImageOptim
26
32
  def read(path)
27
33
  config = YAML.load_file(path)
28
34
  unless config.is_a?(Hash)
29
- raise "excpected hash, got #{config.inspect}"
35
+ fail "excpected hash, got #{config.inspect}"
30
36
  end
31
37
  HashHelpers.deep_symbolise_keys(config)
32
38
  rescue => e
@@ -40,7 +46,7 @@ class ImageOptim
40
46
  Config.global,
41
47
  Config.local,
42
48
  HashHelpers.deep_symbolise_keys(options),
43
- ].inject do |memo, hash|
49
+ ].reduce do |memo, hash|
44
50
  HashHelpers.deep_merge(memo, hash)
45
51
  end
46
52
  @used = Set.new
@@ -53,10 +59,10 @@ class ImageOptim
53
59
  end
54
60
 
55
61
  def assert_no_unused_options!
56
- unknown_options = @options.reject{ |key, value| @used.include?(key) }
57
- unless unknown_options.empty?
58
- raise ConfigurationError, "unknown options #{unknown_options.inspect} for #{self}"
59
- end
62
+ unknown_options = @options.reject{ |key, _value| @used.include?(key) }
63
+ return if unknown_options.empty?
64
+ fail ConfigurationError, "unknown options #{unknown_options.inspect} "\
65
+ "for #{self}"
60
66
  end
61
67
 
62
68
  def nice
@@ -100,7 +106,8 @@ class ImageOptim
100
106
  when false
101
107
  false
102
108
  else
103
- raise ConfigurationError, "Got #{worker_options.inspect} for #{klass.name} options"
109
+ fail ConfigurationError, "Got #{worker_options.inspect} for "\
110
+ "#{klass.name} options"
104
111
  end
105
112
  end
106
113
 
@@ -110,7 +117,7 @@ class ImageOptim
110
117
 
111
118
  private
112
119
 
113
- # http://stackoverflow.com/questions/891537/ruby-detect-number-of-cpus-installed
120
+ # http://stackoverflow.com/a/6420817
114
121
  def processor_count
115
122
  @processor_count ||= case host_os = RbConfig::CONFIG['host_os']
116
123
  when /darwin9/
@@ -123,9 +130,10 @@ class ImageOptim
123
130
  `sysctl -n hw.ncpu`
124
131
  when /mswin|mingw/
125
132
  require 'win32ole'
126
- wmi = WIN32OLE.connect('winmgmts://')
127
- cpu = wmi.ExecQuery('select NumberOfLogicalProcessors from Win32_Processor')
128
- cpu.to_enum.first.NumberOfLogicalProcessors
133
+ WIN32OLE.
134
+ connect('winmgmts://').
135
+ ExecQuery('select NumberOfLogicalProcessors from Win32_Processor').
136
+ to_enum.first.NumberOfLogicalProcessors
129
137
  else
130
138
  warn "Unknown architecture (#{host_os}) assuming one processor."
131
139
  1
@@ -1,35 +1,41 @@
1
1
  require 'image_optim/image_path'
2
2
 
3
3
  class ImageOptim
4
+ # Handles processing of original to result using upto two temp files
4
5
  class Handler
6
+ # Holds latest successful result
5
7
  attr_reader :result
8
+
9
+ # original must respond to temp_path
6
10
  def initialize(original)
7
- raise ArgumentError, 'original should respond to temp_path' unless original.respond_to?(:temp_path)
11
+ unless original.respond_to?(:temp_path)
12
+ fail ArgumentError, 'original should respond to temp_path'
13
+ end
8
14
 
9
15
  @original = original
10
16
  @result = nil
11
17
  end
12
18
 
19
+ # Yields two paths, one to latest successful result or original, second to
20
+ # temp path
13
21
  def process
14
22
  @src ||= @original
15
23
  @dst ||= @original.temp_path
16
24
 
17
- if yield @src, @dst
18
- @result = @dst
19
- if @src == @original
20
- @src, @dst = @dst, nil
21
- else
22
- @src, @dst = @dst, @src
23
- end
25
+ return unless yield @src, @dst
26
+ @result = @dst
27
+ if @src == @original
28
+ @src, @dst = @dst, nil
29
+ else
30
+ @src, @dst = @dst, @src
24
31
  end
25
32
  end
26
33
 
27
34
  # Remove extra temp files
28
35
  def cleanup
29
- if @dst
30
- @dst.unlink
31
- @dst = nil
32
- end
36
+ return unless @dst
37
+ @dst.unlink
38
+ @dst = nil
33
39
  end
34
40
  end
35
41
  end
@@ -1,28 +1,22 @@
1
1
  class ImageOptim
2
+ # Helper methods to manipulate Hash, mainly used in config
2
3
  module HashHelpers
3
4
  class << self
4
- def deep_transform_keys(hash, &block)
5
- new_hash = {}
6
- hash.each do |k, v|
7
- new_hash[block.call(k)] = if v.is_a?(Hash)
8
- deep_transform_keys(v, &block)
9
- else
10
- v
11
- end
12
- end
13
- new_hash
14
- end
15
-
5
+ # Returns a new hash with all keys of root and nested hashes converted to
6
+ # strings
16
7
  def deep_stringify_keys(hash)
17
8
  deep_transform_keys(hash, &:to_s)
18
9
  end
19
10
 
11
+ # Returns a new hash with all keys of root and nested hashes converted to
12
+ # symbols
20
13
  def deep_symbolise_keys(hash)
21
14
  deep_transform_keys(hash, &:to_sym)
22
15
  end
23
16
 
17
+ # Returns a new hash with recursive merge of all keys
24
18
  def deep_merge(a, b)
25
- a.merge(b) do |k, v_a, v_b|
19
+ a.merge(b) do |_k, v_a, v_b|
26
20
  if v_a.is_a?(Hash) && v_b.is_a?(Hash)
27
21
  deep_merge(v_a, v_b)
28
22
  else
@@ -30,6 +24,22 @@ class ImageOptim
30
24
  end
31
25
  end
32
26
  end
27
+
28
+ private
29
+
30
+ # Returns a new hash with all keys of root and nested hashes converted by
31
+ # provided block
32
+ def deep_transform_keys(hash, &block)
33
+ new_hash = {}
34
+ hash.each do |k, v|
35
+ new_hash[block.call(k)] = if v.is_a?(Hash)
36
+ deep_transform_keys(v, &block)
37
+ else
38
+ v
39
+ end
40
+ end
41
+ new_hash
42
+ end
33
43
  end
34
44
  end
35
45
  end
@@ -1,6 +1,7 @@
1
1
  require 'image_size'
2
2
 
3
3
  class ImageOptim
4
+ # Getting format of image at path or as data
4
5
  class ImageMeta
5
6
  def self.for_path(path)
6
7
  is = ImageSize.path(path)
@@ -2,23 +2,24 @@ require 'fspath'
2
2
  require 'image_optim/image_meta'
3
3
 
4
4
  class ImageOptim
5
+ # FSPath with additional helpful methods
5
6
  class ImagePath < FSPath
7
+ # Holds optimized image with reference to original and its size
6
8
  class Optimized < DelegateClass(self)
7
9
  def initialize(path, original_or_size = nil)
8
10
  path = ImagePath.convert(path)
9
11
  __setobj__(path)
10
- if original_or_size
11
- if original_or_size.is_a?(Integer)
12
- @original = path
13
- @original_size = original_or_size
14
- else
15
- @original = ImagePath.convert(original_or_size)
16
- @original_size = @original.size
17
- end
12
+ if original_or_size.is_a?(Integer)
13
+ @original = path
14
+ @original_size = original_or_size
15
+ elsif original_or_size
16
+ @original = ImagePath.convert(original_or_size)
17
+ @original_size = @original.size
18
18
  end
19
19
  end
20
20
 
21
- # Original path, use original_size to get its size as original can be overwritten
21
+ # Original path, use original_size to get its size as original can be
22
+ # overwritten
22
23
  attr_reader :original
23
24
 
24
25
  # Stored size of original
@@ -49,12 +50,12 @@ class ImageOptim
49
50
 
50
51
  # Get format using ImageSize
51
52
  def format
52
- if image_meta = ImageMeta.for_path(self)
53
- image_meta.format
54
- end
53
+ image_meta = ImageMeta.for_path(self)
54
+ image_meta && image_meta.format
55
55
  end
56
56
 
57
- # Returns path if it is already an instance of this class otherwise new instance
57
+ # Returns path if it is already an instance of this class otherwise new
58
+ # instance
58
59
  def self.convert(path)
59
60
  path.is_a?(self) ? path : new(path)
60
61
  end
@@ -1,14 +1,16 @@
1
- class OptionDefinition
1
+ class ImageOptim
2
+ # Hold information about an option
3
+ class OptionDefinition
4
+ attr_reader :name, :default, :type, :description, :proc
2
5
 
3
- attr_reader :name, :default, :type, :description, :proc
6
+ def initialize(name, default, type, description, &proc)
7
+ if type.is_a?(String)
8
+ type, description = default.class, type
9
+ end
4
10
 
5
- def initialize(name, default, type, description, &proc)
6
- if type.is_a?(String)
7
- type, description = default.class, type
11
+ @name = name.to_sym
12
+ @description = description.to_s
13
+ @default, @type, @proc = default, type, proc
8
14
  end
9
-
10
- @name = name.to_sym
11
- @description = description.to_s
12
- @default, @type, @proc = default, type, proc
13
15
  end
14
16
  end
@@ -1,6 +1,5 @@
1
- require 'image_optim/configuration_error'
2
-
3
1
  class ImageOptim
2
+ # Helper methods for options
4
3
  module OptionHelpers
5
4
  # Ensure number is in range
6
5
  def self.limit_with_range(number, range)
@@ -1,28 +1,31 @@
1
1
  require 'image_optim'
2
2
 
3
3
  class ImageOptim
4
+ # Adds image_optim as preprocessor for gif, jpeg, png and svg images
4
5
  class Railtie < Rails::Railtie
5
6
  initializer 'image_optim.initializer' do |app|
6
- if app.config.assets.compress != false && app.config.assets.image_optim != false && app.assets
7
7
 
8
- options = if app.config.assets.image_optim == true
9
- {}
10
- else
11
- app.config.assets.image_optim || {}
12
- end
8
+ break if app.config.assets.compress == false
9
+ break if app.config.assets.image_optim == false
10
+ break unless app.assets
13
11
 
14
- image_optim = ImageOptim.new(options)
15
-
16
- processor = proc do |context, data|
17
- image_optim.optimize_image_data(data) || data
18
- end
12
+ options = if app.config.assets.image_optim == true
13
+ {}
14
+ else
15
+ app.config.assets.image_optim || {}
16
+ end
19
17
 
20
- app.assets.register_preprocessor 'image/gif', :image_optim, &processor
21
- app.assets.register_preprocessor 'image/jpeg', :image_optim, &processor
22
- app.assets.register_preprocessor 'image/png', :image_optim, &processor
23
- app.assets.register_preprocessor 'image/svg+xml', :image_optim, &processor
18
+ image_optim = ImageOptim.new(options)
24
19
 
20
+ processor = proc do |_context, data|
21
+ image_optim.optimize_image_data(data) || data
25
22
  end
23
+
24
+ app.assets.register_preprocessor 'image/gif', :image_optim, &processor
25
+ app.assets.register_preprocessor 'image/jpeg', :image_optim, &processor
26
+ app.assets.register_preprocessor 'image/png', :image_optim, &processor
27
+ app.assets.register_preprocessor 'image/svg+xml', :image_optim, &processor
28
+
26
29
  end
27
30
  end
28
31
  end
@@ -1,67 +1,69 @@
1
- # encoding: UTF-8
2
-
3
1
  require 'image_optim'
4
2
  require 'image_optim/hash_helpers'
5
3
  require 'image_optim/true_false_nil'
4
+ require 'image_optim/space'
6
5
  require 'progress'
7
6
  require 'optparse'
8
7
  require 'find'
9
8
  require 'yaml'
10
9
 
11
10
  class ImageOptim
11
+ # Handling optimization using image_optim binary
12
12
  class Runner
13
- module Space
14
- SIZE_SYMBOLS = %w[B K M G T P E].freeze
15
- PRECISION = 1
16
- LENGTH = 4 + PRECISION + 1
13
+ # Collect and output results of optimization
14
+ class Results
15
+ def initialize
16
+ @lines = []
17
+ @original_size_sum = 0
18
+ @optimized_size_sum = 0
19
+ end
17
20
 
18
- EMPTY_SPACE = ' ' * LENGTH
21
+ def add(original, optimized)
22
+ original_size = optimized ? optimized.original_size : original.size
23
+ optimized_size = optimized ? optimized.size : original.size
24
+ @lines << "#{size_percent(original_size, optimized_size)} #{original}"
25
+ @original_size_sum += original_size
26
+ @optimized_size_sum += optimized_size
27
+ end
19
28
 
20
- class << self
21
- attr_writer :base10
22
- def denominator
23
- @denominator ||= @base10 ? 1000.0 : 1024.0
24
- end
29
+ def print
30
+ puts @lines
31
+ puts "Total: #{size_percent(@original_size_sum, @optimized_size_sum)}"
32
+ end
25
33
 
26
- def space(size)
27
- case size
28
- when 0, nil
29
- EMPTY_SPACE
30
- else
31
- log_denominator = Math.log(size) / Math.log(denominator)
32
- degree = [log_denominator.floor, SIZE_SYMBOLS.length - 1].min
33
- number = size / (denominator ** degree)
34
- "#{degree == 0 ? number.to_i : "%.#{PRECISION}f" % number}#{SIZE_SYMBOLS[degree]}".rjust(LENGTH)
35
- end
34
+ private
35
+
36
+ def size_percent(size_a, size_b)
37
+ if size_a == size_b
38
+ "------ #{Space::EMPTY_SPACE}"
39
+ else
40
+ percent = 100 - 100.0 * size_b / size_a
41
+ space = Space.space(size_a - size_b)
42
+ format('%5.2f%% %s', percent, space)
36
43
  end
37
44
  end
38
45
  end
39
46
 
40
47
  def initialize(args, options)
41
- raise 'specify paths to optimize' if args.empty?
48
+ fail 'specify paths to optimize' if args.empty?
42
49
  options = HashHelpers.deep_symbolise_keys(options)
43
50
  @recursive = options.delete(:recursive)
44
51
  @image_optim = ImageOptim.new(options)
45
- @files = find_files(args)
52
+ @to_optimize = find_to_optimize(args)
46
53
  end
47
54
 
48
55
  def run!
49
- unless @files.empty?
50
- lines, original_sizes, optimized_sizes =
51
- @image_optim.optimize_images!(@files.with_progress('optimizing')) do |original, optimized|
52
- original_size = optimized ? optimized.original_size : original.size
53
- optimized_size = optimized ? optimized.size : original.size
54
- ["#{size_percent(original_size, optimized_size)} #{original}", original_size, optimized_size]
55
- end.transpose
56
-
57
- puts lines, "Total: #{size_percent(original_sizes.inject(:+), optimized_sizes.inject(:+))}"
58
- end
56
+ unless @to_optimize.empty?
57
+ results = Results.new
59
58
 
60
- !warnings?
61
- end
59
+ optimize_images! do |original, optimized|
60
+ results.add(original, optimized)
61
+ end
62
62
 
63
- def warnings?
64
- !!@warnings
63
+ results.print
64
+ end
65
+
66
+ !@warnings
65
67
  end
66
68
 
67
69
  def self.run!(args, options)
@@ -70,42 +72,46 @@ class ImageOptim
70
72
 
71
73
  private
72
74
 
73
- def find_files(args)
74
- files = []
75
- args.each do |arg|
76
- if File.file?(arg)
77
- if @image_optim.optimizable?(arg)
78
- files << arg
75
+ def optimize_images!(&block)
76
+ @image_optim.
77
+ optimize_images!(@to_optimize.with_progress('optimizing'), &block)
78
+ end
79
+
80
+ def find_to_optimize(paths)
81
+ to_optimize = []
82
+ paths.each do |path|
83
+ if File.file?(path)
84
+ if @image_optim.optimizable?(path)
85
+ to_optimize << path
79
86
  else
80
- warning "#{arg} is not an image or there is no optimizer for it"
87
+ warning "#{path} is not an image or there is no optimizer for it"
81
88
  end
82
89
  elsif @recursive
83
- if File.directory?(arg)
84
- Find.find(arg) do |path|
85
- files << path if File.file?(path) && @image_optim.optimizable?(path)
86
- end
90
+ if File.directory?(path)
91
+ to_optimize += find_to_optimize_recursive(path)
87
92
  else
88
- warning "#{arg} is not a file or a directory or does not exist"
93
+ warning "#{path} is not a file or a directory or does not exist"
89
94
  end
90
95
  else
91
- warning "#{arg} is not a file or does not exist"
96
+ warning "#{path} is not a file or does not exist"
92
97
  end
93
98
  end
94
- files
99
+ to_optimize
100
+ end
101
+
102
+ def find_to_optimize_recursive(dir)
103
+ to_optimize = []
104
+ Find.find(dir) do |path|
105
+ next unless File.file?(path)
106
+ next unless @image_optim.optimizable?(path)
107
+ to_optimize << path
108
+ end
109
+ to_optimize
95
110
  end
96
111
 
97
112
  def warning(message)
98
113
  @warnings = true
99
114
  warn message
100
115
  end
101
-
102
- def size_percent(size_a, size_b)
103
- if size_a == size_b
104
- "------ #{Space::EMPTY_SPACE}"
105
- else
106
- '%5.2f%% %s' % [100 - 100.0 * size_b / size_a, Space.space(size_a - size_b)]
107
- end
108
- end
109
-
110
116
  end
111
117
  end