image_optim 0.12.1 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- MzhlYjg1NzA0MjUxYmM3MDhjNWVkZDcyYTAyZTM4MTAyMzUyZWU1ZQ==
4
+ YjI2ZTQxODlhMzRlMzllZjk0YjhiNmU3YWZiNWYzMDA5NTM2Y2VhNw==
5
5
  data.tar.gz: !binary |-
6
- MjE2MjNkNzk1NDIyY2NjM2VjZjc4OTkwNmQ5MGY4NWU5YmI4NmViZA==
6
+ MWM1N2Q3ZDlmYzQ1MzczMWQ3NjJhMjBlN2E5NGU0ZGIwODhhMDkwNQ==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- Yzg2ZDUyY2E3MTNhMTQzODgxOWU5M2ZkMWYxZDE1ZThkN2Q3ZTQyYTM3MThj
10
- OTE0OWQwMDUyYjJlZTFlZjU3YWI1ZmI4NTJlZGU4OTdkZTYyM2YzOTY2NTU0
11
- MjM1ZDQyMmQ1NGZhYTJiNGFlY2MyNThhNTdkYTAxMzExODkxYTM=
9
+ OTM3MDE3NThhYmEzZjI3NTM2NWMxZDZhOWI2YTg0NDk0ZDk1NjM1NzMyMzhj
10
+ NTJjY2UzODhhZmY5MTRhZjRlYTBlYzYxZTY1NzgyMGFmMjY5ZjBhNWU3NDAx
11
+ MmJlZTEwYTY5MDMxMGI1MTI3YWU0N2NkMjdmOTNjNzYzNjE0NTE=
12
12
  data.tar.gz: !binary |-
13
- ZmM1ZDRjNzBkNjNjYmI3MmEwZDRjYWVhYThjMjQ1NjIxMTJjZTgzZDJiMmNl
14
- OTQ4NjM2MjU3YTA4M2NlMGZjNDk3YTIyODYzM2RhZWM5YmVjOThjMGM2NjU3
15
- YzFhMjJkMWEyNDFhOTllYjNjOWZhNTViMzQ1YWJhNmQ3M2NjYzg=
13
+ N2IzNzVhNWFjNzFkOWUwYjg1MjUyYmM4YWJhNjgzYTlhMjgwZDQ0NTYwNWE0
14
+ MGMzYTA3YzMzOWZkMmZlYzk0YzliMWJhMDZkODZiN2JkMzc3MzIwZDQ4ZGE3
15
+ MDE1MGY4NWI5YTA5YWU5YmNhN2Y4MTc4YmExMjkzNDg0NTFmNDk=
@@ -3,6 +3,7 @@
3
3
  Optimize (lossless compress) images (jpeg, png, gif, svg) using external utilities:
4
4
 
5
5
  * [advpng](http://advancemame.sourceforge.net/doc-advpng.html) from [AdvanceCOMP](http://advancemame.sourceforge.net/comp-readme.html)
6
+ (will use [zopfli](https://code.google.com/p/zopfli/) on default/maximum level 4)
6
7
  * [gifsicle](http://www.lcdf.org/gifsicle/)
7
8
  * [jhead](http://www.sentex.net/~mwandel/jhead/)
8
9
  * [jpegoptim](http://www.kokkonen.net/tjko/projects.html)
@@ -218,6 +219,7 @@ optipng:
218
219
  Worker can be disabled by passing `false` instead of options hash.
219
220
 
220
221
  <!---<worker-options>-->
222
+ <!-- worker options markdown is generated by `script/update_worker_options_in_readme` -->
221
223
 
222
224
  ### :pngcrush =>
223
225
  * `:chunks` — List of chunks to remove or `:alla` - all except tRNS/transparency or `:allb` - all except tRNS and gAMA/gamma *(defaults to `:alla`)*
@@ -9,7 +9,7 @@ option_parser = OptionParser.new do |op|
9
9
  op.accept(ImageOptim::TrueFalseNil, OptionParser.top.atype[TrueClass][0].merge('nil' => nil)){ |arg, val| val }
10
10
 
11
11
  op.banner = <<-TEXT.gsub(/^\s*\|/, '')
12
- |#{op.program_name} v#{ImageOptim.version}
12
+ |#{ImageOptim.full_version}
13
13
  |
14
14
  |Usege:
15
15
  | #{op.program_name} [options] image_path …
@@ -103,6 +103,7 @@ begin
103
103
  end
104
104
 
105
105
  option_parser.parse!(args)
106
+ $stderr.puts ImageOptim.full_version if options[:verbose]
106
107
  ImageOptim::Runner.run!(args, options) or exit 1
107
108
  rescue OptionParser::ParseError => e
108
109
  abort "#{e.to_s}\n\n#{option_parser.help}"
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'image_optim'
5
- s.version = '0.12.1'
5
+ s.version = '0.13.0'
6
6
  s.summary = %q{Optimize (lossless compress) images (jpeg, png, gif, svg) using external utilities (advpng, gifsicle, jpegoptim, jpegtran, optipng, pngcrush, pngout, svgo)}
7
7
  s.homepage = "http://github.com/toy/#{s.name}"
8
8
  s.authors = ['Ivan Kuchin']
@@ -1,6 +1,7 @@
1
1
  require 'image_optim/bin_resolver'
2
2
  require 'image_optim/config'
3
3
  require 'image_optim/handler'
4
+ require 'image_optim/image_meta'
4
5
  require 'image_optim/image_path'
5
6
  require 'image_optim/worker'
6
7
  require 'in_threads'
@@ -97,8 +98,9 @@ class ImageOptim
97
98
 
98
99
  # Optimize image data, return new data or nil if optimization failed
99
100
  def optimize_image_data(original_data)
100
- format = ImageSize.new(original_data).format
101
- ImagePath.temp_file %W[image_optim .#{format}] do |temp|
101
+ image_meta = ImageMeta.for_data(original_data)
102
+ return unless image_meta && image_meta.format
103
+ ImagePath.temp_file %W[image_optim .#{image_meta.format}] do |temp|
102
104
  temp.binmode
103
105
  temp.write(original_data)
104
106
  temp.close
@@ -144,6 +146,11 @@ class ImageOptim
144
146
  Gem.loaded_specs['image_optim'].version.to_s rescue 'DEV'
145
147
  end
146
148
 
149
+ # Full version of image_optim
150
+ def self.full_version
151
+ "image_optim v#{version}"
152
+ end
153
+
147
154
  # Are there workers for file at path?
148
155
  def optimizable?(path)
149
156
  !!workers_for_image(path)
@@ -1,5 +1,7 @@
1
1
  require 'thread'
2
2
  require 'fspath'
3
+ require 'image_optim/bin_resolver/simple_version'
4
+ require 'image_optim/bin_resolver/comparable_condition'
3
5
 
4
6
  class ImageOptim
5
7
  class BinNotFoundError < StandardError; end
@@ -9,7 +11,8 @@ class ImageOptim
9
11
  class Bin
10
12
  attr_reader :name, :version
11
13
  def initialize(name, version)
12
- @name, @version = name, version
14
+ @name = name
15
+ @version = version && SimpleVersion.new(version)
13
16
  end
14
17
 
15
18
  def to_s
@@ -94,11 +97,17 @@ class ImageOptim
94
97
  end
95
98
 
96
99
  def check!(bin)
100
+ is = ComparableCondition.is
97
101
  case bin.name
98
102
  when :pngcrush
99
103
  case bin.version
100
- when '1.7.60'..'1.7.65'
101
- raise BadBinVersion, "`#{bin}` is known to produce broken pngs"
104
+ when c = is.between?('1.7.60', '1.7.65')
105
+ raise BadBinVersion, "`#{bin}` (#{c}) is known to produce broken pngs"
106
+ end
107
+ when :advpng
108
+ case bin.version
109
+ when c = is < '1.17'
110
+ warn "Note that `#{bin}` (#{c}) does not use zopfli"
102
111
  end
103
112
  end
104
113
  end
@@ -0,0 +1,44 @@
1
+ class ImageOptim
2
+ class BinResolver
3
+ class ComparableCondition
4
+ class Builder
5
+ Comparable.instance_methods.each do |method|
6
+ define_method method do |*args|
7
+ ComparableCondition.new(method, *args)
8
+ end
9
+ end
10
+ end
11
+
12
+ def self.is
13
+ Builder.new
14
+ end
15
+
16
+ attr_reader :method, :args
17
+ def initialize(method, *args)
18
+ @method = method.to_sym
19
+ @args = args
20
+
21
+ case @method
22
+ when :between?
23
+ raise ArgumentError, "`between?' expects 2 arguments" unless args.length == 2
24
+ when :<, :<=, :==, :>, :>=
25
+ raise ArgumentError, "`#{method}' expects 1 argument" unless args.length == 1
26
+ else
27
+ raise ArgumentError, "Unknown method `#{method}'"
28
+ end
29
+ end
30
+
31
+ def ===(to_compare)
32
+ to_compare.send(@method, *@args)
33
+ end
34
+
35
+ def to_s
36
+ if @method == :between?
37
+ @args.join('..')
38
+ else
39
+ "#{@method} #{@args.first}"
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,22 @@
1
+ class ImageOptim
2
+ class BinResolver
3
+ class SimpleVersion
4
+ include Comparable
5
+
6
+ attr_reader :parts
7
+ def initialize(str)
8
+ @str = String(str)
9
+ @parts = @str.split('.').map(&:to_i).reverse.drop_while(&:zero?).reverse
10
+ end
11
+
12
+ def to_s
13
+ @str
14
+ end
15
+
16
+ def <=>(other)
17
+ other = self.class.new(other) unless other.is_a?(self.class)
18
+ parts <=> other.parts
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ require 'image_size'
2
+
3
+ class ImageOptim
4
+ class ImageMeta
5
+ def self.for_path(path)
6
+ is = ImageSize.path(path)
7
+ new(is.format)
8
+ rescue => e
9
+ warn "#{e} (detecting format of image at #{path})"
10
+ end
11
+
12
+ def self.for_data(data)
13
+ is = ImageSize.new(data)
14
+ new(is.format)
15
+ rescue => e
16
+ warn "#{e} (detecting format of image data)"
17
+ end
18
+
19
+ attr_reader :format
20
+ def initialize(format)
21
+ @format = format
22
+ end
23
+ end
24
+ end
@@ -1,5 +1,5 @@
1
1
  require 'fspath'
2
- require 'image_size'
2
+ require 'image_optim/image_meta'
3
3
 
4
4
  class ImageOptim
5
5
  class ImagePath < FSPath
@@ -49,7 +49,9 @@ class ImageOptim
49
49
 
50
50
  # Get format using ImageSize
51
51
  def format
52
- open{ |f| ImageSize.new(f) }.format
52
+ if image_meta = ImageMeta.for_path(self)
53
+ image_meta.format
54
+ end
53
55
  end
54
56
 
55
57
  # Returns path if it is already an instance of this class otherwise new instance
@@ -8,28 +8,36 @@ require 'image_optim'
8
8
  README_FILE = File.expand_path('../../README.markdown', __FILE__)
9
9
  BEGIN_MARKER = '<!---<worker-options>-->'
10
10
  END_MARKER = '<!---</worker-options>-->'
11
+ GENERATED_NOTE = "<!-- markdown for worker options is generated by `#{$0}` -->"
12
+
13
+ def update_worker_options(text)
14
+ text.clone.sub!(/#{Regexp.escape(BEGIN_MARKER)}.*#{Regexp.escape(END_MARKER)}/m) do
15
+ StringIO.open do |md|
16
+ md.puts BEGIN_MARKER
17
+ md.puts GENERATED_NOTE
18
+ md.puts
19
+
20
+ ImageOptim::Worker.klasses.each_with_index do |klass, i|
21
+ md.puts "### :#{klass.bin_sym} =>"
22
+ if klass.option_definitions.empty?
23
+ md.puts 'Worker has no options'
24
+ else
25
+ klass.option_definitions.each do |option_definition|
26
+ md.puts "* `:#{option_definition.name}` — #{option_definition.description} *(defaults to `#{option_definition.default.inspect}`)*"
27
+ end
28
+ end
29
+ md.puts
30
+ end
11
31
 
12
- def worker_options
13
- io = StringIO.new
32
+ md.puts END_MARKER
14
33
 
15
- ImageOptim::Worker.klasses.each_with_index do |klass, i|
16
- io.puts "### :#{klass.bin_sym} =>"
17
- if klass.option_definitions.empty?
18
- io.puts 'Worker has no options'
19
- else
20
- klass.option_definitions.each do |option_definition|
21
- io.puts "* `:#{option_definition.name}` — #{option_definition.description} *(defaults to `#{option_definition.default.inspect}`)*"
22
- end
34
+ md.string.strip
23
35
  end
24
- io.puts
25
36
  end
26
-
27
- io.string
28
37
  end
29
38
 
30
39
  readme = File.read(README_FILE)
31
-
32
- if readme.sub!(/#{Regexp.escape(BEGIN_MARKER)}.*#{Regexp.escape(END_MARKER)}/m, "#{BEGIN_MARKER}\n\n#{worker_options.strip}\n\n#{END_MARKER}")
40
+ if readme = update_worker_options(readme)
33
41
  File.open(README_FILE, 'w') do |f|
34
42
  f.write readme
35
43
  end
@@ -0,0 +1,38 @@
1
+ $:.unshift File.expand_path('../../../../lib', __FILE__)
2
+ require 'rspec'
3
+ require 'image_optim/bin_resolver/comparable_condition'
4
+
5
+ describe ImageOptim::BinResolver::ComparableCondition do
6
+ is = ImageOptim::BinResolver::ComparableCondition.is
7
+
8
+ it "should build conditions" do
9
+ expect(is.between?(10, 20).method).to eq(:between?)
10
+ expect(is.between?(10, 20).args).to eq([10, 20])
11
+
12
+ expect((is >= 15).method).to eq(:>=)
13
+ expect((is >= 15).args).to eq([15])
14
+
15
+ expect((is < 30).method).to eq(:<)
16
+ expect((is < 30).args).to eq([30])
17
+ end
18
+
19
+ it "should stringify conditions" do
20
+ expect(is.between?(10, 20).to_s).to eq('10..20')
21
+ expect((is >= 15).to_s).to eq('>= 15')
22
+ expect((is < 30).to_s).to eq('< 30')
23
+ end
24
+
25
+ it "should match conditions" do
26
+ expect(is.between?(10, 20)).not_to be === 9
27
+ expect(is.between?(10, 20)).to be === 15
28
+ expect(is.between?(10, 20)).not_to be === 21
29
+
30
+ expect(is >= 15).not_to be === 14
31
+ expect(is >= 15).to be === 15
32
+ expect(is >= 15).to be === 16
33
+
34
+ expect(is < 30).to be === 29
35
+ expect(is < 30).not_to be === 30
36
+ expect(is < 30).not_to be === 31
37
+ end
38
+ end
@@ -0,0 +1,34 @@
1
+ $:.unshift File.expand_path('../../../../lib', __FILE__)
2
+ require 'rspec'
3
+ require 'image_optim/bin_resolver/simple_version'
4
+
5
+ describe ImageOptim::BinResolver::SimpleVersion do
6
+ def v(str)
7
+ ImageOptim::BinResolver::SimpleVersion.new(str)
8
+ end
9
+
10
+ it "should compare versions" do
11
+ expect(v '1.17').to be > '0'
12
+ expect(v '1.17').to be > '0.1'
13
+ expect(v '1.17').to be > '0.9'
14
+ expect(v '1.17').to be > '1.9'
15
+ expect(v '1.17').to be < '1.17.1'
16
+ expect(v '1.17').to be < '1.99'
17
+ expect(v '1.17').to be < '2.1'
18
+ end
19
+
20
+ it "should normalize versions" do
21
+ variations = %w[1 01 1.0 1.00 1.0.0 1.0.0.0]
22
+ variations.each do |a|
23
+ variations.each do |b|
24
+ expect(v a).to eq(b)
25
+ end
26
+ end
27
+ end
28
+
29
+ it "should convert objects" do
30
+ expect(v 1.17).to eq('1.17')
31
+ expect(v '1.17').to eq('1.17')
32
+ expect(v(v 1.17)).to eq('1.17')
33
+ end
34
+ end
@@ -44,6 +44,18 @@ describe ImageOptim do
44
44
  ImageOptim::Config.stub(:global => {}, :local => {})
45
45
  end
46
46
 
47
+ describe "worker" do
48
+ options = Hash[ImageOptim::Worker.klasses.map{ |klass| [klass.bin_sym, false] }]
49
+ ImageOptim::Worker.klasses.reject{ |k| k.new({}).image_formats.empty? }.each do |worker_klass|
50
+ describe worker_klass.bin_sym do
51
+ it "should optimize at least one test image" do
52
+ image_optim = ImageOptim.new(options.merge(worker_klass.bin_sym => true))
53
+ expect(TEST_IMAGES.any?{ |original| image_optim.optimize_image(original.temp_copy) }).to be_true
54
+ end
55
+ end
56
+ end
57
+ end
58
+
47
59
  describe "isolated" do
48
60
  describe "optimize" do
49
61
  TEST_IMAGES.each do |original|
@@ -92,6 +104,7 @@ describe ImageOptim do
92
104
  it "should optimize #{original}" do
93
105
  image_optim = ImageOptim.new
94
106
  optimized_data = image_optim.optimize_image_data(original.read)
107
+ optimized_data.should_not be_nil
95
108
  optimized_data.should == image_optim.optimize_image(original.temp_copy).open('rb', &:read)
96
109
 
97
110
  image_optim.optimize_image_data(optimized_data).should be_nil
@@ -139,6 +152,7 @@ describe ImageOptim do
139
152
  it "should optimize datas" do
140
153
  optimized_images_datas = ImageOptim.optimize_images_data(TEST_IMAGES.map(&:read))
141
154
  TEST_IMAGES.zip(optimized_images_datas).each do |original, optimized_image_data|
155
+ optimized_image_data.should_not be_nil
142
156
  optimized_image_data.should == ImageOptim.optimize_image(original.temp_copy).open('rb', &:read)
143
157
  end
144
158
  end
@@ -165,6 +179,27 @@ describe ImageOptim do
165
179
  Tempfile.init_count.should == 0
166
180
  copy.read.should == original.read
167
181
  end
182
+
183
+ {
184
+ :png => "\211PNG\r\n\032\n",
185
+ :jpeg => "\377\330",
186
+ }.each do |type, data|
187
+ describe "broken #{type}" do
188
+ before do
189
+ ImageOptim::ImageMeta.should_receive(:warn)
190
+ end
191
+
192
+ it "should ignore path" do
193
+ path = FSPath.temp_file_path
194
+ path.write(data)
195
+ ImageOptim.optimize_image(path).should be_nil
196
+ end
197
+
198
+ it "should ignore data" do
199
+ ImageOptim.optimize_image_data(data).should be_nil
200
+ end
201
+ end
202
+ end
168
203
  end
169
204
 
170
205
  describe "optimize multiple" do
@@ -202,15 +237,38 @@ describe ImageOptim do
202
237
  end
203
238
  end
204
239
 
205
- describe "auto orienting" do
206
- original = ImageOptim::ImagePath.new(__FILE__).dirname / 'images/orient/original.jpg'
207
- ImageOptim::ImagePath.new(__FILE__).dirname.glob('images/orient/?.jpg').each do |jpg|
208
- it "should rotate #{jpg}" do
209
- image_optim = ImageOptim.new
210
- oriented = image_optim.optimize_image(jpg)
211
- nrmse = `compare -metric RMSE #{original.to_s.shellescape} #{oriented.to_s.shellescape} /dev/null 2>&1`[/\((\d+(\.\d+)?)\)/, 1]
212
- nrmse.should_not be_nil
213
- nrmse.to_f.should be < 0.005
240
+ describe "losslessness" do
241
+ rotated = ImageOptim::ImagePath.new(__FILE__).dirname / 'images/orient/original.jpg'
242
+ rotate_images = ImageOptim::ImagePath.new(__FILE__).dirname.glob('images/orient/?.jpg')
243
+
244
+ def flatten_animation(image)
245
+ if image.format == :gif
246
+ flattened = image.temp_path
247
+ system("convert #{image.to_s.shellescape} -coalesce -append #{flattened.to_s.shellescape}").should be_true
248
+ flattened
249
+ else
250
+ image
251
+ end
252
+ end
253
+
254
+ def check_lossless_optimization(original, optimized)
255
+ optimized.should_not be_nil
256
+ original = flatten_animation(original)
257
+ optimized = flatten_animation(optimized)
258
+ nrmse = `compare -metric RMSE #{original.to_s.shellescape} #{optimized.to_s.shellescape} /dev/null 2>&1`[/\((\d+(\.\d+)?)\)/, 1]
259
+ nrmse.should_not be_nil
260
+ nrmse.to_f.should == 0
261
+ end
262
+
263
+ rotate_images.each do |image|
264
+ it "should rotate and optimize #{image} losslessly" do
265
+ check_lossless_optimization(rotated, ImageOptim.optimize_image(image))
266
+ end
267
+ end
268
+
269
+ (TEST_IMAGES - rotate_images).each do |image|
270
+ it "should optimize #{image} losslessly" do
271
+ check_lossless_optimization(image, ImageOptim.optimize_image(image))
214
272
  end
215
273
  end
216
274
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: image_optim
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.1
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Kuchin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-03-10 00:00:00.000000000 Z
11
+ date: 2014-04-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fspath
@@ -110,10 +110,13 @@ files:
110
110
  - image_optim.gemspec
111
111
  - lib/image_optim.rb
112
112
  - lib/image_optim/bin_resolver.rb
113
+ - lib/image_optim/bin_resolver/comparable_condition.rb
114
+ - lib/image_optim/bin_resolver/simple_version.rb
113
115
  - lib/image_optim/config.rb
114
116
  - lib/image_optim/configuration_error.rb
115
117
  - lib/image_optim/handler.rb
116
118
  - lib/image_optim/hash_helpers.rb
119
+ - lib/image_optim/image_meta.rb
117
120
  - lib/image_optim/image_path.rb
118
121
  - lib/image_optim/option_definition.rb
119
122
  - lib/image_optim/option_helpers.rb
@@ -131,6 +134,8 @@ files:
131
134
  - lib/image_optim/worker/pngout.rb
132
135
  - lib/image_optim/worker/svgo.rb
133
136
  - script/update_worker_options_in_readme
137
+ - spec/image_optim/bin_resolver/comparable_condition_spec.rb
138
+ - spec/image_optim/bin_resolver/simple_version_spec.rb
134
139
  - spec/image_optim/bin_resolver_spec.rb
135
140
  - spec/image_optim/config_spec.rb
136
141
  - spec/image_optim/handler_spec.rb
@@ -184,6 +189,8 @@ specification_version: 4
184
189
  summary: Optimize (lossless compress) images (jpeg, png, gif, svg) using external
185
190
  utilities (advpng, gifsicle, jpegoptim, jpegtran, optipng, pngcrush, pngout, svgo)
186
191
  test_files:
192
+ - spec/image_optim/bin_resolver/comparable_condition_spec.rb
193
+ - spec/image_optim/bin_resolver/simple_version_spec.rb
187
194
  - spec/image_optim/bin_resolver_spec.rb
188
195
  - spec/image_optim/config_spec.rb
189
196
  - spec/image_optim/handler_spec.rb