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
  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 'image_optim/option_helpers'
3
5
  require 'image_optim/true_false_nil'
@@ -37,8 +39,8 @@ class ImageOptim
37
39
  #{dst}
38
40
  ]
39
41
  args.unshift "-i#{interlace ? 1 : 0}" unless interlace.nil?
40
- if resolve_bin!(:optipng).version >= '0.7'
41
- args.unshift '-strip', 'all' if strip
42
+ if strip && resolve_bin!(:optipng).version >= '0.7'
43
+ args.unshift '-strip', 'all'
42
44
  end
43
45
  execute(:optipng, *args) && optimized?(src, dst)
44
46
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'image_optim/worker'
2
4
 
3
5
  class ImageOptim
@@ -37,8 +39,8 @@ class ImageOptim
37
39
  end
38
40
  flags.push '-fix' if fix
39
41
  flags.push '-brute' if brute
40
- if resolve_bin!(:pngcrush).version >= '1.7.38'
41
- flags.push '-blacken' if blacken
42
+ if blacken && resolve_bin!(:pngcrush).version >= '1.7.38'
43
+ flags.push '-blacken'
42
44
  end
43
45
 
44
46
  args = flags + %W[
@@ -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
 
@@ -35,6 +37,7 @@ class ImageOptim
35
37
  rescue SignalException => e
36
38
  raise unless Signal.list.key(e.signo) == 'SEGV'
37
39
  raise unless resolve_bin!(:pngout).version <= '20150920'
40
+
38
41
  warn "pngout caused Segmentation fault for #{src}"
39
42
  end
40
43
  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
  require 'image_optim/non_negative_integer_range'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'image_optim/worker'
2
4
 
3
5
  class ImageOptim
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env ruby
2
2
  # encoding: UTF-8
3
+ # frozen_string_literal: true
3
4
 
4
5
  require 'bundler/setup'
5
6
 
6
7
  require 'image_optim'
7
8
 
8
9
  README_FILE = File.expand_path('../../README.markdown', __FILE__)
9
- BEGIN_MARKER = '<!---<worker-options>-->'.freeze
10
- END_MARKER = '<!---</worker-options>-->'.freeze
10
+ BEGIN_MARKER = '<!---<worker-options>-->'
11
+ END_MARKER = '<!---</worker-options>-->'
11
12
 
12
13
  def write_worker_options(io, klass)
13
14
  io.puts "### #{klass.bin_sym}:"
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # encoding: UTF-8
3
+ # frozen_string_literal: true
3
4
 
4
5
  require 'bundler/setup'
5
6
 
@@ -12,7 +13,7 @@ require 'digest'
12
13
  require 'erb'
13
14
  require 'ostruct'
14
15
 
15
- DIR = 'tmp'.freeze
16
+ DIR = 'tmp'
16
17
  Pathname(DIR).mkpath
17
18
 
18
19
  Array.class_eval do
@@ -118,8 +119,10 @@ class Analyser
118
119
  def get!(key, etag)
119
120
  raw = DB[Marshal.dump(key)]
120
121
  return unless raw
122
+
121
123
  entry = Marshal.load(raw)
122
124
  return unless entry[1] == etag
125
+
123
126
  entry[0]
124
127
  end
125
128
 
@@ -134,13 +137,14 @@ class Analyser
134
137
  # Delegate to worker with short id
135
138
  class WorkerVariant < DelegateClass(ImageOptim::Worker)
136
139
  attr_reader :name, :id, :cons_id, :required
140
+
137
141
  def initialize(klass, image_optim, options)
138
142
  @required = options.delete(:required)
139
143
  @run_order = options.delete(:run_order)
140
144
  allow_consecutive_on = Array(options.delete(:allow_consecutive_on))
141
145
  @image_optim = image_optim
142
146
  @name = klass.bin_sym.to_s + options_string(options)
143
- __setobj__(klass.new(image_optim, options))
147
+ super(klass.new(image_optim, options))
144
148
  @id = klass.bin_sym.to_s + options_string(self.options)
145
149
  @cons_id = [klass, allow_consecutive_on.map{ |key| [key, send(key)] }]
146
150
  end
@@ -175,6 +179,7 @@ class Analyser
175
179
 
176
180
  def options_string(options)
177
181
  return '' if options.empty?
182
+
178
183
  "(#{options.sort.map{ |k, v| "#{k}:#{v.inspect}" }.join(', ')})"
179
184
  end
180
185
  end
@@ -284,6 +289,7 @@ class Analyser
284
289
  required_workers = workers.select(&:required)
285
290
  with_progress(workers, last_result) do |worker|
286
291
  next if required_workers.any?{ |w| w.run_order < worker.run_order }
292
+
287
293
  worker_result, result_image = run_worker(src, worker)
288
294
 
289
295
  steps = (last_result ? last_result.steps : []) + [worker_result]
@@ -329,6 +335,7 @@ class Analyser
329
335
  unless $CHILD_STATUS.success?
330
336
  fail "failed comparison of #{@path} with #{other}"
331
337
  end
338
+
332
339
  nrmse
333
340
  end
334
341
  end
@@ -446,6 +453,7 @@ class Analyser
446
453
  attr_reader :name
447
454
  attr_reader :success_count
448
455
  attr_reader :time, :avg_time
456
+
449
457
  def initialize(name, steps)
450
458
  @name = name
451
459
  @success_count = steps.count(&:success)
@@ -454,11 +462,12 @@ class Analyser
454
462
  end
455
463
 
456
464
  def unused?
457
- success_count.zero?
465
+ success_count == 0
458
466
  end
459
467
  end
460
468
 
461
469
  attr_reader :name, :results, :ids2names
470
+
462
471
  def initialize(name, results, ids2names)
463
472
  @name = name.to_s
464
473
  @results = results
@@ -482,6 +491,7 @@ class Analyser
482
491
  ImageOptim::Worker.klasses.each do |klass|
483
492
  worker_config = config.delete(klass.bin_sym)
484
493
  next if worker_config == false
494
+
485
495
  worker_config ||= {}
486
496
 
487
497
  option_variants = worker_config.delete(:variants) || [{}]
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
  require 'image_optim/bin_resolver/comparable_condition'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
  require 'image_optim/bin_resolver/simple_version'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
  require 'image_optim/bin_resolver'
3
5
  require 'image_optim/cmd'
@@ -278,9 +280,9 @@ describe ImageOptim::BinResolver do
278
280
  bin = Bin.new(:advpng, '/bin/advpng')
279
281
 
280
282
  expect(Bin).to receive(:new).and_return(bin)
281
- allow(bin).to receive(:version).and_return(SimpleVersion.new('1.15'))
283
+ allow(bin).to receive(:version).and_return(SimpleVersion.new('none'))
282
284
 
283
- expect(bin).to receive(:warn).once.with(match(/does not use zopfli/))
285
+ expect(bin).to receive(:warn).once.with(match(/is of unknown version/))
284
286
 
285
287
  5.times do
286
288
  resolver.resolve!(:pngcrush)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
  require 'image_optim/cache_path'
3
5
  require 'tempfile'
@@ -6,52 +8,95 @@ describe ImageOptim::CachePath do
6
8
  include CapabilityCheckHelpers
7
9
 
8
10
  before do
11
+ stub_const('Path', ImageOptim::Path)
9
12
  stub_const('CachePath', ImageOptim::CachePath)
10
13
  end
11
14
 
12
15
  describe '#replace' do
13
- let(:src){ CachePath.temp_file_path }
14
- let(:dst){ CachePath.temp_file_path }
16
+ let(:src_dir){ Path.temp_dir }
17
+ let(:src){ CachePath.temp_file_path(nil, src_dir) }
18
+ let(:dst){ Path.temp_file_path }
15
19
 
16
- it 'moves data to destination' do
17
- src.write('src')
20
+ shared_examples 'replaces file' do
21
+ it 'moves data to destination' do
22
+ src.write('src')
18
23
 
19
- src.replace(dst)
24
+ src.replace(dst)
20
25
 
21
- expect(dst.read).to eq('src')
22
- end
26
+ expect(dst.read).to eq('src')
27
+ end
23
28
 
24
- it 'does not remove original file' do
25
- src.replace(dst)
29
+ it 'does not remove original file' do
30
+ src.replace(dst)
26
31
 
27
- expect(src).to exist
28
- end
32
+ expect(src).to exist
33
+ end
34
+
35
+ it 'preserves attributes of destination file' do
36
+ skip 'full file modes are not support' unless any_file_modes_allowed?
37
+ mode = 0o666
38
+
39
+ dst.chmod(mode)
40
+
41
+ src.replace(dst)
29
42
 
30
- it 'preserves attributes of destination file' do
31
- skip 'full file modes are not support' unless any_file_modes_allowed?
32
- mode = 0o666
43
+ got = dst.stat.mode & 0o777
44
+ expect(got).to eq(mode), format('expected %04o, got %04o', mode, got)
45
+ end
33
46
 
34
- dst.chmod(mode)
47
+ it 'does not preserve mtime of destination file' do
48
+ time = src.mtime
35
49
 
36
- src.replace(dst)
50
+ dst.utime(time - 1000, time - 1000)
37
51
 
38
- got = dst.stat.mode & 0o777
39
- expect(got).to eq(mode), format('expected %04o, got %04o', mode, got)
52
+ src.replace(dst)
53
+
54
+ expect(dst.mtime).to be >= time
55
+ end
56
+
57
+ it 'changes inode of destination' do
58
+ skip 'inodes are not supported' unless inodes_supported?
59
+ expect{ src.replace(dst) }.to change{ dst.stat.ino }
60
+ end
61
+
62
+ it 'is using temporary file with .tmp extension' do
63
+ expect(src).to receive(:copy).with(having_attributes(:extname => '.tmp')).at_least(:once)
64
+
65
+ src.replace(dst)
66
+ end
40
67
  end
41
68
 
42
- it 'does not preserve mtime of destination file' do
43
- time = src.mtime
69
+ context 'when src and dst are on same device' do
70
+ before do
71
+ allow_any_instance_of(File::Stat).to receive(:dev).and_return(0)
72
+ end
44
73
 
45
- dst.utime(time - 1000, time - 1000)
74
+ include_examples 'replaces file'
75
+ end
46
76
 
47
- src.replace(dst)
77
+ context 'when src and dst are on different devices' do
78
+ before do
79
+ allow_any_instance_of(File::Stat).to receive(:dev, &:__id__)
80
+ end
48
81
 
49
- expect(dst.mtime).to be >= time
82
+ include_examples 'replaces file'
50
83
  end
51
84
 
52
- it 'changes inode of destination' do
53
- skip 'inodes are not supported' unless inodes_supported?
54
- expect{ src.replace(dst) }.to change{ dst.stat.ino }
85
+ context 'when src and dst are on same device, but rename causes Errno::EXDEV' do
86
+ before do
87
+ allow_any_instance_of(File::Stat).to receive(:dev).and_return(0)
88
+ allow(described_class).to receive(:temp_file_path).and_call_original
89
+ expect(described_class).to receive(:temp_file_path).
90
+ with([dst.basename.to_s, '.tmp'], src.dirname).
91
+ and_wrap_original do |m, *args, &block|
92
+ m.call(*args) do |tmp|
93
+ expect(tmp).to receive(:rename).with(dst.to_s).and_raise(Errno::EXDEV)
94
+ block.call(tmp)
95
+ end
96
+ end
97
+ end
98
+
99
+ include_examples 'replaces file'
55
100
  end
56
101
  end
57
102
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
  require 'fspath'
3
5
  require 'image_optim/cache'
@@ -62,6 +64,7 @@ describe ImageOptim::Cache do
62
64
  end
63
65
  end
64
66
 
67
+ # rubocop:disable Style/RedundantFetchBlock
65
68
  shared_examples 'an enabled cache' do
66
69
  context 'when cached file does not exist' do
67
70
  describe :fetch do
@@ -114,6 +117,7 @@ describe ImageOptim::Cache do
114
117
  end
115
118
  end
116
119
  end
120
+ # rubocop:enable Style/RedundantFetchBlock
117
121
 
118
122
  context 'when cache is enabled (without worker digests)' do
119
123
  let(:image_optim) do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
  require 'image_optim/cmd'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
  require 'image_optim/config'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
  require 'image_optim/handler'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
  require 'image_optim/hash_helpers'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'image_optim/image_meta'
2
4
 
3
5
  describe ImageOptim::ImageMeta do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'image_optim/optimized_path'
2
4
 
3
5
  describe ImageOptim::OptimizedPath do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
  require 'image_optim/option_definition'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
  require 'image_optim/option_helpers'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
  require 'image_optim/path'
3
5
  require 'tempfile'
@@ -59,45 +61,84 @@ describe ImageOptim::Path do
59
61
  let(:src){ Path.temp_file_path }
60
62
  let(:dst){ Path.temp_file_path }
61
63
 
62
- it 'moves data to destination' do
63
- src.write('src')
64
+ shared_examples 'replaces file' do
65
+ it 'moves data to destination' do
66
+ src.write('src')
64
67
 
65
- src.replace(dst)
68
+ src.replace(dst)
66
69
 
67
- expect(dst.read).to eq('src')
68
- end
70
+ expect(dst.read).to eq('src')
71
+ end
69
72
 
70
- it 'removes original file' do
71
- src.replace(dst)
73
+ it 'removes original file' do
74
+ src.replace(dst)
72
75
 
73
- expect(src).to_not exist
74
- end
76
+ expect(src).to_not exist
77
+ end
78
+
79
+ it 'preserves attributes of destination file' do
80
+ skip 'full file modes are not support' unless any_file_modes_allowed?
81
+ mode = 0o666
82
+
83
+ dst.chmod(mode)
84
+
85
+ src.replace(dst)
86
+
87
+ got = dst.stat.mode & 0o777
88
+ expect(got).to eq(mode), format('expected %04o, got %04o', mode, got)
89
+ end
90
+
91
+ it 'does not preserve mtime of destination file' do
92
+ time = src.mtime
93
+
94
+ dst.utime(time - 1000, time - 1000)
75
95
 
76
- it 'preserves attributes of destination file' do
77
- skip 'full file modes are not support' unless any_file_modes_allowed?
78
- mode = 0o666
96
+ src.replace(dst)
79
97
 
80
- dst.chmod(mode)
98
+ expect(dst.mtime).to be >= time
99
+ end
100
+
101
+ it 'changes inode of destination' do
102
+ skip 'inodes are not supported' unless inodes_supported?
103
+ expect{ src.replace(dst) }.to change{ dst.stat.ino }
104
+ end
105
+ end
81
106
 
82
- src.replace(dst)
107
+ context 'when src and dst are on same device' do
108
+ before do
109
+ allow_any_instance_of(File::Stat).to receive(:dev).and_return(0)
110
+ end
83
111
 
84
- got = dst.stat.mode & 0o777
85
- expect(got).to eq(mode), format('expected %04o, got %04o', mode, got)
112
+ include_examples 'replaces file'
86
113
  end
87
114
 
88
- it 'does not preserve mtime of destination file' do
89
- time = src.mtime
115
+ context 'when src and dst are on different devices' do
116
+ before do
117
+ allow_any_instance_of(File::Stat).to receive(:dev, &:__id__)
118
+ end
90
119
 
91
- dst.utime(time - 1000, time - 1000)
120
+ include_examples 'replaces file'
92
121
 
93
- src.replace(dst)
122
+ it 'is using temporary file with .tmp extension' do
123
+ expect(src).to receive(:move).with(having_attributes(:extname => '.tmp'))
94
124
 
95
- expect(dst.mtime).to be >= time
125
+ src.replace(dst)
126
+ end
96
127
  end
97
128
 
98
- it 'changes inode of destination' do
99
- skip 'inodes are not supported' unless inodes_supported?
100
- expect{ src.replace(dst) }.to change{ dst.stat.ino }
129
+ context 'when src and dst are on same device, but rename causes Errno::EXDEV' do
130
+ before do
131
+ allow_any_instance_of(File::Stat).to receive(:dev).and_return(0)
132
+ expect(src).to receive(:rename).with(dst.to_s).once.and_raise(Errno::EXDEV)
133
+ end
134
+
135
+ include_examples 'replaces file'
136
+
137
+ it 'is using temporary file with .tmp extension' do
138
+ expect(src).to receive(:move).with(having_attributes(:extname => '.tmp'))
139
+
140
+ src.replace(dst)
141
+ end
101
142
  end
102
143
  end
103
144
  end