class ImageProcessing::Vips::Processor

def self.load_image(path_or_image, loader: nil, autorot: true, **options)

image to be upright.
loader-specific options (e.g. interlacing). Afterwards auto-rotates the
Loads the image on disk into a Vips::Image object. Accepts additional
def self.load_image(path_or_image, loader: nil, autorot: true, **options)
  if path_or_image.is_a?(::Vips::Image)
    image = path_or_image
  else
    path = path_or_image
    if loader
      image = ::Vips::Image.public_send(:"#{loader}load", path, **options)
    else
      options = Utils.select_valid_loader_options(path, options)
      image = ::Vips::Image.new_from_file(path, **options)
    end
  end
  image = image.autorot if autorot && !options.key?(:autorotate)
  image
end

def self.save_image(image, path, saver: nil, quality: nil, **options)

saver-specific options (e.g. quality).
pipeline defined in the Vips::Image object. Accepts additional
Writes the Vips::Image object to disk. This starts the processing
def self.save_image(image, path, saver: nil, quality: nil, **options)
  options[:Q] = quality if quality
  if saver
    image.public_send(:"#{saver}save", path, **options)
  else
    options = Utils.select_valid_saver_options(path, options)
    image.write_to_file(path, **options)
  end
end

def self.supports_resize_on_load?

See #thumbnail.
def self.supports_resize_on_load?
  true
end

def composite(overlay, _mode = nil, mode: "over", gravity: "north-west", offset: nil, **options)

composite mode, direction or offset of the overlay image.
Overlays the specified image over the current one. Supports specifying
def composite(overlay, _mode = nil, mode: "over", gravity: "north-west", offset: nil, **options)
  # if the mode argument is given, call the original Vips::Image#composite
  if _mode
    overlay = [overlay] unless overlay.is_a?(Array)
    overlay = overlay.map { |object| convert_to_image(object, "overlay") }
    return image.composite(overlay, _mode, **options)
  end
  overlay = convert_to_image(overlay, "overlay")
  # add alpha channel so that #gravity can use a transparent background
  overlay = overlay.add_alpha unless overlay.has_alpha?
  # apply offset with correct gravity and make remainder transparent
  if offset
    opposite_gravity = gravity.to_s.gsub(/\w+/, "north"=>"south", "south"=>"north", "east"=>"west", "west"=>"east")
    overlay = overlay.gravity(opposite_gravity, overlay.width + offset.first, overlay.height + offset.last)
  end
  # create image-sized transparent background and apply specified gravity
  overlay = overlay.gravity(gravity, image.width, image.height)
  # apply the composition
  image.composite(overlay, mode, **options)
end

def convert_to_image(object, name)

Converts the image on disk in various forms into a Vips::Image object.
def convert_to_image(object, name)
  return object if object.is_a?(::Vips::Image)
  if object.is_a?(String)
    path = object
  elsif object.respond_to?(:to_path)
    path = object.to_path
  elsif object.respond_to?(:path)
    path = object.path
  else
    raise ArgumentError, "#{name} must be a Vips::Image, String, Pathname, or respond to #path"
  end
  ::Vips::Image.new_from_file(path)
end

def default_dimensions(width, height)

Hack to allow omitting one dimension.
def default_dimensions(width, height)
  raise Error, "either width or height must be specified" unless width || height
  [width || ::Vips::MAX_COORD, height || ::Vips::MAX_COORD]
end

def remove(*args) image.tap { |img| img.remove(*args) } end

def remove(*args)    image.tap { |img| img.remove(*args) }    end

def resize_and_pad(width, height, gravity: "centre", extend: nil, background: nil, alpha: nil, **options)

the remaining area with the specified background color.
Resizes the image to fit within the specified dimensions and fills
def resize_and_pad(width, height, gravity: "centre", extend: nil, background: nil, alpha: nil, **options)
  image = thumbnail(width, height, **options)
  image = image.add_alpha if alpha && !image.has_alpha?
  image.gravity(gravity, width, height, extend: extend, background: background)
end

def resize_to_cover(width, height, **options)

cropping the excess.
Resizes the image to cover the specified dimensions, without
def resize_to_cover(width, height, **options)
  image = self.image.is_a?(String) ? self.class.load_image(self.image) : self.image
  image_ratio = Rational(image.width, image.height)
  thumbnail_ratio = Rational(width, height)
  if image_ratio > thumbnail_ratio
    width = ::Vips::MAX_COORD
  else
    height = ::Vips::MAX_COORD
  end
  thumbnail(width, height, **options, crop: :none)
end

def resize_to_fill(width, height, **options)

necessary cropping.
Resizes the image to fill the specified dimensions, applying any
def resize_to_fill(width, height, **options)
  thumbnail(width, height, crop: :centre, **options)
end

def resize_to_fit(width, height, **options)

Resizes the image to fit within the specified dimensions.
def resize_to_fit(width, height, **options)
  width, height = default_dimensions(width, height)
  thumbnail(width, height, **options)
end

def resize_to_limit(width, height, **options)

Resizes the image to not be larger than the specified dimensions.
def resize_to_limit(width, height, **options)
  width, height = default_dimensions(width, height)
  thumbnail(width, height, size: :down, **options)
end

def rotate(degrees, **options)

Rotates the image by an arbitrary angle.
def rotate(degrees, **options)
  if ([90, 180, 270].include?(degrees) && options.empty?)
    rot_command = "rot#{degrees}".to_sym
    image.public_send rot_command
  else
    image.similarity(angle: degrees, **options)
  end
end

def set(*args) image.tap { |img| img.set(*args) } end

make metadata setter methods chainable
def set(*args)       image.tap { |img| img.set(*args) }       end

def set_type(*args) image.tap { |img| img.set_type(*args) } end

def set_type(*args)  image.tap { |img| img.set_type(*args) }  end

def set_value(*args) image.tap { |img| img.set_value(*args) } end

def set_value(*args) image.tap { |img| img.set_value(*args) } end

def thumbnail(width, height, sharpen: SHARPEN_MASK, **options)

the resulting thumbnail.
Resizes the image according to the specified parameters, and sharpens
def thumbnail(width, height, sharpen: SHARPEN_MASK, **options)
  if self.image.is_a?(String) # path
    # resize on load
    image = ::Vips::Image.thumbnail(self.image, width, height: height, **options)
  else
    # we're already calling Image#autorot when loading the image
    no_rotate = ::Vips.at_least_libvips?(8, 8) ? { no_rotate: true } : { auto_rotate: false }
    options   = no_rotate.merge(options)
    image = self.image.thumbnail_image(width, height: height, **options)
  end
  image = image.conv(sharpen, precision: :integer) if sharpen
  image
end