image_optim 0.22.1 → 0.23.0

Sign up to get free protection for your applications and to get access to all the features.
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