image_optim 0.12.1 → 0.13.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 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