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 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
|
require "mini_magick"
require "image_processing"
module ImageProcessing
module MiniMagick
extend Chainable
# Returns whether the given image file is processable.
def self.valid_image?(file)
::MiniMagick::Tool::Convert.new do |convert|
convert << file.path
convert << "null:"
end
true
rescue ::MiniMagick::Error
false
end
class Processor < ImageProcessing::Processor
accumulator :magick, ::MiniMagick::Tool
# Default sharpening parameters used on generated thumbnails.
SHARPEN_PARAMETERS = { radius: 0, sigma: 1 }
# Initializes the image on disk into a MiniMagick::Tool object. Accepts
# additional options related to loading the image (e.g. geometry).
# Additionally auto-orients the image to be upright.
def self.load_image(path_or_magick, loader: nil, page: nil, geometry: nil, auto_orient: true, **options)
if path_or_magick.is_a?(::MiniMagick::Tool)
magick = path_or_magick
else
source_path = path_or_magick
magick = ::MiniMagick::Tool::Convert.new
Utils.apply_options(magick, **options)
input = source_path
input = "#{loader}:#{input}" if loader
input += "[#{page}]" if page
input += "[#{geometry}]" if geometry
magick << input
end
magick.auto_orient if auto_orient
magick
end
# Calls the built ImageMagick command to perform processing and save
# the result to disk. Accepts additional options related to saving the
# image (e.g. quality).
def self.save_image(magick, destination_path, allow_splitting: false, **options)
Utils.apply_options(magick, **options)
magick << destination_path
magick.call
Utils.disallow_split_layers!(destination_path) unless allow_splitting
end
# Resizes the image to not be larger than the specified dimensions.
def resize_to_limit(width, height, **options)
thumbnail("#{width}x#{height}>", **options)
end
# Resizes the image to fit within the specified dimensions.
def resize_to_fit(width, height, **options)
thumbnail("#{width}x#{height}", **options)
end
# Resizes the image to fill the specified dimensions, applying any
# necessary cropping.
def resize_to_fill(width, height, gravity: "Center", **options)
thumbnail("#{width}x#{height}^", **options)
magick.gravity gravity
magick.background color(:transparent)
magick.extent "#{width}x#{height}"
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, background: :transparent, gravity: "Center", **options)
thumbnail("#{width}x#{height}", **options)
magick.background color(background)
magick.gravity gravity
magick.extent "#{width}x#{height}"
end
# Rotates the image by an arbitrary angle. For angles that are not
# multiple of 90 degrees an optional background color can be specified to
# fill in the gaps.
def rotate(degrees, background: nil)
magick.background color(background) if background
magick.rotate(degrees)
end
# Overlays the specified image over the current one. Supports specifying
# an additional mask, composite mode, direction or offset of the overlay
# image.
def composite(overlay = :none, mask: nil, mode: nil, gravity: nil, offset: nil, args: nil, **options, &block)
return magick.composite if overlay == :none
if options.key?(:compose)
warn "[IMAGE_PROCESSING] The :compose parameter in #composite has been renamed to :mode, the :compose alias will be removed in ImageProcessing 2."
mode = options[:compose]
end
if options.key?(:geometry)
warn "[IMAGE_PROCESSING] The :geometry parameter in #composite has been deprecated and will be removed in ImageProcessing 2. Use :offset instead, e.g. `geometry: \"+10+15\"` should be replaced with `offset: [10, 15]`."
geometry = options[:geometry]
end
geometry = "%+d%+d" % offset if offset
overlay_path = convert_to_path(overlay, "overlay")
mask_path = convert_to_path(mask, "mask") if mask
magick << overlay_path
magick << mask_path if mask_path
magick.compose(mode) if mode
define(compose: { args: args }) if args
magick.gravity(gravity) if gravity
magick.geometry(geometry) if geometry
yield magick if block_given?
magick.composite
end
# Defines settings from the provided hash.
def define(options)
return magick.define(options) if options.is_a?(String)
Utils.apply_define(magick, options)
end
# Specifies resource limits from the provided hash.
def limits(options)
options.each { |type, value| magick.args.unshift("-limit", type.to_s, value.to_s) }
magick
end
# Appends a raw ImageMagick command-line argument to the command.
def append(*args)
magick.merge! args
end
private
# Converts the given color value into an identifier ImageMagick understands.
# This supports specifying RGB(A) values with arrays, which mainly exists
# for compatibility with the libvips implementation.
def color(value)
return "rgba(255,255,255,0.0)" if value.to_s == "transparent"
return "rgb(#{value.join(",")})" if value.is_a?(Array) && value.count == 3
return "rgba(#{value.join(",")})" if value.is_a?(Array) && value.count == 4
return value if value.is_a?(String)
raise ArgumentError, "unrecognized color format: #{value.inspect} (must be one of: string, 3-element RGB array, 4-element RGBA array)"
end
# Resizes the image using the specified geometry, and sharpens the
# resulting thumbnail.
def thumbnail(geometry, sharpen: {})
magick.resize(geometry)
if sharpen
sharpen = SHARPEN_PARAMETERS.merge(sharpen)
magick.sharpen("#{sharpen[:radius]}x#{sharpen[:sigma]}")
end
magick
end
# Converts the image on disk in various forms into a path.
def convert_to_path(file, name)
if file.is_a?(String)
file
elsif file.respond_to?(:to_path)
file.to_path
elsif file.respond_to?(:path)
file.path
else
raise ArgumentError, "#{name} must be a String, Pathname, or respond to #path"
end
end
module Utils
module_function
# When a multi-layer format is being converted into a single-layer
# format, ImageMagick will create multiple images, one for each layer.
# We want to warn the user that this is probably not what they wanted.
def disallow_split_layers!(destination_path)
layers = Dir[destination_path.sub(/\.\w+$/, '-*\0')]
if layers.any?
layers.each { |path| File.delete(path) }
raise Error, "Source format is multi-layer, but destination format is single-layer. If you care only about the first layer, add `.loader(page: 0)` to your pipeline. If you want to process each layer, see https://github.com/janko/image_processing/wiki/Splitting-a-PDF-into-multiple-images or use `.saver(allow_splitting: true)`."
end
end
# Applies options from the provided hash.
def apply_options(magick, define: {}, **options)
options.each do |option, value|
case value
when true, nil then magick.send(option)
when false then magick.send(option).+
else magick.send(option, *value)
end
end
apply_define(magick, define)
end
# Applies settings from the provided (nested) hash.
def apply_define(magick, options)
options.each do |namespace, settings|
namespace = namespace.to_s.tr("_", "-")
settings.each do |key, value|
key = key.to_s.tr("_", "-")
magick.define "#{namespace}:#{key}=#{value}"
end
end
magick
end
end
end
end
end
|