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
@@ -3,26 +3,37 @@
3
3
  module DynamicImage
4
4
  # = DynamicImage Image Sizing
5
5
  #
6
- # Calculates cropping and fitting for image sizes.
6
+ # Calculates cropping and fitting for image sizes. The helpers use it to work out the dimensions of a rendered
7
+ # image, and it can be used on its own when you need the size without rendering anything: reserving space in a
8
+ # layout, filling in <tt>og:image:width</tt>, or laying out a PDF.
9
+ #
10
+ # @example
11
+ # sizing = DynamicImage::ImageSizing.new(image)
12
+ # sizing.fit("400x400") # => Vector2d(400.0, 250.0)
7
13
  class ImageSizing
14
+ # @param record [DynamicImage::Model] the image
15
+ # @param options [Hash]
16
+ # @option options [Boolean] :uncropped Ignore any crop stored on the record and size against the original image
8
17
  def initialize(record, options = {})
9
18
  @record = record
10
19
  @uncropped = options[:uncropped] ? true : false
11
20
  end
12
21
 
13
- # Calculates crop geometry. The given vector is scaled
14
- # to match the image size, since DynamicImage performs
15
- # cropping before resizing.
22
+ # Calculates crop geometry. The given vector is scaled to match the image size, since cropping happens before
23
+ # resizing.
24
+ #
25
+ # The crop is positioned to keep the record's crop gravity as close to the center as possible, clamped to the
26
+ # bounds of the image.
16
27
  #
17
- # ==== Example
28
+ # @param ratio_vector [Vector2d] the aspect ratio to crop to
29
+ # @return [Array(Vector2d, Vector2d)] the crop size and crop start
18
30
  #
31
+ # @example
19
32
  # image = Image.find(params[:id]) # 320x200 image
20
33
  # sizing = DynamicImage::ImageSizing.new(image)
21
34
  #
22
35
  # sizing.crop_geometry(Vector2d(100, 100))
23
36
  # # => [Vector2d(200, 200), Vector2d(60, 0)]
24
- #
25
- # Returns a tuple with crop size and crop start vectors.
26
37
  def crop_geometry(ratio_vector)
27
38
  # Maximize the crop area to fit the image size
28
39
  crop_size = ratio_vector.fit(size).round
@@ -36,18 +47,41 @@ module DynamicImage
36
47
  [crop_size, (start + crop_start)]
37
48
  end
38
49
 
39
- # Adjusts +fit_size+ to fit the image dimensions.
40
- # Any dimension set to zero will be ignored.
50
+ # Returns the widest the image can be rendered at, in pixels.
51
+ #
52
+ # Without a ratio this is the image's own width. With one it is the width of the largest crop matching that
53
+ # ratio.
54
+ #
55
+ # @param ratio [Numeric, Vector2d, String, nil] the aspect ratio, in any form {DynamicImage::Ratio} understands
56
+ # @return [Integer]
41
57
  #
42
- # ==== Options
58
+ # @example
59
+ # image = Image.find(params[:id]) # 320x200 image
60
+ # sizing = DynamicImage::ImageSizing.new(image)
43
61
  #
44
- # * <tt>:crop</tt> - Don't keep aspect ratio. This will allow
45
- # the image to be cropped to the requested size.
46
- # * <tt>:upscale</tt> - Don't limit to the size of the image.
47
- # Images smaller than the given size will be scaled up.
62
+ # sizing.available_width # => 320
63
+ # sizing.available_width(16.0 / 9) # => 320
64
+ # sizing.available_width(9.0 / 16) # => 113
65
+ def available_width(ratio = nil)
66
+ ratio = DynamicImage::Ratio.parse(ratio)
67
+ return size.x.floor unless ratio
68
+
69
+ crop_geometry(vector(ratio, 1)).first.x.floor
70
+ end
71
+
72
+ # Adjusts +fit_size+ to fit the image dimensions. Any dimension set to zero will be ignored.
48
73
  #
49
- # ==== Examples
74
+ # @param fit_size [Vector2d, String] the size to fit within, either a vector or a <tt>"{width}x{height}"</tt>
75
+ # string. Either dimension may be omitted for a fixed width or height.
76
+ # @param options [Hash]
77
+ # @option options [Boolean] :crop Don't keep aspect ratio. This will allow the image to be cropped to the
78
+ # requested size.
79
+ # @option options [Boolean] :upscale Don't limit to the size of the image. Images smaller than the given size will
80
+ # be scaled up.
81
+ # @return [Vector2d] the resulting size
82
+ # @raise [DynamicImage::Errors::InvalidSizeOptions] if <tt>crop: true</tt> is given and either dimension is zero
50
83
  #
84
+ # @example
51
85
  # image = Image.find(params[:id]) # 320x200 image
52
86
  # sizing = DynamicImage::ImageSizing.new(image)
53
87
  #
@@ -62,7 +96,6 @@ module DynamicImage
62
96
  #
63
97
  # sizing.fit(Vector2d(500, 500), upscale: true)
64
98
  # # => Vector2d(500.0, 312.5)
65
- #
66
99
  def fit(fit_size, options = {})
67
100
  fit_size = parse_vector(fit_size)
68
101
  require_dimensions!(fit_size) if options[:crop]
@@ -97,9 +130,8 @@ module DynamicImage
97
130
  end
98
131
  end
99
132
 
100
- # Clamps the rectangle defined by +start+ and +size+
101
- # to fit inside 0, 0 and +max_size+. It is assumed
102
- # that +size+ will always be smaller than +max_size+.
133
+ # Clamps the rectangle defined by +start+ and +size+ to fit inside 0, 0 and +max_size+. It is assumed that +size+
134
+ # will always be smaller than +max_size+.
103
135
  #
104
136
  # Returns the start vector.
105
137
  def clamp(start, size, max_size)
@@ -4,13 +4,24 @@ module DynamicImage
4
4
  # = DynamicImage Metadata
5
5
  #
6
6
  # Parses metadata from an image. Accepts a Pathname, IO, or binary string.
7
+ #
8
+ # Every reader returns nil for data that isn't a readable image.
9
+ #
10
+ # @example
11
+ # metadata = DynamicImage::Metadata.new(Pathname.new("image.jpg"))
12
+ # metadata.valid? # => true
13
+ # metadata.content_type # => "image/jpeg"
14
+ # metadata.dimensions # => Vector2d(320, 200)
7
15
  class Metadata
16
+ # @param data [Pathname, IO, String] the image, as a path, an open file or a binary string
8
17
  def initialize(data)
9
18
  @data = data
10
19
  end
11
20
 
12
- # Returns the color space of the image as a string. The result will be one
13
- # of the following: "rgb", "cmyk", "gray".
21
+ # Returns the color space of the image as a string. The result will be one of the following: "rgb", "cmyk",
22
+ # "gray".
23
+ #
24
+ # @return [String, nil]
14
25
  def colorspace
15
26
  return unless valid?
16
27
 
@@ -25,30 +36,61 @@ module DynamicImage
25
36
  end
26
37
 
27
38
  # Returns the content type of the image.
39
+ #
40
+ # @return [String, nil]
28
41
  def content_type
29
42
  reader.format.content_type if valid?
30
43
  end
31
44
 
45
+ # Returns the name of the detected format.
46
+ #
47
+ # @return [String, nil] the format name, such as "JPEG"
32
48
  def format
33
49
  reader.format.name if valid?
34
50
  end
35
51
 
36
- # Returns the dimensions of the image as a vector.
52
+ # Returns the dimensions of the image as a vector. EXIF rotation is applied, so these are the dimensions the
53
+ # image has once normalized.
54
+ #
55
+ # @return [Vector2d, nil]
37
56
  def dimensions
38
57
  Vector2d.new(metadata[:width], metadata[:height]) if valid?
39
58
  end
40
59
 
41
60
  # Returns the width of the image.
61
+ #
62
+ # @return [Integer, nil] the width in pixels
42
63
  def width
43
64
  metadata[:width] if valid?
44
65
  end
45
66
 
46
67
  # Returns the height of the image.
68
+ #
69
+ # @return [Integer, nil] the height in pixels
47
70
  def height
48
71
  metadata[:height] if valid?
49
72
  end
50
73
 
51
- # Returns true if the image is valid.
74
+ # Returns the number of frames. Animated formats can have more than one; everything else has a single frame.
75
+ #
76
+ # @return [Integer, nil] the number of frames
77
+ def frame_count
78
+ metadata[:frame_count] if valid?
79
+ end
80
+
81
+ # Returns true if the image has an alpha channel.
82
+ #
83
+ # This is the presence of the channel, not of actual transparency: an image can carry a fully opaque alpha
84
+ # channel. Reading it costs nothing, where scanning the channel for transparency would mean decoding every pixel.
85
+ #
86
+ # @return [Boolean, nil] true if the image has an alpha channel
87
+ def alpha?
88
+ metadata[:alpha] if valid?
89
+ end
90
+
91
+ # Returns true if the data is a readable image in a supported format.
92
+ #
93
+ # @return [Boolean]
52
94
  def valid?
53
95
  @data && reader.valid_header? && metadata != :invalid
54
96
  end
@@ -67,11 +109,20 @@ module DynamicImage
67
109
  image = reader.read
68
110
  width, height = dimensions_from(image)
69
111
  width, height = height, width if rotated?(image)
70
- { width:, height:, colorspace: image.get("interpretation") }
112
+ { width:, height:,
113
+ colorspace: image.get("interpretation"),
114
+ frame_count: frame_count_from(image),
115
+ alpha: image.has_alpha? }
71
116
  rescue Vips::Error
72
117
  :invalid
73
118
  end
74
119
 
120
+ def frame_count_from(image)
121
+ return 1 unless image.get_fields.include?("n-pages")
122
+
123
+ image.get("n-pages")
124
+ end
125
+
75
126
  def dimensions_from(image)
76
127
  width = image.get("width")
77
128
  height = if image.get_fields.include?("page-height")
@@ -4,17 +4,17 @@ module DynamicImage
4
4
  module Model
5
5
  # = DynamicImage Model Dimensions
6
6
  #
7
+ # Vector accessors over the +real_*+ and +crop_*+ columns.
8
+ #
9
+ # +real_size+ is the size of the stored image. +size+ is the size after cropping, or +real_size+ when no crop
10
+ # is set.
7
11
  module Dimensions
8
- # Returns the crop gravity.
9
- #
10
- # DynamicImage will try to keep the pixel represented by
11
- # crop_gravity as close to the center as possible when cropping
12
- # images.
12
+ # Returns the crop gravity, the focal point kept as close to the center as possible when cropping.
13
13
  #
14
- # It is relative to 0,0 on the original image.
14
+ # The coordinates are relative to 0,0 on the original image. Unless set explicitly, the gravity is the center
15
+ # of the cropped image.
15
16
  #
16
- # Unless crop_gravity has been explicitely set, it defaults to
17
- # the center of the cropped image.
17
+ # @return [Vector2d, nil]
18
18
  def crop_gravity
19
19
  if crop_gravity?
20
20
  vector(crop_gravity_x, crop_gravity_y)
@@ -25,22 +25,30 @@ module DynamicImage
25
25
  end
26
26
  end
27
27
 
28
- # Returns true if crop gravity has been explicitely set.
28
+ # Returns true if the crop gravity has been set explicitly.
29
+ #
30
+ # @return [Boolean]
29
31
  def crop_gravity?
30
32
  crop_gravity_x.present? && crop_gravity_y.present?
31
33
  end
32
34
 
33
35
  # Returns the crop size, or nil if no cropping is applied.
36
+ #
37
+ # @return [Vector2d, nil]
34
38
  def crop_size
35
39
  vector(crop_width, crop_height) if crop_size?
36
40
  end
37
41
 
38
42
  # Returns true if crop size has been set.
43
+ #
44
+ # @return [Boolean]
39
45
  def crop_size?
40
46
  crop_width? && crop_height?
41
47
  end
42
48
 
43
49
  # Returns the crop start if set, or Vector2d(0, 0) if not.
50
+ #
51
+ # @return [Vector2d] the top left corner of the crop
44
52
  def crop_start
45
53
  if crop_start?
46
54
  vector(crop_start_x, crop_start_y)
@@ -50,32 +58,43 @@ module DynamicImage
50
58
  end
51
59
 
52
60
  # Returns true if crop start has been set.
61
+ #
62
+ # @return [Boolean]
53
63
  def crop_start?
54
64
  crop_start_x.present? && crop_start_y.present?
55
65
  end
56
66
 
57
67
  # Returns true if the image is cropped.
68
+ #
69
+ # @return [Boolean]
58
70
  def cropped?
59
71
  crop_size? && real_size? && crop_size != real_size
60
72
  end
61
73
 
62
74
  # Returns the real size of the image, without any cropping applied.
75
+ #
76
+ # @return [Vector2d, nil] the size of the stored image
63
77
  def real_size
64
78
  vector(real_width, real_height) if real_size?
65
79
  end
66
80
 
67
- # Returns true if the size has been set.
81
+ # Returns true if the real size has been set.
82
+ #
83
+ # @return [Boolean]
68
84
  def real_size?
69
85
  real_width? && real_height?
70
86
  end
71
87
 
72
- # Returns the cropped size if the image has been cropped. If not,
73
- # it returns the actual size.
88
+ # Returns the cropped size, or the real size if the image isn't cropped.
89
+ #
90
+ # @return [Vector2d, nil] the visible size of the image
74
91
  def size
75
92
  crop_size || real_size
76
93
  end
77
94
 
78
- # Returns true if the image has size set.
95
+ # Returns true if the image has a size.
96
+ #
97
+ # @return [Boolean]
79
98
  def size?
80
99
  size ? true : false
81
100
  end
@@ -4,20 +4,32 @@ module DynamicImage
4
4
  module Model
5
5
  # = DynamicImage Model Transformations
6
6
  #
7
+ # Transformations that rewrite the stored file. Per-request processing never touches it.
8
+ #
9
+ # Both methods replace the data and update the stored dimensions, adjusting the crop to match. Neither saves
10
+ # the record.
7
11
  module Transformations
8
- # Resizes the image
12
+ # Resizes the image, replacing the stored file with a smaller one. The crop is scaled along with it.
13
+ #
14
+ # @param max_size [Vector2d] the size to scale down to
15
+ # @return [self]
9
16
  def resize(max_size)
10
17
  transform_image do |image|
11
- new_size = real_size.constrain_both(max_size)
12
- scale = new_size.x / real_size.x
13
- crop_attributes.each do |attr|
14
- self[attr] = self[attr] * scale if self[attr]
15
- end
16
- image.resize(new_size)
18
+ resized = image.resize(real_size.constrain_both(max_size))
19
+ scale_crop(resized.size)
20
+ resized
17
21
  end
18
22
  end
19
23
 
20
- # Rotates the image
24
+ # Rotates the image, taking the crop and crop gravity with it.
25
+ #
26
+ # @param degrees [Integer] the angle, which must be a multiple of 90. Rotating by 0 is a no-op.
27
+ # @return [self]
28
+ # @raise [DynamicImage::Errors::InvalidTransformation] if the angle isn't a multiple of 90
29
+ #
30
+ # @example
31
+ # image.rotate(90)
32
+ # image.save
21
33
  def rotate(degrees = 90)
22
34
  degrees = degrees.to_i % 360
23
35
 
@@ -36,9 +48,33 @@ module DynamicImage
36
48
 
37
49
  private
38
50
 
39
- def crop_attributes
40
- %i[crop_width crop_height crop_start_x crop_start_y
41
- crop_gravity_x crop_gravity_y]
51
+ def scale_crop(new_size)
52
+ scale = new_size.to_f_vector / real_size
53
+
54
+ scale_crop_start(scale, new_size)
55
+ scale_crop_size(scale, new_size)
56
+ scale_crop_gravity(scale, new_size)
57
+ end
58
+
59
+ def scale_crop_start(scale, new_size)
60
+ return unless crop_start?
61
+
62
+ self.crop_start_x, self.crop_start_y =
63
+ (crop_start * scale).floor.clamp(0, new_size - 1).to_a
64
+ end
65
+
66
+ def scale_crop_size(scale, new_size)
67
+ return unless crop_size?
68
+
69
+ self.crop_width, self.crop_height =
70
+ (crop_size * scale).round.clamp(1, new_size - crop_start).to_a
71
+ end
72
+
73
+ def scale_crop_gravity(scale, new_size)
74
+ return unless crop_gravity?
75
+
76
+ self.crop_gravity_x, self.crop_gravity_y =
77
+ (crop_gravity * scale).round.clamp(1, new_size).to_a
42
78
  end
43
79
 
44
80
  def rotate_dimensions(width, height, degrees)
@@ -4,9 +4,10 @@ module DynamicImage
4
4
  module Model
5
5
  # = DynamicImage Model Validations
6
6
  #
7
- # Validates that all necessary attributes are valid. All of these are
8
- # managed by +DynamicImage::Model+, so this is mostly for enforcing
9
- # integrity.
7
+ # Validates the image attributes. +DynamicImage::Model+ manages all of them, so these mostly enforce integrity.
8
+ #
9
+ # The two that can fail on ordinary input are the image itself, which is rejected if the data isn't readable in
10
+ # a supported format, and the crop, which has to fit within the image.
10
11
  module Validations
11
12
  extend ActiveSupport::Concern
12
13
 
@@ -30,9 +31,7 @@ module DynamicImage
30
31
  length: { maximum: 255 }
31
32
 
32
33
  validates :real_width, :real_height,
33
- numericality: { greater_than: 0, only_integer: true }
34
-
35
- validates :real_width, :real_height,
34
+ presence: true,
36
35
  numericality: { greater_than: 0, only_integer: true }
37
36
 
38
37
  validates :crop_width, :crop_height,
@@ -40,8 +39,10 @@ module DynamicImage
40
39
  numericality: { greater_than: 0, only_integer: true },
41
40
  allow_nil: true
42
41
 
43
- validates :real_width, :real_height,
44
- presence: true
42
+ validates :crop_start_x, :crop_start_y,
43
+ numericality: { greater_than_or_equal_to: 0,
44
+ only_integer: true },
45
+ allow_nil: true
45
46
 
46
47
  validates :crop_width, presence: true, if: :crop_height?
47
48
  validates :crop_height, presence: true, if: :crop_width?
@@ -57,10 +58,16 @@ module DynamicImage
57
58
  end
58
59
 
59
60
  module ClassMethods
61
+ # Colorspaces an image is allowed to be in.
62
+ #
63
+ # @return [Array<String>] the colorspace names
60
64
  def allowed_colorspaces
61
65
  %w[rgb cmyk gray]
62
66
  end
63
67
 
68
+ # Content types an image is allowed to have, taken from the registered formats.
69
+ #
70
+ # @return [Array<String>]
64
71
  def allowed_content_types
65
72
  DynamicImage::Format.content_types
66
73
  end
@@ -4,9 +4,12 @@ module DynamicImage
4
4
  module Model
5
5
  # = DynamicImage Model Variants
6
6
  #
7
- # Validates that all necessary attributes are valid. All of these are
8
- # managed by +DynamicImage::Model+, so this is mostly for enforcing
9
- # integrity.
7
+ # Associates the image with its cached renderings.
8
+ #
9
+ # Each processed size is stored as a {DynamicImage::Variant}, so cropping and resizing is done once. Variants
10
+ # are destroyed with the image, and cleared whenever its data changes.
11
+ #
12
+ # @see DynamicImage::ProcessedImage
10
13
  module Variants
11
14
  extend ActiveSupport::Concern
12
15
 
@@ -8,8 +8,8 @@ require "dynamic_image/model/variants"
8
8
  module DynamicImage
9
9
  # = DynamicImage Model
10
10
  #
11
- # ActiveModel extension for the model holding image data. It assumes your
12
- # database table has at least the following attributes:
11
+ # ActiveModel extension for the model holding image data. The table needs at least the attributes in
12
+ # {DynamicImage::Schema::ATTRIBUTES}:
13
13
  #
14
14
  # create_table :images do |t|
15
15
  # t.string :content_hash
@@ -24,7 +24,7 @@ module DynamicImage
24
24
  # t.timestamps
25
25
  # end
26
26
  #
27
- # To use it, simply include it in your model:
27
+ # Include it in your model:
28
28
  #
29
29
  # class Image < ActiveRecord::Base
30
30
  # include DynamicImage::Model
@@ -32,23 +32,18 @@ module DynamicImage
32
32
  #
33
33
  # == Usage
34
34
  #
35
- # To save an image, simply assign to the +file+ attribute.
35
+ # To save an image, assign to the +file+ attribute. The image is parsed and validated when the record is saved.
36
36
  #
37
37
  # image = Image.create(file: params.permit(:file))
38
38
  #
39
- # This will automatically parse and validate the image when your record is
40
- # saved.
41
- #
42
- # To read back the image data, access the +data+ attribute. This will lazily
43
- # load the data from the store.
39
+ # To read back the image data, access the +data+ attribute. The data is loaded lazily from the store.
44
40
  #
45
41
  # data = image.data
46
42
  #
47
43
  # == Cropping
48
44
  #
49
- # Images can be pre-cropped by setting +crop_width+, +crop_height+,
50
- # +crop_start_x+ and +crop_start_y+. The crop dimensions cannot exceed the
51
- # image size.
45
+ # Images can be pre-cropped by setting +crop_width+, +crop_height+, +crop_start_x+ and +crop_start_y+. The crop
46
+ # dimensions cannot exceed the image size.
52
47
  #
53
48
  # image.update(
54
49
  # crop_start_x: 15, crop_start_y: 20,
@@ -56,10 +51,14 @@ module DynamicImage
56
51
  # )
57
52
  # image.size # => Vector2d(300, 200)
58
53
  #
59
- # By default, images will be cropped from the center. You can control this
60
- # by setting +crop_gravity_x+ and +crop_gravity_y+. DynamicImage will make
61
- # sure the pixel referred to by these coordinates are present in the cropped
62
- # image, and as close to the center as possible without zooming in.
54
+ # By default, images will be cropped from the center. You can control this by setting +crop_gravity_x+ and
55
+ # +crop_gravity_y+. DynamicImage will make sure the pixel referred to by these coordinates are present in the
56
+ # cropped image, and as close to the center as possible without zooming in.
57
+ #
58
+ # @see DynamicImage::Model::Dimensions
59
+ # @see DynamicImage::Model::Transformations
60
+ # @see DynamicImage::Model::Validations
61
+ # @see DynamicImage::Model::Variants
63
62
  module Model
64
63
  extend ActiveSupport::Concern
65
64
  include Dis::Model
@@ -72,33 +71,62 @@ module DynamicImage
72
71
  before_validation :read_image_metadata, if: :data_changed?
73
72
  end
74
73
 
75
- # Returns true if the image is in the CMYK colorspace
74
+ # Returns true if the image holds more than one frame.
75
+ #
76
+ # Images stored before +frame_count+ existed have none, and are taken to be still.
77
+ #
78
+ # @return [Boolean]
79
+ def animated?
80
+ has_attribute?(:frame_count) && frame_count.to_i > 1
81
+ end
82
+
83
+ # Returns the alt text for the image, or nil if none has been set.
84
+ #
85
+ # DynamicImage doesn't add this column by default. Either create it yourself or override the method to
86
+ # provide your own implementation.
87
+ #
88
+ # Note that there is a distinction between nil and a blank string. <tt>alt=""</tt> means the image is
89
+ # purely decorative, while a missing attribute is an accessibility defect.
90
+ #
91
+ # @return [String, nil]
92
+ # @see DynamicImage::Helper#dynamic_image_tag
93
+ def alt_text
94
+ self[:alt_text] if has_attribute?(:alt_text)
95
+ end
96
+
97
+ # Returns true if the image is in the CMYK colorspace.
98
+ #
99
+ # @return [Boolean]
76
100
  def cmyk?
77
101
  colorspace == "cmyk"
78
102
  end
79
103
 
80
- # Returns true if the image is in the grayscale colorspace
104
+ # Returns true if the image is in the grayscale colorspace.
105
+ #
106
+ # @return [Boolean]
81
107
  def gray?
82
108
  colorspace == "gray"
83
109
  end
84
110
 
85
- # Returns true if the image is in the RGB colorspace
111
+ # Returns true if the image is in the RGB colorspace.
112
+ #
113
+ # @return [Boolean]
86
114
  def rgb?
87
115
  colorspace == "rgb"
88
116
  end
89
117
 
90
- # Finds a web safe content type. GIF, JPEG and PNG images are allowed,
91
- # any other formats should be converted to JPEG.
118
+ # Finds a web safe content type, negotiated against {DynamicImage.default_formats}.
119
+ #
120
+ # @return [String]
121
+ # @see DynamicImage::FormatNegotiator
92
122
  def safe_content_type
93
- if safe_content_types.include?(content_type)
94
- content_type
95
- else
96
- "image/jpeg"
97
- end
123
+ DynamicImage::FormatNegotiator
124
+ .new(self).negotiate(DynamicImage.default_formats).content_type
98
125
  end
99
126
 
100
- # Includes a timestamp fingerprint in the URL param, so
101
- # that rendered images can be cached indefinitely.
127
+ # Includes a timestamp fingerprint in the URL param, so rendered images can be cached indefinitely.
128
+ #
129
+ # @return [String] the id and an +updated_at+ fingerprint
102
130
  def to_param
103
131
  [id, updated_at.utc.to_fs(cache_timestamp_format)].join("-")
104
132
  end
@@ -119,17 +147,13 @@ module DynamicImage
119
147
  self.real_width = metadata.width
120
148
  self.real_height = metadata.height
121
149
  self.content_type = metadata.content_type
150
+ self.frame_count = metadata.frame_count if has_attribute?(:frame_count)
151
+ self.alpha = metadata.alpha? if has_attribute?(:alpha)
122
152
  @valid_image = true
123
153
  end
124
154
 
125
155
  def valid_image?
126
156
  @valid_image ? true : false
127
157
  end
128
-
129
- def safe_content_types
130
- %w[image/png
131
- image/gif
132
- image/jpeg]
133
- end
134
158
  end
135
159
  end