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