diff --git a/.travis.yml b/.travis.yml index 15b20a96..86332380 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,12 +17,18 @@ script: before_install: - sudo apt-get update -qq - sudo apt-get install -qq advancecomp gifsicle jhead jpegoptim libjpeg-progs optipng pngcrush + - npm install -g svgo + - mkdir ~/bin + # pngquant: + - git clone git://github.com/pornel/pngquant.git + - pushd pngquant && git checkout $(git describe --tags --abbrev=0) && make && popd + - mv pngquant/pngquant ~/bin + # pngout: - wget http://static.jonof.id.au/dl/kenutils/pngout-20130221-linux.tar.gz - tar -xzf pngout-*-linux.tar.gz - - mv pngout-*-linux pngout-linux - - npm install -g svgo + - mv pngout-*-linux/x86_64/pngout ~/bin env: - - PATH=pngout-linux/x86_64:$PATH + - PATH=~/bin:$PATH matrix: allow_failures: - rvm: ruby-head diff --git a/README.markdown b/README.markdown index a995fcbe..47e0e578 100644 --- a/README.markdown +++ b/README.markdown @@ -18,6 +18,7 @@ Optimize (lossless compress) images (jpeg, png, gif, svg) using external utiliti * [optipng](http://optipng.sourceforge.net/) * [pngcrush](http://pmt.sourceforge.net/pngcrush/) * [pngout](http://www.advsys.net/ken/util/pngout.htm) +* [pngquant](http://pngquant.org/) * [svgo](https://github.com/svg/svgo) Based on [ImageOptim.app](http://imageoptim.com/). @@ -81,13 +82,15 @@ Besides permanently setting environment variables in `~/.profile`, `~/.bash_prof ### Linux - Debian/Ubuntu ```bash -sudo apt-get install -y advancecomp gifsicle jhead jpegoptim libjpeg-progs optipng pngcrush +sudo apt-get install -y advancecomp gifsicle jhead jpegoptim libjpeg-progs optipng pngcrush pngquant ``` +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/). + ### Linux - RHEL/Fedora/Centos ```bash -sudo yum install -y advancecomp gifsicle jhead libjpeg optipng +sudo yum install -y advancecomp gifsicle jhead libjpeg optipng pngquant ``` You may also need to install `libjpeg-turbo-utils` instead of `libjpeg`: @@ -127,13 +130,13 @@ make && cp -f pngcrush /usr/local/bin ### OS X: Macports ```bash -sudo port install advancecomp gifsicle jhead jpegoptim jpeg optipng pngcrush +sudo port install advancecomp gifsicle jhead jpegoptim jpeg optipng pngcrush pngquant ``` ### OS X: Brew ```bash -brew install advancecomp gifsicle jhead jpegoptim jpeg optipng pngcrush +brew install advancecomp gifsicle jhead jpegoptim jpeg optipng pngcrush pngquant ``` ### pngout installation (optional) @@ -256,6 +259,10 @@ Worker can be disabled by passing `false` instead of options hash. ### :advpng => * `:level` — Compression level: `0` - don't compress, `1` - fast, `2` - normal, `3` - extra, `4` - extreme *(defaults to `4`)* +### :pngquant => +* `: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`)* +* `:speed` — speed/quality trade-off: `1` - slow, `3` - default, `11` - fast & rough *(defaults to `3`)* + ### :jhead => Worker has no options diff --git a/bin/image_optim b/bin/image_optim index b4c0e93a..8bef697b 100755 --- a/bin/image_optim +++ b/bin/image_optim @@ -3,11 +3,13 @@ require 'image_optim/runner' require 'image_optim/true_false_nil' +require 'image_optim/non_negative_integer_range' options = {} option_parser = OptionParser.new do |op| ImageOptim::TrueFalseNil.add_to_option_parser(op) + ImageOptim::NonNegativeIntegerRange.add_to_option_parser(op) op.banner = <<-TEXT.gsub(/^\s*\|/, '') |#{ImageOptim.full_version} @@ -66,6 +68,8 @@ option_parser = OptionParser.new do |op| [Integer, 'N'] when Array >= type [Array, 'a,b,c'] + when ImageOptim::NonNegativeIntegerRange == type + [type, 'M-N'] else fail "Unknown type #{type}" end diff --git a/image_optim.gemspec b/image_optim.gemspec index 41679973..51ae79c2 100644 --- a/image_optim.gemspec +++ b/image_optim.gemspec @@ -3,7 +3,7 @@ Gem::Specification.new do |s| s.name = 'image_optim' s.version = '0.14.0' - s.summary = %q{Optimize (lossless compress) images (jpeg, png, gif, svg) using external utilities (advpng, gifsicle, jpegoptim, jpegtran, optipng, pngcrush, pngout, svgo)} + s.summary = %q{Optimize (lossless compress) images (jpeg, png, gif, svg) using external utilities (advpng, gifsicle, jpegoptim, jpegtran, optipng, pngcrush, pngout, pngquant, svgo)} s.homepage = "http://github.com/toy/#{s.name}" s.authors = ['Ivan Kuchin'] s.license = 'MIT' diff --git a/lib/image_optim.rb b/lib/image_optim.rb index 09b95f6f..05c12305 100644 --- a/lib/image_optim.rb +++ b/lib/image_optim.rb @@ -201,7 +201,7 @@ def apply_threading(enum) end %w[ - pngcrush pngout optipng advpng + pngcrush pngout optipng advpng pngquant jhead jpegoptim jpegtran gifsicle svgo diff --git a/lib/image_optim/bin_resolver.rb b/lib/image_optim/bin_resolver.rb index 70afa95d..ed2def0e 100644 --- a/lib/image_optim/bin_resolver.rb +++ b/lib/image_optim/bin_resolver.rb @@ -84,7 +84,7 @@ def accessible?(name) def version(name) case name.to_sym - when :advpng, :gifsicle, :jpegoptim, :optipng + when :advpng, :gifsicle, :jpegoptim, :optipng, :pngquant capture_output("#{name} --version")[/\d+(\.\d+){1,}/] when :svgo capture_output("#{name} --version 2>&1")[/\d+(\.\d+){1,}/] @@ -114,6 +114,13 @@ def check!(bin) when c = is < '1.17' warn "Note that `#{bin}` (#{c}) does not use zopfli" end + when :pngquant + case bin.version + when c = is < '2.0' + fail BadBinVersion, "`#{bin}` (#{c}) is not supported" + when c = is < '2.1' + warn "Note that `#{bin}` (#{c}) may be lossy even with quality `100-`" + end end end diff --git a/lib/image_optim/non_negative_integer_range.rb b/lib/image_optim/non_negative_integer_range.rb new file mode 100644 index 00000000..14608128 --- /dev/null +++ b/lib/image_optim/non_negative_integer_range.rb @@ -0,0 +1,11 @@ +class ImageOptim + # Denote range of non negative integers for worker option + class NonNegativeIntegerRange + # Add handling of range of non negative integers in OptionParser instance + def self.add_to_option_parser(option_parser) + option_parser.accept(self, /(\d+)(?:-|\.\.)(\d+)/) do |_, m, n| + m.to_i..n.to_i + end + end + end +end diff --git a/lib/image_optim/worker/pngquant.rb b/lib/image_optim/worker/pngquant.rb new file mode 100644 index 00000000..f85c1bda --- /dev/null +++ b/lib/image_optim/worker/pngquant.rb @@ -0,0 +1,43 @@ +require 'image_optim/worker' +require 'image_optim/option_helpers' +require 'image_optim/non_negative_integer_range' + +class ImageOptim + class Worker + # http://pngquant.org/ + class Pngquant < Worker + QUALITY_OPTION = + option(:quality, 100..100, NonNegativeIntegerRange, 'min..max - don\'t '\ + 'save below min, use less colors below max (both in range `0..100`; '\ + 'in yaml - `!ruby/range 0..100`)') do |v| + min = OptionHelpers.limit_with_range(v.begin, 0..100) + min..OptionHelpers.limit_with_range(v.end, min..100) + end + + SPEED_OPTION = + option(:speed, 3, 'speed/quality trade-off: '\ + '`1` - slow, '\ + '`3` - default, '\ + '`11` - fast & rough') do |v| + OptionHelpers.limit_with_range(v.to_i, 1..11) + end + + # Always run first + def run_order + -5 + end + + def optimize(src, dst) + args = %W[ + --quality=#{quality.begin}-#{quality.end} + --speed=#{speed} + --output=#{dst} + --force + -- + #{src} + ] + execute(:pngquant, *args) && optimized?(src, dst) + end + end + end +end diff --git a/spec/images/quant/64.png b/spec/images/quant/64.png new file mode 100644 index 00000000..0ae1e8d7 Binary files /dev/null and b/spec/images/quant/64.png differ diff --git a/spec/images/quant/generate b/spec/images/quant/generate new file mode 100755 index 00000000..1db4d47b --- /dev/null +++ b/spec/images/quant/generate @@ -0,0 +1,25 @@ +#!/usr/bin/env ruby + +Dir.chdir(File.dirname(__FILE__)) + +require 'shellwords' + +palettes = [64] +side = 256 + +palettes.each do |palette| + IO.popen(%W[ + convert + -depth 8 + -size #{side}x#{side} + -strip + rgb:- + PNG24:#{palette}.png + ].shelljoin, 'w') do |f| + (side * side).times do |i| + color = i * palette / (side * side) * 0x10000 / palette + f << [color / 0x100, color % 0x100, 0].pack('C*') + end + end + system "identify -format 'Wrote %f with %k unique colors\n' #{palette}.png" +end