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.
- checksums.yaml +8 -8
- data/.appveyor.yml +95 -0
- data/.rubocop.yml +3 -0
- data/.travis.yml +27 -22
- data/CHANGELOG.markdown +10 -0
- data/CONTRIBUTING.markdown +2 -1
- data/Gemfile +1 -1
- data/README.markdown +10 -2
- data/image_optim.gemspec +4 -4
- data/lib/image_optim.rb +32 -16
- data/lib/image_optim/bin_resolver/bin.rb +11 -4
- data/lib/image_optim/cache.rb +71 -0
- data/lib/image_optim/cache_path.rb +16 -0
- data/lib/image_optim/config.rb +12 -2
- data/lib/image_optim/handler.rb +1 -1
- data/lib/image_optim/image_meta.rb +5 -10
- data/lib/image_optim/optimized_path.rb +25 -0
- data/lib/image_optim/path.rb +70 -0
- data/lib/image_optim/runner/option_parser.rb +13 -0
- data/lib/image_optim/worker.rb +5 -8
- data/lib/image_optim/worker/class_methods.rb +3 -1
- data/lib/image_optim/worker/jpegoptim.rb +3 -0
- data/lib/image_optim/worker/jpegrecompress.rb +3 -0
- data/lib/image_optim/worker/pngquant.rb +3 -0
- data/script/worker_analysis +10 -9
- data/spec/image_optim/bin_resolver/comparable_condition_spec.rb +1 -1
- data/spec/image_optim/bin_resolver/simple_version_spec.rb +48 -40
- data/spec/image_optim/bin_resolver_spec.rb +190 -172
- data/spec/image_optim/cache_path_spec.rb +59 -0
- data/spec/image_optim/cache_spec.rb +159 -0
- data/spec/image_optim/cmd_spec.rb +11 -7
- data/spec/image_optim/config_spec.rb +92 -71
- data/spec/image_optim/handler_spec.rb +3 -6
- data/spec/image_optim/image_meta_spec.rb +61 -0
- data/spec/image_optim/optimized_path_spec.rb +58 -0
- data/spec/image_optim/option_helpers_spec.rb +25 -0
- data/spec/image_optim/path_spec.rb +105 -0
- data/spec/image_optim/railtie_spec.rb +6 -6
- data/spec/image_optim/runner/glob_helpers_spec.rb +2 -6
- data/spec/image_optim/runner/option_parser_spec.rb +3 -3
- data/spec/image_optim/space_spec.rb +16 -18
- data/spec/image_optim/worker/optipng_spec.rb +3 -3
- data/spec/image_optim/worker/pngquant_spec.rb +47 -7
- data/spec/image_optim/worker_spec.rb +114 -17
- data/spec/image_optim_spec.rb +58 -69
- data/spec/images/broken_jpeg +1 -0
- data/spec/spec_helper.rb +40 -10
- metadata +30 -8
- data/lib/image_optim/image_path.rb +0 -68
- 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)
|
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).
|
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
|
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
|
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
|
120
|
+
describe 'optimizing assets' do
|
121
121
|
before do
|
122
|
-
stub_const('
|
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 =
|
135
|
+
original = Path.convert(asset.pathname)
|
136
136
|
|
137
137
|
expect(asset_data).to be_smaller_than(original)
|
138
138
|
|
139
|
-
|
140
|
-
temp.
|
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
|
-
|
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(
|
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
|
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
|
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
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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/
|
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::
|
12
|
-
let(:dst){ instance_double(ImageOptim::
|
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(:
|
14
|
-
subject{ described_class.new(ImageOptim.new, options).quality }
|
13
|
+
let(:subject){ described_class.new(ImageOptim.new, options).quality }
|
15
14
|
|
16
|
-
context '
|
17
|
-
|
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
|
21
|
-
|
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
|
-
|
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
|