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
@@ -6,12 +6,36 @@ describe ImageOptim::Worker do
6
6
  before do
7
7
  stub_const('Worker', ImageOptim::Worker)
8
8
  stub_const('BinResolver', ImageOptim::BinResolver)
9
+
10
+ # don't add to list of wokers
11
+ allow(ImageOptim::Worker).to receive(:inherited)
12
+ end
13
+
14
+ describe '#initialize' do
15
+ it 'expects first argument to be an instanace of ImageOptim' do
16
+ expect do
17
+ Worker.new(double)
18
+ end.to raise_error ArgumentError
19
+ end
20
+ end
21
+
22
+ describe '#options' do
23
+ it 'returns a Hash with options' do
24
+ worker_class = Class.new(Worker) do
25
+ option(:one, 1, 'One')
26
+ option(:two, 2, 'Two')
27
+ option(:three, 3, 'Three')
28
+ end
29
+
30
+ worker = worker_class.new(ImageOptim.new, :three => '...')
31
+
32
+ expect(worker.options).to eq(:one => 1, :two => 2, :three => '...')
33
+ end
9
34
  end
10
35
 
11
- describe :optimize do
36
+ describe '#optimize' do
12
37
  it 'raises NotImplementedError' do
13
- image_optim = ImageOptim.new
14
- worker = Worker.new(image_optim, {})
38
+ worker = Worker.new(ImageOptim.new, {})
15
39
 
16
40
  expect do
17
41
  worker.optimize(double, double)
@@ -19,7 +43,50 @@ describe ImageOptim::Worker do
19
43
  end
20
44
  end
21
45
 
22
- describe :create_all_by_format do
46
+ describe '#image_formats' do
47
+ {
48
+ 'GifOptim' => :gif,
49
+ 'JpegOptim' => :jpeg,
50
+ 'PngOptim' => :png,
51
+ 'SvgOptim' => :svg,
52
+ }.each do |class_name, image_format|
53
+ it "detects if class name contains #{image_format}" do
54
+ worker = stub_const(class_name, Class.new(Worker)).new(ImageOptim.new)
55
+ expect(worker.image_formats).to eq([image_format])
56
+ end
57
+ end
58
+
59
+ it 'fails if class name does not contain known type' do
60
+ worker = stub_const('TiffOptim', Class.new(Worker)).new(ImageOptim.new)
61
+ expect{ worker.image_formats }.to raise_error(/can't guess/)
62
+ end
63
+ end
64
+
65
+ describe '#inspect' do
66
+ it 'returns inspect String containing options' do
67
+ stub_const('DefOptim', Class.new(Worker) do
68
+ option(:one, 1, 'One')
69
+ option(:two, 2, 'Two')
70
+ option(:three, 3, 'Three')
71
+ end)
72
+
73
+ worker = DefOptim.new(ImageOptim.new, :three => '...')
74
+
75
+ expect(worker.inspect).to eq('#<DefOptim @one=1, @two=2, @three="...">')
76
+ end
77
+ end
78
+
79
+ describe '.inherited' do
80
+ it 'adds subclasses to klasses' do
81
+ base_class = Class.new{ extend ImageOptim::Worker::ClassMethods }
82
+ expect(base_class.klasses.to_a).to eq([])
83
+
84
+ worker_class = Class.new(base_class)
85
+ expect(base_class.klasses.to_a).to eq([worker_class])
86
+ end
87
+ end
88
+
89
+ describe '.create_all_by_format' do
23
90
  it 'passes arguments to create_all' do
24
91
  image_optim = double
25
92
  options_proc = proc{ true }
@@ -52,17 +119,21 @@ describe ImageOptim::Worker do
52
119
  end
53
120
  end
54
121
 
55
- describe :create_all do
122
+ describe '.create_all' do
56
123
  def worker_double(override = {})
57
124
  stubs = {:resolve_used_bins! => nil, :run_order => 0}.merge(override)
58
125
  instance_double(Worker, stubs)
59
126
  end
60
127
 
128
+ def worker_class_doubles(workers)
129
+ workers.map{ |worker| class_double(Worker, :init => worker) }
130
+ end
131
+
61
132
  let(:image_optim){ double(:allow_lossy => false) }
62
133
 
63
134
  it 'creates all workers for which options_proc returns true' do
64
135
  workers = Array.new(3){ worker_double }
65
- klasses = workers.map{ |worker| double(:init => worker) }
136
+ klasses = worker_class_doubles(workers)
66
137
  options_proc = proc do |klass|
67
138
  klass == klasses[1] ? {:disable => true} : {}
68
139
  end
@@ -79,7 +150,7 @@ describe ImageOptim::Worker do
79
150
  [worker_double, worker_double, worker_double],
80
151
  worker_double,
81
152
  ]
82
- klasses = workers.map{ |worker| double(:init => worker) }
153
+ klasses = worker_class_doubles(workers)
83
154
 
84
155
  allow(Worker).to receive(:klasses).and_return(klasses)
85
156
 
@@ -98,7 +169,7 @@ describe ImageOptim::Worker do
98
169
  worker
99
170
  end
100
171
  end
101
- let(:klasses){ workers.map{ |worker| double(:init => worker) } }
172
+ let(:klasses){ worker_class_doubles(workers) }
102
173
 
103
174
  before do
104
175
  allow(Worker).to receive(:klasses).and_return(klasses)
@@ -136,12 +207,11 @@ describe ImageOptim::Worker do
136
207
  end
137
208
 
138
209
  it 'orders workers by run_order' do
139
- image_optim = double(:allow_lossy => false)
140
210
  run_orders = [10, -10, 0, 0, 0, 10, -10]
141
211
  workers = run_orders.map do |run_order|
142
212
  worker_double(:run_order => run_order)
143
213
  end
144
- klasses_list = workers.map{ |worker| double(:init => worker) }
214
+ klasses_list = worker_class_doubles(workers)
145
215
 
146
216
  [
147
217
  klasses_list,
@@ -157,13 +227,43 @@ describe ImageOptim::Worker do
157
227
  expect(Worker.create_all(image_optim){ {} }).to eq(expected_order)
158
228
  end
159
229
  end
230
+
231
+ describe 'passing allow_lossy' do
232
+ it 'passes allow_lossy if worker has such attribute' do
233
+ klasses = worker_class_doubles([worker_double, worker_double])
234
+
235
+ allow(Worker).to receive(:klasses).and_return(klasses)
236
+
237
+ klasses[0].send(:attr_reader, :allow_lossy)
238
+ expect(klasses[0]).to receive(:init).
239
+ with(image_optim, hash_including(:allow_lossy))
240
+ expect(klasses[1]).to receive(:init).
241
+ with(image_optim, hash_not_including(:allow_lossy))
242
+
243
+ Worker.create_all(image_optim){ {} }
244
+ end
245
+
246
+ it 'allows overriding per worker' do
247
+ klasses = worker_class_doubles([worker_double, worker_double])
248
+ options_proc = proc do |klass|
249
+ klass == klasses[1] ? {:allow_lossy => :b} : {}
250
+ end
251
+
252
+ allow(Worker).to receive(:klasses).and_return(klasses)
253
+
254
+ klasses.each{ |klass| klass.send(:attr_reader, :allow_lossy) }
255
+ expect(klasses[0]).to receive(:init).
256
+ with(image_optim, hash_including(:allow_lossy => false))
257
+ expect(klasses[1]).to receive(:init).
258
+ with(image_optim, hash_including(:allow_lossy => :b))
259
+
260
+ Worker.create_all(image_optim, &options_proc)
261
+ end
262
+ end
160
263
  end
161
264
 
162
- describe :option do
265
+ describe '.option' do
163
266
  it 'runs option block in context of worker' do
164
- # don't add Abc to list of wokers
165
- allow(ImageOptim::Worker).to receive(:inherited)
166
-
167
267
  stub_const('Abc', Class.new(Worker) do
168
268
  option(:test, 1, 'Test context') do |_v|
169
269
  some_instance_method
@@ -176,9 +276,6 @@ describe ImageOptim::Worker do
176
276
  end
177
277
 
178
278
  it 'returns instance of OptionDefinition' do
179
- # don't add Abc to list of wokers
180
- allow(ImageOptim::Worker).to receive(:inherited)
181
-
182
279
  definition = nil
183
280
  Class.new(Worker) do
184
281
  definition = option(:test, 1, 'Test'){ |v| v }
@@ -5,7 +5,7 @@ require 'tempfile'
5
5
  require 'English'
6
6
 
7
7
  describe ImageOptim do
8
- root_dir = ImageOptim::ImagePath.new(__FILE__).dirname.dirname
8
+ root_dir = ImageOptim::Path.new(__FILE__).dirname.dirname
9
9
  images_dir = root_dir / 'spec/images'
10
10
  test_images = images_dir.glob('**/*.*').freeze
11
11
 
@@ -17,15 +17,8 @@ describe ImageOptim do
17
17
  include helpers
18
18
  extend helpers
19
19
 
20
- matcher :be_in_range do |expected|
21
- match{ |actual| expected.include?(actual) }
22
- end
23
-
24
20
  before do
25
21
  stub_const('Cmd', ImageOptim::Cmd)
26
-
27
- allow(ImageOptim::Config).to receive(:global).and_return({})
28
- allow(ImageOptim::Config).to receive(:local).and_return({})
29
22
  end
30
23
 
31
24
  isolated_options_base = {:skip_missing_workers => false}
@@ -50,7 +43,7 @@ describe ImageOptim do
50
43
  end
51
44
  end
52
45
 
53
- describe :optimize_image do
46
+ describe '#optimize_image' do
54
47
  define :have_same_data_as do |expected|
55
48
  match{ |actual| actual.binread == expected.binread }
56
49
  end
@@ -68,30 +61,28 @@ describe ImageOptim do
68
61
  ['lossless', base_options, 0],
69
62
  ['lossy', base_options.merge(:allow_lossy => true), 0.001],
70
63
  ].each do |type, options, max_difference|
71
- image_optim = ImageOptim.new(options)
72
- describe type do
64
+ it "does it #{type}" do
65
+ image_optim = ImageOptim.new(options)
73
66
  copies = test_images.map{ |image| temp_copy(image) }
74
67
  pairs = image_optim.optimize_images(copies)
75
68
  test_images.zip(*pairs.transpose).each do |original, copy, optimized|
76
- it "optimizes #{original.relative_path_from(root_dir)}" do
77
- expect(copy).to have_same_data_as(original)
78
-
79
- expect(optimized).not_to be_nil
80
- expect(optimized).to be_a(ImageOptim::ImagePath::Optimized)
81
- expect(optimized).to have_size
82
- expect(optimized).to be_smaller_than(original)
83
- expect(optimized).not_to have_same_data_as(original)
84
-
85
- compare_to = rotate_images.include?(original) ? rotated : original
86
- expect(optimized).to be_similar_to(compare_to, max_difference)
87
- end
69
+ expect(copy).to have_same_data_as(original)
70
+
71
+ expect(optimized).not_to be_nil
72
+ expect(optimized).to be_a(ImageOptim::OptimizedPath)
73
+ expect(optimized).to have_size
74
+ expect(optimized).to be_smaller_than(original)
75
+ expect(optimized).not_to have_same_data_as(original)
76
+
77
+ compare_to = rotate_images.include?(original) ? rotated : original
78
+ expect(optimized).to be_similar_to(compare_to, max_difference)
88
79
  end
89
80
  end
90
81
  end
91
82
  end
92
83
 
93
84
  it 'ignores text file' do
94
- original = ImageOptim::ImagePath.new(__FILE__)
85
+ original = ImageOptim::Path.new(__FILE__)
95
86
  copy = temp_copy(original)
96
87
 
97
88
  expect(Tempfile).not_to receive(:new)
@@ -106,27 +97,27 @@ describe ImageOptim do
106
97
  }.each do |type, data|
107
98
  it "ingores broken #{type}" do
108
99
  path = FSPath.temp_file_path
109
- path.write(data)
100
+ path.binwrite(data)
110
101
  expect(ImageOptim::ImageMeta).to receive(:warn)
111
102
  expect(ImageOptim.optimize_image(path)).to be_nil
112
103
  end
113
104
  end
114
105
  end
115
106
 
116
- describe :optimize_image! do
107
+ describe '#optimize_image!' do
117
108
  it 'optimizes image and replaces original' do
118
109
  original = double
119
110
  optimized = double(:original_size => 12_345)
120
111
  optimized_wrap = double
121
112
  image_optim = ImageOptim.new
122
113
 
123
- allow(ImageOptim::ImagePath).to receive(:convert).
114
+ allow(ImageOptim::Path).to receive(:convert).
124
115
  with(original).and_return(original)
125
116
 
126
117
  expect(image_optim).to receive(:optimize_image).
127
118
  with(original).and_return(optimized)
128
119
  expect(optimized).to receive(:replace).with(original)
129
- expect(ImageOptim::ImagePath::Optimized).to receive(:new).
120
+ expect(ImageOptim::OptimizedPath).to receive(:new).
130
121
  with(original, 12_345).and_return(optimized_wrap)
131
122
 
132
123
  expect(image_optim.optimize_image!(original)).to eq(optimized_wrap)
@@ -136,18 +127,18 @@ describe ImageOptim do
136
127
  original = double
137
128
  image_optim = ImageOptim.new
138
129
 
139
- allow(ImageOptim::ImagePath).to receive(:convert).
130
+ allow(ImageOptim::Path).to receive(:convert).
140
131
  with(original).and_return(original)
141
132
 
142
133
  expect(image_optim).to receive(:optimize_image).
143
134
  with(original).and_return(nil)
144
- expect(ImageOptim::ImagePath::Optimized).not_to receive(:new)
135
+ expect(ImageOptim::OptimizedPath).not_to receive(:new)
145
136
 
146
137
  expect(image_optim.optimize_image!(original)).to eq(nil)
147
138
  end
148
139
  end
149
140
 
150
- describe :optimize_image_data do
141
+ describe '#optimize_image_data' do
151
142
  it 'create temp file, optimizes image and returns data' do
152
143
  data = double
153
144
  temp = double(:path => double)
@@ -155,10 +146,10 @@ describe ImageOptim do
155
146
  optimized_data = double
156
147
  image_optim = ImageOptim.new
157
148
 
158
- allow(ImageOptim::ImageMeta).to receive(:for_data).
159
- with(data).and_return(double(:format => 'xxx'))
149
+ allow(ImageOptim::ImageMeta).to receive(:format_for_data).
150
+ with(data).and_return('xxx')
160
151
 
161
- expect(ImageOptim::ImagePath).to receive(:temp_file).and_yield(temp)
152
+ expect(ImageOptim::Path).to receive(:temp_file).and_yield(temp)
162
153
  expect(temp).to receive(:binmode)
163
154
  expect(temp).to receive(:write).with(data)
164
155
  expect(temp).to receive(:close)
@@ -174,10 +165,10 @@ describe ImageOptim do
174
165
  temp = double(:path => double)
175
166
  image_optim = ImageOptim.new
176
167
 
177
- allow(ImageOptim::ImageMeta).to receive(:for_data).
178
- with(data).and_return(double(:format => 'xxx'))
168
+ allow(ImageOptim::ImageMeta).to receive(:format_for_data).
169
+ with(data).and_return('xxx')
179
170
 
180
- expect(ImageOptim::ImagePath).to receive(:temp_file).and_yield(temp)
171
+ expect(ImageOptim::Path).to receive(:temp_file).and_yield(temp)
181
172
  expect(temp).to receive(:binmode)
182
173
  expect(temp).to receive(:write).with(data)
183
174
  expect(temp).to receive(:close)
@@ -191,49 +182,47 @@ describe ImageOptim do
191
182
  data = double
192
183
  image_optim = ImageOptim.new
193
184
 
194
- allow(ImageOptim::ImageMeta).to receive(:for_data).
195
- with(data).and_return(double(:format => nil))
185
+ allow(ImageOptim::ImageMeta).to receive(:format_for_data).
186
+ with(data).and_return(nil)
196
187
 
197
- expect(ImageOptim::ImagePath).not_to receive(:temp_file)
188
+ expect(ImageOptim::Path).not_to receive(:temp_file)
198
189
  expect(image_optim).not_to receive(:optimize_image)
199
190
 
200
191
  expect(image_optim.optimize_image_data(data)).to eq(nil)
201
192
  end
202
193
  end
203
194
 
204
- describe 'optimize multiple' do
205
- %w[
206
- optimize_images
207
- optimize_images!
208
- optimize_images_data
209
- ].each do |list_method|
210
- describe list_method do
211
- method = list_method.sub('images', 'image')
212
- describe 'without block' do
213
- it 'optimizes images and returns array of results' do
214
- image_optim = ImageOptim.new
215
- results = test_images.map do |src|
216
- dst = double
217
- expect(image_optim).to receive(method).with(src).and_return(dst)
218
- [src, dst]
219
- end
220
- expect(image_optim.send(list_method, test_images)).to eq(results)
195
+ %w[
196
+ optimize_images
197
+ optimize_images!
198
+ optimize_images_data
199
+ ].each do |list_method|
200
+ describe "##{list_method}" do
201
+ method = list_method.sub('images', 'image')
202
+ describe 'without block' do
203
+ it 'optimizes images and returns array of results' do
204
+ image_optim = ImageOptim.new
205
+ results = test_images.map do |src|
206
+ dst = double
207
+ expect(image_optim).to receive(method).with(src).and_return(dst)
208
+ [src, dst]
221
209
  end
210
+ expect(image_optim.send(list_method, test_images)).to eq(results)
222
211
  end
212
+ end
223
213
 
224
- describe 'given block' do
225
- it 'optimizes images, yields path and result for each and '\
226
- 'returns array of yield results' do
227
- image_optim = ImageOptim.new
228
- results = test_images.map do |src|
229
- dst = double
230
- expect(image_optim).to receive(method).with(src).and_return(dst)
231
- [src, dst, :test]
232
- end
233
- expect(image_optim.send(list_method, test_images) do |src, dst|
234
- [src, dst, :test]
235
- end).to eq(results)
214
+ describe 'given block' do
215
+ it 'optimizes images, yields path and result for each and '\
216
+ 'returns array of yield results' do
217
+ image_optim = ImageOptim.new
218
+ results = test_images.map do |src|
219
+ dst = double
220
+ expect(image_optim).to receive(method).with(src).and_return(dst)
221
+ [src, dst, :test]
236
222
  end
223
+ expect(image_optim.send(list_method, test_images) do |src, dst|
224
+ [src, dst, :test]
225
+ end).to eq(results)
237
226
  end
238
227
  end
239
228
  end
@@ -0,0 +1 @@
1
+ ��
@@ -1,18 +1,28 @@
1
- if ENV['CODECLIMATE_REPO_TOKEN']
2
- begin
3
- require 'codeclimate-test-reporter'
4
- CodeClimate::TestReporter.start
5
- rescue LoadError => e
6
- $stderr.puts "Got following while loading codeclimate-test-reporter: #{e}"
7
- end
1
+ if ENV['CODECLIMATE'] && ENV['CODECLIMATE_REPO_TOKEN']
2
+ require 'codeclimate-test-reporter'
3
+ CodeClimate::TestReporter.start
8
4
  end
9
5
 
6
+ require 'image_optim/pack'
7
+ require 'image_optim/path'
8
+
9
+ ENV['PATH'] = [
10
+ ImageOptim::Pack.path,
11
+ ENV['PATH'],
12
+ ].compact.join File::PATH_SEPARATOR
13
+
10
14
  RSpec.configure do |c|
11
- c.alias_example_to :they
15
+ c.before do
16
+ stub_const('ImageOptim::Config::GLOBAL_PATH', ImageOptim::Path::NULL)
17
+ stub_const('ImageOptim::Config::LOCAL_PATH', ImageOptim::Path::NULL)
18
+ ImageOptim.class_eval{ def pack; end }
19
+ end
20
+
21
+ c.order = :random
12
22
  end
13
23
 
14
24
  def flatten_animation(image)
15
- if image.format == :gif
25
+ if image.image_format == :gif
16
26
  flattened = image.temp_path
17
27
  command = %W[
18
28
  convert
@@ -37,7 +47,7 @@ def mepp(image_a, image_b)
37
47
  -alpha Background
38
48
  #{coalesce_a.to_s.shellescape}
39
49
  #{coalesce_b.to_s.shellescape}
40
- /dev/null
50
+ #{ImageOptim::Path::NULL}
41
51
  2>&1
42
52
  ].join(' ')
43
53
  output = ImageOptim::Cmd.capture(command)
@@ -63,3 +73,23 @@ RSpec::Matchers.define :be_similar_to do |expected, max_difference|
63
73
  "#{expected}, got normalized root-mean-square error of #{@diff}"
64
74
  end
65
75
  end
76
+
77
+ module CapabilityCheckHelpers
78
+ def any_file_modes_allowed?
79
+ Tempfile.open 'posix' do |f|
80
+ File.chmod(0, f.path)
81
+ File.stat(f.path).mode & 0o777 == 0
82
+ end
83
+ end
84
+
85
+ def inodes_supported?
86
+ File.stat(__FILE__).ino != 0
87
+ end
88
+
89
+ def signals_supported?
90
+ Process.kill(0, 0)
91
+ true
92
+ rescue
93
+ false
94
+ end
95
+ end