lib/carrierwave/processing/vips.rb
module CarrierWave
##
# This module simplifies manipulation with vips by providing a set
# of convenient helper methods. If you want to use them, you'll need to
# require this file:
#
# require 'carrierwave/processing/vips'
#
# And then include it in your uploader:
#
# class MyUploader < CarrierWave::Uploader::Base
# include CarrierWave::Vips
# end
#
# You can now use the provided helpers:
#
# class MyUploader < CarrierWave::Uploader::Base
# include CarrierWave::Vips
#
# process :resize_to_fit => [200, 200]
# end
#
# Or create your own helpers with the powerful vips! method, which
# yields an ImageProcessing::Builder object. Check out the ImageProcessing
# docs at http://github.com/janko-m/image_processing and the list of all
# available Vips options at
# https://libvips.github.io/libvips/API/current/using-cli.html for more info.
#
# class MyUploader < CarrierWave::Uploader::Base
# include CarrierWave::Vips
#
# process :radial_blur => 10
#
# def radial_blur(amount)
# vips! do |builder|
# builder.radial_blur(amount)
# builder = yield(builder) if block_given?
# builder
# end
# end
# end
#
# === Note
#
# The ImageProcessing gem uses ruby-vips, a binding for the vips image
# library. You can find more information here:
#
# https://github.com/libvips/ruby-vips
#
#
module Vips
extend ActiveSupport::Concern
included do
require "image_processing/vips"
# We need to disable caching since we're editing images in place.
::Vips.cache_set_max(0)
end
module ClassMethods
def convert(format)
process :convert => format
end
def resize_to_limit(width, height)
process :resize_to_limit => [width, height]
end
def resize_to_fit(width, height)
process :resize_to_fit => [width, height]
end
def resize_to_fill(width, height, gravity='centre')
process :resize_to_fill => [width, height, gravity]
end
def resize_and_pad(width, height, background=nil, gravity='centre', alpha=nil)
process :resize_and_pad => [width, height, background, gravity, alpha]
end
def crop(left, top, width, height)
process :crop => [left, top, width, height]
end
end
##
# Changes the image encoding format to the given format
#
# See https://libvips.github.io/libvips/API/current/using-cli.html#using-command-line-conversion
#
# === Parameters
#
# [format (#to_s)] an abbreviation of the format
#
# === Yields
#
# [Vips::Image] additional manipulations to perform
#
# === Examples
#
# image.convert(:png)
#
def convert(format, page=nil)
vips! do |builder|
builder = builder.convert(format)
builder = builder.loader(page: page) if page
builder
end
end
##
# Resize the image to fit within the specified dimensions while retaining
# the original aspect ratio. Will only resize the image if it is larger than the
# specified dimensions. The resulting image may be shorter or narrower than specified
# in the smaller dimension but will not be larger than the specified values.
#
# === Parameters
#
# [width (Integer)] the width to scale the image to
# [height (Integer)] the height to scale the image to
# [combine_options (Hash)] additional Vips options to apply before resizing
#
# === Yields
#
# [Vips::Image] additional manipulations to perform
#
def resize_to_limit(width, height, combine_options: {})
width, height = resolve_dimensions(width, height)
vips! do |builder|
builder.resize_to_limit(width, height)
.apply(combine_options)
end
end
##
# Resize the image to fit within the specified dimensions while retaining
# the original aspect ratio. The image may be shorter or narrower than
# specified in the smaller dimension but will not be larger than the specified values.
#
# === Parameters
#
# [width (Integer)] the width to scale the image to
# [height (Integer)] the height to scale the image to
# [combine_options (Hash)] additional Vips options to apply before resizing
#
# === Yields
#
# [Vips::Image] additional manipulations to perform
#
def resize_to_fit(width, height, combine_options: {})
width, height = resolve_dimensions(width, height)
vips! do |builder|
builder.resize_to_fit(width, height)
.apply(combine_options)
end
end
##
# Resize the image to fit within the specified dimensions while retaining
# the aspect ratio of the original image. If necessary, crop the image in the
# larger dimension.
#
# === Parameters
#
# [width (Integer)] the width to scale the image to
# [height (Integer)] the height to scale the image to
# [combine_options (Hash)] additional vips options to apply before resizing
#
# === Yields
#
# [Vips::Image] additional manipulations to perform
#
def resize_to_fill(width, height, _gravity = nil, combine_options: {})
width, height = resolve_dimensions(width, height)
vips! do |builder|
builder.resize_to_fill(width, height).apply(combine_options)
end
end
##
# Resize the image to fit within the specified dimensions while retaining
# the original aspect ratio. If necessary, will pad the remaining area
# with the given color, which defaults to transparent (for gif and png,
# white for jpeg).
#
# See https://libvips.github.io/libvips/API/current/libvips-conversion.html#VipsCompassDirection
# for gravity options.
#
# === Parameters
#
# [width (Integer)] the width to scale the image to
# [height (Integer)] the height to scale the image to
# [background (List, nil)] the color of the background as a RGB, like [0, 255, 255], nil indicates transparent
# [gravity (String)] how to position the image
# [alpha (Boolean, nil)] pad the image with the alpha channel if supported
# [combine_options (Hash)] additional vips options to apply before resizing
#
# === Yields
#
# [Vips::Image] additional manipulations to perform
#
def resize_and_pad(width, height, background=nil, gravity='centre', alpha=nil, combine_options: {})
width, height = resolve_dimensions(width, height)
vips! do |builder|
builder.resize_and_pad(width, height, background: background, gravity: gravity, alpha: alpha)
.apply(combine_options)
end
end
##
# Crop the image to the contents of a box positioned at [left] and [top], with the dimensions given
# by [width] and [height]. The original image bottom/right edge is preserved if the cropping box falls
# outside the image bounds.
#
# === Parameters
#
# [left (integer)] left edge of area to extract
# [top (integer)] top edge of area to extract
# [width (Integer)] width of area to extract
# [height (Integer)] height of area to extract
#
# === Yields
#
# [Vips::Image] additional manipulations to perform
#
def crop(left, top, width, height, combine_options: {})
width, height = resolve_dimensions(width, height)
width = vips_image.width - left if width + left > vips_image.width
height = vips_image.height - top if height + top > vips_image.height
vips! do |builder|
builder.crop(left, top, width, height)
.apply(combine_options)
end
end
##
# Returns the width of the image in pixels.
#
# === Returns
#
# [Integer] the image's width in pixels
#
def width
vips_image.width
end
##
# Returns the height of the image in pixels.
#
# === Returns
#
# [Integer] the image's height in pixels
#
def height
vips_image.height
end
# Process the image with vip, using the ImageProcessing gem. This
# method will build a "convert" vips command and execute it on the
# current image.
#
# === Gotcha
#
# This method assumes that the object responds to +current_path+.
# Any class that this module is mixed into must have a +current_path+ method.
# CarrierWave::Uploader does, so you won't need to worry about this in
# most cases.
#
# === Yields
#
# [ImageProcessing::Builder] use it to define processing to be performed
#
# === Raises
#
# [CarrierWave::ProcessingError] if processing failed.
def vips!
builder = ImageProcessing::Vips.source(current_path)
builder = yield(builder)
result = builder.call
result.close
FileUtils.mv result.path, current_path
if File.extname(result.path) != File.extname(current_path)
move_to = current_path.chomp(File.extname(current_path)) + File.extname(result.path)
file.content_type = Marcel::Magic.by_path(move_to).try(:type)
file.move_to(move_to, permissions, directory_permissions)
end
rescue ::Vips::Error
message = I18n.translate(:"errors.messages.processing_error")
raise CarrierWave::ProcessingError, message
end
private
def resolve_dimensions(*dimensions)
dimensions.map do |value|
next value unless value.instance_of?(Proc)
value.arity >= 1 ? value.call(self) : value.call
end
end
def vips_image
::Vips::Image.new_from_buffer(read, "")
end
end # Vips
end # CarrierWave