From 3ed04a088bf9325bd459e0c1ae19bf41aab1ff66 Mon Sep 17 00:00:00 2001 From: Blake Erickson Date: Fri, 19 Feb 2021 16:28:22 -0700 Subject: [PATCH] Add support for Oxipng Based on https://github.com/toy/image_optim/issues/167 Oxipng is a multi-threaded rust implementation of Optipng. https://github.com/shssoichiro/oxipng --- CHANGELOG.markdown | 2 + lib/image_optim.rb | 2 +- lib/image_optim/bin_resolver/bin.rb | 2 +- lib/image_optim/worker/oxipng.rb | 52 +++++++++++++++ spec/image_optim/worker/oxipng_spec.rb | 89 ++++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 lib/image_optim/worker/oxipng.rb create mode 100644 spec/image_optim/worker/oxipng_spec.rb diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown index dfb57221..76b0a16b 100644 --- a/CHANGELOG.markdown +++ b/CHANGELOG.markdown @@ -2,6 +2,8 @@ ## unreleased +* Add support for Oxipng [#167](https://github.com/toy/image_optim/issues/167) [@oblakeerickson](https://github.com/oblakeerickson) + ## v0.28.0 (2020-12-18) * Fix and update list of markers in jpegoptim worker: allow to pass `com` instead of incorrect `comments` and add missing `xmp` and `none` [#188](https://github.com/toy/image_optim/issues/188) [@toy](https://github.com/toy) diff --git a/lib/image_optim.rb b/lib/image_optim.rb index b20a3f04..af087a29 100644 --- a/lib/image_optim.rb +++ b/lib/image_optim.rb @@ -12,7 +12,7 @@ require 'shellwords' %w[ - pngcrush pngout advpng optipng pngquant + pngcrush pngout advpng optipng pngquant oxipng jhead jpegoptim jpegrecompress jpegtran gifsicle svgo diff --git a/lib/image_optim/bin_resolver/bin.rb b/lib/image_optim/bin_resolver/bin.rb index ebca9bce..22537a2c 100644 --- a/lib/image_optim/bin_resolver/bin.rb +++ b/lib/image_optim/bin_resolver/bin.rb @@ -109,7 +109,7 @@ def version_string case name when :advpng capture("#{escaped_path} --version 2> #{Path::NULL}")[/\bv(\d+(\.\d+)+|none)/, 1] - when :gifsicle, :jpegoptim, :optipng + when :gifsicle, :jpegoptim, :optipng, :oxipng capture("#{escaped_path} --version 2> #{Path::NULL}")[/\d+(\.\d+)+/] when :svgo, :pngquant capture("#{escaped_path} --version 2>&1")[/\d+(\.\d+)+/] diff --git a/lib/image_optim/worker/oxipng.rb b/lib/image_optim/worker/oxipng.rb new file mode 100644 index 00000000..83a02198 --- /dev/null +++ b/lib/image_optim/worker/oxipng.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'image_optim/worker' +require 'image_optim/option_helpers' +require 'image_optim/true_false_nil' + +class ImageOptim + class Worker + # https://github.com/shssoichiro/oxipng + class Oxipng < Worker + LEVEL_OPTION = + option(:level, 3, 'Optimization level preset: '\ + '`0` is least, '\ + '`6` is best') do |v| + OptionHelpers.limit_with_range(v.to_i, 0..6) + end + + INTERLACE_OPTION = + option(:interlace, false, TrueFalseNil, 'Interlace: '\ + '`true` - interlace on, '\ + '`false` - interlace off, '\ + '`nil` - as is in original image') do |v| + TrueFalseNil.convert(v) + end + + STRIP_OPTION = + option(:strip, true, 'Remove all auxiliary chunks'){ |v| !!v } + + def run_order + -4 + end + + def optimize(src, dst) + src.copy(dst) + args = %W[ + -o #{level} + --quiet + #{dst} + ] + args.unshift "-i#{interlace ? 1 : 0}" unless interlace.nil? + if strip + args.unshift '--strip', 'all' + end + execute(:oxipng, *args) && optimized?(src, dst) + end + + def optimized?(src, dst) + interlace ? dst.size? : super + end + end + end +end diff --git a/spec/image_optim/worker/oxipng_spec.rb b/spec/image_optim/worker/oxipng_spec.rb new file mode 100644 index 00000000..bf61ab18 --- /dev/null +++ b/spec/image_optim/worker/oxipng_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'image_optim/worker/oxipng' +require 'image_optim/path' + +describe ImageOptim::Worker::Oxipng do + describe 'strip option' do + subject{ described_class.new(ImageOptim.new, options) } + + let(:options){ {} } + let(:src){ instance_double(ImageOptim::Path, :copy => nil) } + let(:dst){ instance_double(ImageOptim::Path) } + + before do + oxipng_bin = instance_double(ImageOptim::BinResolver::Bin) + allow(subject).to receive(:resolve_bin!). + with(:oxipng).and_return(oxipng_bin) + + allow(subject).to receive(:optimized?) + end + + context 'by default' do + it 'should add --strip all to arguments' do + expect(subject).to receive(:execute) do |_bin, *args| + expect(args.join(' ')).to match(/(^| )--strip all($| )/) + end + + subject.optimize(src, dst) + end + end + + context 'when strip is disabled' do + let(:options){ {:strip => false} } + + it 'should not add --strip all to arguments' do + expect(subject).to receive(:execute) do |_bin, *args| + expect(args.join(' ')).not_to match(/(^| )--strip all($| )/) + end + + subject.optimize(src, dst) + end + end + end + + describe '#optimized?' do + let(:src){ instance_double(ImageOptim::Path, src_options) } + let(:dst){ instance_double(ImageOptim::Path, dst_options) } + let(:src_options){ {:size => 10} } + let(:dst_options){ {:size? => 9} } + let(:instance){ described_class.new(ImageOptim.new, instance_options) } + let(:instance_options){ {} } + + subject{ instance.optimized?(src, dst) } + + context 'when interlace option is enabled' do + let(:instance_options){ {:interlace => true} } + + context 'when dst is empty' do + let(:dst_options){ {:size? => nil} } + it{ is_expected.to be_falsy } + end + + context 'when dst is not empty' do + let(:dst_options){ {:size? => 20} } + it{ is_expected.to be_truthy } + end + end + + context 'when interlace option is disabled' do + let(:instance_options){ {:interlace => false} } + + context 'when dst is empty' do + let(:dst_options){ {:size? => nil} } + it{ is_expected.to be_falsy } + end + + context 'when dst is greater than or equal to src' do + let(:dst_options){ {:size? => 10} } + it{ is_expected.to be_falsy } + end + + context 'when dst is less than src' do + let(:dst_options){ {:size? => 9} } + it{ is_expected.to be_truthy } + end + end + end +end