lib/image_optim/bin_resolver/bin.rb
# 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