image_optim 0.22.1 → 0.23.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 (50) hide show
  1. checksums.yaml +8 -8
  2. data/.appveyor.yml +95 -0
  3. data/.rubocop.yml +3 -0
  4. data/.travis.yml +27 -22
  5. data/CHANGELOG.markdown +10 -0
  6. data/CONTRIBUTING.markdown +2 -1
  7. data/Gemfile +1 -1
  8. data/README.markdown +10 -2
  9. data/image_optim.gemspec +4 -4
  10. data/lib/image_optim.rb +32 -16
  11. data/lib/image_optim/bin_resolver/bin.rb +11 -4
  12. data/lib/image_optim/cache.rb +71 -0
  13. data/lib/image_optim/cache_path.rb +16 -0
  14. data/lib/image_optim/config.rb +12 -2
  15. data/lib/image_optim/handler.rb +1 -1
  16. data/lib/image_optim/image_meta.rb +5 -10
  17. data/lib/image_optim/optimized_path.rb +25 -0
  18. data/lib/image_optim/path.rb +70 -0
  19. data/lib/image_optim/runner/option_parser.rb +13 -0
  20. data/lib/image_optim/worker.rb +5 -8
  21. data/lib/image_optim/worker/class_methods.rb +3 -1
  22. data/lib/image_optim/worker/jpegoptim.rb +3 -0
  23. data/lib/image_optim/worker/jpegrecompress.rb +3 -0
  24. data/lib/image_optim/worker/pngquant.rb +3 -0
  25. data/script/worker_analysis +10 -9
  26. data/spec/image_optim/bin_resolver/comparable_condition_spec.rb +1 -1
  27. data/spec/image_optim/bin_resolver/simple_version_spec.rb +48 -40
  28. data/spec/image_optim/bin_resolver_spec.rb +190 -172
  29. data/spec/image_optim/cache_path_spec.rb +59 -0
  30. data/spec/image_optim/cache_spec.rb +159 -0
  31. data/spec/image_optim/cmd_spec.rb +11 -7
  32. data/spec/image_optim/config_spec.rb +92 -71
  33. data/spec/image_optim/handler_spec.rb +3 -6
  34. data/spec/image_optim/image_meta_spec.rb +61 -0
  35. data/spec/image_optim/optimized_path_spec.rb +58 -0
  36. data/spec/image_optim/option_helpers_spec.rb +25 -0
  37. data/spec/image_optim/path_spec.rb +105 -0
  38. data/spec/image_optim/railtie_spec.rb +6 -6
  39. data/spec/image_optim/runner/glob_helpers_spec.rb +2 -6
  40. data/spec/image_optim/runner/option_parser_spec.rb +3 -3
  41. data/spec/image_optim/space_spec.rb +16 -18
  42. data/spec/image_optim/worker/optipng_spec.rb +3 -3
  43. data/spec/image_optim/worker/pngquant_spec.rb +47 -7
  44. data/spec/image_optim/worker_spec.rb +114 -17
  45. data/spec/image_optim_spec.rb +58 -69
  46. data/spec/images/broken_jpeg +1 -0
  47. data/spec/spec_helper.rb +40 -10
  48. metadata +30 -8
  49. data/lib/image_optim/image_path.rb +0 -68
  50. data/spec/image_optim/image_path_spec.rb +0 -54
@@ -0,0 +1,71 @@
1
+ require 'digest/sha1'
2
+ require 'fspath'
3
+ require 'image_optim/cache_path'
4
+
5
+ class ImageOptim
6
+ # Handles image cache
7
+ class Cache
8
+ def initialize(image_optim, workers_by_format)
9
+ return unless image_optim.cache_dir
10
+ @cache_dir = FSPath.new(image_optim.cache_dir)
11
+ @cache_worker_digests = image_optim.cache_worker_digests
12
+ @options_by_format = Hash[workers_by_format.map do |format, workers|
13
+ [format, workers.map(&:inspect).sort.join(', ')]
14
+ end]
15
+ @bins_by_format = Hash[workers_by_format.map do |format, workers|
16
+ [format, workers.map(&:used_bins).flatten!.map! do |sym|
17
+ bin = image_optim.resolve_bin!(sym)
18
+ "#{bin.name}[#{bin.digest}]"
19
+ end.sort!.uniq.join(', ')]
20
+ end]
21
+ end
22
+
23
+ def fetch(original)
24
+ return yield unless @cache_dir
25
+
26
+ digest = digest(original, original.image_format)
27
+ cached = @cache_dir / digest
28
+ return cached.size? && CachePath.convert(cached) if cached.file?
29
+
30
+ optimized = yield
31
+
32
+ cached.dirname.mkpath
33
+
34
+ if optimized
35
+ tmp = FSPath.temp_file_path(digest, @cache_dir)
36
+ FileUtils.mv(optimized, tmp)
37
+ tmp.rename(cached)
38
+ cached_path = CachePath.convert(cached)
39
+
40
+ # mark cached image as already optimized
41
+ cached = @cache_dir / digest(cached, original.image_format)
42
+ cached.dirname.mkpath
43
+ FileUtils.touch(cached)
44
+
45
+ cached_path
46
+ else
47
+ # mark image as already optimized
48
+ FileUtils.touch(cached)
49
+ nil
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def options_by_format(format)
56
+ @options_by_format[format]
57
+ end
58
+
59
+ def bins_by_format(format)
60
+ @bins_by_format[format]
61
+ end
62
+
63
+ def digest(path, format)
64
+ digest = Digest::SHA1.file(path)
65
+ digest.update options_by_format(format)
66
+ digest.update bins_by_format(format) if @cache_worker_digests
67
+ s = digest.hexdigest
68
+ "#{s[0..1]}/#{s[2..-1]}"
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,16 @@
1
+ require 'image_optim/path'
2
+
3
+ class ImageOptim
4
+ # ImageOptiom::Path with a non self destructing #replace method
5
+ class CachePath < Path
6
+ # Atomic replace dst with self
7
+ def replace(dst)
8
+ dst = self.class.new(dst)
9
+ dst.temp_path(dst.dirname) do |temp|
10
+ copy(temp)
11
+ dst.copy_metadata(temp)
12
+ temp.rename(dst.to_s)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -22,7 +22,8 @@ class ImageOptim
22
22
 
23
23
  class << self
24
24
  # Read options at path: expand path (warn on failure), return {} if file
25
- # does not exist, read yaml, check if it is a Hash, deep symbolise keys
25
+ # does not exist or is empty, read yaml, check if it is a Hash, deep
26
+ # symbolise keys
26
27
  def read_options(path)
27
28
  begin
28
29
  full_path = File.expand_path(path)
@@ -30,7 +31,7 @@ class ImageOptim
30
31
  warn "Can't expand path #{path}: #{e}"
31
32
  return {}
32
33
  end
33
- return {} unless File.file?(full_path)
34
+ return {} unless File.size?(full_path)
34
35
  config = YAML.load_file(full_path)
35
36
  unless config.is_a?(Hash)
36
37
  fail "expected hash, got #{config.inspect}"
@@ -147,6 +148,15 @@ class ImageOptim
147
148
  !!get!(:allow_lossy)
148
149
  end
149
150
 
151
+ def cache_dir
152
+ dir = get!(:cache_dir)
153
+ dir unless dir.nil? || dir.empty?
154
+ end
155
+
156
+ def cache_worker_digests
157
+ !!get!(:cache_worker_digests)
158
+ end
159
+
150
160
  # Options for worker class by its `bin_sym`:
151
161
  # * `Hash` passed as is
152
162
  # * `{}` for `true` or `nil`
@@ -1,4 +1,4 @@
1
- require 'image_optim/image_path'
1
+ require 'image_optim/path'
2
2
 
3
3
  class ImageOptim
4
4
  # Handles processing of original to result using upto two temp files
@@ -2,24 +2,19 @@ require 'image_size'
2
2
 
3
3
  class ImageOptim
4
4
  # Getting format of image at path or as data
5
- class ImageMeta
6
- def self.for_path(path)
5
+ module ImageMeta
6
+ def self.format_for_path(path)
7
7
  is = ImageSize.path(path)
8
- new(is.format)
8
+ is.format if is
9
9
  rescue ImageSize::FormatError => e
10
10
  warn "#{e} (detecting format of image at #{path})"
11
11
  end
12
12
 
13
- def self.for_data(data)
13
+ def self.format_for_data(data)
14
14
  is = ImageSize.new(data)
15
- new(is.format)
15
+ is.format if is
16
16
  rescue ImageSize::FormatError => e
17
17
  warn "#{e} (detecting format of image data)"
18
18
  end
19
-
20
- attr_reader :format
21
- def initialize(format)
22
- @format = format
23
- end
24
19
  end
25
20
  end
@@ -0,0 +1,25 @@
1
+ require 'image_optim/path'
2
+
3
+ class ImageOptim
4
+ # Holds optimized image with reference to original and its size
5
+ class OptimizedPath < DelegateClass(Path)
6
+ def initialize(path, original_or_size = nil)
7
+ path = Path.convert(path)
8
+ __setobj__(path)
9
+ if original_or_size.is_a?(Integer)
10
+ @original = path
11
+ @original_size = original_or_size
12
+ elsif original_or_size
13
+ @original = Path.convert(original_or_size)
14
+ @original_size = @original.size
15
+ end
16
+ end
17
+
18
+ # Original path, use original_size to get its size as original can be
19
+ # overwritten
20
+ attr_reader :original
21
+
22
+ # Stored size of original
23
+ attr_reader :original_size
24
+ end
25
+ end
@@ -0,0 +1,70 @@
1
+ require 'fspath'
2
+ require 'image_optim/image_meta'
3
+
4
+ class ImageOptim
5
+ # FSPath with additional helpful methods
6
+ class Path < FSPath
7
+ NULL = if defined?(IO::NULL)
8
+ IO::NULL
9
+ else
10
+ %w[/dev/null NUL: NUL nul NIL: NL:].find{ |dev| File.exist?(dev) }
11
+ end
12
+
13
+ # Get temp path for this file with same extension
14
+ def temp_path(*args, &block)
15
+ ext = extname
16
+ self.class.temp_file_path([basename(ext).to_s, ext], *args, &block)
17
+ end
18
+
19
+ # Copy file to dst, optionally preserving attributes
20
+ #
21
+ # See FileUtils.copy_file
22
+ def copy(dst, preserve = false)
23
+ FileUtils.copy_file(self, dst, preserve)
24
+ end
25
+
26
+ # Move file to dst: rename on same device, copy and unlink original
27
+ # otherwise
28
+ #
29
+ # See FileUtils.mv
30
+ def move(dst)
31
+ FileUtils.move(self, dst)
32
+ end
33
+
34
+ # Copy metadata: uid, gid, mode, optionally atime and mtime
35
+ #
36
+ # Adapted from FileUtils::Entry_#copy_metadata by Minero Aoki
37
+ def copy_metadata(dst, time = false)
38
+ stat = lstat
39
+ dst.utime(stat.atime, stat.mtime) if time
40
+ begin
41
+ dst.chown(stat.uid, stat.gid)
42
+ rescue Errno::EPERM
43
+ dst.chmod(stat.mode & 0o1777)
44
+ else
45
+ dst.chmod(stat.mode)
46
+ end
47
+ end
48
+
49
+ # Atomic replace dst with self
50
+ 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)
56
+ end
57
+ end
58
+
59
+ # Get format using ImageSize
60
+ def image_format
61
+ ImageMeta.format_for_path(self)
62
+ end
63
+
64
+ # Returns path if it is already an instance of this class otherwise new
65
+ # instance
66
+ def self.convert(path)
67
+ path.is_a?(self) ? path : new(path)
68
+ end
69
+ end
70
+ end
@@ -143,6 +143,19 @@ ImageOptim::Runner::OptionParser::DEFINE = proc do |op, options|
143
143
  options[:pack] = pack
144
144
  end
145
145
 
146
+ op.separator nil
147
+ op.separator ' Caching:'
148
+
149
+ op.on('--cache-dir DIR', 'Cache optimized images '\
150
+ 'into the specified directory') do |cache_dir|
151
+ options[:cache_dir] = cache_dir
152
+ end
153
+
154
+ op.on('--cache-worker-digests', 'Cache worker digests '\
155
+ '(updating workers invalidates cache)') do |cache_worker_digests|
156
+ options[:cache_worker_digests] = cache_worker_digests
157
+ end
158
+
146
159
  op.separator nil
147
160
  op.separator ' Disabling workers:'
148
161
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'image_optim/cmd'
4
4
  require 'image_optim/configuration_error'
5
+ require 'image_optim/path'
5
6
  require 'image_optim/worker/class_methods'
6
7
  require 'shellwords'
7
8
  require 'English'
@@ -17,16 +18,12 @@ class ImageOptim
17
18
  alias_method :init, :new
18
19
  end
19
20
 
20
- # Allow lossy optimizations
21
- attr_reader :allow_lossy
22
-
23
21
  # Configure (raises on extra options)
24
22
  def initialize(image_optim, options = {})
25
23
  unless image_optim.is_a?(ImageOptim)
26
24
  fail ArgumentError, 'first parameter should be an ImageOptim instance'
27
25
  end
28
26
  @image_optim = image_optim
29
- @allow_lossy = !!options.delete(:allow_lossy)
30
27
  parse_options(options)
31
28
  assert_no_unknown_options!(options)
32
29
  end
@@ -81,8 +78,8 @@ class ImageOptim
81
78
 
82
79
  # Short inspect
83
80
  def inspect
84
- options_string = options.map do |name, value|
85
- " @#{name}=#{value.inspect}"
81
+ options_string = self.class.option_definitions.map do |option|
82
+ " @#{option.name}=#{send(option.name).inspect}"
86
83
  end.join(',')
87
84
  "#<#{self.class}#{options_string}>"
88
85
  end
@@ -143,14 +140,14 @@ class ImageOptim
143
140
  %W[
144
141
  env PATH=#{@image_optim.env_path.shellescape}
145
142
  nice -n #{@image_optim.nice}
146
- #{cmd_args.shelljoin} > /dev/null 2>&1
143
+ #{cmd_args.shelljoin} > #{Path::NULL} 2>&1
147
144
  ].join(' ')
148
145
  else
149
146
  [
150
147
  {'PATH' => @image_optim.env_path},
151
148
  %W[nice -n #{@image_optim.nice}],
152
149
  cmd_args,
153
- {:out => '/dev/null', :err => '/dev/null'},
150
+ {:out => Path::NULL, :err => Path::NULL},
154
151
  ].flatten
155
152
  end
156
153
  Cmd.run(*args)
@@ -82,7 +82,9 @@ class ImageOptim
82
82
  klasses.map do |klass|
83
83
  options = options_proc[klass]
84
84
  next if options[:disable]
85
- options = options.merge(:allow_lossy => image_optim.allow_lossy)
85
+ if !options.key?(:allow_lossy) && klass.method_defined?(:allow_lossy)
86
+ options[:allow_lossy] = image_optim.allow_lossy
87
+ end
86
88
  klass.init(image_optim, options)
87
89
  end.compact.flatten
88
90
  end
@@ -5,6 +5,9 @@ class ImageOptim
5
5
  class Worker
6
6
  # http://www.kokkonen.net/tjko/projects.html
7
7
  class Jpegoptim < Worker
8
+ ALLOW_LOSSY_OPTION =
9
+ option(:allow_lossy, false, 'Allow limiting maximum quality'){ |v| !!v }
10
+
8
11
  STRIP_OPTION =
9
12
  option(:strip, :all, Array, 'List of extra markers to strip: '\
10
13
  '`:comments`, '\
@@ -5,6 +5,9 @@ class ImageOptim
5
5
  class Worker
6
6
  # https://github.com/danielgtaylor/jpeg-archive#jpeg-recompress
7
7
  class Jpegrecompress < Worker
8
+ ALLOW_LOSSY_OPTION =
9
+ option(:allow_lossy, false, 'Allow worker, it is always lossy'){ |v| !!v }
10
+
8
11
  # Initialize only if allow_lossy
9
12
  def self.init(image_optim, options = {})
10
13
  super if options[:allow_lossy]
@@ -6,6 +6,9 @@ class ImageOptim
6
6
  class Worker
7
7
  # http://pngquant.org/
8
8
  class Pngquant < Worker
9
+ ALLOW_LOSSY_OPTION =
10
+ option(:allow_lossy, false, 'Allow quality option'){ |v| !!v }
11
+
9
12
  QUALITY_OPTION =
10
13
  option(:quality, '`100..100`, `0..100` in lossy mode',
11
14
  NonNegativeIntegerRange, 'min..max - don\'t '\
@@ -80,7 +80,7 @@ Process.times.class.class_eval do
80
80
  end
81
81
  end
82
82
 
83
- ImageOptim::ImagePath.class_eval do
83
+ ImageOptim::Path.class_eval do
84
84
  def shellescape
85
85
  to_s.shellescape
86
86
  end
@@ -192,7 +192,8 @@ class Analyser
192
192
  time = Process.times.sum - start
193
193
 
194
194
  dst_size = success ? dst.size : nil
195
- cache = (success ? dst : src).digest.sub(/../, '\0/') + ".#{src.format}"
195
+ digest = (success ? dst : src).digest
196
+ cache = digest.sub(/../, '\0/') + ".#{src.image_format}"
196
197
  result = new(worker.id, success, time, src.size, dst_size, cache)
197
198
  if success
198
199
  path = result.path
@@ -209,7 +210,7 @@ class Analyser
209
210
  end
210
211
 
211
212
  def path
212
- ImageOptim::ImagePath.convert("#{DIR}/worker-analysis/#{cache}")
213
+ ImageOptim::Path.convert("#{DIR}/worker-analysis/#{cache}")
213
214
  end
214
215
 
215
216
  def inspect
@@ -251,7 +252,7 @@ class Analyser
251
252
  # Run all possible worker chains
252
253
  class WorkerRunner
253
254
  def initialize(path, workers)
254
- @path = ImageOptim::ImagePath.convert(path)
255
+ @path = ImageOptim::Path.convert(path)
255
256
  @workers = workers
256
257
  end
257
258
 
@@ -281,7 +282,7 @@ class Analyser
281
282
  worker_result, result_image = run_worker(src, worker)
282
283
 
283
284
  steps = (last_result ? last_result.steps : []) + [worker_result]
284
- chain_result = ChainResult.new(src.format, steps)
285
+ chain_result = ChainResult.new(src.image_format, steps)
285
286
  chain_result.difference = difference_with(result_image)
286
287
 
287
288
  yield chain_result
@@ -337,7 +338,7 @@ class Analyser
337
338
 
338
339
  def flatten_animation(image)
339
340
  run_cache[:flatten][image.digest] ||= begin
340
- if image.format == :gif
341
+ if image.image_format == :gif
341
342
  flattened = image.temp_path
342
343
  Cmd.run(*%W[
343
344
  convert
@@ -535,10 +536,10 @@ private
535
536
  end
536
537
 
537
538
  def process_paths(paths)
538
- paths = paths.map{ |path| ImageOptim::ImagePath.convert(path) }
539
+ paths = paths.map{ |path| ImageOptim::Path.convert(path) }
539
540
  paths.select!{ |path| path.exist? || warn("#{path} doesn't exits") }
540
541
  paths.select!{ |path| path.file? || warn("#{path} is not a file") }
541
- paths.select!{ |path| path.format || warn("#{path} is not an image") }
542
+ paths.select!{ |path| path.image_format || warn("#{path} is not an image") }
542
543
  paths.select! do |path|
543
544
  workers_for_image(path) || warn("#{path} can't be handled by any worker")
544
545
  end
@@ -546,7 +547,7 @@ private
546
547
  end
547
548
 
548
549
  def workers_for_image(path)
549
- @workers_by_format[ImageOptim::ImagePath.convert(path).format]
550
+ @workers_by_format[ImageOptim::Path.convert(path).image_format]
550
551
  end
551
552
 
552
553
  def log_workers_by_format