image_optim 0.26.2 → 0.27.1

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/.appveyor.yml +9 -2
  3. data/.rubocop.yml +38 -10
  4. data/.travis.yml +19 -15
  5. data/CHANGELOG.markdown +21 -0
  6. data/Gemfile +3 -1
  7. data/LICENSE.txt +1 -1
  8. data/README.markdown +9 -3
  9. data/Vagrantfile +2 -0
  10. data/bin/image_optim +1 -0
  11. data/image_optim.gemspec +5 -7
  12. data/lib/image_optim.rb +5 -0
  13. data/lib/image_optim/bin_resolver.rb +5 -0
  14. data/lib/image_optim/bin_resolver/bin.rb +45 -19
  15. data/lib/image_optim/bin_resolver/comparable_condition.rb +3 -0
  16. data/lib/image_optim/bin_resolver/error.rb +2 -0
  17. data/lib/image_optim/bin_resolver/simple_version.rb +2 -0
  18. data/lib/image_optim/cache.rb +3 -0
  19. data/lib/image_optim/cache_path.rb +21 -2
  20. data/lib/image_optim/cmd.rb +2 -0
  21. data/lib/image_optim/config.rb +7 -1
  22. data/lib/image_optim/configuration_error.rb +2 -0
  23. data/lib/image_optim/handler.rb +4 -0
  24. data/lib/image_optim/hash_helpers.rb +2 -0
  25. data/lib/image_optim/image_meta.rb +2 -0
  26. data/lib/image_optim/non_negative_integer_range.rb +2 -0
  27. data/lib/image_optim/optimized_path.rb +3 -1
  28. data/lib/image_optim/option_definition.rb +2 -0
  29. data/lib/image_optim/option_helpers.rb +2 -0
  30. data/lib/image_optim/path.rb +30 -5
  31. data/lib/image_optim/runner.rb +3 -0
  32. data/lib/image_optim/runner/glob_helpers.rb +3 -1
  33. data/lib/image_optim/runner/option_parser.rb +4 -2
  34. data/lib/image_optim/space.rb +3 -1
  35. data/lib/image_optim/true_false_nil.rb +2 -0
  36. data/lib/image_optim/worker.rb +5 -0
  37. data/lib/image_optim/worker/advpng.rb +2 -0
  38. data/lib/image_optim/worker/class_methods.rb +5 -0
  39. data/lib/image_optim/worker/gifsicle.rb +2 -0
  40. data/lib/image_optim/worker/jhead.rb +4 -1
  41. data/lib/image_optim/worker/jpegoptim.rb +2 -0
  42. data/lib/image_optim/worker/jpegrecompress.rb +2 -0
  43. data/lib/image_optim/worker/jpegtran.rb +2 -0
  44. data/lib/image_optim/worker/optipng.rb +4 -2
  45. data/lib/image_optim/worker/pngcrush.rb +4 -2
  46. data/lib/image_optim/worker/pngout.rb +3 -0
  47. data/lib/image_optim/worker/pngquant.rb +2 -0
  48. data/lib/image_optim/worker/svgo.rb +2 -0
  49. data/script/update_worker_options_in_readme +3 -2
  50. data/script/worker_analysis +13 -3
  51. data/spec/image_optim/bin_resolver/comparable_condition_spec.rb +2 -0
  52. data/spec/image_optim/bin_resolver/simple_version_spec.rb +2 -0
  53. data/spec/image_optim/bin_resolver_spec.rb +4 -2
  54. data/spec/image_optim/cache_path_spec.rb +71 -26
  55. data/spec/image_optim/cache_spec.rb +4 -0
  56. data/spec/image_optim/cmd_spec.rb +2 -0
  57. data/spec/image_optim/config_spec.rb +2 -0
  58. data/spec/image_optim/handler_spec.rb +2 -0
  59. data/spec/image_optim/hash_helpers_spec.rb +2 -0
  60. data/spec/image_optim/image_meta_spec.rb +2 -0
  61. data/spec/image_optim/optimized_path_spec.rb +2 -0
  62. data/spec/image_optim/option_definition_spec.rb +2 -0
  63. data/spec/image_optim/option_helpers_spec.rb +2 -0
  64. data/spec/image_optim/path_spec.rb +65 -24
  65. data/spec/image_optim/runner/glob_helpers_spec.rb +2 -0
  66. data/spec/image_optim/runner/option_parser_spec.rb +2 -0
  67. data/spec/image_optim/space_spec.rb +13 -11
  68. data/spec/image_optim/worker/optipng_spec.rb +2 -0
  69. data/spec/image_optim/worker/pngquant_spec.rb +2 -0
  70. data/spec/image_optim/worker_spec.rb +2 -0
  71. data/spec/image_optim_spec.rb +7 -4
  72. data/spec/images/invisiblepixels/generate +1 -0
  73. data/spec/images/quant/generate +3 -2
  74. data/spec/spec_helper.rb +3 -0
  75. metadata +18 -13
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ImageOptim
2
4
  class BinResolver
3
5
  # Allows to externalize conditions for an instance of Comparable to use in
@@ -24,6 +26,7 @@ class ImageOptim
24
26
  end
25
27
 
26
28
  attr_reader :method, :args
29
+
27
30
  def initialize(method, *args)
28
31
  @method, @args = method.to_sym, args
29
32
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ImageOptim
2
4
  class BinResolver
3
5
  # Base error during bin resolving
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ImageOptim
2
4
  class BinResolver
3
5
  # Allows comparision of simple versions, only numbers separated by dots are
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'digest/sha1'
2
4
  require 'fspath'
3
5
  require 'image_optim/cache_path'
@@ -7,6 +9,7 @@ class ImageOptim
7
9
  class Cache
8
10
  def initialize(image_optim, workers_by_format)
9
11
  return unless image_optim.cache_dir
12
+
10
13
  @cache_dir = FSPath.new(image_optim.cache_dir)
11
14
  @cache_worker_digests = image_optim.cache_worker_digests
12
15
  @options_by_format = Hash[workers_by_format.map do |format, workers|
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'image_optim/path'
2
4
 
3
5
  class ImageOptim
@@ -5,8 +7,25 @@ class ImageOptim
5
7
  class CachePath < Path
6
8
  # Atomic replace dst with self
7
9
  def replace(dst)
8
- dst = self.class.new(dst)
9
- dst.temp_path(dst.dirname) do |temp|
10
+ dst = self.class.convert(dst)
11
+ tmpdir = [dirname, Path.new(Dir.tmpdir)].find do |dir|
12
+ dir.same_dev?(dst.dirname)
13
+ end
14
+ if tmpdir
15
+ begin
16
+ replace_using_tmp_file(dst, tmpdir)
17
+ rescue Errno::EXDEV
18
+ replace_using_tmp_file(dst, dst.dirname)
19
+ end
20
+ else
21
+ replace_using_tmp_file(dst, dst.dirname)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def replace_using_tmp_file(dst, tmpdir)
28
+ dst.temp_path_with_tmp_ext(tmpdir) do |temp|
10
29
  copy(temp)
11
30
  dst.copy_metadata(temp)
12
31
  temp.rename(dst.to_s)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'English'
2
4
 
3
5
  class ImageOptim
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'image_optim/option_helpers'
2
4
  require 'image_optim/configuration_error'
3
5
  require 'image_optim/hash_helpers'
@@ -18,7 +20,7 @@ class ImageOptim
18
20
  end
19
21
 
20
22
  # Local config path at `./.image_optim.yml`
21
- LOCAL_PATH = './.image_optim.yml'.freeze
23
+ LOCAL_PATH = './.image_optim.yml'
22
24
 
23
25
  class << self
24
26
  # Read options at path: expand path (warn on failure), return {} if file
@@ -32,10 +34,12 @@ class ImageOptim
32
34
  return {}
33
35
  end
34
36
  return {} unless File.size?(full_path)
37
+
35
38
  config = YAML.load_file(full_path)
36
39
  unless config.is_a?(Hash)
37
40
  fail "expected hash, got #{config.inspect}"
38
41
  end
42
+
39
43
  HashHelpers.deep_symbolise_keys(config)
40
44
  rescue => e
41
45
  warn "exception when reading #{full_path}: #{e}"
@@ -77,6 +81,7 @@ class ImageOptim
77
81
  def assert_no_unused_options!
78
82
  unknown_options = @options.reject{ |key, _value| @used.include?(key) }
79
83
  return if unknown_options.empty?
84
+
80
85
  fail ConfigurationError, "unknown options #{unknown_options.inspect}"
81
86
  end
82
87
 
@@ -131,6 +136,7 @@ class ImageOptim
131
136
  true
132
137
  rescue LoadError => e
133
138
  raise "Cannot load image_optim_pack: #{e}" if pack
139
+
134
140
  false
135
141
  end
136
142
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ImageOptim
2
4
  class ConfigurationError < StandardError; end
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'image_optim/path'
2
4
 
3
5
  class ImageOptim
@@ -39,6 +41,7 @@ class ImageOptim
39
41
  @dst ||= @original.temp_path
40
42
 
41
43
  return unless yield @src, @dst
44
+
42
45
  @result = @dst
43
46
  if @src == @original
44
47
  @src, @dst = @dst, nil
@@ -50,6 +53,7 @@ class ImageOptim
50
53
  # Remove extra temp files
51
54
  def cleanup
52
55
  return unless @dst
56
+
53
57
  @dst.unlink
54
58
  @dst = nil
55
59
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ImageOptim
2
4
  # Helper methods to manipulate Hash, mainly used in config
3
5
  module HashHelpers
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'image_size'
2
4
 
3
5
  class ImageOptim
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ImageOptim
2
4
  # Denote range of non negative integers for worker option
3
5
  class NonNegativeIntegerRange
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'image_optim/path'
2
4
 
3
5
  class ImageOptim
@@ -5,7 +7,7 @@ class ImageOptim
5
7
  class OptimizedPath < DelegateClass(Path)
6
8
  def initialize(path, original_or_size = nil)
7
9
  path = Path.convert(path)
8
- __setobj__(path)
10
+ super(path)
9
11
  if original_or_size.is_a?(Integer)
10
12
  @original = path
11
13
  @original_size = original_or_size
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ImageOptim
2
4
  # Hold information about an option
3
5
  class OptionDefinition
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ImageOptim
2
4
  # Helper methods for options
3
5
  module OptionHelpers
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'fspath'
2
4
  require 'image_optim/image_meta'
3
5
 
@@ -48,11 +50,16 @@ class ImageOptim
48
50
 
49
51
  # Atomic replace dst with self
50
52
  def replace(dst)
51
- dst = self.class.new(dst)
52
- dst.temp_path(dst.dirname) do |temp|
53
- move(temp)
54
- dst.copy_metadata(temp)
55
- temp.rename(dst.to_s)
53
+ dst = self.class.convert(dst)
54
+ if same_dev?(dst.dirname)
55
+ dst.copy_metadata(self)
56
+ begin
57
+ rename(dst.to_s)
58
+ rescue Errno::EXDEV
59
+ replace_using_tmp_file(dst)
60
+ end
61
+ else
62
+ replace_using_tmp_file(dst)
56
63
  end
57
64
  end
58
65
 
@@ -66,5 +73,23 @@ class ImageOptim
66
73
  def self.convert(path)
67
74
  path.is_a?(self) ? path : new(path)
68
75
  end
76
+
77
+ protected
78
+
79
+ def same_dev?(other)
80
+ stat.dev == other.stat.dev
81
+ end
82
+
83
+ def replace_using_tmp_file(dst)
84
+ dst.temp_path_with_tmp_ext(dst.dirname) do |temp|
85
+ move(temp)
86
+ dst.copy_metadata(temp)
87
+ temp.rename(dst.to_s)
88
+ end
89
+ end
90
+
91
+ def temp_path_with_tmp_ext(*args, &block)
92
+ self.class.temp_file_path([basename.to_s, '.tmp'], *args, &block)
93
+ end
69
94
  end
70
95
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'image_optim'
2
4
  require 'image_optim/hash_helpers'
3
5
  require 'image_optim/runner/glob_helpers'
@@ -104,6 +106,7 @@ class ImageOptim
104
106
  if File.file?(path)
105
107
  next if exclude_file?(dir, path)
106
108
  next unless @image_optim.optimizable?(path)
109
+
107
110
  to_optimize << path
108
111
  elsif File.directory?(path)
109
112
  Find.prune if dir != path && exclude_dir?(dir, path)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ImageOptim
2
4
  class Runner
3
5
  # Helper methods for glob
@@ -21,7 +23,7 @@ class ImageOptim
21
23
  .* # what is left
22
24
  )
23
25
  \z
24
- /x
26
+ /x.freeze
25
27
 
26
28
  # Expand curly braces in glob as fnmatch in ruby before 2.0 doesn't
27
29
  # support them
@@ -1,4 +1,5 @@
1
1
  # encoding: UTF-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'image_optim'
4
5
  require 'image_optim/true_false_nil'
@@ -42,7 +43,7 @@ class ImageOptim
42
43
  # don't try to wrap if there is too little space for description
43
44
  return text if wrapped_width < 20
44
45
 
45
- wrapped = ''
46
+ wrapped = ''.dup
46
47
  text.split("\n").each do |line|
47
48
  if line.length <= columns
48
49
  wrapped << line << "\n"
@@ -185,7 +186,8 @@ ImageOptim::Runner::OptionParser::DEFINE = proc do |op, options|
185
186
 
186
187
  ImageOptim::Worker.klasses.each_with_index do |klass, i|
187
188
  next if klass.option_definitions.empty?
188
- op.separator nil unless i.zero?
189
+
190
+ op.separator nil unless i == 0
189
191
 
190
192
  bin = klass.bin_sym
191
193
  klass.option_definitions.each do |option_definition|
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ImageOptim
2
4
  # Present size in readable form as fixed length string
3
5
  module Space
@@ -15,7 +17,7 @@ class ImageOptim
15
17
  else
16
18
  log_denominator = Math.log(size.abs) / Math.log(BASE)
17
19
  degree = [log_denominator.floor, SIZE_SYMBOLS.length - 1].min
18
- number_string = if degree.zero?
20
+ number_string = if degree == 0
19
21
  size.to_s
20
22
  else
21
23
  denominator = BASE**degree
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ImageOptim
2
4
  # Denote ternary value (`true`/`false`/`nil`) for worker option
3
5
  class TrueFalseNil
@@ -1,4 +1,5 @@
1
1
  # encoding: UTF-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'image_optim/cmd'
4
5
  require 'image_optim/configuration_error'
@@ -23,6 +24,7 @@ class ImageOptim
23
24
  unless image_optim.is_a?(ImageOptim)
24
25
  fail ArgumentError, 'first parameter should be an ImageOptim instance'
25
26
  end
27
+
26
28
  @image_optim = image_optim
27
29
  parse_options(options)
28
30
  assert_no_unknown_options!(options)
@@ -49,6 +51,7 @@ class ImageOptim
49
51
  unless format_from_name
50
52
  fail "#{self.class}: can't guess applicable format from worker name"
51
53
  end
54
+
52
55
  [format_from_name.to_sym]
53
56
  end
54
57
 
@@ -68,6 +71,7 @@ class ImageOptim
68
71
  @image_optim.resolve_bin!(bin)
69
72
  end
70
73
  return if errors.empty?
74
+
71
75
  fail BinResolver::Error, wrap_resolver_error_message(errors.join(', '))
72
76
  end
73
77
 
@@ -98,6 +102,7 @@ class ImageOptim
98
102
  known_keys = self.class.option_definitions.map(&:name)
99
103
  unknown_options = options.reject{ |key, _value| known_keys.include?(key) }
100
104
  return if unknown_options.empty?
105
+
101
106
  fail ConfigurationError, "unknown options #{unknown_options.inspect} "\
102
107
  "for #{self}"
103
108
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'image_optim/worker'
2
4
  require 'image_optim/option_helpers'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'image_optim/bin_resolver'
2
4
  require 'image_optim/option_definition'
3
5
 
@@ -16,6 +18,7 @@ class ImageOptim
16
18
 
17
19
  # Remember all classes inheriting from this one
18
20
  def inherited(base)
21
+ super
19
22
  @klasses << base
20
23
  end
21
24
 
@@ -34,6 +37,7 @@ class ImageOptim
34
37
 
35
38
  def option(name, default, type, description = nil, &proc)
36
39
  attr_reader name
40
+
37
41
  OptionDefinition.new(name, default, type, description, &proc).
38
42
  tap{ |option_definition| option_definitions << option_definition }
39
43
  end
@@ -83,6 +87,7 @@ class ImageOptim
83
87
  klasses.map do |klass|
84
88
  options = options_proc[klass]
85
89
  next if options[:disable]
90
+
86
91
  if !options.key?(:allow_lossy) && klass.method_defined?(:allow_lossy)
87
92
  options[:allow_lossy] = image_optim.allow_lossy
88
93
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'image_optim/worker'
2
4
 
3
5
  class ImageOptim
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'image_optim/worker'
2
4
  require 'exifr/jpeg'
3
5
 
@@ -7,7 +9,7 @@ class ImageOptim
7
9
  #
8
10
  # Jhead internally uses jpegtran which should be on path
9
11
  class Jhead < Worker
10
- ORIENTED = 2..8 # not top-left
12
+ ORIENTED = (2..8).freeze # not top-left
11
13
 
12
14
  # Works on jpegs
13
15
  def image_formats
@@ -25,6 +27,7 @@ class ImageOptim
25
27
 
26
28
  def optimize(src, dst)
27
29
  return false unless oriented?(src)
30
+
28
31
  src.copy(dst)
29
32
  args = %W[
30
33
  -autorot
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'image_optim/worker'
2
4
  require 'image_optim/option_helpers'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'image_optim/worker'
2
4
  require 'image_optim/option_helpers'
3
5