app/models/concerns/alchemy/picture_thumbnails.rb
# frozen_string_literal: true
module Alchemy
# Picture thumbnails and cropping concerns
module PictureThumbnails
extend ActiveSupport::Concern
included do
before_save :fix_crop_values
delegate :image_file_width, :image_file_height, :image_file, to: :picture, allow_nil: true
end
# The url to show the picture.
#
# Takes all values like +name+ and crop sizes (+crop_from+, +crop_size+ from the build in graphical image cropper)
# and also adds the security token.
#
# You typically want to set the size the picture should be resized to.
#
# === Example:
#
# picture_view.picture_url(size: '200x300', crop: true, format: 'gif')
# # '/pictures/1/show/200x300/crop/cats.gif?sh=765rfghj'
#
# @option options size [String]
# The size the picture should be resized to.
#
# @option options format [String]
# The format the picture should be rendered in.
# Defaults to the +image_output_format+ from the +Alchemy::Config+.
#
# @option options crop [Boolean]
# If set to true the picture will be cropped to fit the size value.
#
# @return [String]
def picture_url(options = {})
return if picture.nil?
picture.url(picture_url_options.merge(options)) || "missing-image.png"
end
# Picture rendering options
#
# Returns the +default_render_format+ of the associated +Alchemy::Picture+
# together with the +crop_from+ and +crop_size+ values
#
# @return [HashWithIndifferentAccess]
def picture_url_options
return {} if picture.nil?
crop = !!settings[:crop]
{
format: picture.default_render_format,
crop: crop,
crop_from: crop && crop_from.presence || nil,
crop_size: crop && crop_size.presence || nil,
size: settings[:size]
}.with_indifferent_access
end
# Returns an url for the thumbnail representation of the assigned picture
#
# It takes cropping values into account, so it always represents the current
# image displayed in the frontend.
#
# @return [String]
def thumbnail_url
return if picture.nil?
picture.url(thumbnail_url_options) || "alchemy/missing-image.svg"
end
# Thumbnail rendering options
#
# @return [HashWithIndifferentAccess]
def thumbnail_url_options
crop = !!settings[:crop]
{
size: "160x120",
crop: crop,
crop_from: crop && crop_from.presence || default_crop_from&.join("x"),
crop_size: crop && crop_size.presence || default_crop_size&.join("x"),
flatten: true,
format: picture&.image_file_format || "jpg"
}
end
# Settings for the graphical JS image cropper
def image_cropper_settings
Alchemy::ImageCropperSettings.new(
render_size: dimensions_from_string(render_size.presence || settings[:size]),
default_crop_from: default_crop_from,
default_crop_size: default_crop_size,
fixed_ratio: settings[:fixed_ratio],
image_width: picture&.image_file_width,
image_height: picture&.image_file_height
).to_h
end
# Show image cropping link for ingredient
def allow_image_cropping?
settings[:crop] && picture &&
picture.can_be_cropped_to?(
settings[:size],
settings[:upsample]
) && !!picture.image_file
end
private
def default_crop_size
return nil unless settings[:crop] && settings[:size]
mask = inferred_dimensions_from_string(settings[:size])
return if mask.nil?
zoom = thumbnail_zoom_factor(mask)
return nil if zoom.zero?
[(mask[0] / zoom), (mask[1] / zoom)].map(&:round)
end
def thumbnail_zoom_factor(mask)
[
mask[0].to_f / (image_file_width || 1),
mask[1].to_f / (image_file_height || 1)
].max
end
def default_crop_from
return nil unless settings[:crop]
return nil if default_crop_size.nil?
[
((image_file_width || 0) - default_crop_size[0]) / 2,
((image_file_height || 0) - default_crop_size[1]) / 2
].map(&:round)
end
def dimensions_from_string(string)
return if string.nil?
string.split("x", 2).map(&:to_i)
end
def inferred_dimensions_from_string(string)
return if string.nil?
width, height = dimensions_from_string(string)
ratio = image_file_width.to_f / image_file_height.to_i
return if ratio.nan?
if width.zero? && ratio.is_a?(Float)
width = height * ratio
end
if height.zero? && ratio.is_a?(Float)
height = width / ratio
end
[width.to_i, height.to_i]
end
def fix_crop_values
%i[crop_from crop_size].each do |crop_value|
if public_send(crop_value).is_a?(String)
public_send(:"#{crop_value}=", normalize_crop_value(crop_value))
end
end
end
def normalize_crop_value(crop_value)
public_send(crop_value).split("x").map { |n| normalize_number(n) }.join("x")
end
def normalize_number(number)
number = number.to_f.round
number.negative? ? 0 : number
end
end
end