app/models/alchemy/picture_variant.rb
# frozen_string_literal: true
require "forwardable"
module Alchemy
# Represents a rendered picture
#
# Resizes, crops and encodes the image with imagemagick
#
class PictureVariant
extend Forwardable
include Alchemy::Logger
include Alchemy::Picture::Transformations
ANIMATED_IMAGE_FORMATS = %w[gif webp]
TRANSPARENT_IMAGE_FORMATS = %w[gif webp png]
ENCODABLE_IMAGE_FORMATS = %w[jpg jpeg webp]
attr_reader :picture, :render_format
def_delegators :@picture,
:image_file,
:image_file_width,
:image_file_height,
:image_file_name,
:image_file_size
# @param [Alchemy::Picture]
#
# @param [Hash] options passed to the image processor
# @option options [Boolean] :crop Pass true to enable cropping
# @option options [String] :crop_from Coordinates to start cropping from
# @option options [String] :crop_size Size of the cropping area
# @option options [Boolean] :flatten Pass true to flatten GIFs
# @option options [String|Symbol] :format Image format to encode the image in
# @option options [Integer] :quality JPEG compress quality
# @option options [String] :size Size of resulting image in WxH
# @option options [Boolean] :upsample Pass true to upsample (grow) an image if the original size is lower than the resulting size
#
def initialize(picture, options = {})
raise ArgumentError, "Picture missing!" if picture.nil?
@picture = picture
@options = options
@render_format = (options[:format] || picture.default_render_format).to_s
end
# Process a variant of picture
#
# @return [Dragonfly::Attachment|Dragonfly::Job] The processed image variant
#
def image
image = image_file
raise MissingImageFileError, "Missing image file for #{picture.inspect}" if image.nil?
image = processed_image(image, @options)
encoded_image(image, @options)
rescue MissingImageFileError, WrongImageFormatError => e
log_warning(e.message)
nil
end
private
# Returns the processed image dependent of size and cropping parameters
def processed_image(image, options = {})
size = options[:size]
upsample = !!options[:upsample]
return image unless size.present? && picture.has_convertible_format?
if options[:crop]
crop(size, options[:crop_from], options[:crop_size], upsample)
else
resize(size, upsample)
end
end
# Returns the encoded image
#
# Flatten animated gifs, only if converting to a different format.
# Can be overwritten via +options[:flatten]+.
#
def encoded_image(image, options = {})
unless render_format.in?(Alchemy::Picture.allowed_filetypes)
raise WrongImageFormatError.new(picture, @render_format)
end
options = {
flatten: !render_format.in?(ANIMATED_IMAGE_FORMATS) && picture.image_file_format == "gif"
}.with_indifferent_access.merge(options)
encoding_options = []
convert_format = render_format.sub("jpeg", "jpg") != picture.image_file_format.sub("jpeg", "jpg")
if encodable_image? && (convert_format || options[:quality])
quality = options[:quality] || Config.get(:output_image_quality)
encoding_options << "-quality #{quality}"
end
if options[:flatten]
if render_format.in?(TRANSPARENT_IMAGE_FORMATS) && picture.image_file_format.in?(TRANSPARENT_IMAGE_FORMATS)
encoding_options << "-background transparent"
end
encoding_options << "-flatten"
end
convertion_needed = convert_format || encoding_options.present?
if picture.has_convertible_format? && convertion_needed
image = image.encode(render_format, encoding_options.join(" "))
end
image
end
def encodable_image?
render_format.in?(ENCODABLE_IMAGE_FORMATS)
end
end
end