From 10fca397278b8d7ee6705d2634d67b746ba83537 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 --- .github/workflows/check.yml | 2 + CHANGELOG.markdown | 2 + lib/image_optim.rb | 2 +- lib/image_optim/bin_resolver/bin.rb | 2 +- lib/image_optim/worker/oxipng.rb | 53 +++++++++++++++ spec/image_optim/worker/oxipng_spec.rb | 89 ++++++++++++++++++++++++++ 6 files changed, 148 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/.github/workflows/check.yml b/.github/workflows/check.yml index e7818508..836b6aaa 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -29,6 +29,7 @@ jobs: bundler-cache: true - run: sudo npm install -g svgo - run: curl -L "https://www.jonof.id.au/files/kenutils/pngout-20200115-linux.tar.gz" | sudo tar -xz -C /usr/local/bin --strip-components 2 --wildcards '*/amd64/pngout' + - run: curl -L "https://github.com/shssoichiro/oxipng/releases/download/v4.0.3/oxipng-4.0.3-x86_64-unknown-linux-musl.tar.gz" | tar -xz -C /usr/local/bin --strip-components 1 --wildcards '*oxipng' - run: bundle exec image_optim --info - run: bundle exec rspec coverage: @@ -43,6 +44,7 @@ jobs: bundler-cache: true - run: sudo npm install -g svgo - run: curl -L "https://www.jonof.id.au/files/kenutils/pngout-20200115-linux.tar.gz" | sudo tar -xz -C /usr/local/bin --strip-components 2 --wildcards '*/amd64/pngout' + - run: curl -L "https://github.com/shssoichiro/oxipng/releases/download/v4.0.3/oxipng-4.0.3-x86_64-unknown-linux-musl.tar.gz" | tar -xz -C /usr/local/bin --strip-components 1 --wildcards '*oxipng' - uses: paambaati/codeclimate-action@v2.7.5 with: coverageCommand: bundle exec rspec diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown index eeb29290..463a3113 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) [#190](https://github.com/toy/image_optim/pull/190) [@oblakeerickson](https://github.com/oblakeerickson) + ## v0.30.0 (2021-05-11) * Add `timeout` option to restrict maximum time spent on every image [#21](https://github.com/toy/image_optim/issues/21) [#148](https://github.com/toy/image_optim/pull/148) [#149](https://github.com/toy/image_optim/pull/149) [#162](https://github.com/toy/image_optim/pull/162) [#184](https://github.com/toy/image_optim/pull/184) [#189](https://github.com/toy/image_optim/pull/189) [@tgxworld](https://github.com/tgxworld) [@oblakeerickson](https://github.com/oblakeerickson) [@toy](https://github.com/toy) diff --git a/lib/image_optim.rb b/lib/image_optim.rb index 2263f9be..184d170e 100644 --- a/lib/image_optim.rb +++ b/lib/image_optim.rb @@ -14,7 +14,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 3bb4d8a6..28972cc2 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")[/\A\d+(\.\d+)+/] diff --git a/lib/image_optim/worker/oxipng.rb b/lib/image_optim/worker/oxipng.rb new file mode 100644 index 00000000..38c0a241 --- /dev/null +++ b/lib/image_optim/worker/oxipng.rb @@ -0,0 +1,53 @@ +# 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, options = {}) + 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, options) && 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..874616e5 --- /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