toy/image_optim

View on GitHub
lib/image_optim/bin_resolver/bin.rb

Summary

Maintainability
A
1 hr
Test Coverage
A
95%
# frozen_string_literal: true

require 'image_optim/bin_resolver/error'
require 'image_optim/bin_resolver/simple_version'
require 'image_optim/bin_resolver/comparable_condition'
require 'image_optim/cmd'
require 'image_optim/path'
require 'shellwords'
require 'digest/sha1'
require 'date'

class ImageOptim
  class BinResolver
    # Holds bin name and path, gets version
    class Bin
      class UnknownVersion < Error; end

      class BadVersion < Error; end

      attr_reader :name, :path, :version

      def initialize(name, path)
        @name = name.to_sym
        @path = path.to_s
        @version = detect_version
      end

      def digest
        return @digest if defined?(@digest)

        @digest = File.exist?(@path) && Digest::SHA1.file(@path).hexdigest
      end

      def to_s
        "#{name} #{version || '?'} at #{path}"
      end

      is = ComparableCondition.is

      FAIL_CHECKS = {
        pngcrush: [
          [is.between?('1.7.60', '1.7.65'), 'is known to produce broken pngs'],
          [is == '1.7.80', 'loses one color in indexed images'],
        ],
        pngquant: [
          [is < '2.0', 'is not supported'],
        ],
      }.freeze

      WARN_CHECKS = {
        advpng: [
          [is == 'none', 'is of unknown version'],
          [is < '1.17', 'does not use zopfli'],
        ],
        gifsicle: [
          [is < '1.85', 'does not support removing extension blocks'],
        ],
        pngcrush: [
          [is < '1.7.38', 'does not have blacken flag'],
        ],
        pngquant: [
          [is < '2.1', 'may be lossy even with quality `100-`'],
        ],
        optipng: [
          [is < '0.7', 'does not support -strip option'],
        ],
      }.freeze

      # Fail if version will not work properly
      def check_fail!
        unless version
          fail UnknownVersion, "could not get version of #{name} at #{path}"
        end

        return unless FAIL_CHECKS[name]

        FAIL_CHECKS[name].each do |matcher, message|
          next unless matcher.match(version)

          fail BadVersion, "#{self} (#{matcher}) #{message}"
        end
      end

      # Run check_fail!, otherwise warn if version is known to misbehave
      def check!
        check_fail!

        return unless WARN_CHECKS[name]

        WARN_CHECKS[name].each do |matcher, message|
          next unless matcher.match(version)

          warn "WARN: #{self} (#{matcher}) #{message}"

          break
        end
      end

    private

      # Wrap version_string with SimpleVersion
      def detect_version
        str = version_string
        str && SimpleVersion.new(str)
      end

      # Getting version of bin, will fail for an unknown name
      def version_string
        case name
        when :advpng
          capture("#{escaped_path} --version 2> #{Path::NULL}")[/\bv(\d+(\.\d+)+|none)/, 1]
        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+)+/]
        when :jhead, :'jpeg-recompress'
          capture("#{escaped_path} -V 2> #{Path::NULL}")[/\d+(\.\d+)+/]
        when :jpegtran
          capture("#{escaped_path} -v - 2>&1")[/version (\d+\S*)/, 1]
        when :pngcrush
          capture("#{escaped_path} -version 2>&1")[/pngcrush (\d+(\.\d+)+)/, 1]
        when :pngout
          date_regexp = /[A-Z][a-z]{2} (?: |\d)\d \d{4}/
          date_str = capture("#{escaped_path} 2>&1")[date_regexp]
          Date.parse(date_str).strftime('%Y%m%d') if date_str
        when :jpegrescan
          # jpegrescan has no version so use first 8 characters of sha1 hex
          Digest::SHA1.file(path).hexdigest[0, 8] if path
        else
          fail "getting `#{name}` version is not defined"
        end
      end

      def capture(cmd)
        Cmd.capture(cmd)
      end

      def escaped_path
        path.shellescape
      end
    end
  end
end