Skip to content

Commit

Permalink
Merge pull request #2141 from tvdeyen/croppable-resource
Browse files Browse the repository at this point in the history
Extract Thumbnails and CropAction concerns
  • Loading branch information
tvdeyen authored Jun 28, 2021
2 parents 953abfa + 3b20f27 commit 52694b3
Show file tree
Hide file tree
Showing 11 changed files with 984 additions and 942 deletions.
18 changes: 7 additions & 11 deletions app/controllers/alchemy/admin/essence_pictures_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
module Alchemy
module Admin
class EssencePicturesController < Alchemy::Admin::BaseController
include CropAction

authorize_resource class: Alchemy::EssencePicture

before_action :load_essence_picture, only: [:edit, :crop, :update]
before_action :load_essence_picture, only: [:edit, :update]
before_action :load_content, only: [:edit, :update]

helper "alchemy/admin/contents"
Expand All @@ -15,16 +17,6 @@ class EssencePicturesController < Alchemy::Admin::BaseController
def edit
end

def crop
@picture = Picture.find_by(id: params[:picture_id])
if @picture
@essence_picture.picture = @picture
@settings = @essence_picture.image_cropper_settings
else
@no_image_notice = Alchemy.t(:no_image_for_cropper_found)
end
end

def update
@essence_picture.update(essence_picture_params)
end
Expand All @@ -35,6 +27,10 @@ def load_essence_picture
@essence_picture = EssencePicture.find(params[:id])
end

def load_croppable_resource
@croppable_resource = EssencePicture.find(params[:id])
end

def load_content
@content = Content.find(params[:content_id])
end
Expand Down
26 changes: 26 additions & 0 deletions app/controllers/concerns/alchemy/admin/crop_action.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

module Alchemy
module Admin
module CropAction
extend ActiveSupport::Concern

included do
before_action :load_croppable_resource, only: [:crop]
end

def crop
@picture = Alchemy::Picture.find_by(id: params[:picture_id])
if @picture
@croppable_resource.picture = @picture
@settings = @croppable_resource.image_cropper_settings
@element = @croppable_resource.element
else
@no_image_notice = Alchemy.t(:no_image_for_cropper_found)
end

render template: "alchemy/admin/crop"
end
end
end
end
176 changes: 4 additions & 172 deletions app/models/alchemy/essence_picture.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,93 +23,18 @@

module Alchemy
class EssencePicture < BaseRecord
include Alchemy::PictureThumbnails

acts_as_essence ingredient_column: :picture, belongs_to: {
class_name: "Alchemy::Picture",
foreign_key: :picture_id,
inverse_of: :essence_pictures,
optional: true,
}

delegate :image_file_width, :image_file_height, :image_file, to: :picture, allow_nil: true
before_save :fix_crop_values
before_save :replace_newlines

# 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:
#
# essence_picture.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
delegate :settings, to: :content

# 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 = crop_values_present? || content.settings[:crop]

{
format: picture.default_render_format,
crop: !!crop,
crop_from: crop_from.presence,
crop_size: crop_size.presence,
size: content.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 = crop_values_present? || content.settings[:crop]

{
size: "160x120",
crop: !!crop,
crop_from: crop_from.presence || default_crop_from&.join("x"),
crop_size: crop_size.presence || default_crop_size&.join("x"),
flatten: true,
format: picture&.image_file_format || "jpg",
}
end
before_save :replace_newlines

# The name of the picture used as preview text in element editor views.
#
Expand All @@ -130,101 +55,8 @@ def serialized_ingredient
picture_url(content.settings)
end

# Show image cropping link for content
def allow_image_cropping?
content && content.settings[:crop] && picture &&
picture.can_be_cropped_to?(
content.settings[:size],
content.settings[:upsample],
) && !!picture.image_file
end

def crop_values_present?
crop_from.present? && crop_size.present?
end

# Settings for the graphical JS image cropper

def image_cropper_settings
Alchemy::ImageCropperSettings.new(
render_size: dimensions_from_string(render_size.presence || content.settings[:size]),
default_crop_from: default_crop_from,
default_crop_size: default_crop_size,
fixed_ratio: content.settings[:fixed_ratio],
image_width: picture&.image_file_width,
image_height: picture&.image_file_height,
).to_h
end

private

def fix_crop_values
%i(crop_from crop_size).each do |crop_value|
if self[crop_value].is_a?(String)
write_attribute crop_value, normalize_crop_value(crop_value)
end
end
end

def default_crop_size
return nil unless content.settings[:crop] && content.settings[:size]

mask = inferred_dimensions_from_string(content.settings[:size])
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 content.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

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 normalize_crop_value(crop_value)
self[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

def replace_newlines
return nil if caption.nil?

Expand Down
Loading

0 comments on commit 52694b3

Please sign in to comment.