image_optim 0.14.0 → 0.15.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
- 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