image_optim 0.14.0 → 0.15.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
- MDYzNDg2MTY5NjdlNGIyY2U3NzY0Mzc5MTc1YzA3N2I3MGRmNDQ0Yw==
4
+ ODA5N2IyMjgwM2FjYTU3OGM4NjE2OTU0NzM5NGRhOTRiNGExNDM5Mw==
5
5
  data.tar.gz: !binary |-
6
- OTcyM2IxMWJjOTA4NDYyYTVkMjcwNTI4MDI5NzE0OTVmYjI5MzE0NQ==
6
+ YTNiZmYwOTk0OWY5NmE0OGM3OTliNzRmNTE5NDIwMGQ4OWM4NmMyMQ==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- N2ViYzljYTU3OGVkMzQ5OTNmNmYyM2RmZjdmZDZmN2E4MDA0MmJmNGI5MjFk
10
- Yzc1NDFjYWM0NjgxZTU0ZWQwMjc0MmVjNGM1MjEyYmQ3MzhmNTlmYWNkZjU3
11
- Mzc1Mjc2Y2M0NWNkZGM3OTFlN2ZjZDNiM2EwOGRmNjY0MmIxM2M=
9
+ YzI4NWY2MmU5Y2Y3YjJjMmExNzQ1NDk4NmE5NjhmOWFjMjVlZmY1NmQyMmM3
10
+ M2RmZTAzZWI1Mzg2MTQ3ZDQ4YzkyZjM5MGYxYjcwM2NiMTE3Mzg1ZmMwY2Vl
11
+ MmQwODgxMDkwYmVlOTE2ZjY1MTA2YzY4ODgxMzJmODFiN2FmMmU=
12
12
  data.tar.gz: !binary |-
13
- Y2M4Y2I4ZTNiYjZhYThmYThhNTQxYjQ4N2UxZDhhNmU4NjQ1MmI2NmY1YTdh
14
- MGI3YTI1YjRiZmIyYmQ5NGE4MDI2NmVjNGY1YWNmNmRkMDM3MzZhMGY3ZjQ5
15
- MjYwZmJhOWNkYTM3YWU3NTc4OTQwMThiZjhmY2RkY2Y5ZTc3ZmY=
13
+ NzViMzQ4Yjc0Y2MyYjdlNDBiNjM1N2U5ZmIzYTkzYzZlZjNkNjZlMzMxMDVk
14
+ NWEwYjk0MzEzMTU3NDdlNmZiZjlmMjMzYWM2ZjRjNGNlNjRmY2I2MTY2MjRm
15
+ YmFjNmE0ZTc2M2I1MGVlM2RiOTg1OTJmOGFhZjEwYzkxMjNiNTY=
data/.gitignore CHANGED
@@ -11,3 +11,5 @@ Makefile
11
11
  *.o
12
12
  *.bundle
13
13
  /tmp/
14
+
15
+ /analysis/
data/.travis.yml CHANGED
@@ -17,12 +17,18 @@ script:
17
17
  before_install:
18
18
  - sudo apt-get update -qq
19
19
  - sudo apt-get install -qq advancecomp gifsicle jhead jpegoptim libjpeg-progs optipng pngcrush
20
+ - npm install -g svgo
21
+ - mkdir ~/bin
22
+ # pngquant:
23
+ - git clone git://github.com/pornel/pngquant.git
24
+ - pushd pngquant && git checkout $(git describe --tags --abbrev=0) && make && popd
25
+ - mv pngquant/pngquant ~/bin
26
+ # pngout:
20
27
  - wget http://static.jonof.id.au/dl/kenutils/pngout-20130221-linux.tar.gz
21
28
  - tar -xzf pngout-*-linux.tar.gz
22
- - mv pngout-*-linux pngout-linux
23
- - npm install -g svgo
29
+ - mv pngout-*-linux/x86_64/pngout ~/bin
24
30
  env:
25
- - PATH=pngout-linux/x86_64:$PATH
31
+ - PATH=~/bin:$PATH
26
32
  matrix:
27
33
  allow_failures:
28
34
  - rvm: ruby-head
@@ -0,0 +1,9 @@
1
+ # Contributing
2
+
3
+ * Create topic/feature branch: `git checkout -b awesome-changes`
4
+ * Commit…
5
+ * Run tests: `bundle exec rspec`
6
+ * Check code style: `bundle exec rubocop`
7
+ * Push your branch: `git push origin awesome-changes`
8
+ * Create pull request
9
+ * Check if [travis is happy](https://travis-ci.org/toy/image_optim/pull_requests)
data/README.markdown CHANGED
@@ -1,9 +1,9 @@
1
- [![Gem Version](https://img.shields.io/gem/v/image_optim.svg)](https://rubygems.org/gems/image_optim)
2
- [![Build Status](https://img.shields.io/travis/toy/image_optim/master.svg)](https://travis-ci.org/toy/image_optim)
3
- [![Code Climate](https://img.shields.io/codeclimate/github/toy/image_optim.svg)](https://codeclimate.com/github/toy/image_optim)
4
- [![Dependency Status](https://img.shields.io/gemnasium/toy/image_optim.svg)](https://gemnasium.com/toy/image_optim)
5
- [![Inch CI](http://inch-ci.org/github/toy/image_optim.svg?branch=master)](http://inch-ci.org/github/toy/image_optim)
6
- [![Gittip](https://img.shields.io/gittip/toy.svg)](https://www.gittip.com/toy/)
1
+ [![Gem Version](https://img.shields.io/gem/v/image_optim.svg?style=flat)](https://rubygems.org/gems/image_optim)
2
+ [![Build Status](https://img.shields.io/travis/toy/image_optim/master.svg?style=flat)](https://travis-ci.org/toy/image_optim)
3
+ [![Code Climate](https://img.shields.io/codeclimate/github/toy/image_optim.svg?style=flat)](https://codeclimate.com/github/toy/image_optim)
4
+ [![Dependency Status](https://img.shields.io/gemnasium/toy/image_optim.svg?style=flat)](https://gemnasium.com/toy/image_optim)
5
+ [![Inch CI](http://inch-ci.org/github/toy/image_optim.svg?branch=master&style=flat)](http://inch-ci.org/github/toy/image_optim)
6
+ [![Gittip](https://img.shields.io/gittip/toy.svg?style=flat)](https://www.gittip.com/toy/)
7
7
 
8
8
  # image_optim
9
9
 
@@ -18,6 +18,7 @@ Optimize (lossless compress) images (jpeg, png, gif, svg) using external utiliti
18
18
  * [optipng](http://optipng.sourceforge.net/)
19
19
  * [pngcrush](http://pmt.sourceforge.net/pngcrush/)
20
20
  * [pngout](http://www.advsys.net/ken/util/pngout.htm)
21
+ * [pngquant](http://pngquant.org/)
21
22
  * [svgo](https://github.com/svg/svgo)
22
23
 
23
24
  Based on [ImageOptim.app](http://imageoptim.com/).
@@ -81,13 +82,15 @@ Besides permanently setting environment variables in `~/.profile`, `~/.bash_prof
81
82
  ### Linux - Debian/Ubuntu
82
83
 
83
84
  ```bash
84
- sudo apt-get install -y advancecomp gifsicle jhead jpegoptim libjpeg-progs optipng pngcrush
85
+ sudo apt-get install -y advancecomp gifsicle jhead jpegoptim libjpeg-progs optipng pngcrush pngquant
85
86
  ```
86
87
 
88
+ If you get an old version of `pngquant`, please check how to install up-to-date version or compile from source at [http://pngquant.org/](http://pngquant.org/).
89
+
87
90
  ### Linux - RHEL/Fedora/Centos
88
91
 
89
92
  ```bash
90
- sudo yum install -y advancecomp gifsicle jhead libjpeg optipng
93
+ sudo yum install -y advancecomp gifsicle jhead libjpeg optipng pngquant
91
94
  ```
92
95
 
93
96
  You may also need to install `libjpeg-turbo-utils` instead of `libjpeg`:
@@ -127,13 +130,13 @@ make && cp -f pngcrush /usr/local/bin
127
130
  ### OS X: Macports
128
131
 
129
132
  ```bash
130
- sudo port install advancecomp gifsicle jhead jpegoptim jpeg optipng pngcrush
133
+ sudo port install advancecomp gifsicle jhead jpegoptim jpeg optipng pngcrush pngquant
131
134
  ```
132
135
 
133
136
  ### OS X: Brew
134
137
 
135
138
  ```bash
136
- brew install advancecomp gifsicle jhead jpegoptim jpeg optipng pngcrush
139
+ brew install advancecomp gifsicle jhead jpegoptim jpeg optipng pngcrush pngquant
137
140
  ```
138
141
 
139
142
  ### pngout installation (optional)
@@ -238,24 +241,14 @@ optipng:
238
241
  Worker can be disabled by passing `false` instead of options hash.
239
242
 
240
243
  <!---<worker-options>-->
241
- <!-- worker options markdown is generated by `script/update_worker_options_in_readme` -->
242
-
243
- ### :pngcrush =>
244
- * `:chunks` — List of chunks to remove or `:alla` - all except tRNS/transparency or `:allb` - all except tRNS and gAMA/gamma *(defaults to `:alla`)*
245
- * `:fix` — Fix otherwise fatal conditions such as bad CRCs *(defaults to `false`)*
246
- * `:brute` — Brute force try all methods, very time-consuming and generally not worthwhile *(defaults to `false`)*
247
-
248
- ### :pngout =>
249
- * `:copy_chunks` — Copy optional chunks *(defaults to `false`)*
250
- * `:strategy` — Strategy: `0` - xtreme, `1` - intense, `2` - longest Match, `3` - huffman Only, `4` - uncompressed *(defaults to `0`)*
251
-
252
- ### :optipng =>
253
- * `:level` — Optimization level preset: `0` is least, `7` is best *(defaults to `6`)*
254
- * `:interlace` — Interlace, `true` - interlace on, `false` - interlace off, `nil` - as is in original image *(defaults to `false`)*
244
+ <!-- markdown for worker options is generated by `script/update_worker_options_in_readme` -->
255
245
 
256
246
  ### :advpng =>
257
247
  * `:level` — Compression level: `0` - don't compress, `1` - fast, `2` - normal, `3` - extra, `4` - extreme *(defaults to `4`)*
258
248
 
249
+ ### :gifsicle =>
250
+ * `:interlace` — Turn interlacing on *(defaults to `false`)*
251
+
259
252
  ### :jhead =>
260
253
  Worker has no options
261
254
 
@@ -268,14 +261,32 @@ Worker has no options
268
261
  * `:progressive` — Create progressive JPEG file *(defaults to `true`)*
269
262
  * `:jpegrescan` — Use jpegtran through jpegrescan, ignore progressive option *(defaults to `false`)*
270
263
 
271
- ### :gifsicle =>
272
- * `:interlace` — Turn interlacing on *(defaults to `false`)*
264
+ ### :optipng =>
265
+ * `:level` — Optimization level preset: `0` is least, `7` is best *(defaults to `6`)*
266
+ * `:interlace` — Interlace, `true` - interlace on, `false` - interlace off, `nil` - as is in original image *(defaults to `false`)*
267
+
268
+ ### :pngcrush =>
269
+ * `:chunks` — List of chunks to remove or `:alla` - all except tRNS/transparency or `:allb` - all except tRNS and gAMA/gamma *(defaults to `:alla`)*
270
+ * `:fix` — Fix otherwise fatal conditions such as bad CRCs *(defaults to `false`)*
271
+ * `:brute` — Brute force try all methods, very time-consuming and generally not worthwhile *(defaults to `false`)*
272
+
273
+ ### :pngout =>
274
+ * `:copy_chunks` — Copy optional chunks *(defaults to `false`)*
275
+ * `:strategy` — Strategy: `0` - xtreme, `1` - intense, `2` - longest Match, `3` - huffman Only, `4` - uncompressed *(defaults to `0`)*
276
+
277
+ ### :pngquant =>
278
+ * `:quality` — min..max - don't save below min, use less colors below max (both in range `0..100`; in yaml - `!ruby/range 0..100`) *(defaults to `100..100`)*
279
+ * `:speed` — speed/quality trade-off: `1` - slow, `3` - default, `11` - fast & rough *(defaults to `3`)*
273
280
 
274
281
  ### :svgo =>
275
282
  Worker has no options
276
283
 
277
284
  <!---</worker-options>-->
278
285
 
286
+ ## Contributing
287
+
288
+ If you would like to contribute - that is great and you are very welcome. Please check few notes about [CONTRIBUTING](CONTRIBUTING.markdown).
289
+
279
290
  ## Copyright
280
291
 
281
- Copyright (c) 2012-2014 Ivan Kuchin. See LICENSE.txt for details.
292
+ Copyright (c) 2012-2014 Ivan Kuchin. See [LICENSE.txt](LICENSE.txt) for details.
data/bin/image_optim CHANGED
@@ -2,11 +2,14 @@
2
2
  # encoding: UTF-8
3
3
 
4
4
  require 'image_optim/runner'
5
+ require 'image_optim/true_false_nil'
6
+ require 'image_optim/non_negative_integer_range'
5
7
 
6
8
  options = {}
7
9
 
8
10
  option_parser = OptionParser.new do |op|
9
11
  ImageOptim::TrueFalseNil.add_to_option_parser(op)
12
+ ImageOptim::NonNegativeIntegerRange.add_to_option_parser(op)
10
13
 
11
14
  op.banner = <<-TEXT.gsub(/^\s*\|/, '')
12
15
  |#{ImageOptim.full_version}
@@ -65,6 +68,8 @@ option_parser = OptionParser.new do |op|
65
68
  [Integer, 'N']
66
69
  when Array >= type
67
70
  [Array, 'a,b,c']
71
+ when ImageOptim::NonNegativeIntegerRange == type
72
+ [type, 'M-N']
68
73
  else
69
74
  fail "Unknown type #{type}"
70
75
  end
data/image_optim.gemspec CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'image_optim'
5
- s.version = '0.14.0'
6
- s.summary = %q{Optimize (lossless compress) images (jpeg, png, gif, svg) using external utilities (advpng, gifsicle, jpegoptim, jpegtran, optipng, pngcrush, pngout, svgo)}
5
+ s.version = '0.15.0'
6
+ s.summary = %q{Optimize (lossless compress) images (jpeg, png, gif, svg) using external utilities (advpng, gifsicle, jpegoptim, jpegtran, optipng, pngcrush, pngout, pngquant, svgo)}
7
7
  s.homepage = "http://github.com/toy/#{s.name}"
8
8
  s.authors = ['Ivan Kuchin']
9
9
  s.license = 'MIT'
@@ -18,8 +18,8 @@ Gem::Specification.new do |s|
18
18
  s.add_dependency 'fspath', '~> 2.1.0'
19
19
  s.add_dependency 'image_size', '~> 1.3.0'
20
20
  s.add_dependency 'exifr', '~> 1.1.3'
21
- s.add_dependency 'progress', '~> 3.0.0'
22
- s.add_dependency 'in_threads', '~> 1.2.0'
21
+ s.add_dependency 'progress', '~> 3.0.1'
22
+ s.add_dependency 'in_threads', '~> 1.2.2'
23
23
  s.add_development_dependency 'rspec', '~> 3.0'
24
24
  if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('1.9.2')
25
25
  s.add_development_dependency 'rubocop', '~> 0.24.1'
data/lib/image_optim.rb CHANGED
@@ -68,15 +68,15 @@ class ImageOptim
68
68
  def optimize_image(original)
69
69
  original = ImagePath.convert(original)
70
70
  return unless (workers = workers_for_image(original))
71
- handler = Handler.new(original)
72
- workers.each do |worker|
73
- handler.process do |src, dst|
74
- worker.optimize(src, dst)
71
+ result = Handler.for(original) do |handler|
72
+ workers.each do |worker|
73
+ handler.process do |src, dst|
74
+ worker.optimize(src, dst)
75
+ end
75
76
  end
76
77
  end
77
- handler.cleanup
78
- return unless handler.result
79
- ImagePath::Optimized.new(handler.result, original)
78
+ return unless result
79
+ ImagePath::Optimized.new(result, original)
80
80
  end
81
81
 
82
82
  # Optimize one file in place, return original as OptimizedImagePath or nil if
@@ -106,7 +106,7 @@ class ImageOptim
106
106
  # Optimize multiple images
107
107
  # if block given yields path and result for each image and returns array of
108
108
  # yield results
109
- # else return array of results
109
+ # else return array of path and result pairs
110
110
  def optimize_images(paths, &block)
111
111
  run_method_for(paths, :optimize_image, &block)
112
112
  end
@@ -114,7 +114,7 @@ class ImageOptim
114
114
  # Optimize multiple images in place
115
115
  # if block given yields path and result for each image and returns array of
116
116
  # yield results
117
- # else return array of results
117
+ # else return array of path and result pairs
118
118
  def optimize_images!(paths, &block)
119
119
  run_method_for(paths, :optimize_image!, &block)
120
120
  end
@@ -122,7 +122,7 @@ class ImageOptim
122
122
  # Optimize multiple image datas
123
123
  # if block given yields original and result for each image data and returns
124
124
  # array of yield results
125
- # else return array of results
125
+ # else return array of path and result pairs
126
126
  def optimize_images_data(datas, &block)
127
127
  run_method_for(datas, :optimize_image_data, &block)
128
128
  end
@@ -178,14 +178,17 @@ private
178
178
  by_format.each{ |_format, workers| workers.sort! }
179
179
  end
180
180
 
181
- # Run method for each path and yield each path and result if block given
182
- def run_method_for(paths, method_name, &block)
183
- apply_threading(paths).map do |path|
184
- result = send(method_name, path)
181
+ # Run method for each item in list
182
+ # if block given yields item and result for item and returns array of yield
183
+ # results
184
+ # else return array of item and result pairs
185
+ def run_method_for(list, method_name, &block)
186
+ apply_threading(list).map do |item|
187
+ result = send(method_name, item)
185
188
  if block
186
- block.call(path, result)
189
+ block.call(item, result)
187
190
  else
188
- result
191
+ [item, result]
189
192
  end
190
193
  end
191
194
  end
@@ -201,7 +204,7 @@ private
201
204
  end
202
205
 
203
206
  %w[
204
- pngcrush pngout optipng advpng
207
+ pngcrush pngout advpng optipng pngquant
205
208
  jhead jpegoptim jpegtran
206
209
  gifsicle
207
210
  svgo
@@ -4,15 +4,16 @@ require 'image_optim/bin_resolver/simple_version'
4
4
  require 'image_optim/bin_resolver/comparable_condition'
5
5
 
6
6
  class ImageOptim
7
- class BinNotFoundError < StandardError; end
8
- class BadBinVersion < StandardError; end
9
-
10
7
  # Handles resolving binaries and checking versions
11
8
  #
12
9
  # If there is an environment variable XXX_BIN when resolbing xxx, then a
13
10
  # symlink to binary will be created in a temporary directory which will be
14
11
  # added to PATH
15
12
  class BinResolver
13
+ class Error < StandardError; end
14
+ class BinNotFound < Error; end
15
+ class BadBinVersion < Error; end
16
+
16
17
  # Holds name and version of an executable
17
18
  class Bin
18
19
  attr_reader :name, :version
@@ -47,7 +48,7 @@ class ImageOptim
47
48
  if @bins[name]
48
49
  check!(@bins[name])
49
50
  else
50
- fail BinNotFoundError, "`#{name}` not found"
51
+ fail BinNotFound, "`#{name}` not found"
51
52
  end
52
53
  end
53
54
 
@@ -84,7 +85,7 @@ class ImageOptim
84
85
 
85
86
  def version(name)
86
87
  case name.to_sym
87
- when :advpng, :gifsicle, :jpegoptim, :optipng
88
+ when :advpng, :gifsicle, :jpegoptim, :optipng, :pngquant
88
89
  capture_output("#{name} --version")[/\d+(\.\d+){1,}/]
89
90
  when :svgo
90
91
  capture_output("#{name} --version 2>&1")[/\d+(\.\d+){1,}/]
@@ -114,6 +115,13 @@ class ImageOptim
114
115
  when c = is < '1.17'
115
116
  warn "Note that `#{bin}` (#{c}) does not use zopfli"
116
117
  end
118
+ when :pngquant
119
+ case bin.version
120
+ when c = is < '2.0'
121
+ fail BadBinVersion, "`#{bin}` (#{c}) is not supported"
122
+ when c = is < '2.1'
123
+ warn "Note that `#{bin}` (#{c}) may be lossy even with quality `100-`"
124
+ end
117
125
  end
118
126
  end
119
127
 
@@ -5,16 +5,23 @@ class ImageOptim
5
5
  class SimpleVersion
6
6
  include Comparable
7
7
 
8
+ # Numbers extracted from version string
8
9
  attr_reader :parts
10
+
11
+ # Initialize with a string or an object convertible to string
12
+ #
13
+ # SimpleVersion.new('2.0.1') <=> SimpleVersion.new(2)
9
14
  def initialize(str)
10
15
  @str = String(str)
11
16
  @parts = @str.split('.').map(&:to_i).reverse.drop_while(&:zero?).reverse
12
17
  end
13
18
 
19
+ # Returns original version string
14
20
  def to_s
15
21
  @str
16
22
  end
17
23
 
24
+ # Compare version parts of self with other
18
25
  def <=>(other)
19
26
  other = self.class.new(other) unless other.is_a?(self.class)
20
27
  parts <=> other.parts
@@ -16,6 +16,22 @@ class ImageOptim
16
16
  @result = nil
17
17
  end
18
18
 
19
+ # with no associated block, works as new. Otherwise creates instance and
20
+ # passes it to block, runs cleanup and returns result of handler
21
+ def self.for(original)
22
+ handler = new(original)
23
+ if block_given?
24
+ begin
25
+ yield handler
26
+ handler.result
27
+ ensure
28
+ handler.cleanup
29
+ end
30
+ else
31
+ handler
32
+ end
33
+ end
34
+
19
35
  # Yields two paths, one to latest successful result or original, second to
20
36
  # temp path
21
37
  def process
@@ -0,0 +1,11 @@
1
+ class ImageOptim
2
+ # Denote range of non negative integers for worker option
3
+ class NonNegativeIntegerRange
4
+ # Add handling of range of non negative integers in OptionParser instance
5
+ def self.add_to_option_parser(option_parser)
6
+ option_parser.accept(self, /(\d+)(?:-|\.\.)(\d+)/) do |_, m, n|
7
+ m.to_i..n.to_i
8
+ end
9
+ end
10
+ end
11
+ end
@@ -4,11 +4,17 @@ class ImageOptim
4
4
  # Adds image_optim as preprocessor for gif, jpeg, png and svg images
5
5
  class Railtie < Rails::Railtie
6
6
  initializer 'image_optim.initializer' do |app|
7
+ register_preprocessor(app) if register_preprocessor?(app)
8
+ end
9
+
10
+ def register_preprocessor?(app)
11
+ return if app.config.assets.compress == false
12
+ return if app.config.assets.image_optim == false
7
13
 
8
- break if app.config.assets.compress == false
9
- break if app.config.assets.image_optim == false
10
- break unless app.assets
14
+ app.assets
15
+ end
11
16
 
17
+ def register_preprocessor(app)
12
18
  options = if app.config.assets.image_optim == true
13
19
  {}
14
20
  else
@@ -25,7 +31,6 @@ class ImageOptim
25
31
  app.assets.register_preprocessor 'image/jpeg', :image_optim, &processor
26
32
  app.assets.register_preprocessor 'image/png', :image_optim, &processor
27
33
  app.assets.register_preprocessor 'image/svg+xml', :image_optim, &processor
28
-
29
34
  end
30
35
  end
31
36
  end
@@ -1,6 +1,5 @@
1
1
  require 'image_optim'
2
2
  require 'image_optim/hash_helpers'
3
- require 'image_optim/true_false_nil'
4
3
  require 'image_optim/space'
5
4
  require 'progress'
6
5
  require 'optparse'
@@ -56,7 +55,7 @@ class ImageOptim
56
55
  unless @to_optimize.empty?
57
56
  results = Results.new
58
57
 
59
- optimize_images! do |original, optimized|
58
+ optimize_images!.each do |original, optimized|
60
59
  results.add(original, optimized)
61
60
  end
62
61
 
@@ -13,7 +13,7 @@ class ImageOptim
13
13
  when 0, nil
14
14
  EMPTY_SPACE
15
15
  else
16
- log_denominator = Math.log(size) / Math.log(BASE)
16
+ log_denominator = Math.log(size.abs) / Math.log(BASE)
17
17
  degree = [log_denominator.floor, SIZE_SYMBOLS.length - 1].min
18
18
  number_string = if degree == 0
19
19
  size.to_s
@@ -7,5 +7,10 @@ class ImageOptim
7
7
  completing = OptionParser.top.atype[TrueClass][0].merge('nil' => nil)
8
8
  option_parser.accept(self, completing){ |_arg, val| val }
9
9
  end
10
+
11
+ # Convert everything truthy to `true`, leave `false` and `nil` as is
12
+ def self.convert(v)
13
+ v && true
14
+ end
10
15
  end
11
16
  end
@@ -1,5 +1,6 @@
1
1
  # encoding: UTF-8
2
2
 
3
+ require 'image_optim/configuration_error'
3
4
  require 'image_optim/option_definition'
4
5
  require 'image_optim/option_helpers'
5
6
  require 'shellwords'
@@ -40,6 +41,9 @@ class ImageOptim
40
41
 
41
42
  # Configure (raises on extra options)
42
43
  def initialize(image_optim, options = {})
44
+ unless image_optim.is_a?(ImageOptim)
45
+ fail ArgumentError, 'first parameter should be an ImageOptim instance'
46
+ end
43
47
  @image_optim = image_optim
44
48
  self.class.option_definitions.each do |option_definition|
45
49
  value = if options.key?(option_definition.name)
@@ -98,6 +102,10 @@ class ImageOptim
98
102
  # Forward bin resolving to image_optim
99
103
  def resolve_bin!(bin)
100
104
  @image_optim.resolve_bin!(bin)
105
+ rescue BinResolver::Error => e
106
+ name = self.class.bin_sym
107
+ raise e, "#{name} worker: #{e.message}; please provide proper binary or "\
108
+ "disable this worker (`:#{name} => false`)", e.backtrace
101
109
  end
102
110
 
103
111
  # Run command setting priority and hiding output
@@ -18,8 +18,7 @@ class ImageOptim
18
18
  '`true` - interlace on, '\
19
19
  '`false` - interlace off, '\
20
20
  '`nil` - as is in original image') do |v|
21
- # convert everything truthy to `true`, leave `false` and `nil` as is
22
- v && true
21
+ TrueFalseNil.convert(v)
23
22
  end
24
23
 
25
24
  def optimize(src, dst)
@@ -0,0 +1,43 @@
1
+ require 'image_optim/worker'
2
+ require 'image_optim/option_helpers'
3
+ require 'image_optim/non_negative_integer_range'
4
+
5
+ class ImageOptim
6
+ class Worker
7
+ # http://pngquant.org/
8
+ class Pngquant < Worker
9
+ QUALITY_OPTION =
10
+ option(:quality, 100..100, NonNegativeIntegerRange, 'min..max - don\'t '\
11
+ 'save below min, use less colors below max (both in range `0..100`; '\
12
+ 'in yaml - `!ruby/range 0..100`)') do |v|
13
+ min = OptionHelpers.limit_with_range(v.begin, 0..100)
14
+ min..OptionHelpers.limit_with_range(v.end, min..100)
15
+ end
16
+
17
+ SPEED_OPTION =
18
+ option(:speed, 3, 'speed/quality trade-off: '\
19
+ '`1` - slow, '\
20
+ '`3` - default, '\
21
+ '`11` - fast & rough') do |v|
22
+ OptionHelpers.limit_with_range(v.to_i, 1..11)
23
+ end
24
+
25
+ # Always run first
26
+ def run_order
27
+ -5
28
+ end
29
+
30
+ def optimize(src, dst)
31
+ args = %W[
32
+ --quality=#{quality.begin}-#{quality.end}
33
+ --speed=#{speed}
34
+ --output=#{dst}
35
+ --force
36
+ --
37
+ #{src}
38
+ ]
39
+ execute(:pngquant, *args) && optimized?(src, dst)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -33,7 +33,7 @@ def write_marked(io)
33
33
  io.puts GENERATED_NOTE
34
34
  io.puts
35
35
 
36
- ImageOptim::Worker.klasses.each do |klass|
36
+ ImageOptim::Worker.klasses.sort_by(&:name).each do |klass|
37
37
  write_worker_options(io, klass)
38
38
  end
39
39
 
@@ -71,7 +71,7 @@ describe ImageOptim::BinResolver do
71
71
  5.times do
72
72
  expect do
73
73
  resolver.resolve!(:should_not_exist)
74
- end.to raise_error ImageOptim::BinNotFoundError
74
+ end.to raise_error ImageOptim::BinResolver::BinNotFound
75
75
  end
76
76
  expect(resolver.env_path).to eq([
77
77
  ENV['PATH'],
@@ -103,7 +103,7 @@ describe ImageOptim::BinResolver do
103
103
  5.times do
104
104
  expect do
105
105
  resolver.resolve!(:should_not_exist)
106
- end.to raise_error ImageOptim::BinNotFoundError
106
+ end.to raise_error ImageOptim::BinResolver::BinNotFound
107
107
  end
108
108
  expect(resolver.env_path).to eq([
109
109
  tmpdir,
@@ -139,7 +139,7 @@ describe ImageOptim::BinResolver do
139
139
  5.times do
140
140
  expect do
141
141
  resolver.resolve!(:pngcrush)
142
- end.to raise_error ImageOptim::BadBinVersion
142
+ end.to raise_error ImageOptim::BinResolver::BadBinVersion
143
143
  end
144
144
  expect(resolver.env_path).to eq([
145
145
  ENV['PATH'],
@@ -56,4 +56,37 @@ describe ImageOptim::Handler do
56
56
  handler.cleanup
57
57
  handler.cleanup
58
58
  end
59
+
60
+ describe :open do
61
+ it 'should yield instance, run cleanup and return result' do
62
+ original = double
63
+ handler = double
64
+ result = double
65
+
66
+ expect(ImageOptim::Handler).to receive(:new).
67
+ with(original).and_return(handler)
68
+ expect(handler).to receive(:process)
69
+ expect(handler).to receive(:cleanup)
70
+ expect(handler).to receive(:result).and_return(result)
71
+
72
+ expect(ImageOptim::Handler.for(original) do |h|
73
+ h.process
74
+ end).to eq(result)
75
+ end
76
+
77
+ it 'should cleanup if exception is raised' do
78
+ original = double
79
+ handler = double
80
+
81
+ expect(ImageOptim::Handler).to receive(:new).
82
+ with(original).and_return(handler)
83
+ expect(handler).to receive(:cleanup)
84
+
85
+ expect do
86
+ ImageOptim::Handler.for(original) do
87
+ fail 'hello'
88
+ end
89
+ end.to raise_error 'hello'
90
+ end
91
+ end
59
92
  end
@@ -9,8 +9,10 @@ describe ImageOptim::Worker do
9
9
  it 'should raise NotImplementedError unless overriden' do
10
10
  class Abc < ImageOptim::Worker; end
11
11
 
12
+ image_optim = ImageOptim.new
13
+
12
14
  expect do
13
- Abc.new({}).optimize(double, double)
15
+ Abc.new(image_optim, {}).optimize(double, double)
14
16
  end.to raise_error NotImplementedError
15
17
  end
16
18
  end
@@ -47,9 +47,10 @@ describe ImageOptim do
47
47
 
48
48
  describe 'workers' do
49
49
  it 'should be ordered by run_order' do
50
+ image_optim = ImageOptim.new
50
51
  original_klasses = ImageOptim::Worker.klasses
51
52
  formats = original_klasses.map do |klass|
52
- klass.new({}).image_formats
53
+ klass.new(image_optim, {}).image_formats
53
54
  end.flatten.uniq
54
55
 
55
56
  [
@@ -62,7 +63,7 @@ describe ImageOptim do
62
63
  image_optim = ImageOptim.new
63
64
 
64
65
  formats.each do |format|
65
- path = ImagePath.new("test.#{format}")
66
+ path = ImageOptim::ImagePath.new("test.#{format}")
66
67
  expect(path).to receive(:format).and_return(format)
67
68
 
68
69
  workers = image_optim.workers_for_image(path)
@@ -73,12 +74,14 @@ describe ImageOptim do
73
74
  end
74
75
 
75
76
  describe 'worker' do
77
+ image_optim = ImageOptim.new
78
+
76
79
  base_options = Hash[ImageOptim::Worker.klasses.map do |klass|
77
80
  [klass.bin_sym, false]
78
81
  end]
79
82
 
80
83
  real_workers = ImageOptim::Worker.klasses.reject do |klass|
81
- klass.new({}).image_formats.empty?
84
+ klass.new(image_optim, {}).image_formats.empty?
82
85
  end
83
86
 
84
87
  real_workers.each do |worker_klass|
@@ -171,32 +174,37 @@ describe ImageOptim do
171
174
  describe 'bunch' do
172
175
  it 'should optimize' do
173
176
  copies = TEST_IMAGES.map(&:temp_copy)
174
- optimized_images = ImageOptim.optimize_images(copies)
175
- zipped = TEST_IMAGES.zip(copies, optimized_images)
176
- zipped.each do |original, copy, optimized_image|
177
- expect(optimized_image).to be_a(ImageOptim::ImagePath::Optimized)
178
- expect(optimized_image.size).to be_in_range(1...original.size)
179
- expect(optimized_image.read).not_to eq(original.read)
177
+ results = ImageOptim.optimize_images(copies)
178
+ zipped = TEST_IMAGES.zip(copies, results)
179
+ zipped.each do |original, copy, result|
180
+ expect(result[0]).to eq(copy)
181
+ expect(result[1]).to be_a(ImageOptim::ImagePath::Optimized)
182
+ expect(result[1].size).to be_in_range(1...original.size)
180
183
  expect(copy.read).to eq(original.read)
181
184
  end
182
185
  end
183
186
 
184
187
  it 'should optimize in place' do
185
188
  copies = TEST_IMAGES.map(&:temp_copy)
186
- ImageOptim.optimize_images!(copies)
187
- TEST_IMAGES.zip(copies).each do |original, copy|
189
+ results = ImageOptim.optimize_images!(copies)
190
+ zipped = TEST_IMAGES.zip(copies, results)
191
+ zipped.each do |original, copy, result|
192
+ expect(result[0]).to eq(copy)
193
+ expect(result[1]).to be_a(ImageOptim::ImagePath::Optimized)
188
194
  expect(copy.size).to be_in_range(1...original.size)
189
- expect(copy.read).not_to eq(original.read)
190
195
  end
191
196
  end
192
197
 
193
198
  it 'should optimize datas' do
194
- optimized_datas = ImageOptim.optimize_images_data(TEST_IMAGES.map(&:read))
195
- TEST_IMAGES.zip(optimized_datas).each do |original, optimized_data|
196
- expect(optimized_data).not_to be_nil
199
+ results = ImageOptim.optimize_images_data(TEST_IMAGES.map(&:read))
200
+ zipped = TEST_IMAGES.zip(results)
201
+ zipped.each do |original, result|
202
+ expect(result[0]).to eq(original.read)
203
+ expect(result[1]).to be_a(String)
204
+ expect(result[1].size).to be_in_range(1...original.size)
197
205
 
198
206
  expected_path = ImageOptim.optimize_image(original.temp_copy)
199
- expect(optimized_data).to eq(expected_path.open('rb', &:read))
207
+ expect(result[1]).to eq(expected_path.open('rb', &:read))
200
208
  end
201
209
  end
202
210
  end
@@ -259,7 +267,7 @@ describe ImageOptim do
259
267
  expect(image_optim).to receive(method).with(src).and_return(dst)
260
268
  dst
261
269
  end
262
- expect(image_optim.send(list_method, srcs)).to eq(dsts)
270
+ expect(image_optim.send(list_method, srcs)).to eq(srcs.zip(dsts))
263
271
  end
264
272
  end
265
273
 
@@ -1,5 +1,7 @@
1
1
  #!/bin/sh
2
2
 
3
+ cd "$(dirname "$0")"
4
+
3
5
  convert -size 2x2 xc:black -fill '#f00' -draw 'point 0,1' -fill '#0f0' -draw 'point 1,0' -fill '#00f' -draw 'point 1,1' -scale 64x64 original.png
4
6
 
5
7
  convert original.png 0.jpg
Binary file
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ Dir.chdir(File.dirname(__FILE__))
4
+
5
+ require 'shellwords'
6
+
7
+ palettes = [64]
8
+ side = 256
9
+
10
+ palettes.each do |palette|
11
+ IO.popen(%W[
12
+ convert
13
+ -depth 8
14
+ -size #{side}x#{side}
15
+ -strip
16
+ rgb:-
17
+ PNG24:#{palette}.png
18
+ ].shelljoin, 'w') do |f|
19
+ (side * side).times do |i|
20
+ color = i * palette / (side * side) * 0x10000 / palette
21
+ f << [color / 0x100, color % 0x100, 0].pack('C*')
22
+ end
23
+ end
24
+ system "identify -format 'Wrote %f with %k unique colors\n' #{palette}.png"
25
+ 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.14.0
4
+ version: 0.15.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-07-16 00:00:00.000000000 Z
11
+ date: 2014-08-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fspath
@@ -58,28 +58,28 @@ dependencies:
58
58
  requirements:
59
59
  - - ~>
60
60
  - !ruby/object:Gem::Version
61
- version: 3.0.0
61
+ version: 3.0.1
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - ~>
67
67
  - !ruby/object:Gem::Version
68
- version: 3.0.0
68
+ version: 3.0.1
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: in_threads
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ~>
74
74
  - !ruby/object:Gem::Version
75
- version: 1.2.0
75
+ version: 1.2.2
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - ~>
81
81
  - !ruby/object:Gem::Version
82
- version: 1.2.0
82
+ version: 1.2.2
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: rspec
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -118,6 +118,7 @@ files:
118
118
  - .gitignore
119
119
  - .rubocop.yml
120
120
  - .travis.yml
121
+ - CONTRIBUTING.markdown
121
122
  - Gemfile
122
123
  - LICENSE.txt
123
124
  - README.markdown
@@ -133,6 +134,7 @@ files:
133
134
  - lib/image_optim/hash_helpers.rb
134
135
  - lib/image_optim/image_meta.rb
135
136
  - lib/image_optim/image_path.rb
137
+ - lib/image_optim/non_negative_integer_range.rb
136
138
  - lib/image_optim/option_definition.rb
137
139
  - lib/image_optim/option_helpers.rb
138
140
  - lib/image_optim/railtie.rb
@@ -148,6 +150,7 @@ files:
148
150
  - lib/image_optim/worker/optipng.rb
149
151
  - lib/image_optim/worker/pngcrush.rb
150
152
  - lib/image_optim/worker/pngout.rb
153
+ - lib/image_optim/worker/pngquant.rb
151
154
  - lib/image_optim/worker/svgo.rb
152
155
  - script/update_worker_options_in_readme
153
156
  - spec/image_optim/bin_resolver/comparable_condition_spec.rb
@@ -176,6 +179,8 @@ files:
176
179
  - spec/images/orient/8.jpg
177
180
  - spec/images/orient/generate
178
181
  - spec/images/orient/original.jpg
182
+ - spec/images/quant/64.png
183
+ - spec/images/quant/generate
179
184
  - spec/images/test.svg
180
185
  - spec/images/transparency1.png
181
186
  - spec/images/transparency2.png
@@ -205,7 +210,8 @@ rubygems_version: 2.0.3
205
210
  signing_key:
206
211
  specification_version: 4
207
212
  summary: Optimize (lossless compress) images (jpeg, png, gif, svg) using external
208
- utilities (advpng, gifsicle, jpegoptim, jpegtran, optipng, pngcrush, pngout, svgo)
213
+ utilities (advpng, gifsicle, jpegoptim, jpegtran, optipng, pngcrush, pngout, pngquant,
214
+ svgo)
209
215
  test_files:
210
216
  - spec/image_optim/bin_resolver/comparable_condition_spec.rb
211
217
  - spec/image_optim/bin_resolver/simple_version_spec.rb
@@ -233,6 +239,8 @@ test_files:
233
239
  - spec/images/orient/8.jpg
234
240
  - spec/images/orient/generate
235
241
  - spec/images/orient/original.jpg
242
+ - spec/images/quant/64.png
243
+ - spec/images/quant/generate
236
244
  - spec/images/test.svg
237
245
  - spec/images/transparency1.png
238
246
  - spec/images/transparency2.png