alchemy_cms 5.0.10 → 5.1.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/PULL_REQUEST_TEMPLATE.md +1 -1
- data/.github/workflows/stale.yml +1 -1
- data/.gitignore +1 -0
- data/.travis.yml +48 -0
- data/CHANGELOG.md +50 -40
- data/CONTRIBUTING.md +2 -2
- data/Gemfile +2 -2
- data/README.md +2 -2
- data/alchemy_cms.gemspec +4 -4
- data/app/assets/images/alchemy/missing-image.svg +1 -0
- data/app/assets/stylesheets/alchemy/_variables.scss +1 -0
- data/app/assets/stylesheets/alchemy/archive.scss +23 -17
- data/app/assets/stylesheets/alchemy/errors.scss +1 -1
- data/app/assets/stylesheets/alchemy/navigation.scss +7 -10
- data/app/assets/stylesheets/alchemy/pagination.scss +1 -1
- data/app/assets/stylesheets/alchemy/search.scss +12 -2
- data/app/assets/stylesheets/alchemy/tags.scss +19 -31
- data/app/assets/stylesheets/tinymce/skins/alchemy/content.min.css.scss +3 -3
- data/app/assets/stylesheets/tinymce/skins/alchemy/skin.min.css.scss +7 -7
- data/app/controllers/alchemy/admin/base_controller.rb +3 -9
- data/app/controllers/alchemy/admin/pictures_controller.rb +13 -6
- data/app/controllers/alchemy/admin/resources_controller.rb +3 -3
- data/app/controllers/alchemy/pages_controller.rb +49 -14
- data/app/helpers/alchemy/admin/base_helper.rb +0 -44
- data/app/helpers/alchemy/admin/navigation_helper.rb +2 -1
- data/app/helpers/alchemy/pages_helper.rb +1 -1
- data/app/models/alchemy/attachment/url.rb +40 -0
- data/app/models/alchemy/attachment.rb +21 -4
- data/app/models/alchemy/element.rb +1 -1
- data/app/models/alchemy/essence_picture.rb +3 -3
- data/app/models/alchemy/essence_picture_view.rb +5 -3
- data/app/models/alchemy/node.rb +1 -1
- data/app/models/alchemy/page/page_natures.rb +2 -0
- data/app/models/alchemy/page/url_path.rb +8 -6
- data/app/models/alchemy/page.rb +17 -2
- data/app/models/alchemy/picture/calculations.rb +55 -0
- data/app/models/alchemy/picture/transformations.rb +8 -52
- data/app/models/alchemy/picture/url.rb +28 -77
- data/app/models/alchemy/picture.rb +59 -3
- data/app/models/alchemy/picture_thumb/create.rb +39 -0
- data/app/models/alchemy/picture_thumb/signature.rb +23 -0
- data/app/models/alchemy/picture_thumb/uid.rb +22 -0
- data/app/models/alchemy/picture_thumb.rb +57 -0
- data/app/models/alchemy/picture_variant.rb +114 -0
- data/app/serializers/alchemy/page_tree_serializer.rb +4 -4
- data/app/views/alchemy/admin/attachments/show.html.erb +8 -8
- data/app/views/alchemy/admin/dashboard/index.html.erb +13 -16
- data/app/views/alchemy/admin/elements/_element_toolbar.html.erb +1 -1
- data/app/views/alchemy/admin/essence_pictures/crop.html.erb +1 -1
- data/app/views/alchemy/admin/essence_pictures/edit.html.erb +2 -2
- data/app/views/alchemy/admin/layoutpages/edit.html.erb +4 -6
- data/app/views/alchemy/admin/pages/_form.html.erb +4 -6
- data/app/views/alchemy/admin/pages/_new_page_form.html.erb +2 -1
- data/app/views/alchemy/admin/partials/_remote_search_form.html.erb +14 -13
- data/app/views/alchemy/admin/partials/_search_form.html.erb +8 -8
- data/app/views/alchemy/admin/pictures/_archive.html.erb +1 -1
- data/app/views/alchemy/admin/pictures/_form.html.erb +1 -1
- data/app/views/alchemy/admin/pictures/_picture.html.erb +3 -3
- data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +1 -1
- data/app/views/alchemy/admin/pictures/edit_multiple.html.erb +1 -1
- data/app/views/alchemy/admin/pictures/index.html.erb +1 -1
- data/app/views/alchemy/admin/pictures/show.html.erb +3 -3
- data/app/views/alchemy/admin/resources/_per_page_select.html.erb +3 -3
- data/app/views/alchemy/admin/resources/index.html.erb +4 -1
- data/app/views/alchemy/admin/tags/index.html.erb +14 -15
- data/app/views/alchemy/base/500.html.erb +11 -13
- data/app/views/alchemy/essences/_essence_file_view.html.erb +3 -3
- data/app/views/alchemy/essences/_essence_picture_view.html.erb +3 -3
- data/config/alchemy/config.yml +15 -11
- data/config/alchemy/modules.yml +12 -12
- data/config/initializers/dragonfly.rb +0 -8
- data/config/routes.rb +1 -1
- data/db/migrate/20200617110713_create_alchemy_picture_thumbs.rb +22 -0
- data/db/migrate/20200907111332_remove_tri_state_booleans.rb +33 -0
- data/lib/alchemy/auth_accessors.rb +12 -5
- data/lib/alchemy/config.rb +1 -3
- data/lib/alchemy/engine.rb +6 -8
- data/lib/alchemy/modules.rb +11 -1
- data/lib/alchemy/resource.rb +3 -5
- data/lib/alchemy/test_support/factories/picture_factory.rb +0 -1
- data/lib/alchemy/test_support/factories/picture_thumb_factory.rb +12 -0
- data/lib/alchemy/upgrader/five_point_zero.rb +0 -32
- data/lib/alchemy/version.rb +1 -1
- data/lib/alchemy_cms.rb +0 -1
- data/lib/generators/alchemy/install/files/alchemy.en.yml +2 -2
- data/lib/generators/alchemy/install/install_generator.rb +1 -2
- data/lib/generators/alchemy/install/templates/dragonfly.rb.tt +5 -5
- data/lib/tasks/alchemy/thumbnails.rake +37 -0
- data/lib/tasks/alchemy/upgrade.rake +0 -20
- data/package/admin.js +0 -2
- data/package/src/__tests__/i18n.spec.js +0 -23
- data/package/src/i18n.js +3 -1
- data/package.json +1 -1
- metadata +34 -23
- data/.github/workflows/ci.yml +0 -126
- data/.github/workflows/greetings.yml +0 -13
- data/app/controllers/concerns/alchemy/locale_redirects.rb +0 -40
- data/app/controllers/concerns/alchemy/page_redirects.rb +0 -68
- data/lib/alchemy/dragonfly/processors/crop_resize.rb +0 -35
- data/lib/alchemy/error_tracking/airbrake_handler.rb +0 -13
- data/lib/alchemy/error_tracking.rb +0 -14
- data/lib/alchemy/userstamp.rb +0 -12
|
@@ -7,6 +7,10 @@ module Alchemy
|
|
|
7
7
|
module Picture::Transformations
|
|
8
8
|
extend ActiveSupport::Concern
|
|
9
9
|
|
|
10
|
+
included do
|
|
11
|
+
include Alchemy::Picture::Calculations
|
|
12
|
+
end
|
|
13
|
+
|
|
10
14
|
THUMBNAIL_WIDTH = 160
|
|
11
15
|
THUMBNAIL_HEIGHT = 120
|
|
12
16
|
|
|
@@ -66,24 +70,6 @@ module Alchemy
|
|
|
66
70
|
image_file.thumb(upsample ? size : "#{size}>")
|
|
67
71
|
end
|
|
68
72
|
|
|
69
|
-
# Given a string with an x, this function returns a Hash with point
|
|
70
|
-
# :width and :height.
|
|
71
|
-
#
|
|
72
|
-
def sizes_from_string(string = "0x0")
|
|
73
|
-
string = "0x0" if string.nil? || string.empty?
|
|
74
|
-
|
|
75
|
-
raise ArgumentError unless string =~ /(\d*x\d*)/
|
|
76
|
-
|
|
77
|
-
width, height = string.scan(/(\d*)x(\d*)/)[0].map(&:to_i)
|
|
78
|
-
|
|
79
|
-
width = 0 if width.nil?
|
|
80
|
-
height = 0 if height.nil?
|
|
81
|
-
{
|
|
82
|
-
width: width,
|
|
83
|
-
height: height,
|
|
84
|
-
}
|
|
85
|
-
end
|
|
86
|
-
|
|
87
73
|
# Returns true if picture's width is greater than it's height
|
|
88
74
|
#
|
|
89
75
|
def landscape_format?
|
|
@@ -105,24 +91,6 @@ module Alchemy
|
|
|
105
91
|
end
|
|
106
92
|
alias_method :square?, :square_format?
|
|
107
93
|
|
|
108
|
-
# This function returns the :width and :height of the image file
|
|
109
|
-
# as a Hash
|
|
110
|
-
def image_size
|
|
111
|
-
{
|
|
112
|
-
width: image_file_width,
|
|
113
|
-
height: image_file_height,
|
|
114
|
-
}
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# An Image smaller than dimensions
|
|
118
|
-
# can not be cropped to given size - unless upsample is true.
|
|
119
|
-
#
|
|
120
|
-
def can_be_cropped_to(string, upsample = false)
|
|
121
|
-
return true if upsample
|
|
122
|
-
|
|
123
|
-
is_bigger_than sizes_from_string(string)
|
|
124
|
-
end
|
|
125
|
-
|
|
126
94
|
# Returns true if the class we're included in has a meaningful render_size attribute
|
|
127
95
|
#
|
|
128
96
|
def render_size?
|
|
@@ -217,22 +185,10 @@ module Alchemy
|
|
|
217
185
|
"#{dimensions[:width]}x#{dimensions[:height]}"
|
|
218
186
|
end
|
|
219
187
|
|
|
220
|
-
# Returns true if both dimensions of the base image are bigger than the dimensions hash.
|
|
221
|
-
#
|
|
222
|
-
def is_bigger_than(dimensions)
|
|
223
|
-
image_file_width > dimensions[:width] && image_file_height > dimensions[:height]
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
# Returns true is one dimension of the base image is smaller than the dimensions hash.
|
|
227
|
-
#
|
|
228
|
-
def is_smaller_than(dimensions)
|
|
229
|
-
!is_bigger_than(dimensions)
|
|
230
|
-
end
|
|
231
|
-
|
|
232
188
|
# Uses imagemagick to make a centercropped thumbnail. Does not scale the image up.
|
|
233
189
|
#
|
|
234
190
|
def center_crop(dimensions, upsample)
|
|
235
|
-
if is_smaller_than(dimensions) && upsample == false
|
|
191
|
+
if is_smaller_than?(dimensions) && upsample == false
|
|
236
192
|
dimensions = reduce_to_image(dimensions)
|
|
237
193
|
end
|
|
238
194
|
image_file.thumb("#{dimensions_to_string(dimensions)}#")
|
|
@@ -241,12 +197,12 @@ module Alchemy
|
|
|
241
197
|
# Use imagemagick to custom crop an image. Uses -thumbnail for better performance when resizing.
|
|
242
198
|
#
|
|
243
199
|
def xy_crop_resize(dimensions, top_left, crop_dimensions, upsample)
|
|
244
|
-
crop_argument = dimensions_to_string(crop_dimensions)
|
|
200
|
+
crop_argument = "-crop #{dimensions_to_string(crop_dimensions)}"
|
|
245
201
|
crop_argument += "+#{top_left[:x]}+#{top_left[:y]}"
|
|
246
202
|
|
|
247
|
-
resize_argument = dimensions_to_string(dimensions)
|
|
203
|
+
resize_argument = "-resize #{dimensions_to_string(dimensions)}"
|
|
248
204
|
resize_argument += ">" unless upsample
|
|
249
|
-
image_file.
|
|
205
|
+
image_file.convert "#{crop_argument} #{resize_argument}"
|
|
250
206
|
end
|
|
251
207
|
|
|
252
208
|
# Used when centercropping.
|
|
@@ -1,94 +1,45 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Alchemy
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
class Picture < BaseRecord
|
|
5
|
+
class Url
|
|
6
|
+
attr_reader :variant
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
:flatten,
|
|
12
|
-
:format,
|
|
13
|
-
:quality,
|
|
14
|
-
:size,
|
|
15
|
-
:upsample,
|
|
16
|
-
]
|
|
8
|
+
# @param [Alchemy::PictureVariant]
|
|
9
|
+
#
|
|
10
|
+
def initialize(variant)
|
|
11
|
+
raise ArgumentError, "Variant missing!" if variant.nil?
|
|
17
12
|
|
|
18
|
-
|
|
19
|
-
#
|
|
20
|
-
# Any additional options are passed to the url_helper, so you can add arguments to your url.
|
|
21
|
-
#
|
|
22
|
-
# Example:
|
|
23
|
-
#
|
|
24
|
-
# <%= image_tag picture.url(size: '320x200', format: 'png') %>
|
|
25
|
-
#
|
|
26
|
-
def url(options = {})
|
|
27
|
-
image = image_file
|
|
28
|
-
|
|
29
|
-
raise MissingImageFileError, "Missing image file for #{inspect}" if image.nil?
|
|
30
|
-
|
|
31
|
-
image = processed_image(image, options)
|
|
32
|
-
image = encoded_image(image, options)
|
|
33
|
-
|
|
34
|
-
image.url(options.except(*TRANSFORMATION_OPTIONS).merge(name: name))
|
|
35
|
-
rescue MissingImageFileError, WrongImageFormatError => e
|
|
36
|
-
log_warning e.message
|
|
37
|
-
nil
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
private
|
|
41
|
-
|
|
42
|
-
# Returns the processed image dependent of size and cropping parameters
|
|
43
|
-
def processed_image(image, options = {})
|
|
44
|
-
size = options[:size]
|
|
45
|
-
upsample = !!options[:upsample]
|
|
46
|
-
|
|
47
|
-
return image unless size.present? && has_convertible_format?
|
|
48
|
-
|
|
49
|
-
if options[:crop]
|
|
50
|
-
crop(size, options[:crop_from], options[:crop_size], upsample)
|
|
51
|
-
else
|
|
52
|
-
resize(size, upsample)
|
|
13
|
+
@variant = variant
|
|
53
14
|
end
|
|
54
|
-
end
|
|
55
15
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
target_format = options[:format] || default_render_format
|
|
16
|
+
# The URL to a variant of a picture
|
|
17
|
+
#
|
|
18
|
+
# @return [String]
|
|
19
|
+
#
|
|
20
|
+
def call(params = {})
|
|
21
|
+
return variant.image.url(params) unless processible_image?
|
|
63
22
|
|
|
64
|
-
|
|
65
|
-
raise WrongImageFormatError.new(self, target_format)
|
|
23
|
+
"/#{uid}"
|
|
66
24
|
end
|
|
67
25
|
|
|
68
|
-
|
|
69
|
-
flatten: target_format != "gif" && image_file_format == "gif",
|
|
70
|
-
}.with_indifferent_access.merge(options)
|
|
71
|
-
|
|
72
|
-
encoding_options = []
|
|
26
|
+
private
|
|
73
27
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if target_format =~ /jpe?g/ && convert_format
|
|
77
|
-
quality = options[:quality] || Config.get(:output_image_jpg_quality)
|
|
78
|
-
encoding_options << "-quality #{quality}"
|
|
28
|
+
def processible_image?
|
|
29
|
+
variant.image.is_a?(::Dragonfly::Job)
|
|
79
30
|
end
|
|
80
31
|
|
|
81
|
-
|
|
82
|
-
|
|
32
|
+
def uid
|
|
33
|
+
signature = PictureThumb::Signature.call(variant)
|
|
34
|
+
thumb = variant.picture.thumbs.detect { |t| t.signature == signature }
|
|
35
|
+
if thumb
|
|
36
|
+
uid = thumb.uid
|
|
37
|
+
else
|
|
38
|
+
uid = PictureThumb::Uid.call(signature, variant)
|
|
39
|
+
PictureThumb.generator_class.call(variant, signature, uid)
|
|
40
|
+
end
|
|
41
|
+
uid
|
|
83
42
|
end
|
|
84
|
-
|
|
85
|
-
convertion_needed = convert_format || encoding_options.present?
|
|
86
|
-
|
|
87
|
-
if has_convertible_format? && convertion_needed
|
|
88
|
-
image = image.encode(target_format, encoding_options.join(" "))
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
image
|
|
92
43
|
end
|
|
93
44
|
end
|
|
94
45
|
end
|
|
@@ -30,11 +30,22 @@ module Alchemy
|
|
|
30
30
|
|
|
31
31
|
CONVERTIBLE_FILE_FORMATS = %w(gif jpg jpeg png).freeze
|
|
32
32
|
|
|
33
|
+
TRANSFORMATION_OPTIONS = [
|
|
34
|
+
:crop,
|
|
35
|
+
:crop_from,
|
|
36
|
+
:crop_size,
|
|
37
|
+
:flatten,
|
|
38
|
+
:format,
|
|
39
|
+
:quality,
|
|
40
|
+
:size,
|
|
41
|
+
:upsample,
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
include Alchemy::Logger
|
|
33
45
|
include Alchemy::NameConversions
|
|
34
46
|
include Alchemy::Taggable
|
|
35
47
|
include Alchemy::TouchElements
|
|
36
|
-
include
|
|
37
|
-
include Alchemy::Picture::Url
|
|
48
|
+
include Calculations
|
|
38
49
|
|
|
39
50
|
has_many :essence_pictures,
|
|
40
51
|
class_name: "Alchemy::EssencePicture",
|
|
@@ -44,6 +55,7 @@ module Alchemy
|
|
|
44
55
|
has_many :contents, through: :essence_pictures
|
|
45
56
|
has_many :elements, through: :contents
|
|
46
57
|
has_many :pages, through: :elements
|
|
58
|
+
has_many :thumbs, class_name: "Alchemy::PictureThumb", dependent: :destroy
|
|
47
59
|
|
|
48
60
|
# Raise error, if picture is in use (aka. assigned to an EssencePicture)
|
|
49
61
|
#
|
|
@@ -78,6 +90,9 @@ module Alchemy
|
|
|
78
90
|
end
|
|
79
91
|
end
|
|
80
92
|
|
|
93
|
+
# Create important thumbnails upfront
|
|
94
|
+
after_create -> { PictureThumb.generate_thumbs!(self) }
|
|
95
|
+
|
|
81
96
|
# We need to define this method here to have it available in the validations below.
|
|
82
97
|
class << self
|
|
83
98
|
def allowed_filetypes
|
|
@@ -93,7 +108,7 @@ module Alchemy
|
|
|
93
108
|
case_sensitive: false,
|
|
94
109
|
message: Alchemy.t("not a valid image")
|
|
95
110
|
|
|
96
|
-
stampable stamper_class_name: Alchemy.
|
|
111
|
+
stampable stamper_class_name: Alchemy.user_class_name
|
|
97
112
|
|
|
98
113
|
scope :named, ->(name) { where("#{table_name}.name LIKE ?", "%#{name}%") }
|
|
99
114
|
scope :recent, -> { where("#{table_name}.created_at > ?", Time.current - 24.hours).order(:created_at) }
|
|
@@ -103,6 +118,20 @@ module Alchemy
|
|
|
103
118
|
# Class methods
|
|
104
119
|
|
|
105
120
|
class << self
|
|
121
|
+
# The class used to generate URLs for pictures
|
|
122
|
+
#
|
|
123
|
+
# @see Alchemy::Picture::Url
|
|
124
|
+
def url_class
|
|
125
|
+
@_url_class ||= Alchemy::Picture::Url
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Set a different picture url class
|
|
129
|
+
#
|
|
130
|
+
# @see Alchemy::Picture::Url
|
|
131
|
+
def url_class=(klass)
|
|
132
|
+
@_url_class = klass
|
|
133
|
+
end
|
|
134
|
+
|
|
106
135
|
def searchable_alchemy_resource_attributes
|
|
107
136
|
%w(name image_file_name)
|
|
108
137
|
end
|
|
@@ -145,6 +174,33 @@ module Alchemy
|
|
|
145
174
|
|
|
146
175
|
# Instance methods
|
|
147
176
|
|
|
177
|
+
# Returns an url (or relative path) to a processed image for use inside an image_tag helper.
|
|
178
|
+
#
|
|
179
|
+
# Any additional options are passed to the url method, so you can add params to your url.
|
|
180
|
+
#
|
|
181
|
+
# Example:
|
|
182
|
+
#
|
|
183
|
+
# <%= image_tag picture.url(size: '320x200', format: 'png') %>
|
|
184
|
+
#
|
|
185
|
+
# @see Alchemy::PictureVariant#call for transformation options
|
|
186
|
+
# @see Alchemy::Picture::Url#call for url options
|
|
187
|
+
# @return [String|Nil]
|
|
188
|
+
def url(options = {})
|
|
189
|
+
return unless image_file
|
|
190
|
+
|
|
191
|
+
variant = PictureVariant.new(self, options.slice(*TRANSFORMATION_OPTIONS))
|
|
192
|
+
self.class.url_class.new(variant).call(
|
|
193
|
+
options.except(*TRANSFORMATION_OPTIONS).merge(
|
|
194
|
+
basename: name,
|
|
195
|
+
ext: variant.render_format,
|
|
196
|
+
name: name,
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
rescue ::Dragonfly::Job::Fetch::NotFound => e
|
|
200
|
+
log_warning(e.message)
|
|
201
|
+
nil
|
|
202
|
+
end
|
|
203
|
+
|
|
148
204
|
def previous(params = {})
|
|
149
205
|
query = Picture.ransack(params[:q])
|
|
150
206
|
Picture.search_by(params, query).where("name < ?", name).last
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
class PictureThumb < BaseRecord
|
|
5
|
+
# Stores the render result of a Alchemy::PictureVariant
|
|
6
|
+
# in the configured Dragonfly datastore
|
|
7
|
+
# (Default: Dragonfly::FileDataStore)
|
|
8
|
+
#
|
|
9
|
+
class Create
|
|
10
|
+
class << self
|
|
11
|
+
# @param [Alchemy::PictureVariant] variant the to be rendered image
|
|
12
|
+
# @param [String] signature A unique hashed version of the rendering options
|
|
13
|
+
# @param [String] uid The Unique Image Identifier the image is stored at
|
|
14
|
+
#
|
|
15
|
+
# @return [Alchemy::PictureThumb] The persisted thumbnail record
|
|
16
|
+
#
|
|
17
|
+
def call(variant, signature, uid)
|
|
18
|
+
image = variant.image
|
|
19
|
+
image.to_file(server_path(uid)).close
|
|
20
|
+
variant.picture.thumbs.create!(
|
|
21
|
+
picture: variant.picture,
|
|
22
|
+
signature: signature,
|
|
23
|
+
uid: uid,
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# Alchemys dragonfly datastore config seperates the storage path from the public server
|
|
30
|
+
# path for security reasons. The Dragonfly FileDataStorage does not support that,
|
|
31
|
+
# so we need to build the path on our own.
|
|
32
|
+
def server_path(uid)
|
|
33
|
+
dragonfly_app = ::Dragonfly.app(:alchemy_pictures)
|
|
34
|
+
"#{dragonfly_app.datastore.server_root}/#{uid}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
class PictureThumb < BaseRecord
|
|
5
|
+
class Signature
|
|
6
|
+
# Returns a unique image process signature
|
|
7
|
+
#
|
|
8
|
+
# @param [Alchemy::PictureVariant]
|
|
9
|
+
#
|
|
10
|
+
# @return [String]
|
|
11
|
+
def self.call(variant)
|
|
12
|
+
steps_without_fetch = variant.image.steps.reject do |step|
|
|
13
|
+
step.is_a?(::Dragonfly::Job::Fetch)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
steps_with_id = [[variant.picture.id]] + steps_without_fetch
|
|
17
|
+
job_string = steps_with_id.map(&:to_a).to_dragonfly_unique_s
|
|
18
|
+
|
|
19
|
+
Digest::SHA1.hexdigest(job_string)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
class PictureThumb < BaseRecord
|
|
5
|
+
class Uid
|
|
6
|
+
# Returns a image variant uid for storage
|
|
7
|
+
#
|
|
8
|
+
# @param [String]
|
|
9
|
+
# @param [Alchemy::PictureVariant]
|
|
10
|
+
#
|
|
11
|
+
# @return [String]
|
|
12
|
+
def self.call(signature, variant)
|
|
13
|
+
picture = variant.picture
|
|
14
|
+
filename = variant.image_file_name || "image"
|
|
15
|
+
name = File.basename(filename, ".*").gsub(/[^\w.]+/, "_")
|
|
16
|
+
ext = variant.render_format
|
|
17
|
+
|
|
18
|
+
"pictures/#{picture.id}/#{signature}/#{name}.#{ext}"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
# The persisted version of a rendered picture variant
|
|
5
|
+
#
|
|
6
|
+
# You can configure the generator class to implement a
|
|
7
|
+
# different thumbnail store (ie. a remote file storage).
|
|
8
|
+
#
|
|
9
|
+
# config/initializers/alchemy.rb
|
|
10
|
+
# Alchemy::PictureThumb.generator_class = My::ThumbnailGenerator
|
|
11
|
+
#
|
|
12
|
+
class PictureThumb < BaseRecord
|
|
13
|
+
belongs_to :picture, class_name: "Alchemy::Picture"
|
|
14
|
+
|
|
15
|
+
validates :signature, presence: true
|
|
16
|
+
validates :uid, presence: true
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# Thumbnail generator class
|
|
20
|
+
#
|
|
21
|
+
# @see Alchemy::PictureThumb::Create
|
|
22
|
+
def generator_class
|
|
23
|
+
@_generator_class ||= Alchemy::PictureThumb::Create
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Set a thumbnail generator class
|
|
27
|
+
#
|
|
28
|
+
# @see Alchemy::PictureThumb::Create
|
|
29
|
+
def generator_class=(klass)
|
|
30
|
+
@_generator_class = klass
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Upfront generation of picture thumbnails
|
|
34
|
+
#
|
|
35
|
+
# Called after a Alchemy::Picture has been created (after an image has been uploaded)
|
|
36
|
+
#
|
|
37
|
+
# Generates three types of thumbnails that are used by Alchemys picture archive and
|
|
38
|
+
# persists them in the configures file store (Default Dragonfly::FileDataStore).
|
|
39
|
+
#
|
|
40
|
+
# @see Picture::THUMBNAIL_SIZES
|
|
41
|
+
def generate_thumbs!(picture)
|
|
42
|
+
Alchemy::Picture::THUMBNAIL_SIZES.values.each do |size|
|
|
43
|
+
variant = Alchemy::PictureVariant.new(picture, {
|
|
44
|
+
size: size,
|
|
45
|
+
flatten: true,
|
|
46
|
+
})
|
|
47
|
+
signature = Alchemy::PictureThumb::Signature.call(variant)
|
|
48
|
+
thumb = find_by(signature: signature)
|
|
49
|
+
next if thumb
|
|
50
|
+
|
|
51
|
+
uid = Alchemy::PictureThumb::Uid.call(signature, variant)
|
|
52
|
+
generator_class.call(variant, signature, uid)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
5
|
+
module Alchemy
|
|
6
|
+
# Represents a rendered picture
|
|
7
|
+
#
|
|
8
|
+
# Resizes, crops and encodes the image with imagemagick
|
|
9
|
+
#
|
|
10
|
+
class PictureVariant
|
|
11
|
+
extend Forwardable
|
|
12
|
+
|
|
13
|
+
include Alchemy::Logger
|
|
14
|
+
include Alchemy::Picture::Transformations
|
|
15
|
+
|
|
16
|
+
attr_reader :picture, :render_format
|
|
17
|
+
|
|
18
|
+
def_delegators :@picture,
|
|
19
|
+
:image_file,
|
|
20
|
+
:image_file_width,
|
|
21
|
+
:image_file_height,
|
|
22
|
+
:image_file_name,
|
|
23
|
+
:image_file_size
|
|
24
|
+
|
|
25
|
+
# @param [Alchemy::Picture]
|
|
26
|
+
#
|
|
27
|
+
# @param [Hash] options passed to the image processor
|
|
28
|
+
# @option options [Boolean] :crop Pass true to enable cropping
|
|
29
|
+
# @option options [String] :crop_from Coordinates to start cropping from
|
|
30
|
+
# @option options [String] :crop_size Size of the cropping area
|
|
31
|
+
# @option options [Boolean] :flatten Pass true to flatten GIFs
|
|
32
|
+
# @option options [String|Symbol] :format Image format to encode the image in
|
|
33
|
+
# @option options [Integer] :quality JPEG compress quality
|
|
34
|
+
# @option options [String] :size Size of resulting image in WxH
|
|
35
|
+
# @option options [Boolean] :upsample Pass true to upsample (grow) an image if the original size is lower than the resulting size
|
|
36
|
+
#
|
|
37
|
+
def initialize(picture, options = {})
|
|
38
|
+
raise ArgumentError, "Picture missing!" if picture.nil?
|
|
39
|
+
|
|
40
|
+
@picture = picture
|
|
41
|
+
@options = options
|
|
42
|
+
@render_format = options[:format] || picture.default_render_format
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Process a variant of picture
|
|
46
|
+
#
|
|
47
|
+
# @return [Dragonfly::Attachment|Dragonfly::Job] The processed image variant
|
|
48
|
+
#
|
|
49
|
+
def image
|
|
50
|
+
image = image_file
|
|
51
|
+
|
|
52
|
+
raise MissingImageFileError, "Missing image file for #{picture.inspect}" if image.nil?
|
|
53
|
+
|
|
54
|
+
image = processed_image(image, @options)
|
|
55
|
+
image = encoded_image(image, @options)
|
|
56
|
+
image
|
|
57
|
+
rescue MissingImageFileError, WrongImageFormatError => e
|
|
58
|
+
log_warning(e.message)
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# Returns the processed image dependent of size and cropping parameters
|
|
65
|
+
def processed_image(image, options = {})
|
|
66
|
+
size = options[:size]
|
|
67
|
+
upsample = !!options[:upsample]
|
|
68
|
+
|
|
69
|
+
return image unless size.present? && picture.has_convertible_format?
|
|
70
|
+
|
|
71
|
+
if options[:crop]
|
|
72
|
+
crop(size, options[:crop_from], options[:crop_size], upsample)
|
|
73
|
+
else
|
|
74
|
+
resize(size, upsample)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns the encoded image
|
|
79
|
+
#
|
|
80
|
+
# Flatten animated gifs, only if converting to a different format.
|
|
81
|
+
# Can be overwritten via +options[:flatten]+.
|
|
82
|
+
#
|
|
83
|
+
def encoded_image(image, options = {})
|
|
84
|
+
unless render_format.in?(Alchemy::Picture.allowed_filetypes)
|
|
85
|
+
raise WrongImageFormatError.new(picture, @render_format)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
options = {
|
|
89
|
+
flatten: render_format != "gif" && picture.image_file_format == "gif",
|
|
90
|
+
}.with_indifferent_access.merge(options)
|
|
91
|
+
|
|
92
|
+
encoding_options = []
|
|
93
|
+
|
|
94
|
+
convert_format = render_format.sub("jpeg", "jpg") != picture.image_file_format.sub("jpeg", "jpg")
|
|
95
|
+
|
|
96
|
+
if render_format =~ /jpe?g/ && convert_format
|
|
97
|
+
quality = options[:quality] || Config.get(:output_image_jpg_quality)
|
|
98
|
+
encoding_options << "-quality #{quality}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
if options[:flatten]
|
|
102
|
+
encoding_options << "-flatten"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
convertion_needed = convert_format || encoding_options.present?
|
|
106
|
+
|
|
107
|
+
if picture.has_convertible_format? && convertion_needed
|
|
108
|
+
image = image.encode(render_format, encoding_options.join(" "))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
image
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -40,7 +40,7 @@ module Alchemy
|
|
|
40
40
|
|
|
41
41
|
level = path.count + base_level
|
|
42
42
|
|
|
43
|
-
path.last[:children] << page_hash(page, level, folded)
|
|
43
|
+
path.last[:children] << page_hash(page, has_children, level, folded)
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
tree
|
|
@@ -48,7 +48,7 @@ module Alchemy
|
|
|
48
48
|
|
|
49
49
|
protected
|
|
50
50
|
|
|
51
|
-
def page_hash(page, level, folded)
|
|
51
|
+
def page_hash(page, has_children, level, folded)
|
|
52
52
|
p_hash = {
|
|
53
53
|
id: page.id,
|
|
54
54
|
name: page.name,
|
|
@@ -59,8 +59,8 @@ module Alchemy
|
|
|
59
59
|
urlname: page.urlname,
|
|
60
60
|
url_path: page.url_path,
|
|
61
61
|
level: level,
|
|
62
|
-
root: page.
|
|
63
|
-
root_or_leaf: page.
|
|
62
|
+
root: page.depth == 1,
|
|
63
|
+
root_or_leaf: page.depth == 1 || !has_children,
|
|
64
64
|
children: [],
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
</div>
|
|
8
8
|
<div class="value with-icon">
|
|
9
9
|
<label><%= Alchemy::Attachment.human_attribute_name(:url) %></label>
|
|
10
|
-
<p><%=
|
|
11
|
-
<a data-clipboard-text="<%=
|
|
10
|
+
<p><%= @attachment.url %></p>
|
|
11
|
+
<a data-clipboard-text="<%= @attachment.url %>" class="icon_button--right">
|
|
12
12
|
<%= render_icon(:clipboard, style: 'regular') %>
|
|
13
13
|
</a>
|
|
14
14
|
</div>
|
|
15
15
|
<div class="value with-icon">
|
|
16
16
|
<label><%= Alchemy::Attachment.human_attribute_name(:download_url) %></label>
|
|
17
|
-
<p><%=
|
|
18
|
-
<a data-clipboard-text="<%=
|
|
17
|
+
<p><%= @attachment.url(download: true) %></p>
|
|
18
|
+
<a data-clipboard-text="<%= @attachment.url(download: true) %>" class="icon_button--right">
|
|
19
19
|
<%= render_icon(:clipboard, style: 'regular') %>
|
|
20
20
|
</a>
|
|
21
21
|
</div>
|
|
@@ -24,18 +24,18 @@
|
|
|
24
24
|
<% case @attachment.icon_css_class %>
|
|
25
25
|
<% when "file-image" %>
|
|
26
26
|
<div class="attachment_preview_container image-preview">
|
|
27
|
-
<%= image_tag(
|
|
27
|
+
<%= image_tag(@attachment.url, class: "full_width") %>
|
|
28
28
|
</div>
|
|
29
29
|
<% when "file-audio" %>
|
|
30
30
|
<div class="attachment_preview_container player-preview">
|
|
31
|
-
<%= audio_tag(
|
|
31
|
+
<%= audio_tag(@attachment.url, preload: "none", controls: true, class: "full_width") %>
|
|
32
32
|
</div>
|
|
33
33
|
<% when "file-video" %>
|
|
34
34
|
<div class="attachment_preview_container player-preview">
|
|
35
|
-
<%= video_tag(
|
|
35
|
+
<%= video_tag(@attachment.url, preload: "metadata", controls: true, class: "full_width") %>
|
|
36
36
|
</div>
|
|
37
37
|
<% when "file-pdf" %>
|
|
38
|
-
<iframe src="<%=
|
|
38
|
+
<iframe src="<%= @attachment.url %>" frameborder=0 class="full-iframe">
|
|
39
39
|
Your browser does not support frames.
|
|
40
40
|
</iframe>
|
|
41
41
|
<% end %>
|