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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +393 -67
  3. data/app/models/dynamic_image/variant.rb +10 -0
  4. data/lib/dynamic_image/backfill.rb +85 -0
  5. data/lib/dynamic_image/belongs_to.rb +22 -0
  6. data/lib/dynamic_image/breakpoints.rb +93 -0
  7. data/lib/dynamic_image/controller.rb +51 -7
  8. data/lib/dynamic_image/digest_verifier.rb +20 -9
  9. data/lib/dynamic_image/engine.rb +3 -0
  10. data/lib/dynamic_image/errors.rb +30 -0
  11. data/lib/dynamic_image/format.rb +149 -8
  12. data/lib/dynamic_image/format_negotiator.rb +84 -0
  13. data/lib/dynamic_image/helper/formats.rb +57 -0
  14. data/lib/dynamic_image/helper/pictures.rb +124 -0
  15. data/lib/dynamic_image/helper.rb +100 -60
  16. data/lib/dynamic_image/image_processor/colors.rb +3 -7
  17. data/lib/dynamic_image/image_processor/frames.rb +10 -1
  18. data/lib/dynamic_image/image_processor/transform.rb +17 -2
  19. data/lib/dynamic_image/image_processor.rb +33 -6
  20. data/lib/dynamic_image/image_reader.rb +19 -1
  21. data/lib/dynamic_image/image_sizing.rb +51 -19
  22. data/lib/dynamic_image/metadata.rb +56 -5
  23. data/lib/dynamic_image/model/dimensions.rb +32 -13
  24. data/lib/dynamic_image/model/transformations.rb +47 -11
  25. data/lib/dynamic_image/model/validations.rb +15 -8
  26. data/lib/dynamic_image/model/variants.rb +6 -3
  27. data/lib/dynamic_image/model.rb +58 -34
  28. data/lib/dynamic_image/picture/format_policy.rb +72 -0
  29. data/lib/dynamic_image/picture.rb +215 -0
  30. data/lib/dynamic_image/processed_image.rb +48 -14
  31. data/lib/dynamic_image/ratio.rb +43 -0
  32. data/lib/dynamic_image/routing.rb +14 -3
  33. data/lib/dynamic_image/schema.rb +47 -0
  34. data/lib/dynamic_image/version.rb +1 -1
  35. data/lib/dynamic_image.rb +61 -1
  36. data/lib/rails/generators/dynamic_image/resource/USAGE +17 -0
  37. data/lib/rails/generators/dynamic_image/resource/resource_generator.rb +57 -21
  38. data/lib/rails/generators/dynamic_image/resource/templates/create_table_migration.rb.tt +32 -0
  39. data/lib/rails/generators/dynamic_image/upgrade/USAGE +17 -0
  40. data/lib/rails/generators/dynamic_image/upgrade/templates/upgrade_migration.rb.tt +10 -0
  41. data/lib/rails/generators/dynamic_image/upgrade/upgrade_generator.rb +111 -0
  42. data/lib/tasks/dynamic_image.rake +25 -0
  43. metadata +23 -12
  44. data/lib/dynamic_image/jobs/create_variant.rb +0 -22
  45. data/lib/dynamic_image/jobs.rb +0 -3
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicImage
4
+ class Picture
5
+ # = DynamicImage Picture Format Policy
6
+ #
7
+ # Decides what a responsive image is rendered in. The <tt>source</tt> gets the best format available for
8
+ # output, transcoded unconditionally. The <tt>img</tt> gets one of {DynamicImage::COMPATIBLE_FORMATS}.
9
+ #
10
+ # @see DynamicImage::FormatNegotiator
11
+ class FormatPolicy
12
+ # The format the candidates are rendered in when the view doesn't ask for one.
13
+ DEFAULT_SOURCE_FORMAT = :webp
14
+
15
+ # @!attribute [r] record
16
+ # @return [DynamicImage::Model]
17
+ # @!attribute [r] requested
18
+ # @return [Symbol, Array<Symbol>, nil]
19
+ attr_reader :record, :requested
20
+
21
+ # @param record [DynamicImage::Model] the image
22
+ # @param requested [Symbol, Array<Symbol>, nil] the format the candidates are rendered in. A symbol forces
23
+ # that format, an array is negotiated, nil takes the default.
24
+ def initialize(record, requested = nil)
25
+ @record = record
26
+ @requested = requested
27
+ end
28
+
29
+ # The format the candidates are rendered in.
30
+ #
31
+ # @return [DynamicImage::Format]
32
+ def source
33
+ @source ||= resolve(requested || default_source)
34
+ end
35
+
36
+ # The format the fallback image is rendered in.
37
+ #
38
+ # @return [DynamicImage::Format]
39
+ def fallback
40
+ @fallback ||= resolve(fallback_formats)
41
+ end
42
+
43
+ private
44
+
45
+ def default_source
46
+ record.animated? ? fallback_formats : DEFAULT_SOURCE_FORMAT
47
+ end
48
+
49
+ def resolve(value)
50
+ return negotiate(value) if value.is_a?(Array)
51
+
52
+ DynamicImage::Format.find(value) ||
53
+ raise(ArgumentError, "unknown format: #{value.inspect}")
54
+ end
55
+
56
+ def negotiate(formats)
57
+ DynamicImage::FormatNegotiator.new(record).negotiate(formats)
58
+ end
59
+
60
+ # An animated image keeps its own format if it can.
61
+ def fallback_formats
62
+ return DynamicImage::COMPATIBLE_FORMATS unless record.animated?
63
+
64
+ [stored_format_name, *DynamicImage::COMPATIBLE_FORMATS].compact
65
+ end
66
+
67
+ def stored_format_name
68
+ DynamicImage::Format.content_type(record.content_type)&.name
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicImage
4
+ # = DynamicImage Picture
5
+ #
6
+ # Everything needed to render an image responsively: the candidate widths, the signed URLs behind them, and the
7
+ # fallback the <tt>img</tt> points at.
8
+ #
9
+ # Sizing is expressed as an optional ratio instead of a size.
10
+ #
11
+ # @example
12
+ # picture = DynamicImage::Picture.new(self, image, ratio: "16:9")
13
+ # picture.srcset # => "/images/… 420w, /images/… 590w, …"
14
+ # picture.src # => "/images/…/1200x675/…jpg"
15
+ #
16
+ # @see DynamicImage::Breakpoints for how the widths are chosen
17
+ # @see DynamicImage::Helper#dynamic_picture_tag
18
+ class Picture
19
+ # The options this class consumes. Anything else is passed on to the router.
20
+ OPTIONS = %i[ratio sizes breakpoints step fallback_width format uncropped].freeze
21
+
22
+ # @!attribute [r] record
23
+ # @return [DynamicImage::Model]
24
+ # @!attribute [r] ratio
25
+ # @return [Float, nil]
26
+ # @!attribute [r] sizes
27
+ # @return [String] the +sizes+ attribute
28
+ # @!attribute [r] breakpoints
29
+ # @return [DynamicImage::Breakpoints] the candidate widths
30
+ # @!attribute [r] fallback_width
31
+ # @return [Integer] the width asked for the fallback image
32
+ attr_reader :template, :record_or_array, :ratio, :sizes, :breakpoints, :fallback_width, :url_options
33
+
34
+ # @param template [ActionView::Base] the view context, for routing
35
+ # @param record_or_array [DynamicImage::Model, Array] the record, or an array of records for a nested route
36
+ # @param options [Hash]
37
+ # @option options [Numeric, Vector2d, String, nil] :ratio The aspect ratio to crop to, as a number, a vector, or a
38
+ # string like <tt>"16:9"</tt>. Implies cropping. Omit for the image's own.
39
+ # @option options [String] :sizes The +sizes+ attribute
40
+ # @option options [Range, Array<Integer>, Integer] :breakpoints The widths to offer, overriding
41
+ # {DynamicImage.default_breakpoints}
42
+ # @option options [Numeric] :step The step between breakpoints, overriding {DynamicImage.breakpoint_step}
43
+ # @option options [Integer] :fallback_width The width to ask for the fallback image, overriding
44
+ # {DynamicImage.picture_fallback_width}
45
+ # @option options [Symbol, Array<Symbol>] :format The format the candidates are rendered in. A symbol forces
46
+ # that format, an array is negotiated. Defaults to WebP, or the image's own format if it is animated.
47
+ # @option options [Boolean] :uncropped Size against the whole image, ignoring any pre-cropping. Defaults to
48
+ # whether the URL points at the +uncropped+ action.
49
+ #
50
+ # Any options supported by +polymorphic_url+ are also accepted, and passed on to the router.
51
+ def initialize(template, record_or_array, options = {})
52
+ options = options.symbolize_keys
53
+ @template = template
54
+ @record_or_array = Array(record_or_array)
55
+ @ratio = DynamicImage::Ratio.parse(options[:ratio])
56
+ @sizes = options[:sizes] || "100vw"
57
+ @requested_format = options[:format]
58
+ @fallback_width = fallback_width_from(options)
59
+ @breakpoints = breakpoints_from(options)
60
+ @url_options = options.except(*OPTIONS)
61
+ @uncropped = options.fetch(:uncropped) { url_options[:action].to_s == "uncropped" }
62
+ end
63
+
64
+ # Returns true if the image is cropped, which it is whenever a ratio is given.
65
+ #
66
+ # @return [Boolean]
67
+ def crop?
68
+ !ratio.nil?
69
+ end
70
+
71
+ # The widest the image can be rendered at: its own width, or the width of the largest crop matching the ratio.
72
+ #
73
+ # @return [Integer]
74
+ def available_width
75
+ @available_width ||= sizing.available_width(ratio)
76
+ end
77
+
78
+ # The candidate widths, smallest first.
79
+ #
80
+ # @return [Array<Integer>]
81
+ def widths
82
+ @widths ||= breakpoints.widths(available_width)
83
+ end
84
+
85
+ # Every candidate, as the URL and the size it is actually rendered at.
86
+ #
87
+ # @return [Array<Hash>]
88
+ def variants
89
+ @variants ||= widths.map { |width| variant(width) }
90
+ end
91
+
92
+ # The +srcset+ attribute for the candidates, or nil if there are none. It belongs on the <tt>source</tt> when
93
+ # {#sources} has one, and on the <tt>img</tt> when it doesn't.
94
+ #
95
+ # @return [String, nil]
96
+ def srcset
97
+ return if variants.empty?
98
+
99
+ variants.map { |v| "#{v[:url]} #{v[:width]}w" }.join(", ")
100
+ end
101
+
102
+ # The sources to render, as +type+ and +srcset+ pairs.
103
+ #
104
+ # Empty when the candidates are in the same format as the fallback; {#srcset} goes on the <tt>img</tt> instead.
105
+ # That is what happens to an animated image, which keeps its own format.
106
+ #
107
+ # @return [Array<Hash>]
108
+ def sources
109
+ candidates = srcset
110
+ return [] if candidates.nil? || format == fallback_format
111
+
112
+ [{ type:, srcset: candidates }]
113
+ end
114
+
115
+ # The content type the candidates are rendered as, for the +type+ attribute.
116
+ #
117
+ # @return [String]
118
+ def type
119
+ format.content_type
120
+ end
121
+
122
+ # The format the fallback image is rendered in, negotiated from {DynamicImage::COMPATIBLE_FORMATS}.
123
+ #
124
+ # @return [DynamicImage::Format]
125
+ def fallback_format
126
+ format_policy.fallback
127
+ end
128
+
129
+ # The size asked for the fallback image, as a <tt>"{width}x{height}"</tt> string.
130
+ #
131
+ # @return [String]
132
+ def fallback_size
133
+ @fallback_size ||= size_for(fallback_width)
134
+ end
135
+
136
+ # The size the fallback image is actually rendered at. Smaller than {#fallback_size} when the image is.
137
+ #
138
+ # @return [Vector2d]
139
+ def dimensions
140
+ @dimensions ||= sizing.fit(fallback_size, crop: crop?).floor
141
+ end
142
+
143
+ # The URL for the fallback image.
144
+ #
145
+ # @return [String]
146
+ def src
147
+ url_for(fallback_size, fallback_format)
148
+ end
149
+
150
+ # The width of the fallback image.
151
+ #
152
+ # @return [Integer]
153
+ def width
154
+ dimensions.x.to_i
155
+ end
156
+
157
+ # The height of the fallback image.
158
+ #
159
+ # @return [Integer]
160
+ def height
161
+ dimensions.y.to_i
162
+ end
163
+
164
+ private
165
+
166
+ def fallback_width_from(options)
167
+ (options[:fallback_width] || DynamicImage.picture_fallback_width).to_i
168
+ end
169
+
170
+ def breakpoints_from(options)
171
+ DynamicImage::Breakpoints.new(options[:breakpoints], step: options[:step])
172
+ end
173
+
174
+ def record
175
+ record_or_array.last
176
+ end
177
+
178
+ def sizing
179
+ @sizing ||= DynamicImage::ImageSizing.new(record, uncropped: @uncropped)
180
+ end
181
+
182
+ def variant(width)
183
+ requested = size_for(width)
184
+ size = sizing.fit(requested, crop: crop?).floor
185
+ # {#url_for} fits the size it is given. +size+ has been floored to whole pixels, so it is no longer exactly
186
+ # proportional and would fit smaller a second time. Pass the request instead.
187
+ { url: url_for(requested, format),
188
+ width: size.x.to_i,
189
+ height: size.y.to_i }
190
+ end
191
+
192
+ def size_for(width)
193
+ return "#{width}x" unless ratio
194
+
195
+ "#{width}x#{(width / ratio).round}"
196
+ end
197
+
198
+ def url_for(requested_size, image_format)
199
+ template.dynamic_image_path(record_or_array,
200
+ url_options.merge(
201
+ size: requested_size,
202
+ crop: crop?,
203
+ format: image_format.mime_type.to_sym
204
+ ))
205
+ end
206
+
207
+ def format
208
+ format_policy.source
209
+ end
210
+
211
+ def format_policy
212
+ @format_policy ||= FormatPolicy.new(record, @requested_format)
213
+ end
214
+ end
215
+ end
@@ -3,11 +3,22 @@
3
3
  module DynamicImage
4
4
  # = DynamicImage Processed Image
5
5
  #
6
- # Handles all processing of images. Takes an instance of
7
- # +DynamicImage::Model+ as argument.
6
+ # Crops, resizes and converts images, and reads back the processed data.
7
+ #
8
+ # Each processed size is stored as a variant, so subsequent calls for the same size are cheap.
9
+ #
10
+ # @example
11
+ # size = DynamicImage::ImageSizing.new(image).fit("800x800")
12
+ # data = DynamicImage::ProcessedImage.new(image, format: :jpg)
13
+ # .cropped_and_resized(size)
8
14
  class ProcessedImage
15
+ # @return [DynamicImage::Model]
9
16
  attr_reader :record
10
17
 
18
+ # @param record [DynamicImage::Model] the image to process
19
+ # @param options [Hash]
20
+ # @option options [Boolean] :uncropped Ignore any crop stored on the record
21
+ # @option options [Symbol, String] :format Format to convert to. Defaults to the format of the stored image.
11
22
  def initialize(record, options = {})
12
23
  @record = record
13
24
  @uncropped = options[:uncropped] ? true : false
@@ -15,28 +26,40 @@ module DynamicImage
15
26
  @format_name = "JPEG" if defined?(@format_name) && @format_name == "JPG"
16
27
  end
17
28
 
18
- # Crops and resizes the image. Normalization is performed as well.
29
+ # Crops, resizes and normalizes the image.
19
30
  #
20
- # ==== Example
31
+ # @param size [Vector2d] the size to render, in pixels
32
+ # @return [String] the image data as a binary string
33
+ # @raise [DynamicImage::Errors::InvalidImage] if the record isn't a valid image, or if the stored data can't be
34
+ # processed
21
35
  #
36
+ # @example
22
37
  # processed = DynamicImage::ProcessedImage.new(image)
23
38
  # image_data = processed.cropped_and_resized(Vector2d.new(200, 200))
24
- #
25
- # Returns a binary string.
26
39
  def cropped_and_resized(size)
27
40
  return crop_and_resize(size) unless record.persisted?
28
41
 
29
42
  find_or_create_variant(size).data
30
43
  end
31
44
 
32
- # Find or create a variant with the given size.
45
+ # Returns the variant for the given size, creating it if it doesn't already exist.
46
+ #
47
+ # @param size [Vector2d] the size to render, in pixels
48
+ # @return [DynamicImage::Variant]
49
+ # @raise [DynamicImage::Errors::InvalidImage] if the record isn't a valid image, or if the stored data can't be
50
+ # processed
33
51
  def find_or_create_variant(size)
34
52
  find_variant(size) || create_variant(size)
35
53
  rescue ActiveRecord::RecordNotUnique
36
54
  find_variant(size)
37
55
  end
38
56
 
39
- # Find a variant with the given size.
57
+ # Returns the variant for the given size, if one exists.
58
+ #
59
+ # Variants whose data has gone missing from storage are destroyed and treated as absent.
60
+ #
61
+ # @param size [Vector2d] the size to look for, in pixels
62
+ # @return [DynamicImage::Variant, nil] the variant, if one exists
40
63
  def find_variant(size)
41
64
  return nil unless record.persisted?
42
65
 
@@ -51,8 +74,10 @@ module DynamicImage
51
74
  end
52
75
  end
53
76
 
54
- # Find or create a variant for the given size, returning the variant
55
- # record (not data). Returns nil if the record is not persisted.
77
+ # Same as {#cropped_and_resized}, but returns the variant record instead of its data.
78
+ #
79
+ # @param size [Vector2d] the size to render, in pixels
80
+ # @return [DynamicImage::Variant, nil] the variant, or nil if the record isn't persisted
56
81
  def variant_for(size)
57
82
  return nil unless record.persisted?
58
83
 
@@ -61,6 +86,9 @@ module DynamicImage
61
86
  find_variant(size)
62
87
  end
63
88
 
89
+ # The format the image is rendered in. Defaults to the format of the stored image.
90
+ #
91
+ # @return [DynamicImage::Format]
64
92
  def format
65
93
  DynamicImage::Format.find(@format_name) || record_format
66
94
  end
@@ -73,12 +101,16 @@ module DynamicImage
73
101
  # * Optimizes GIFs
74
102
  # * Performs format conversion if the requested format is different
75
103
  #
76
- # ==== Example
104
+ # @yield [image] an optional block to transform the image before it is written out
105
+ # @yieldparam image [DynamicImage::ImageProcessor] the processor
106
+ # @yieldreturn [DynamicImage::ImageProcessor] the transformed processor
107
+ # @return [String] the image data as a binary string
108
+ # @raise [DynamicImage::Errors::InvalidImage] if the record isn't a valid image, or if the stored data can't be
109
+ # processed
77
110
  #
78
- # processed = DynamicImage::ProcessedImage.new(image, :jpeg)
111
+ # @example
112
+ # processed = DynamicImage::ProcessedImage.new(image, format: :jpeg)
79
113
  # jpg_data = processed.normalized
80
- #
81
- # Returns a binary string.
82
114
  def normalized
83
115
  require_valid_image!
84
116
 
@@ -87,6 +119,8 @@ module DynamicImage
87
119
  image = yield(image) if block_given?
88
120
  image.convert(format).read
89
121
  end
122
+ rescue Vips::Error => e
123
+ raise DynamicImage::Errors::InvalidImage, e.message
90
124
  end
91
125
 
92
126
  private
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicImage
4
+ # = DynamicImage Ratio
5
+ #
6
+ # Coerces an aspect ratio into a float. Strings, rationals, floats and vectors are all accepted.
7
+ #
8
+ # @example
9
+ # DynamicImage::Ratio.parse("16:9") # => 1.7777..
10
+ # DynamicImage::Ratio.parse("16x9") # => 1.7777..
11
+ # DynamicImage::Ratio.parse(Rational(16, 9)) # => 1.7777..
12
+ # DynamicImage::Ratio.parse(Vector2d(16, 9)) # => 1.7777..
13
+ module Ratio
14
+ class << self
15
+ # Returns the ratio as width divided by height.
16
+ #
17
+ # @param value [Numeric, Vector2d, String, nil] the ratio, as a number, a vector, or a string written with
18
+ # <tt>:</tt>, <tt>x</tt> or <tt>/</tt> between the two sides
19
+ # @return [Float, nil]
20
+ # @raise [ArgumentError] if the value isn't a usable ratio
21
+ def parse(value)
22
+ return if value.nil?
23
+
24
+ ratio = coerce(value)
25
+ unless ratio.is_a?(Float) && ratio.positive? && ratio.finite?
26
+ raise ArgumentError, "invalid ratio: #{value.inspect}"
27
+ end
28
+
29
+ ratio
30
+ end
31
+
32
+ private
33
+
34
+ def coerce(value)
35
+ case value
36
+ when Numeric then value.to_f
37
+ when Vector2d then value.x.to_f / value.y
38
+ when String then value.split(%r{[:x/]}).map(&:to_f).reduce(:/)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -3,12 +3,23 @@
3
3
  module DynamicImage
4
4
  # = DynamicImage Routing
5
5
  #
6
- # Extends +ActionDispatch::Routing::Mapper+ and provides a shortcut for
7
- # defining routes for +DynamicImage::Controller+.
6
+ # Extends +ActionDispatch::Routing::Mapper+ and provides a shortcut for defining routes for
7
+ # +DynamicImage::Controller+.
8
8
  module Routing
9
- # Declares an image resource.
9
+ # Declares an image resource, routing +show+ along with the +uncropped+, +original+ and +download+ member actions.
10
10
  #
11
+ # The default path includes the digest and an optional size. Pass +:path+ to change it if the resource collides
12
+ # with a directory in +public+.
13
+ #
14
+ # @param resource_name [Symbol, String] the resource
15
+ # @param options [Hash] options passed to +resources+, merged over the defaults
16
+ # @return [void]
17
+ #
18
+ # @example
11
19
  # image_resources :avatars
20
+ #
21
+ # @example Keeping clear of public/images
22
+ # image_resources :images, path: "dynamic_images/:digest(/:size)"
12
23
  def image_resources(resource_name, options = {})
13
24
  options = {
14
25
  path: "#{resource_name}/:digest(/:size)",
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicImage
4
+ # = DynamicImage Schema
5
+ #
6
+ # The database schema {DynamicImage::Model} expects on the table holding the image.
7
+ #
8
+ # DynamicImage doesn't own that table. Unlike +dynamic_image_variants+, which has a fixed name and an engine
9
+ # migration, the image table is created in the application by the +dynamic_image:resource+ generator, under
10
+ # whatever name the application picked, and there may be more than one. This is the single definition of what it
11
+ # should contain.
12
+ #
13
+ # @see DynamicImage::Model
14
+ module Schema
15
+ # The expected columns, in the order they should appear in a migration. Each is described by its migration type
16
+ # and whether it accepts +NULL+.
17
+ #
18
+ # +frame_count+ and +alpha+ are nullable. NULL means the image predates them being recorded, which is not the
19
+ # same as knowing the image is a still or opaque.
20
+ #
21
+ # @return [Hash{Symbol => Hash}]
22
+ ATTRIBUTES = {
23
+ content_hash: { type: :string, null: false },
24
+ content_type: { type: :string, null: false },
25
+ content_length: { type: :integer, null: false },
26
+ filename: { type: :string, null: false },
27
+ colorspace: { type: :string, null: false },
28
+ real_width: { type: :integer, null: false },
29
+ real_height: { type: :integer, null: false },
30
+ frame_count: { type: :integer, null: true },
31
+ alpha: { type: :boolean, null: true },
32
+ crop_width: { type: :integer, null: true },
33
+ crop_height: { type: :integer, null: true },
34
+ crop_start_x: { type: :integer, null: true },
35
+ crop_start_y: { type: :integer, null: true },
36
+ crop_gravity_x: { type: :integer, null: true },
37
+ crop_gravity_y: { type: :integer, null: true }
38
+ }.freeze
39
+
40
+ # The expected indexes.
41
+ #
42
+ # @return [Array<Hash>]
43
+ INDEXES = [
44
+ { columns: %i[content_hash], unique: false }
45
+ ].freeze
46
+ end
47
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DynamicImage
4
- VERSION = "3.0.9"
4
+ VERSION = "3.1.0"
5
5
  end
data/lib/dynamic_image.rb CHANGED
@@ -4,22 +4,82 @@ require "dis"
4
4
  require "vector2d"
5
5
  require "vips"
6
6
 
7
+ require "dynamic_image/backfill"
7
8
  require "dynamic_image/belongs_to"
9
+ require "dynamic_image/breakpoints"
8
10
  require "dynamic_image/controller"
9
11
  require "dynamic_image/digest_verifier"
10
12
  require "dynamic_image/engine"
11
13
  require "dynamic_image/errors"
12
14
  require "dynamic_image/format"
15
+ require "dynamic_image/format_negotiator"
13
16
  require "dynamic_image/helper"
14
17
  require "dynamic_image/image_processor"
15
18
  require "dynamic_image/image_reader"
16
19
  require "dynamic_image/image_sizing"
17
- require "dynamic_image/jobs"
18
20
  require "dynamic_image/metadata"
19
21
  require "dynamic_image/model"
22
+ require "dynamic_image/picture"
23
+ require "dynamic_image/picture/format_policy"
20
24
  require "dynamic_image/processed_image"
25
+ require "dynamic_image/ratio"
21
26
  require "dynamic_image/routing"
27
+ require "dynamic_image/schema"
22
28
 
29
+ # A Rails engine for image uploads. Rather than creating a pre-defined set of images when a file is uploaded, it
30
+ # stores the original file and generates images on demand. It handles cropping, resizing, format and colorspace
31
+ # conversion, and stores each processed size as a variant.
32
+ #
33
+ # Include {DynamicImage::Model} in the model holding the image and {DynamicImage::Controller} in the controller
34
+ # serving it, then declare the routes with {DynamicImage::Routing#image_resources}. Views render images through the
35
+ # helpers in {DynamicImage::Helper}, which sign the URLs.
36
+ #
37
+ # Files are stored with Dis, and processing is done with libvips.
38
+ #
39
+ # @see DynamicImage::Model
40
+ # @see DynamicImage::Controller
41
+ # @see DynamicImage::Helper
42
+ # @see DynamicImage::ImageSizing
43
+ # @see DynamicImage::ProcessedImage
23
44
  module DynamicImage
45
+ # The formats anything that renders images will understand. Fallback images in a <tt>picture</tt> are negotiated
46
+ # from this list, and it is the default for {DynamicImage.mailer_formats}.
47
+ #
48
+ # @return [Array<Symbol>] the format names, most preferred first
49
+ COMPATIBLE_FORMATS = %i[jpeg png gif].freeze
50
+
51
+ # Verifies the HMAC digests embedded in image URLs. Set by the engine from the application's key generator, which
52
+ # derives it from <tt>secret_key_base</tt>.
53
+ #
54
+ # @return [DynamicImage::DigestVerifier]
24
55
  cattr_accessor :digest_verifier
56
+
57
+ # The formats a view accepts when it doesn't pass <tt>format:</tt>. Also decides what
58
+ # {DynamicImage::Model#safe_content_type} considers safe.
59
+ #
60
+ # @return [Array<Symbol>] the format names, most preferred first
61
+ mattr_accessor :default_formats, default: %i[jpeg png gif webp]
62
+
63
+ # The formats a mailer view accepts when it doesn't pass <tt>format:</tt>.
64
+ #
65
+ # @return [Array<Symbol>] the format names, most preferred first
66
+ mattr_accessor :mailer_formats, default: COMPATIBLE_FORMATS
67
+
68
+ # The breakpoints responsive images are rendered at. This can be a range, an array of explicit widths, or a single
69
+ # width.
70
+ #
71
+ # @return [Range, Array<Integer>, Integer]
72
+ # @see DynamicImage::Breakpoints
73
+ mattr_accessor :default_breakpoints, default: 320..3200
74
+
75
+ # The ratio between two breakpoints. Lower means a closer fit to what the browser needs, at the price of more
76
+ # variants to generate and store.
77
+ #
78
+ # @return [Float]
79
+ mattr_accessor :breakpoint_step, default: 1.4
80
+
81
+ # The width of the fallback image in a <tt>picture</tt> element.
82
+ #
83
+ # @return [Integer]
84
+ mattr_accessor :picture_fallback_width, default: 1200
25
85
  end
@@ -0,0 +1,17 @@
1
+ Description:
2
+ Creates a DynamicImage resource: a model including
3
+ DynamicImage::Model, a controller including DynamicImage::Controller,
4
+ a migration with the columns DynamicImage needs, and an
5
+ image_resources route.
6
+
7
+ Additional attributes are passed through to the migration, so a
8
+ resource can carry fields of its own.
9
+
10
+ Example:
11
+ bin/rails generate dynamic_image:resource image
12
+
13
+ creates an Image model, an ImagesController and a migration.
14
+
15
+ bin/rails generate dynamic_image:resource photo caption:string
16
+
17
+ adds a caption column to the migration.