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
@@ -9,15 +9,12 @@ describe ImageOptim::Handler do
9
9
  it 'uses original as source for first conversion '\
10
10
  'and two temp files for further conversions' do
11
11
  original = double(:original)
12
- allow(original).to receive(:temp_path) do
13
- fail 'temp_path called unexpectedly'
14
- end
12
+ allow(original).to receive(:respond_to?).with(:temp_path).and_return(true)
15
13
 
16
14
  handler = Handler.new(original)
17
15
  temp_a = double(:temp_a)
18
16
  temp_b = double(:temp_b)
19
- expect(original).to receive(:temp_path).once.and_return(temp_a)
20
- expect(original).to receive(:temp_path).once.and_return(temp_b)
17
+ expect(original).to receive(:temp_path).and_return(temp_a, temp_b)
21
18
 
22
19
  # first unsuccessful run
23
20
  handler.process do |src, dst|
@@ -60,7 +57,7 @@ describe ImageOptim::Handler do
60
57
  handler.cleanup
61
58
  end
62
59
 
63
- describe :open do
60
+ describe '.for' do
64
61
  it 'yields instance, runs cleanup and returns result' do
65
62
  original = double
66
63
  handler = double
@@ -0,0 +1,61 @@
1
+ require 'image_optim/image_meta'
2
+
3
+ describe ImageOptim::ImageMeta do
4
+ let(:image_path){ 'spec/images/lena.jpg' }
5
+ let(:non_image_path){ __FILE__ }
6
+ let(:broken_image_path){ 'spec/images/broken_jpeg' }
7
+
8
+ describe '.format_for_path' do
9
+ context 'for an image' do
10
+ it 'returns format' do
11
+ expect(described_class.format_for_path(image_path)).
12
+ to eq(:jpeg)
13
+ end
14
+ end
15
+
16
+ context 'for broken image' do
17
+ it 'warns and returns nil' do
18
+ expect(described_class).to receive(:warn)
19
+
20
+ expect(described_class.format_for_path(broken_image_path)).
21
+ to eq(nil)
22
+ end
23
+ end
24
+
25
+ context 'for not an image' do
26
+ it 'does not warn and returns nil' do
27
+ expect(described_class).not_to receive(:warn)
28
+
29
+ expect(described_class.format_for_path(non_image_path)).
30
+ to eq(nil)
31
+ end
32
+ end
33
+ end
34
+
35
+ describe '.format_for_data' do
36
+ context 'for image data' do
37
+ it 'returns format' do
38
+ expect(described_class.format_for_data(File.read(image_path))).
39
+ to eq(:jpeg)
40
+ end
41
+ end
42
+
43
+ context 'for broken image data' do
44
+ it 'warns and returns nil' do
45
+ expect(described_class).to receive(:warn)
46
+
47
+ expect(described_class.format_for_data(File.read(broken_image_path))).
48
+ to eq(nil)
49
+ end
50
+ end
51
+
52
+ context 'for not an image data' do
53
+ it 'does not warn and returns nil' do
54
+ expect(described_class).not_to receive(:warn)
55
+
56
+ expect(described_class.format_for_data(File.read(non_image_path))).
57
+ to eq(nil)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,58 @@
1
+ require 'image_optim/optimized_path'
2
+
3
+ describe ImageOptim::OptimizedPath do
4
+ describe '#initialize' do
5
+ context 'when second argument is original' do
6
+ subject{ described_class.new('a', 'b') }
7
+
8
+ before do
9
+ allow_any_instance_of(ImageOptim::Path).
10
+ to receive(:size).and_return(616)
11
+ end
12
+
13
+ it 'delegates to optimized path as Path' do
14
+ is_expected.to eq(ImageOptim::Path.new('a'))
15
+ end
16
+
17
+ it 'returns original path as Path for original' do
18
+ expect(subject.original).to eq(ImageOptim::Path.new('b'))
19
+ end
20
+
21
+ it 'returns original size for original_size' do
22
+ expect(subject.original_size).to eq(616)
23
+ end
24
+ end
25
+
26
+ context 'when second argument is size' do
27
+ subject{ described_class.new('a', 616) }
28
+
29
+ it 'delegates to optimized path as Path' do
30
+ is_expected.to eq(ImageOptim::Path.new('a'))
31
+ end
32
+
33
+ it 'returns optimized path as Path for original' do
34
+ expect(subject.original).to eq(ImageOptim::Path.new('a'))
35
+ end
36
+
37
+ it 'returns size for original_size' do
38
+ expect(subject.original_size).to eq(616)
39
+ end
40
+ end
41
+
42
+ context 'when no second argument' do
43
+ subject{ described_class.new('a') }
44
+
45
+ it 'delegates to optimized path as Path' do
46
+ is_expected.to eq(ImageOptim::Path.new('a'))
47
+ end
48
+
49
+ it 'returns nil for original' do
50
+ expect(subject.original).to eq(nil)
51
+ end
52
+
53
+ it 'returns nil for original_size' do
54
+ expect(subject.original_size).to eq(nil)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+ require 'image_optim/option_helpers'
3
+
4
+ describe ImageOptim::OptionHelpers do
5
+ describe '.limit_with_range' do
6
+ {
7
+ 2..4 => 'inclusive',
8
+ 2...5 => 'exclusive',
9
+ }.each do |range, type|
10
+ context "for an end #{type} range" do
11
+ it 'returns number when it is in range' do
12
+ expect(described_class.limit_with_range(4, range)).to eq(4)
13
+ end
14
+
15
+ it 'returns begin when it is less than minimum' do
16
+ expect(described_class.limit_with_range(1, range)).to eq(2)
17
+ end
18
+
19
+ it 'returns end when it is more than maximum' do
20
+ expect(described_class.limit_with_range(10, range)).to eq(4)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+ require 'image_optim/path'
3
+ require 'tempfile'
4
+
5
+ describe ImageOptim::Path do
6
+ include CapabilityCheckHelpers
7
+
8
+ before do
9
+ stub_const('Path', ImageOptim::Path)
10
+ end
11
+
12
+ describe '.convert' do
13
+ it 'returns Path for string' do
14
+ path = 'a'
15
+
16
+ expect(Path.convert(path)).to be_a(Path)
17
+ expect(Path.convert(path)).to eq(Path.new(path))
18
+
19
+ expect(Path.convert(path)).not_to eq(path)
20
+ expect(Path.convert(path)).not_to be(path)
21
+ end
22
+
23
+ it 'returns Path for Pathname' do
24
+ pathname = Pathname.new('a')
25
+
26
+ expect(Path.convert(pathname)).to be_a(Path)
27
+ expect(Path.convert(pathname)).to eq(Path.new(pathname))
28
+
29
+ expect(Path.convert(pathname)).to eq(pathname)
30
+ expect(Path.convert(pathname)).not_to be(pathname)
31
+ end
32
+
33
+ it 'returns same instance for Path' do
34
+ image_path = Path.new('a')
35
+
36
+ expect(Path.convert(image_path)).to be_a(Path)
37
+ expect(Path.convert(image_path)).to eq(Path.new(image_path))
38
+
39
+ expect(Path.convert(image_path)).to eq(image_path)
40
+ expect(Path.convert(image_path)).to be(image_path)
41
+ end
42
+ end
43
+
44
+ describe '#binread' do
45
+ it 'reads binary data' do
46
+ data = (0..255).to_a.pack('c*')
47
+
48
+ path = Path.temp_file_path
49
+ path.binwrite(data)
50
+
51
+ expect(path.binread).to eq(data)
52
+ if ''.respond_to?(:encoding)
53
+ expect(path.binread.encoding).to eq(Encoding.find('ASCII-8BIT'))
54
+ end
55
+ end
56
+ end
57
+
58
+ describe '#replace' do
59
+ let(:src){ Path.temp_file_path }
60
+ let(:dst){ Path.temp_file_path }
61
+
62
+ it 'moves data to destination' do
63
+ src.write('src')
64
+
65
+ src.replace(dst)
66
+
67
+ expect(dst.read).to eq('src')
68
+ end
69
+
70
+ it 'removes original file' do
71
+ src.replace(dst)
72
+
73
+ expect(src).to_not exist
74
+ end
75
+
76
+ it 'preserves attributes of destination file' do
77
+ skip 'full file modes are not support' unless any_file_modes_allowed?
78
+ mode = 0o666
79
+
80
+ dst.chmod(mode)
81
+
82
+ src.replace(dst)
83
+
84
+ got = dst.stat.mode & 0o777
85
+ expect(got).to eq(mode), format('expected %04o, got %04o', mode, got)
86
+ end
87
+
88
+ it 'does not preserve mtime of destination file' do
89
+ time = src.mtime
90
+
91
+ dst.utime(time - 1000, time - 1000)
92
+
93
+ src.replace(dst)
94
+
95
+ expect(dst.mtime).to be >= time
96
+ end
97
+
98
+ it 'changes inode of destination' do
99
+ skip 'inodes are not supported' unless inodes_supported?
100
+ expect do
101
+ src.replace(dst)
102
+ end.to change{ dst.stat.ino }
103
+ end
104
+ end
105
+ end
@@ -33,7 +33,7 @@ describe 'ImageOptim::Railtie' do
33
33
  Rails.application = nil
34
34
  end
35
35
 
36
- describe :initialization do
36
+ describe 'initialization' do
37
37
  it 'initializes by default' do
38
38
  expect(ImageOptim).to receive(:new)
39
39
  init_rails_app
@@ -117,9 +117,9 @@ describe 'ImageOptim::Railtie' do
117
117
  end
118
118
  end
119
119
 
120
- describe :assets do
120
+ describe 'optimizing assets' do
121
121
  before do
122
- stub_const('ImagePath', ImageOptim::ImagePath)
122
+ stub_const('Path', ImageOptim::Path)
123
123
  end
124
124
 
125
125
  %w[
@@ -132,12 +132,12 @@ describe 'ImageOptim::Railtie' do
132
132
  asset = init_rails_app.assets.find_asset(asset_name)
133
133
 
134
134
  asset_data = asset.source
135
- original = ImagePath.convert(asset.pathname)
135
+ original = Path.convert(asset.pathname)
136
136
 
137
137
  expect(asset_data).to be_smaller_than(original)
138
138
 
139
- ImagePath.temp_file_path %W[spec .#{original.format}] do |temp|
140
- temp.write(asset_data)
139
+ Path.temp_file_path %W[spec .#{original.image_format}] do |temp|
140
+ temp.binwrite(asset_data)
141
141
 
142
142
  expect(temp).to be_similar_to(original, 0)
143
143
  end
@@ -2,11 +2,7 @@ require 'spec_helper'
2
2
  require 'image_optim/runner/glob_helpers'
3
3
 
4
4
  describe ImageOptim::Runner::GlobHelpers do
5
- before do
6
- stub_const('GlobHelpers', ImageOptim::Runner::GlobHelpers)
7
- end
8
-
9
- describe :expand_braces do
5
+ describe '.expand_braces' do
10
6
  {
11
7
  'hello.world' => %w[hello.world],
12
8
  '{hello,.world}' => %w[hello .world],
@@ -18,7 +14,7 @@ describe ImageOptim::Runner::GlobHelpers do
18
14
  'test{ing,}' => %w[testing test],
19
15
  }.each do |glob, expected|
20
16
  it "expands #{glob}" do
21
- expect(GlobHelpers.expand_braces(glob)).to match_array(expected)
17
+ expect(described_class.expand_braces(glob)).to match_array(expected)
22
18
  end
23
19
  end
24
20
  end
@@ -12,7 +12,7 @@ describe ImageOptim::Runner::OptionParser do
12
12
  end
13
13
  end
14
14
 
15
- describe :parse! do
15
+ describe '.parse!' do
16
16
  it 'returns empty hash for arguments without options' do
17
17
  args = %w[foo bar]
18
18
  expect(OptionParser.parse!(args)).to eq({})
@@ -25,7 +25,7 @@ describe ImageOptim::Runner::OptionParser do
25
25
  expect(args).to eq(%w[foo bar])
26
26
  end
27
27
 
28
- it 'stops parsing optiosn after --' do
28
+ it 'stops parsing options after --' do
29
29
  args = %w[-- -r foo bar]
30
30
  OptionParser.parse!(args)
31
31
  expect(args).to eq(%w[-r foo bar])
@@ -92,7 +92,7 @@ describe ImageOptim::Runner::OptionParser do
92
92
  end
93
93
  end
94
94
 
95
- describe :help do
95
+ describe '#help' do
96
96
  it 'returns wrapped text' do
97
97
  parser = OptionParser.new({})
98
98
 
@@ -2,24 +2,22 @@ require 'spec_helper'
2
2
  require 'image_optim/space'
3
3
 
4
4
  describe ImageOptim::Space do
5
- before do
6
- stub_const('Space', ImageOptim::Space)
7
- end
8
-
9
- {
10
- 0 => ' ',
11
- 1 => ' 1B',
12
- 10 => ' 10B',
13
- 100 => ' 100B',
14
- 1_000 => ' 1000B',
15
- 10_000 => ' 9.8K',
16
- 100_000 => ' 97.7K',
17
- 1_000_000 => '976.6K',
18
- 10_000_000 => ' 9.5M',
19
- 100_000_000 => ' 95.4M',
20
- }.each do |size, space|
21
- it "converts #{size} to #{space}" do
22
- expect(Space.space(size)).to eq(space)
5
+ describe '.space' do
6
+ {
7
+ 0 => ' ',
8
+ 1 => ' 1B',
9
+ 10 => ' 10B',
10
+ 100 => ' 100B',
11
+ 1_000 => ' 1000B',
12
+ 10_000 => ' 9.8K',
13
+ 100_000 => ' 97.7K',
14
+ 1_000_000 => '976.6K',
15
+ 10_000_000 => ' 9.5M',
16
+ 100_000_000 => ' 95.4M',
17
+ }.each do |size, space|
18
+ it "converts #{size} to #{space}" do
19
+ expect(described_class.space(size)).to eq(space)
20
+ end
23
21
  end
24
22
  end
25
23
  end
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
  require 'image_optim/worker/optipng'
3
- require 'image_optim/image_path'
3
+ require 'image_optim/path'
4
4
 
5
5
  describe ImageOptim::Worker::Optipng do
6
6
  describe 'strip option' do
@@ -8,8 +8,8 @@ describe ImageOptim::Worker::Optipng do
8
8
 
9
9
  let(:options){ {} }
10
10
  let(:optipng_version){ '0.7' }
11
- let(:src){ instance_double(ImageOptim::ImagePath, :copy => nil) }
12
- let(:dst){ instance_double(ImageOptim::ImagePath) }
11
+ let(:src){ instance_double(ImageOptim::Path, :copy => nil) }
12
+ let(:dst){ instance_double(ImageOptim::Path) }
13
13
 
14
14
  before do
15
15
  optipng_bin = instance_double(ImageOptim::BinResolver::Bin)
@@ -10,17 +10,57 @@ describe ImageOptim::Worker::Pngquant do
10
10
  end
11
11
 
12
12
  describe 'value' do
13
- let(:options){ {} }
14
- subject{ described_class.new(ImageOptim.new, options).quality }
13
+ let(:subject){ described_class.new(ImageOptim.new, options).quality }
15
14
 
16
- context 'by default' do
17
- it{ is_expected.to eq(100..100) }
15
+ context 'when lossy not allowed' do
16
+ context 'by default' do
17
+ let(:options){ {} }
18
+
19
+ it{ is_expected.to eq(100..100) }
20
+ end
21
+
22
+ context 'when value is passed through options' do
23
+ let(:options){ {:quality => 10..90} }
24
+
25
+ it 'warns and keeps default' do
26
+ expect_any_instance_of(described_class).
27
+ to receive(:warn).with(/ignored in lossless mode/)
28
+ is_expected.to eq(100..100)
29
+ end
30
+ end
18
31
  end
19
32
 
20
- context 'when lossy allowed by default' do
21
- let(:options){ {:allow_lossy => true} }
33
+ context 'when lossy allowed' do
34
+ context 'by default' do
35
+ let(:options){ {:allow_lossy => true} }
36
+
37
+ it{ is_expected.to eq(0..100) }
38
+ end
39
+
40
+ context 'when value is passed through options' do
41
+ let(:options){ {:allow_lossy => true, :quality => 10..90} }
42
+
43
+ it 'sets the value without warning' do
44
+ expect_any_instance_of(described_class).not_to receive(:warn)
45
+ is_expected.to eq(10..90)
46
+ end
47
+ end
48
+
49
+ context 'when passed range begin is less than 0' do
50
+ let(:options){ {:allow_lossy => true, :quality => -50..50} }
51
+
52
+ it 'sets begin to 0' do
53
+ is_expected.to eq(0..50)
54
+ end
55
+ end
56
+
57
+ context 'when passed range end is more than 100' do
58
+ let(:options){ {:allow_lossy => true, :quality => 50..150} }
22
59
 
23
- it{ is_expected.to eq(0..100) }
60
+ it 'sets end to 100' do
61
+ is_expected.to eq(50..100)
62
+ end
63
+ end
24
64
  end
25
65
  end
26
66
  end