dynamic_image 3.0.9 → 3.1.0
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/README.md +393 -67
- data/app/models/dynamic_image/variant.rb +10 -0
- data/lib/dynamic_image/backfill.rb +85 -0
- data/lib/dynamic_image/belongs_to.rb +22 -0
- data/lib/dynamic_image/breakpoints.rb +93 -0
- data/lib/dynamic_image/controller.rb +51 -7
- data/lib/dynamic_image/digest_verifier.rb +20 -9
- data/lib/dynamic_image/engine.rb +3 -0
- data/lib/dynamic_image/errors.rb +30 -0
- data/lib/dynamic_image/format.rb +149 -8
- data/lib/dynamic_image/format_negotiator.rb +84 -0
- data/lib/dynamic_image/helper/formats.rb +57 -0
- data/lib/dynamic_image/helper/pictures.rb +124 -0
- data/lib/dynamic_image/helper.rb +100 -60
- data/lib/dynamic_image/image_processor/colors.rb +3 -7
- data/lib/dynamic_image/image_processor/frames.rb +10 -1
- data/lib/dynamic_image/image_processor/transform.rb +17 -2
- data/lib/dynamic_image/image_processor.rb +33 -6
- data/lib/dynamic_image/image_reader.rb +19 -1
- data/lib/dynamic_image/image_sizing.rb +51 -19
- data/lib/dynamic_image/metadata.rb +56 -5
- data/lib/dynamic_image/model/dimensions.rb +32 -13
- data/lib/dynamic_image/model/transformations.rb +47 -11
- data/lib/dynamic_image/model/validations.rb +15 -8
- data/lib/dynamic_image/model/variants.rb +6 -3
- data/lib/dynamic_image/model.rb +58 -34
- data/lib/dynamic_image/picture/format_policy.rb +72 -0
- data/lib/dynamic_image/picture.rb +215 -0
- data/lib/dynamic_image/processed_image.rb +48 -14
- data/lib/dynamic_image/ratio.rb +43 -0
- data/lib/dynamic_image/routing.rb +14 -3
- data/lib/dynamic_image/schema.rb +47 -0
- data/lib/dynamic_image/version.rb +1 -1
- data/lib/dynamic_image.rb +61 -1
- data/lib/rails/generators/dynamic_image/resource/USAGE +17 -0
- data/lib/rails/generators/dynamic_image/resource/resource_generator.rb +57 -21
- data/lib/rails/generators/dynamic_image/resource/templates/create_table_migration.rb.tt +32 -0
- data/lib/rails/generators/dynamic_image/upgrade/USAGE +17 -0
- data/lib/rails/generators/dynamic_image/upgrade/templates/upgrade_migration.rb.tt +10 -0
- data/lib/rails/generators/dynamic_image/upgrade/upgrade_generator.rb +111 -0
- data/lib/tasks/dynamic_image.rake +25 -0
- metadata +23 -12
- data/lib/dynamic_image/jobs/create_variant.rb +0 -22
- data/lib/dynamic_image/jobs.rb +0 -3
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DynamicImage
|
|
4
|
+
module Helper
|
|
5
|
+
# = DynamicImage Helper Formats
|
|
6
|
+
#
|
|
7
|
+
# Resolves the <tt>format:</tt> option the helpers in {DynamicImage::Helper} pass to the router.
|
|
8
|
+
#
|
|
9
|
+
# A symbol forces that format. An array, or nothing at all, is negotiated against the image. The default list
|
|
10
|
+
# depends on where the view is rendered: {DynamicImage.mailer_formats} in a mailer,
|
|
11
|
+
# {DynamicImage.default_formats} everywhere else.
|
|
12
|
+
#
|
|
13
|
+
# The +original+ and +download+ actions serve the stored file untouched, so they always get the record's own
|
|
14
|
+
# format. Negotiating one would put an extension on the URL that the response doesn't match.
|
|
15
|
+
#
|
|
16
|
+
# @see DynamicImage::FormatNegotiator
|
|
17
|
+
module Formats
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def dynamic_image_format(record, format, action = nil)
|
|
21
|
+
return stored_format_for_image(record) if raw_action?(action)
|
|
22
|
+
return format unless format.nil? || format.is_a?(Array)
|
|
23
|
+
|
|
24
|
+
negotiated_format_for_image(record, format || accepted_image_formats)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def raw_action?(action)
|
|
28
|
+
%w[original download].include?(action.to_s)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def stored_format_for_image(record)
|
|
32
|
+
mime_format(DynamicImage::Format.content_type(record.content_type))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def negotiated_format_for_image(record, accepted)
|
|
36
|
+
mime_format(DynamicImage::FormatNegotiator.new(record)
|
|
37
|
+
.negotiate(accepted))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def mime_format(format)
|
|
41
|
+
format.mime_type.to_sym
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def accepted_image_formats
|
|
45
|
+
if mailer_view?
|
|
46
|
+
DynamicImage.mailer_formats
|
|
47
|
+
else
|
|
48
|
+
DynamicImage.default_formats
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def mailer_view?
|
|
53
|
+
defined?(ActionMailer::Base) && controller.is_a?(ActionMailer::Base)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DynamicImage
|
|
4
|
+
module Helper
|
|
5
|
+
# = DynamicImage Helper Pictures
|
|
6
|
+
#
|
|
7
|
+
# Renders responsive images: a <tt>picture</tt> element with a WebP <tt>source</tt> covering a range of widths,
|
|
8
|
+
# and an <tt>img</tt> fallback in a format everything understands.
|
|
9
|
+
#
|
|
10
|
+
# These helpers take an optional +ratio+ instead of a +size+, leaving the width to the browser.
|
|
11
|
+
# {DynamicImage::Helper#dynamic_image_tag} renders at a size you pick; these render across a range of widths.
|
|
12
|
+
#
|
|
13
|
+
# @see DynamicImage::Picture
|
|
14
|
+
# @see DynamicImage::Breakpoints
|
|
15
|
+
module Pictures
|
|
16
|
+
# Returns a {DynamicImage::Picture} for the record, which is what the tag helpers render.
|
|
17
|
+
#
|
|
18
|
+
# @param record_or_array [DynamicImage::Model, Array] the record, or an array of records for a nested route
|
|
19
|
+
# @param options [Hash] sizing and routing options, as taken by {DynamicImage::Picture#initialize}
|
|
20
|
+
# @return [DynamicImage::Picture]
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# picture = dynamic_picture(image, ratio: "16:9")
|
|
24
|
+
# picture.srcset # => "/images/… 420w, /images/… 590w, …"
|
|
25
|
+
# picture.sources # => [{ type: "image/webp", srcset: "…" }]
|
|
26
|
+
def dynamic_picture(record_or_array, options = {})
|
|
27
|
+
DynamicImage::Picture.new(self, record_or_array, picture_options(options.symbolize_keys))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Renders a responsive <tt>picture</tt> element.
|
|
31
|
+
#
|
|
32
|
+
# The <tt>img</tt> carries the +width+ and +height+ of the fallback, which needn't match whichever candidate is
|
|
33
|
+
# chosen.
|
|
34
|
+
#
|
|
35
|
+
# Any other options are passed on to {DynamicImage::Helper#dynamic_image_tag}.
|
|
36
|
+
#
|
|
37
|
+
# @param record_or_array [DynamicImage::Model, Array] the record, or an array of records for a nested route
|
|
38
|
+
# @param options [Hash] sizing options, routing options and HTML attributes
|
|
39
|
+
# @option options [Numeric, Vector2d, String] :ratio The aspect ratio to crop to. Implies cropping.
|
|
40
|
+
# @option options [String] :sizes The +sizes+ attribute, telling the browser how large the image will be
|
|
41
|
+
# rendered.
|
|
42
|
+
# @option options [Range, Array<Integer>, Integer] :breakpoints The widths to offer, overriding
|
|
43
|
+
# {DynamicImage.default_breakpoints}
|
|
44
|
+
# @option options [Numeric] :step The step between breakpoints, overriding {DynamicImage.breakpoint_step}
|
|
45
|
+
# @option options [Integer] :fallback_width The width to ask for the <tt>img</tt>, overriding
|
|
46
|
+
# {DynamicImage.picture_fallback_width}
|
|
47
|
+
# @return [String] the picture element
|
|
48
|
+
#
|
|
49
|
+
# @example
|
|
50
|
+
# dynamic_picture_tag(image, sizes: "50vw", alt: "A kitten")
|
|
51
|
+
# dynamic_picture_tag(image, ratio: "16:9", sizes: "50vw")
|
|
52
|
+
def dynamic_picture_tag(record_or_array, options = {})
|
|
53
|
+
options = options.symbolize_keys
|
|
54
|
+
picture = dynamic_picture(record_or_array, options)
|
|
55
|
+
source = picture_source_tag(picture) unless picture.sources.empty?
|
|
56
|
+
image = picture_fallback_tag(record_or_array, picture, options)
|
|
57
|
+
|
|
58
|
+
tag.picture { safe_join([source, image].compact) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Renders a single <tt>source</tt> element, for composing a <tt>picture</tt> by hand.
|
|
62
|
+
#
|
|
63
|
+
# Use it for different crops per media query: several sources, each with its own crop and query. The browser
|
|
64
|
+
# takes the first one whose +media+ matches and whose +type+ it supports, so put the specific queries first
|
|
65
|
+
# and the unconditional one last.
|
|
66
|
+
#
|
|
67
|
+
# @param record_or_array [DynamicImage::Model, Array] the record, or an array of records for a nested route
|
|
68
|
+
# @param options [Hash] sizing and routing options
|
|
69
|
+
# @option options [String] :media The media query this source answers
|
|
70
|
+
# @option options [Symbol, Array<Symbol>] :format The format to render in. A symbol forces that format, an
|
|
71
|
+
# array is negotiated. Defaults to WebP.
|
|
72
|
+
# @return [String, nil] the source element
|
|
73
|
+
#
|
|
74
|
+
# @example Different crops per media query
|
|
75
|
+
# <picture>
|
|
76
|
+
# <%= dynamic_picture_source_tag(image, ratio: "21:9",
|
|
77
|
+
# media: "(min-width: 1000px)") %>
|
|
78
|
+
# <%= dynamic_picture_source_tag(image, ratio: "1:1") %>
|
|
79
|
+
# <%= dynamic_image_tag(image, size: "1200x1200", crop: true) %>
|
|
80
|
+
# </picture>
|
|
81
|
+
#
|
|
82
|
+
# @example A source for browsers without WebP
|
|
83
|
+
# dynamic_picture_source_tag(image, ratio: "21:9",
|
|
84
|
+
# format: DynamicImage::COMPATIBLE_FORMATS)
|
|
85
|
+
def dynamic_picture_source_tag(record_or_array, options = {})
|
|
86
|
+
options = options.symbolize_keys
|
|
87
|
+
|
|
88
|
+
picture_source_tag(dynamic_picture(record_or_array, options), options[:media])
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def picture_source_tag(picture, media = nil)
|
|
94
|
+
srcset = picture.srcset
|
|
95
|
+
return unless srcset
|
|
96
|
+
|
|
97
|
+
tag.source(type: picture.type, srcset:, sizes: picture.sizes, media:)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def picture_fallback_tag(record_or_array, picture, options)
|
|
101
|
+
dynamic_image_tag(
|
|
102
|
+
record_or_array,
|
|
103
|
+
options.except(*DynamicImage::Picture::OPTIONS, :media)
|
|
104
|
+
.merge(img_srcset_attributes(picture))
|
|
105
|
+
.merge(size: picture.fallback_size,
|
|
106
|
+
crop: picture.crop?,
|
|
107
|
+
format: mime_format(picture.fallback_format))
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The candidates ride on the img itself when no source carries them.
|
|
112
|
+
def img_srcset_attributes(picture)
|
|
113
|
+
return {} unless picture.sources.empty?
|
|
114
|
+
|
|
115
|
+
{ srcset: picture.srcset, sizes: picture.sizes }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Passes on only the options the picture and the router understand.
|
|
119
|
+
def picture_options(options)
|
|
120
|
+
options.slice(*DynamicImage::Picture::OPTIONS, *allowed_dynamic_image_url_options)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
data/lib/dynamic_image/helper.rb
CHANGED
|
@@ -1,38 +1,62 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "dynamic_image/helper/formats"
|
|
4
|
+
require "dynamic_image/helper/pictures"
|
|
5
|
+
|
|
3
6
|
module DynamicImage
|
|
4
7
|
# = DynamicImage Helper
|
|
5
8
|
#
|
|
6
|
-
#
|
|
9
|
+
# Helper methods for rendering and linking to images.
|
|
10
|
+
#
|
|
11
|
+
# Every URL is signed with an HMAC digest of the action, the record id and the size, and the controller rejects
|
|
12
|
+
# anything that doesn't match. URLs can only be built server-side.
|
|
13
|
+
#
|
|
14
|
+
# +_path+ returns a relative path and +_url+ an absolute one. +_tag+ renders an image tag, and is available for
|
|
15
|
+
# +dynamic_image+ and +uncropped_dynamic_image+ only. +original+ and +download+ point at the stored file, which is
|
|
16
|
+
# never rendered.
|
|
17
|
+
#
|
|
18
|
+
# @see DynamicImage::ImageSizing for how sizes are calculated
|
|
19
|
+
# @see DynamicImage::Helper::Formats for how the format is chosen
|
|
20
|
+
# @see DynamicImage::Helper::Pictures for responsive images
|
|
7
21
|
module Helper
|
|
8
|
-
|
|
9
|
-
|
|
22
|
+
include DynamicImage::Helper::Formats
|
|
23
|
+
include DynamicImage::Helper::Pictures
|
|
24
|
+
|
|
25
|
+
# Returns the path for a {DynamicImage::Model} record. Takes the same options as {#dynamic_image_url}.
|
|
26
|
+
#
|
|
27
|
+
# @param record_or_array [DynamicImage::Model, Array] the record, or an array of records for a nested route
|
|
28
|
+
# @param options [Hash] sizing and routing options
|
|
29
|
+
# @return [String]
|
|
10
30
|
def dynamic_image_path(record_or_array, options = {})
|
|
11
31
|
dynamic_image_url(record_or_array, { routing_type: :path }.merge(options))
|
|
12
32
|
end
|
|
13
33
|
|
|
14
|
-
# Returns an HTML image tag for the record
|
|
15
|
-
#
|
|
34
|
+
# Returns an HTML image tag for the record, with +width+ and +height+ set to the rendered size. Without a
|
|
35
|
+
# size, it renders at the original size.
|
|
16
36
|
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# filename of the uploaded image.
|
|
37
|
+
# See {#dynamic_image_url} for sizing and cropping. Options supported by +polymorphic_url+ are passed to the
|
|
38
|
+
# router, and any others are added as HTML attributes.
|
|
20
39
|
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# options will be added as HTML attributes.
|
|
40
|
+
# Unless an +alt+ option is given, the alt text is resolved from {DynamicImage::Model#alt_text}.
|
|
41
|
+
# Pass <tt>alt: ""</tt> to mark an image as decorative.
|
|
24
42
|
#
|
|
25
|
-
#
|
|
43
|
+
# @param record_or_array [DynamicImage::Model, Array] the record, or an array of records for a nested route
|
|
44
|
+
# @param options [Hash] sizing options, routing options and HTML attributes
|
|
45
|
+
# @return [String]
|
|
26
46
|
#
|
|
47
|
+
# @example
|
|
27
48
|
# image = Image.find(params[:id])
|
|
49
|
+
#
|
|
28
50
|
# dynamic_image_tag(image)
|
|
29
|
-
# # => <img
|
|
30
|
-
#
|
|
31
|
-
#
|
|
51
|
+
# # => <img src="..." width="320" height="200" />
|
|
52
|
+
#
|
|
53
|
+
# dynamic_image_tag(image, size: "100x100", alt: "Avatar")
|
|
54
|
+
# # => <img alt="Avatar" src="..." width="100" height="62" />
|
|
32
55
|
def dynamic_image_tag(record_or_array, options = {})
|
|
56
|
+
record = extract_dynamic_image_record(record_or_array)
|
|
33
57
|
size = fit_size!(record_or_array, options)
|
|
34
58
|
url_options = options.extract!(*allowed_dynamic_image_url_options)
|
|
35
|
-
html_options = { size: }.merge(options)
|
|
59
|
+
html_options = { size:, alt: record.alt_text }.merge(options)
|
|
36
60
|
|
|
37
61
|
image_tag(dynamic_image_path_with_size(record_or_array,
|
|
38
62
|
size,
|
|
@@ -40,26 +64,33 @@ module DynamicImage
|
|
|
40
64
|
html_options)
|
|
41
65
|
end
|
|
42
66
|
|
|
43
|
-
# Returns the URL for a DynamicImage::Model record.
|
|
44
|
-
#
|
|
45
|
-
# ==== Options
|
|
67
|
+
# Returns the URL for a {DynamicImage::Model} record.
|
|
46
68
|
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
69
|
+
# @param record_or_array [DynamicImage::Model, Array] the record, or an array of records for a nested route
|
|
70
|
+
# @param options [Hash] sizing and routing options
|
|
71
|
+
# @option options [String] :size Desired image size, as <tt>"{width}x{height}"</tt>. The image is scaled to fit. A
|
|
72
|
+
# partial size like <tt>"100x"</tt> or <tt>"x100"</tt> can be given for a fixed width or height.
|
|
73
|
+
# @option options [Boolean] :crop Crop the image to the given size instead of fitting it. Both dimensions are
|
|
74
|
+
# required.
|
|
75
|
+
# @option options [Boolean] :upscale By default images are only scaled down, never up. Pass true to force
|
|
76
|
+
# upscaling.
|
|
77
|
+
# @option options [Symbol, Array<Symbol>] :format Render in a different format. A symbol forces that format,
|
|
78
|
+
# an array is negotiated against the image by {DynamicImage::FormatNegotiator}. Defaults to
|
|
79
|
+
# {DynamicImage.default_formats}, or {DynamicImage.mailer_formats} in a mailer view.
|
|
80
|
+
# @return [String]
|
|
81
|
+
# @raise [DynamicImage::Errors::InvalidSizeOptions] if <tt>crop: true</tt> is given without both dimensions
|
|
53
82
|
#
|
|
54
83
|
# Any options supported by +polymorphic_url+ are also accepted.
|
|
55
84
|
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
85
|
+
# @example
|
|
58
86
|
# image = Image.find(params[:id])
|
|
87
|
+
#
|
|
59
88
|
# dynamic_image_url(image)
|
|
60
89
|
# # => "http://example.com/images/96...d1/300x187/1-2014062020...00.jpg"
|
|
90
|
+
#
|
|
61
91
|
# dynamic_image_url(image, size: '100x100')
|
|
62
92
|
# # => "http://example.com/images/72...c2/100x62/1-2014062020...00.jpg"
|
|
93
|
+
#
|
|
63
94
|
# dynamic_image_url(image, size: '100x100', crop: true)
|
|
64
95
|
# # => "http://example.com/images/a4...6b/100x100/1-2014062020...00.jpg"
|
|
65
96
|
def dynamic_image_url(record_or_array, options = {})
|
|
@@ -67,46 +98,68 @@ module DynamicImage
|
|
|
67
98
|
dynamic_image_url_with_size(record_or_array, size, options)
|
|
68
99
|
end
|
|
69
100
|
|
|
70
|
-
# Returns a path to the original uploaded file
|
|
71
|
-
#
|
|
72
|
-
#
|
|
101
|
+
# Returns a path to the original uploaded file, served as an attachment so the browser downloads it. The file
|
|
102
|
+
# is not processed, and sizing options are ignored. The URL carries the stored format's extension, and
|
|
103
|
+
# +:format+ has no effect.
|
|
104
|
+
#
|
|
105
|
+
# @param record_or_array [DynamicImage::Model, Array] the record
|
|
106
|
+
# @param options [Hash] routing options
|
|
107
|
+
# @return [String]
|
|
73
108
|
def download_dynamic_image_path(record_or_array, options = {})
|
|
74
109
|
dynamic_image_path(record_or_array, { action: :download }.merge(options))
|
|
75
110
|
end
|
|
76
111
|
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
112
|
+
# Same as {#download_dynamic_image_path}, but returns an absolute URL.
|
|
113
|
+
#
|
|
114
|
+
# @param record_or_array [DynamicImage::Model, Array] the record
|
|
115
|
+
# @param options [Hash] routing options
|
|
116
|
+
# @return [String]
|
|
80
117
|
def download_dynamic_image_url(record_or_array, options = {})
|
|
81
118
|
dynamic_image_url(record_or_array, { action: :download }.merge(options))
|
|
82
119
|
end
|
|
83
120
|
|
|
84
|
-
# Returns a path to the original uploaded file,
|
|
85
|
-
#
|
|
121
|
+
# Returns a path to the original uploaded file, exactly as it was uploaded. The file is not processed, and
|
|
122
|
+
# sizing options are ignored. The URL carries the stored format's extension, and +:format+ has no effect.
|
|
123
|
+
#
|
|
124
|
+
# @param record_or_array [DynamicImage::Model, Array] the record
|
|
125
|
+
# @param options [Hash] routing options
|
|
126
|
+
# @return [String]
|
|
86
127
|
def original_dynamic_image_path(record_or_array, options = {})
|
|
87
128
|
dynamic_image_path(record_or_array, { action: :original }.merge(options))
|
|
88
129
|
end
|
|
89
130
|
|
|
90
|
-
#
|
|
91
|
-
#
|
|
131
|
+
# Same as {#original_dynamic_image_path}, but returns an absolute URL.
|
|
132
|
+
#
|
|
133
|
+
# @param record_or_array [DynamicImage::Model, Array] the record
|
|
134
|
+
# @param options [Hash] routing options
|
|
135
|
+
# @return [String]
|
|
92
136
|
def original_dynamic_image_url(record_or_array, options = {})
|
|
93
137
|
dynamic_image_url(record_or_array, { action: :original }.merge(options))
|
|
94
138
|
end
|
|
95
139
|
|
|
96
|
-
# Same as
|
|
97
|
-
#
|
|
140
|
+
# Same as {#dynamic_image_path}, but points to an image with any pre-cropping disabled.
|
|
141
|
+
#
|
|
142
|
+
# @param record_or_array [DynamicImage::Model, Array] the record
|
|
143
|
+
# @param options [Hash] sizing and routing options
|
|
144
|
+
# @return [String]
|
|
98
145
|
def uncropped_dynamic_image_path(record_or_array, options = {})
|
|
99
146
|
dynamic_image_path(record_or_array, { action: :uncropped }.merge(options))
|
|
100
147
|
end
|
|
101
148
|
|
|
102
|
-
# Same as
|
|
103
|
-
#
|
|
149
|
+
# Same as {#dynamic_image_tag}, but renders an image with any pre-cropping disabled.
|
|
150
|
+
#
|
|
151
|
+
# @param record_or_array [DynamicImage::Model, Array] the record
|
|
152
|
+
# @param options [Hash] sizing options, routing options and HTML attributes
|
|
153
|
+
# @return [String]
|
|
104
154
|
def uncropped_dynamic_image_tag(record_or_array, options = {})
|
|
105
155
|
dynamic_image_tag(record_or_array, { action: :uncropped }.merge(options))
|
|
106
156
|
end
|
|
107
157
|
|
|
108
|
-
# Same as
|
|
109
|
-
#
|
|
158
|
+
# Same as {#dynamic_image_url}, but points to an image with any pre-cropping disabled.
|
|
159
|
+
#
|
|
160
|
+
# @param record_or_array [DynamicImage::Model, Array] the record
|
|
161
|
+
# @param options [Hash] sizing and routing options
|
|
162
|
+
# @return [String]
|
|
110
163
|
def uncropped_dynamic_image_url(record_or_array, options = {})
|
|
111
164
|
dynamic_image_url(record_or_array, { action: :uncropped }.merge(options))
|
|
112
165
|
end
|
|
@@ -119,10 +172,6 @@ module DynamicImage
|
|
|
119
172
|
action routing_type ]
|
|
120
173
|
end
|
|
121
174
|
|
|
122
|
-
def default_format_for_image(record)
|
|
123
|
-
Mime::Type.lookup(record.safe_content_type).to_sym
|
|
124
|
-
end
|
|
125
|
-
|
|
126
175
|
def dynamic_image_digest(record, action, size = nil)
|
|
127
176
|
key = [action || "show", record.id, size].compact.join("-")
|
|
128
177
|
DynamicImage.digest_verifier.generate(key)
|
|
@@ -136,12 +185,10 @@ module DynamicImage
|
|
|
136
185
|
|
|
137
186
|
def dynamic_image_url_with_size(record_or_array, size = nil, options = {})
|
|
138
187
|
record = extract_dynamic_image_record(record_or_array)
|
|
139
|
-
options = {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
size:
|
|
144
|
-
}.merge(options)
|
|
188
|
+
options = { routing_type: :url, action: nil, size: }.merge(options)
|
|
189
|
+
options[:routing_type] = :url if mailer_view?
|
|
190
|
+
options[:format] = dynamic_image_format(record, options[:format],
|
|
191
|
+
options[:action])
|
|
145
192
|
options[:digest] =
|
|
146
193
|
dynamic_image_digest(record, options[:action], options[:size])
|
|
147
194
|
polymorphic_url(record_or_array, options)
|
|
@@ -156,13 +203,6 @@ module DynamicImage
|
|
|
156
203
|
end
|
|
157
204
|
end
|
|
158
205
|
|
|
159
|
-
def filename_to_alt(str)
|
|
160
|
-
File.basename(str, ".*")
|
|
161
|
-
.sub(/-[[:xdigit:]]{32,64}\z/, "")
|
|
162
|
-
.tr("-_", " ")
|
|
163
|
-
.capitalize
|
|
164
|
-
end
|
|
165
|
-
|
|
166
206
|
def fit_size!(record_or_array, options)
|
|
167
207
|
record = extract_dynamic_image_record(record_or_array)
|
|
168
208
|
action = options[:action].try(:to_s)
|
|
@@ -4,10 +4,8 @@ module DynamicImage
|
|
|
4
4
|
class ImageProcessor
|
|
5
5
|
# = ImageProcessor::Colors
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
# images are converted to
|
|
9
|
-
# embedded profile, or the built-in generic profile. Grayscale
|
|
10
|
-
# images are converted back to grayscale after processing.
|
|
7
|
+
# Converts color profiles. All images are converted to the sRGB colorspace using either the embedded profile
|
|
8
|
+
# or the built-in generic one. Grayscale images are converted back to grayscale after processing.
|
|
11
9
|
module Colors
|
|
12
10
|
private
|
|
13
11
|
|
|
@@ -22,9 +20,7 @@ module DynamicImage
|
|
|
22
20
|
end
|
|
23
21
|
|
|
24
22
|
def screen_profile(image)
|
|
25
|
-
if !icc_profile?(image) && %i[rgb b-w].include?(image.interpretation)
|
|
26
|
-
return image
|
|
27
|
-
end
|
|
23
|
+
return image if !icc_profile?(image) && %i[rgb b-w].include?(image.interpretation)
|
|
28
24
|
|
|
29
25
|
target_space = image.interpretation == :"b-w" ? "b-w" : "srgb"
|
|
30
26
|
icc_transform_srgb(image).colourspace(target_space)
|
|
@@ -2,13 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
module DynamicImage
|
|
4
4
|
class ImageProcessor
|
|
5
|
+
# = ImageProcessor::Frames
|
|
6
|
+
#
|
|
7
|
+
# Support for animated GIF and WebP images, which vips represents as a single tall image with a +page-height+ of
|
|
8
|
+
# one frame. Operations are applied to each frame and the results reassembled.
|
|
5
9
|
module Frames
|
|
6
10
|
# Extracts a single frame from a multi-frame image.
|
|
11
|
+
#
|
|
12
|
+
# @param index [Integer] the frame to extract, zero based
|
|
13
|
+
# @return [DynamicImage::ImageProcessor] a new processor
|
|
7
14
|
def frame(index)
|
|
8
15
|
apply extract_frame(index)
|
|
9
16
|
end
|
|
10
17
|
|
|
11
|
-
# Returns the number of frames.
|
|
18
|
+
# Returns the number of frames. Still images have one.
|
|
19
|
+
#
|
|
20
|
+
# @return [Integer]
|
|
12
21
|
def frame_count
|
|
13
22
|
image.get("height") / size.y
|
|
14
23
|
end
|
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module DynamicImage
|
|
4
4
|
class ImageProcessor
|
|
5
|
+
# = ImageProcessor::Transform
|
|
6
|
+
#
|
|
7
|
+
# Cropping, resizing and rotation. Each operation is applied to every frame of an animated image.
|
|
5
8
|
module Transform
|
|
6
|
-
# Crops the image
|
|
9
|
+
# Crops the image.
|
|
10
|
+
#
|
|
11
|
+
# @param crop_size [Vector2d] the size of the crop
|
|
12
|
+
# @param crop_start [Vector2d] its top left corner
|
|
13
|
+
# @return [DynamicImage::ImageProcessor] a new processor
|
|
14
|
+
# @raise [DynamicImage::Errors::InvalidTransformation] if the crop falls outside the image
|
|
7
15
|
def crop(crop_size, crop_start)
|
|
8
16
|
return self if crop_start == Vector2d(0, 0) && crop_size == size
|
|
9
17
|
|
|
@@ -17,7 +25,10 @@ module DynamicImage
|
|
|
17
25
|
end
|
|
18
26
|
end
|
|
19
27
|
|
|
20
|
-
# Resize the image to a new size.
|
|
28
|
+
# Resize the image to a new size. The aspect ratio is not preserved; crop first if that matters.
|
|
29
|
+
#
|
|
30
|
+
# @param new_size [Vector2d] the size to resize to
|
|
31
|
+
# @return [DynamicImage::ImageProcessor] a new processor
|
|
21
32
|
def resize(new_size)
|
|
22
33
|
new_size = Vector2d(new_size)
|
|
23
34
|
apply image.thumbnail_image(new_size.x.to_i,
|
|
@@ -27,6 +38,10 @@ module DynamicImage
|
|
|
27
38
|
end
|
|
28
39
|
|
|
29
40
|
# Rotates the image. The rotation must be a multiple of 90 degrees.
|
|
41
|
+
#
|
|
42
|
+
# @param degrees [Integer] the angle
|
|
43
|
+
# @return [DynamicImage::ImageProcessor] a new processor
|
|
44
|
+
# @raise [DynamicImage::Errors::InvalidTransformation] if the angle isn't a multiple of 90
|
|
30
45
|
def rotate(degrees)
|
|
31
46
|
degrees = degrees.to_i % 360
|
|
32
47
|
return self if degrees.zero?
|
|
@@ -7,10 +7,17 @@ require "dynamic_image/image_processor/transform"
|
|
|
7
7
|
module DynamicImage
|
|
8
8
|
# = ImageProcessor
|
|
9
9
|
#
|
|
10
|
-
#
|
|
10
|
+
# The image processing pipeline.
|
|
11
11
|
#
|
|
12
|
-
#
|
|
12
|
+
# Every operation returns a new processor instead of modifying the one it was called on, so operations chain.
|
|
13
|
+
# Images are converted to sRGB and EXIF rotation is applied when the processor is built, so the pipeline always
|
|
14
|
+
# starts from a normalized image.
|
|
13
15
|
#
|
|
16
|
+
# @see DynamicImage::ImageProcessor::Colors
|
|
17
|
+
# @see DynamicImage::ImageProcessor::Frames
|
|
18
|
+
# @see DynamicImage::ImageProcessor::Transform
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
14
21
|
# DynamicImage::ImageProcessor
|
|
15
22
|
# .new(file)
|
|
16
23
|
# .crop(crop_start, crop_size)
|
|
@@ -22,8 +29,16 @@ module DynamicImage
|
|
|
22
29
|
include DynamicImage::ImageProcessor::Frames
|
|
23
30
|
include DynamicImage::ImageProcessor::Transform
|
|
24
31
|
|
|
32
|
+
# @!attribute [r] image
|
|
33
|
+
# @return [Vips::Image]
|
|
34
|
+
# @!attribute [r] target_format
|
|
35
|
+
# @return [DynamicImage::Format] the format it will be written in
|
|
25
36
|
attr_reader :image, :target_format
|
|
26
37
|
|
|
38
|
+
# @param image [Vips::Image, Pathname, IO, String] the image, either an already loaded vips image or something
|
|
39
|
+
# {DynamicImage::ImageReader} can read
|
|
40
|
+
# @param target_format [DynamicImage::Format, nil] the format to write in, defaulting to the format the image was
|
|
41
|
+
# read from
|
|
27
42
|
def initialize(image, target_format: nil)
|
|
28
43
|
if image.is_a?(Vips::Image)
|
|
29
44
|
@image = image
|
|
@@ -36,10 +51,14 @@ module DynamicImage
|
|
|
36
51
|
end
|
|
37
52
|
|
|
38
53
|
# Convert the image to a different format.
|
|
54
|
+
#
|
|
55
|
+
# Converting a multi-frame image to a format that doesn't support animation keeps the first frame.
|
|
56
|
+
#
|
|
57
|
+
# @param new_format [DynamicImage::Format, Symbol, String] the format to convert to
|
|
58
|
+
# @return [DynamicImage::ImageProcessor] a new processor
|
|
39
59
|
def convert(new_format)
|
|
40
|
-
unless new_format.is_a?(DynamicImage::Format)
|
|
41
|
-
|
|
42
|
-
end
|
|
60
|
+
new_format = DynamicImage::Format.find(new_format) unless new_format.is_a?(DynamicImage::Format)
|
|
61
|
+
|
|
43
62
|
if frame_count > 1 && !new_format.animated?
|
|
44
63
|
self.class.new(extract_frame(0), target_format: new_format)
|
|
45
64
|
else
|
|
@@ -48,12 +67,17 @@ module DynamicImage
|
|
|
48
67
|
end
|
|
49
68
|
|
|
50
69
|
# Returns the image data as a binary string.
|
|
70
|
+
#
|
|
71
|
+
# @return [String] the encoded image
|
|
51
72
|
def read
|
|
52
73
|
image.write_to_buffer(target_format.extension,
|
|
53
74
|
**target_format.save_options)
|
|
54
75
|
end
|
|
55
76
|
|
|
56
|
-
# Returns the image size as a Vector2d.
|
|
77
|
+
# Returns the image size as a Vector2d. For multi-frame images this is the size of a single frame, not of the
|
|
78
|
+
# filmstrip vips holds them in.
|
|
79
|
+
#
|
|
80
|
+
# @return [Vector2d]
|
|
57
81
|
def size
|
|
58
82
|
Vector2d.new(
|
|
59
83
|
image.get("width"),
|
|
@@ -64,6 +88,9 @@ module DynamicImage
|
|
|
64
88
|
end
|
|
65
89
|
|
|
66
90
|
# Write the image to a file.
|
|
91
|
+
#
|
|
92
|
+
# @param path [String] the path to write to
|
|
93
|
+
# @return [void]
|
|
67
94
|
def write(path)
|
|
68
95
|
image.write_to_file(path, **target_format.save_options)
|
|
69
96
|
end
|
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module DynamicImage
|
|
4
|
+
# = DynamicImage Image Reader
|
|
5
|
+
#
|
|
6
|
+
# Reads an image into vips, identifying the format from the file header and not from anything the client claims.
|
|
7
|
+
# Accepts a Pathname, an IO object or a binary string.
|
|
8
|
+
#
|
|
9
|
+
# Animated formats are opened with all their frames.
|
|
4
10
|
class ImageReader
|
|
5
|
-
|
|
11
|
+
# Number of bytes needed to identify a format.
|
|
12
|
+
HEADER_BYTES = 32
|
|
6
13
|
|
|
14
|
+
# @param data [Pathname, IO, String] the image
|
|
7
15
|
def initialize(data)
|
|
8
16
|
@data = data
|
|
9
17
|
end
|
|
10
18
|
|
|
19
|
+
# The format of the image, sniffed from its header.
|
|
20
|
+
#
|
|
21
|
+
# @return [DynamicImage::Format, nil] the format, if recognized
|
|
11
22
|
def format
|
|
12
23
|
DynamicImage::Format.sniff(file_header)
|
|
13
24
|
end
|
|
14
25
|
|
|
26
|
+
# Reads the image.
|
|
27
|
+
#
|
|
28
|
+
# @return [Vips::Image]
|
|
29
|
+
# @raise [DynamicImage::Errors::InvalidHeader] if the data isn't in a recognized format
|
|
15
30
|
def read
|
|
16
31
|
raise DynamicImage::Errors::InvalidHeader unless valid_header?
|
|
17
32
|
|
|
@@ -23,6 +38,9 @@ module DynamicImage
|
|
|
23
38
|
end
|
|
24
39
|
end
|
|
25
40
|
|
|
41
|
+
# Returns true if the header belongs to a supported format.
|
|
42
|
+
#
|
|
43
|
+
# @return [Boolean]
|
|
26
44
|
def valid_header?
|
|
27
45
|
format ? true : false
|
|
28
46
|
end
|