image_optim 0.26.5 → 0.30.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.appveyor.yml +2 -0
  3. data/.pre-commit-hooks.yaml +9 -0
  4. data/.rubocop.yml +33 -14
  5. data/.travis.yml +17 -15
  6. data/CHANGELOG.markdown +26 -0
  7. data/CONTRIBUTING.markdown +4 -1
  8. data/LICENSE.txt +1 -1
  9. data/README.markdown +9 -3
  10. data/Vagrantfile +1 -1
  11. data/image_optim.gemspec +7 -4
  12. data/lib/image_optim.rb +15 -9
  13. data/lib/image_optim/bin_resolver/bin.rb +10 -8
  14. data/lib/image_optim/bin_resolver/comparable_condition.rb +1 -0
  15. data/lib/image_optim/cache.rb +6 -0
  16. data/lib/image_optim/cache_path.rb +19 -2
  17. data/lib/image_optim/cmd.rb +45 -6
  18. data/lib/image_optim/config.rb +13 -7
  19. data/lib/image_optim/elapsed_time.rb +26 -0
  20. data/lib/image_optim/errors.rb +9 -0
  21. data/lib/image_optim/optimized_path.rb +1 -1
  22. data/lib/image_optim/path.rb +29 -6
  23. data/lib/image_optim/runner/option_parser.rb +6 -0
  24. data/lib/image_optim/timer.rb +25 -0
  25. data/lib/image_optim/worker.rb +29 -26
  26. data/lib/image_optim/worker/advpng.rb +2 -2
  27. data/lib/image_optim/worker/class_methods.rb +2 -0
  28. data/lib/image_optim/worker/gifsicle.rb +3 -3
  29. data/lib/image_optim/worker/jhead.rb +2 -2
  30. data/lib/image_optim/worker/jpegoptim.rb +8 -6
  31. data/lib/image_optim/worker/jpegrecompress.rb +17 -2
  32. data/lib/image_optim/worker/jpegtran.rb +3 -3
  33. data/lib/image_optim/worker/optipng.rb +4 -4
  34. data/lib/image_optim/worker/pngcrush.rb +4 -4
  35. data/lib/image_optim/worker/pngout.rb +2 -2
  36. data/lib/image_optim/worker/pngquant.rb +3 -2
  37. data/lib/image_optim/worker/svgo.rb +2 -2
  38. data/script/update_worker_options_in_readme +1 -1
  39. data/script/worker_analysis +20 -19
  40. data/spec/image_optim/bin_resolver_spec.rb +5 -5
  41. data/spec/image_optim/cache_path_spec.rb +67 -28
  42. data/spec/image_optim/cache_spec.rb +10 -8
  43. data/spec/image_optim/cmd_spec.rb +58 -6
  44. data/spec/image_optim/config_spec.rb +36 -20
  45. data/spec/image_optim/elapsed_time_spec.rb +14 -0
  46. data/spec/image_optim/hash_helpers_spec.rb +18 -18
  47. data/spec/image_optim/option_definition_spec.rb +6 -6
  48. data/spec/image_optim/path_spec.rb +61 -26
  49. data/spec/image_optim/runner/option_parser_spec.rb +4 -4
  50. data/spec/image_optim/timer_spec.rb +32 -0
  51. data/spec/image_optim/worker/jpegrecompress_spec.rb +32 -0
  52. data/spec/image_optim/worker/optipng_spec.rb +11 -11
  53. data/spec/image_optim/worker/pngquant_spec.rb +5 -5
  54. data/spec/image_optim/worker_spec.rb +17 -17
  55. data/spec/image_optim_spec.rb +50 -12
  56. data/spec/images/quant/generate +2 -2
  57. data/spec/spec_helper.rb +16 -15
  58. metadata +36 -12
@@ -12,7 +12,7 @@ class ImageOptim
12
12
  return super if options.key?(:interlace)
13
13
 
14
14
  [false, true].map do |interlace|
15
- new(image_optim, options.merge(:interlace => interlace))
15
+ new(image_optim, options.merge(interlace: interlace))
16
16
  end
17
17
  end
18
18
 
@@ -37,7 +37,7 @@ class ImageOptim
37
37
  CAREFUL_OPTION =
38
38
  option(:careful, false, 'Avoid bugs with some software'){ |v| !!v }
39
39
 
40
- def optimize(src, dst)
40
+ def optimize(src, dst, options = {})
41
41
  args = %W[
42
42
  --output=#{dst}
43
43
  --no-comments
@@ -58,7 +58,7 @@ class ImageOptim
58
58
  end
59
59
  args.unshift '--careful' if careful
60
60
  args.unshift "--optimize=#{level}" if level
61
- execute(:gifsicle, *args) && optimized?(src, dst)
61
+ execute(:gifsicle, args, options) && optimized?(src, dst)
62
62
  end
63
63
  end
64
64
  end
@@ -25,7 +25,7 @@ class ImageOptim
25
25
  [:jhead, :jpegtran]
26
26
  end
27
27
 
28
- def optimize(src, dst)
28
+ def optimize(src, dst, options = {})
29
29
  return false unless oriented?(src)
30
30
 
31
31
  src.copy(dst)
@@ -34,7 +34,7 @@ class ImageOptim
34
34
  #{dst}
35
35
  ]
36
36
  resolve_bin!(:jpegtran)
37
- execute(:jhead, *args) && dst.size?
37
+ execute(:jhead, args, options) && dst.size?
38
38
  end
39
39
 
40
40
  private
@@ -11,14 +11,16 @@ class ImageOptim
11
11
  option(:allow_lossy, false, 'Allow limiting maximum quality'){ |v| !!v }
12
12
 
13
13
  STRIP_OPTION =
14
- option(:strip, :all, Array, 'List of extra markers to strip: '\
15
- '`:comments`, '\
14
+ option(:strip, :all, Array, 'List of markers to strip: '\
15
+ '`:com`, '\
16
16
  '`:exif`, '\
17
17
  '`:iptc`, '\
18
- '`:icc` or '\
18
+ '`:icc`, '\
19
+ '`:xmp`, '\
20
+ '`:none` or '\
19
21
  '`:all`') do |v|
20
22
  values = Array(v).map(&:to_s)
21
- known_values = %w[all comments exif iptc icc]
23
+ known_values = %w[com exif iptc icc xmp none all]
22
24
  unknown_values = values - known_values
23
25
  unless unknown_values.empty?
24
26
  warn "Unknown markers for jpegoptim: #{unknown_values.join(', ')}"
@@ -45,7 +47,7 @@ class ImageOptim
45
47
  max_quality < 100 ? -1 : 0
46
48
  end
47
49
 
48
- def optimize(src, dst)
50
+ def optimize(src, dst, options = {})
49
51
  src.copy(dst)
50
52
  args = %W[
51
53
  --quiet
@@ -56,7 +58,7 @@ class ImageOptim
56
58
  args.unshift "--strip-#{strip_marker}"
57
59
  end
58
60
  args.unshift "--max=#{max_quality}" if max_quality < 100
59
- execute(:jpegoptim, *args) && optimized?(src, dst)
61
+ execute(:jpegoptim, args, options) && optimized?(src, dst)
60
62
  end
61
63
  end
62
64
  end
@@ -26,6 +26,20 @@ class ImageOptim
26
26
  OptionHelpers.limit_with_range(v.to_i, 0...QUALITY_NAMES.length)
27
27
  end
28
28
 
29
+ METHOD_OPTION =
30
+ option(:method, 'ssim', 'Comparison Metric: '\
31
+ '`mpe` - Mean pixel error, '\
32
+ '`ssim` - Structural similarity, '\
33
+ '`ms-ssim` - Multi-scale structural similarity (slow!), '\
34
+ '`smallfry` - Linear-weighted BBCQ-like (may be patented)') do |v, opt_def|
35
+ if %w[mpe ssim ms-ssim smallfry].include? v
36
+ v
37
+ else
38
+ warn "Unknown method for jpegrecompress: #{v}"
39
+ opt_def.default
40
+ end
41
+ end
42
+
29
43
  def used_bins
30
44
  [:'jpeg-recompress']
31
45
  end
@@ -35,14 +49,15 @@ class ImageOptim
35
49
  -5
36
50
  end
37
51
 
38
- def optimize(src, dst)
52
+ def optimize(src, dst, options = {})
39
53
  args = %W[
40
54
  --quality #{QUALITY_NAMES[quality]}
55
+ --method #{method}
41
56
  --no-copy
42
57
  #{src}
43
58
  #{dst}
44
59
  ]
45
- execute(:'jpeg-recompress', *args) && optimized?(src, dst)
60
+ execute(:'jpeg-recompress', args, options) && optimized?(src, dst)
46
61
  end
47
62
  end
48
63
  end
@@ -23,7 +23,7 @@ class ImageOptim
23
23
  jpegrescan ? [:jpegtran, :jpegrescan] : [:jpegtran]
24
24
  end
25
25
 
26
- def optimize(src, dst)
26
+ def optimize(src, dst, options = {})
27
27
  if jpegrescan
28
28
  args = %W[
29
29
  #{src}
@@ -31,7 +31,7 @@ class ImageOptim
31
31
  ]
32
32
  args.unshift '-s' unless copy_chunks
33
33
  resolve_bin!(:jpegtran)
34
- execute(:jpegrescan, *args) && optimized?(src, dst)
34
+ execute(:jpegrescan, args, options) && optimized?(src, dst)
35
35
  else
36
36
  args = %W[
37
37
  -optimize
@@ -40,7 +40,7 @@ class ImageOptim
40
40
  ]
41
41
  args.unshift '-copy', (copy_chunks ? 'all' : 'none')
42
42
  args.unshift '-progressive' if progressive
43
- execute(:jpegtran, *args) && optimized?(src, dst)
43
+ execute(:jpegtran, args, options) && optimized?(src, dst)
44
44
  end
45
45
  end
46
46
  end
@@ -30,7 +30,7 @@ class ImageOptim
30
30
  -4
31
31
  end
32
32
 
33
- def optimize(src, dst)
33
+ def optimize(src, dst, options = {})
34
34
  src.copy(dst)
35
35
  args = %W[
36
36
  -o #{level}
@@ -39,10 +39,10 @@ class ImageOptim
39
39
  #{dst}
40
40
  ]
41
41
  args.unshift "-i#{interlace ? 1 : 0}" unless interlace.nil?
42
- if resolve_bin!(:optipng).version >= '0.7'
43
- args.unshift '-strip', 'all' if strip
42
+ if strip && resolve_bin!(:optipng).version >= '0.7'
43
+ args.unshift '-strip', 'all'
44
44
  end
45
- execute(:optipng, *args) && optimized?(src, dst)
45
+ execute(:optipng, args, options) && optimized?(src, dst)
46
46
  end
47
47
 
48
48
  def optimized?(src, dst)
@@ -28,7 +28,7 @@ class ImageOptim
28
28
  -6
29
29
  end
30
30
 
31
- def optimize(src, dst)
31
+ def optimize(src, dst, options = {})
32
32
  flags = %w[
33
33
  -reduce
34
34
  -cc
@@ -39,8 +39,8 @@ class ImageOptim
39
39
  end
40
40
  flags.push '-fix' if fix
41
41
  flags.push '-brute' if brute
42
- if resolve_bin!(:pngcrush).version >= '1.7.38'
43
- flags.push '-blacken' if blacken
42
+ if blacken && resolve_bin!(:pngcrush).version >= '1.7.38'
43
+ flags.push '-blacken'
44
44
  end
45
45
 
46
46
  args = flags + %W[
@@ -49,7 +49,7 @@ class ImageOptim
49
49
  #{dst}
50
50
  ]
51
51
 
52
- execute(:pngcrush, *args) && optimized?(src, dst)
52
+ execute(:pngcrush, args, options) && optimized?(src, dst)
53
53
  end
54
54
  end
55
55
  end
@@ -24,7 +24,7 @@ class ImageOptim
24
24
  2
25
25
  end
26
26
 
27
- def optimize(src, dst)
27
+ def optimize(src, dst, options = {})
28
28
  args = %W[
29
29
  -k#{copy_chunks ? 1 : 0}
30
30
  -s#{strategy}
@@ -33,7 +33,7 @@ class ImageOptim
33
33
  #{src}
34
34
  #{dst}
35
35
  ]
36
- execute(:pngout, *args) && optimized?(src, dst)
36
+ execute(:pngout, args, options) && optimized?(src, dst)
37
37
  rescue SignalException => e
38
38
  raise unless Signal.list.key(e.signo) == 'SEGV'
39
39
  raise unless resolve_bin!(:pngout).version <= '20150920'
@@ -50,17 +50,18 @@ class ImageOptim
50
50
  -2
51
51
  end
52
52
 
53
- def optimize(src, dst)
53
+ def optimize(src, dst, options = {})
54
54
  args = %W[
55
55
  --quality=#{quality.begin}-#{quality.end}
56
56
  --speed=#{speed}
57
57
  --output=#{dst}
58
+ --skip-if-larger
58
59
  --force
59
60
  #{max_colors}
60
61
  --
61
62
  #{src}
62
63
  ]
63
- execute(:pngquant, *args) && optimized?(src, dst)
64
+ execute(:pngquant, args, options) && optimized?(src, dst)
64
65
  end
65
66
  end
66
67
  end
@@ -16,7 +16,7 @@ class ImageOptim
16
16
  Array(v).map(&:to_s)
17
17
  end
18
18
 
19
- def optimize(src, dst)
19
+ def optimize(src, dst, options = {})
20
20
  args = %W[
21
21
  --input #{src}
22
22
  --output #{dst}
@@ -27,7 +27,7 @@ class ImageOptim
27
27
  enable_plugins.each do |plugin_name|
28
28
  args.unshift "--enable=#{plugin_name}"
29
29
  end
30
- execute(:svgo, *args) && optimized?(src, dst)
30
+ execute(:svgo, args, options) && optimized?(src, dst)
31
31
  end
32
32
  end
33
33
  end
@@ -18,7 +18,7 @@ def write_worker_options(io, klass)
18
18
  klass.option_definitions.each do |option_definition|
19
19
  line = "* `:#{option_definition.name}` — #{option_definition.description}"
20
20
  unless line['(defaults']
21
- line << " *(defaults to #{option_definition.default_description})*"
21
+ line += " *(defaults to #{option_definition.default_description})*"
22
22
  end
23
23
  io.puts line
24
24
  end
@@ -137,13 +137,14 @@ class Analyser
137
137
  # Delegate to worker with short id
138
138
  class WorkerVariant < DelegateClass(ImageOptim::Worker)
139
139
  attr_reader :name, :id, :cons_id, :required
140
+
140
141
  def initialize(klass, image_optim, options)
141
142
  @required = options.delete(:required)
142
143
  @run_order = options.delete(:run_order)
143
144
  allow_consecutive_on = Array(options.delete(:allow_consecutive_on))
144
145
  @image_optim = image_optim
145
146
  @name = klass.bin_sym.to_s + options_string(options)
146
- __setobj__(klass.new(image_optim, options))
147
+ super(klass.new(image_optim, options))
147
148
  @id = klass.bin_sym.to_s + options_string(self.options)
148
149
  @cons_id = [klass, allow_consecutive_on.map{ |key| [key, send(key)] }]
149
150
  end
@@ -357,20 +358,18 @@ class Analyser
357
358
  end
358
359
 
359
360
  def flatten_animation(image)
360
- run_cache[:flatten][image.digest] ||= begin
361
- if image.image_format == :gif
362
- flattened = image.temp_path
363
- Cmd.run(*%W[
364
- convert
365
- #{image.image_format}:#{image}
366
- -coalesce
367
- -append
368
- #{image.image_format}:#{flattened}
369
- ]) || fail("failed flattening of #{image}")
370
- flattened
371
- else
372
- image
373
- end
361
+ run_cache[:flatten][image.digest] ||= if image.image_format == :gif
362
+ flattened = image.temp_path
363
+ Cmd.run(*%W[
364
+ convert
365
+ #{image.image_format}:#{image}
366
+ -coalesce
367
+ -append
368
+ #{image.image_format}:#{flattened}
369
+ ]) || fail("failed flattening of #{image}")
370
+ flattened
371
+ else
372
+ image
374
373
  end
375
374
  end
376
375
 
@@ -452,6 +451,7 @@ class Analyser
452
451
  attr_reader :name
453
452
  attr_reader :success_count
454
453
  attr_reader :time, :avg_time
454
+
455
455
  def initialize(name, steps)
456
456
  @name = name
457
457
  @success_count = steps.count(&:success)
@@ -465,6 +465,7 @@ class Analyser
465
465
  end
466
466
 
467
467
  attr_reader :name, :results, :ids2names
468
+
468
469
  def initialize(name, results, ids2names)
469
470
  @name = name.to_s
470
471
  @results = results
@@ -524,10 +525,10 @@ class Analyser
524
525
  stats = Stats.new('all', by_format[format], worker_ids2names)
525
526
  path = FSPath("#{DIR}/#{basenames[format]}")
526
527
  model = {
527
- :stats_format => format,
528
- :stats => stats,
529
- :format_links => basenames,
530
- :template_dir => template_path.dirname.relative_path_from(path.dirname),
528
+ stats_format: format,
529
+ stats: stats,
530
+ format_links: basenames,
531
+ template_dir: template_path.dirname.relative_path_from(path.dirname),
531
532
  }
532
533
  html = template.result(OpenStruct.new(model).instance_eval{ binding })
533
534
  path.write(html)
@@ -19,7 +19,7 @@ describe ImageOptim::BinResolver do
19
19
  allow(ENV).to receive(:[]).and_call_original
20
20
  end
21
21
 
22
- let(:image_optim){ double(:image_optim, :verbose => false, :pack => false) }
22
+ let(:image_optim){ double(:image_optim, verbose: false, pack: false) }
23
23
  let(:resolver){ BinResolver.new(image_optim) }
24
24
 
25
25
  describe '#full_path' do
@@ -130,7 +130,7 @@ describe ImageOptim::BinResolver do
130
130
  it 'resolves bin specified in ENV' do
131
131
  path = 'bin/the_optimizer'
132
132
  stub_env 'THE_OPTIMIZER_BIN', path
133
- tmpdir = double(:tmpdir, :to_str => 'tmpdir')
133
+ tmpdir = double(:tmpdir, to_str: 'tmpdir')
134
134
  symlink = double(:symlink)
135
135
 
136
136
  full_path = File.expand_path(path)
@@ -180,9 +180,9 @@ describe ImageOptim::BinResolver do
180
180
  stub_env 'THE_OPTIMIZER_BIN', path
181
181
  expect(FSPath).not_to receive(:temp_dir)
182
182
  expect(resolver).not_to receive(:at_exit)
183
- allow(File).to receive_messages(:exist? => exist?,
184
- :file? => file?,
185
- :executable? => executable?)
183
+ allow(File).to receive_messages(exist?: exist?,
184
+ file?: file?,
185
+ executable?: executable?)
186
186
  end
187
187
 
188
188
  after do
@@ -5,55 +5,94 @@ require 'image_optim/cache_path'
5
5
  require 'tempfile'
6
6
 
7
7
  describe ImageOptim::CachePath do
8
- include CapabilityCheckHelpers
9
-
10
8
  before do
9
+ stub_const('Path', ImageOptim::Path)
11
10
  stub_const('CachePath', ImageOptim::CachePath)
12
11
  end
13
12
 
14
13
  describe '#replace' do
15
- let(:src){ CachePath.temp_file_path }
16
- let(:dst){ CachePath.temp_file_path }
14
+ let(:src_dir){ Path.temp_dir }
15
+ let(:src){ CachePath.temp_file_path(nil, src_dir) }
16
+ let(:dst){ Path.temp_file_path }
17
17
 
18
- it 'moves data to destination' do
19
- src.write('src')
18
+ shared_examples 'replaces file' do
19
+ it 'moves data to destination' do
20
+ src.write('src')
20
21
 
21
- src.replace(dst)
22
+ src.replace(dst)
22
23
 
23
- expect(dst.read).to eq('src')
24
- end
24
+ expect(dst.read).to eq('src')
25
+ end
25
26
 
26
- it 'does not remove original file' do
27
- src.replace(dst)
27
+ it 'does not remove original file' do
28
+ src.replace(dst)
28
29
 
29
- expect(src).to exist
30
- end
30
+ expect(src).to exist
31
+ end
32
+
33
+ it 'preserves attributes of destination file', skip: SkipConditions[:any_file_mode_allowed] do
34
+ mode = 0o666
35
+
36
+ dst.chmod(mode)
37
+
38
+ src.replace(dst)
31
39
 
32
- it 'preserves attributes of destination file' do
33
- skip 'full file modes are not support' unless any_file_modes_allowed?
34
- mode = 0o666
40
+ got = dst.stat.mode & 0o777
41
+ expect(got).to eq(mode), format('expected %04o, got %04o', mode, got)
42
+ end
35
43
 
36
- dst.chmod(mode)
44
+ it 'does not preserve mtime of destination file' do
45
+ time = src.mtime
37
46
 
38
- src.replace(dst)
47
+ dst.utime(time - 1000, time - 1000)
39
48
 
40
- got = dst.stat.mode & 0o777
41
- expect(got).to eq(mode), format('expected %04o, got %04o', mode, got)
49
+ src.replace(dst)
50
+
51
+ expect(dst.mtime).to be >= time
52
+ end
53
+
54
+ it 'changes inode of destination', skip: SkipConditions[:inodes_support] do
55
+ expect{ src.replace(dst) }.to change{ dst.stat.ino }
56
+ end
57
+
58
+ it 'is using temporary file with .tmp extension' do
59
+ expect(src).to receive(:copy).with(having_attributes(extname: '.tmp')).at_least(:once)
60
+
61
+ src.replace(dst)
62
+ end
42
63
  end
43
64
 
44
- it 'does not preserve mtime of destination file' do
45
- time = src.mtime
65
+ context 'when src and dst are on same device' do
66
+ before do
67
+ allow_any_instance_of(File::Stat).to receive(:dev).and_return(0)
68
+ end
46
69
 
47
- dst.utime(time - 1000, time - 1000)
70
+ include_examples 'replaces file'
71
+ end
48
72
 
49
- src.replace(dst)
73
+ context 'when src and dst are on different devices' do
74
+ before do
75
+ allow_any_instance_of(File::Stat).to receive(:dev, &:__id__)
76
+ end
50
77
 
51
- expect(dst.mtime).to be >= time
78
+ include_examples 'replaces file'
52
79
  end
53
80
 
54
- it 'changes inode of destination' do
55
- skip 'inodes are not supported' unless inodes_supported?
56
- expect{ src.replace(dst) }.to change{ dst.stat.ino }
81
+ context 'when src and dst are on same device, but rename causes Errno::EXDEV' do
82
+ before do
83
+ allow_any_instance_of(File::Stat).to receive(:dev).and_return(0)
84
+ allow(described_class).to receive(:temp_file_path).and_call_original
85
+ expect(described_class).to receive(:temp_file_path).
86
+ with([dst.basename.to_s, '.tmp'], src.dirname).
87
+ and_wrap_original do |m, *args, &block|
88
+ m.call(*args) do |tmp|
89
+ expect(tmp).to receive(:rename).with(dst.to_s).and_raise(Errno::EXDEV)
90
+ block.call(tmp)
91
+ end
92
+ end
93
+ end
94
+
95
+ include_examples 'replaces file'
57
96
  end
58
97
  end
59
98
  end