1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
|
require "vips"
require "image_processing"
fail "image_processing/vips requires libvips 8.6+" unless Vips.at_least_libvips?(8, 6)
module ImageProcessing
module Vips
extend Chainable
# Returns whether the given image file is processable.
def self.valid_image?(file)
::Vips::Image.new_from_file(file.path, access: :sequential).avg
true
rescue ::Vips::Error
false
end
class Processor < ImageProcessing::Processor
accumulator :image, ::Vips::Image
# Default sharpening mask that provides a fast and mild sharpen.
SHARPEN_MASK = ::Vips::Image.new_from_array [[-1, -1, -1],
[-1, 32, -1],
[-1, -1, -1]], 24
# Loads the image on disk into a Vips::Image object. Accepts additional
# loader-specific options (e.g. interlacing). Afterwards auto-rotates the
# image to be upright.
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
# See #thumbnail.
def self.supports_resize_on_load?
true
end
# Writes the Vips::Image object to disk. This starts the processing
# pipeline defined in the Vips::Image object. Accepts additional
# saver-specific options (e.g. quality).
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
# 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
# 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
# Resizes the image to fill the specified dimensions, applying any
# necessary cropping.
def resize_to_fill(width, height, **options)
thumbnail(width, height, crop: :centre, **options)
end
# Resizes the image to fit within the specified dimensions and fills
# the remaining area with the specified background color.
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
# Rotates the image by an arbitrary angle.
def rotate(degrees, **options)
image.similarity(angle: degrees, **options)
end
# Overlays the specified image over the current one. Supports specifying
# composite mode, direction or offset of the overlay image.
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
# 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_value(*args) image.tap { |img| img.set_value(*args) } end
def remove(*args) image.tap { |img| img.remove(*args) } end
private
# Resizes the image according to the specified parameters, and sharpens
# the resulting thumbnail.
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
# 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
# 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
module Utils
module_function
# libvips uses various loaders depending on the input format.
def select_valid_loader_options(source_path, options)
loader = ::Vips.vips_foreign_find_load(source_path)
loader ? select_valid_options(loader, options) : options
end
# Filters out unknown options for saving images.
def select_valid_saver_options(destination_path, options)
saver = ::Vips.vips_foreign_find_save(destination_path)
saver ? select_valid_options(saver, options) : options
end
# libvips uses various loaders and savers depending on the input and
# output image format. Each of these loaders and savers accept slightly
# different options, so to allow the user to be able to specify options
# for a specific loader/saver and have it ignored for other
# loaders/savers, we do some introspection and filter out options that
# don't exist for a particular loader or saver.
def select_valid_options(operation_name, options)
introspect = ::Vips::Introspect.get(operation_name)
operation_options = introspect.optional_input.keys.map(&:to_sym)
options.select { |name, value| operation_options.include?(name) }
end
end
end
end
end
|