dynamic_image 2.1.5 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83e8a6f17f1f898a5b5fcc7715560e17b4c8867819751710332d106d4afc5f1a
4
- data.tar.gz: 50517764f87f4ee0283f59211260987574f3567146a62be571a4885bb23b318e
3
+ metadata.gz: d9d7c5e9f872a132439d46c725089651caee0b684a52b425c2e572026e733bd0
4
+ data.tar.gz: 787251fd6c8d096cb1a918bf8667b482d92a59aec19dd3b8420a2e22b3dcce69
5
5
  SHA512:
6
- metadata.gz: e53e8dcadd56d1c2bcb10f0a059882fbb1dd0efe1bc695822eb892755202c2cad620b27f9e4c85d0528b7d2a53662c1cddca2fa7ba991ce148c46fdc1912bd66
7
- data.tar.gz: 69eca07ce497f09bde153dada61c688b5612d5e71790c0008f760955712a93005c7df188b28bddd5d5ba4d98b6bdc8902e199dbd33a6c440f3b2be0ae82560de
6
+ metadata.gz: 442830ff44a5ea152920a483985dd0b737599ef9c68a07da4f677eb1de366ce3cb6a55acf4a9de2a59441b14c901d45ec3d8e71b846169895ca8913fae2b04ed
7
+ data.tar.gz: '079bd5660c3109a67ae5ad616fd0ddbd6c1102ecee17e9a9ca1d57a555d21cc64bc8e30228be4edd12a64e7cba69d8d0739f58e275396c53d7d6351cf8c00bb3'
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Code Climate](https://codeclimate.com/github/elektronaut/dynamic_image/badges/gpa.svg)](https://codeclimate.com/github/elektronaut/dynamic_image)
4
4
  [![Code Climate](https://codeclimate.com/github/elektronaut/dynamic_image/badges/coverage.svg)](https://codeclimate.com/github/elektronaut/dynamic_image)
5
5
  [![Inline docs](http://inch-ci.org/github/elektronaut/dynamic_image.svg)](http://inch-ci.org/github/elektronaut/dynamic_image)
6
- [![Security](https://hakiri.io/github/elektronaut/dynamic_image/master.svg)](https://hakiri.io/github/elektronaut/dynamic_image/master)
6
+ [![Security](https://hakiri.io/github/elektronaut/dynamic_image/main.svg)](https://hakiri.io/github/elektronaut/dynamic_image/main)
7
7
 
8
8
  # DynamicImage
9
9
 
@@ -21,17 +21,16 @@ images will be converted to RGB, and RGB images will be converted to the sRGB
21
21
  colorspace for consistent appearance in all browsers.
22
22
 
23
23
  DynamicImage is built on [Dis](https://github.com/elektronaut/dis)
24
- and [MiniMagick](https://github.com/minimagick/minimagick).
24
+ and [ruby-vips](https://github.com/libvips/ruby-vips).
25
25
 
26
26
  All URLs are signed with a HMAC to protect against denial of service
27
27
  and enumeration attacks.
28
28
 
29
29
  ## Requirements
30
30
 
31
- * Rails 5
32
- * Ruby 2.4+
33
- * ImageMagick command line tools
34
- * ExifTool
31
+ * Rails 5+
32
+ * Ruby 2.7+
33
+ * libvips 8.8+
35
34
 
36
35
  ## Documentation
37
36
 
@@ -42,7 +41,7 @@ and enumeration attacks.
42
41
  Add the gem to your Gemfile and run `bundle install`.
43
42
 
44
43
  ```ruby
45
- gem "dynamic_image", "~> 2.0.0"
44
+ gem "dynamic_image", "~> 3.0"
46
45
  ```
47
46
 
48
47
  Run the `dis:install` generator to set up your storage.
@@ -33,7 +33,7 @@ module DynamicImage
33
33
  end
34
34
 
35
35
  def download
36
- render_raw_image(disposition: "attachment", filename: @record.filename)
36
+ render_raw_image(disposition: "attachment")
37
37
  end
38
38
 
39
39
  # Returns the requested size as a vector.
@@ -44,34 +44,31 @@ module DynamicImage
44
44
  private
45
45
 
46
46
  def cache_expiration_header
47
- expires_in 1.year, public: true if response.status == 200
47
+ return unless response.status == 200
48
+
49
+ response.headers["Cache-Control"] = "max-age=#{1.year}, public"
50
+ expires_in 1.year, public: true
48
51
  end
49
52
 
50
53
  def find_record
51
54
  @record = model.find(params[:id])
52
55
  end
53
56
 
54
- def process_and_send(image, options)
55
- processed_image = DynamicImage::ProcessedImage.new(image, options)
56
- if process_later?(processed_image, requested_size)
57
- process_later(image, options, requested_size)
58
- head 503, retry_after: 10
57
+ def filename(format = nil)
58
+ if format.is_a?(DynamicImage::Format)
59
+ File.basename(@record.filename, ".*") + format.extension
59
60
  else
60
- send_image(processed_image, requested_size)
61
+ filename(DynamicImage::Format.find(format) ||
62
+ DynamicImage::Format.content_type(@record.content_type))
61
63
  end
62
64
  end
63
65
 
64
- def process_later(image, options, requested_size)
65
- DynamicImage::Jobs::CreateVariant
66
- .perform_later(image, options, requested_size.to_s)
67
- end
68
-
69
- def process_later?(processed_image, size)
70
- return false unless DynamicImage.process_later_limit
71
-
72
- image_size = processed_image.record.size.x * processed_image.record.size.y
73
- image_size > DynamicImage.process_later_limit &&
74
- !processed_image.find_variant(size)
66
+ def process_and_send(image, options)
67
+ processed_image = DynamicImage::ProcessedImage.new(image, options)
68
+ send_data(processed_image.cropped_and_resized(requested_size),
69
+ filename: filename(processed_image.format),
70
+ content_type: processed_image.format.content_type,
71
+ disposition: "inline")
75
72
  end
76
73
 
77
74
  def render_image(options)
@@ -88,7 +85,7 @@ module DynamicImage
88
85
  end
89
86
  end
90
87
 
91
- def render_raw_image(disposition: "inline", filename: nil)
88
+ def render_raw_image(disposition: "inline")
92
89
  return unless stale?(@record)
93
90
 
94
91
  respond_to do |format|
@@ -105,12 +102,6 @@ module DynamicImage
105
102
  params[:format]
106
103
  end
107
104
 
108
- def send_image(processed_image, requested_size)
109
- send_data(processed_image.cropped_and_resized(requested_size),
110
- content_type: processed_image.content_type,
111
- disposition: "inline")
112
- end
113
-
114
105
  def verify_signed_params
115
106
  key = %i[action id size].map do |k|
116
107
  k == :id ? params.require(k).to_i : params.require(k)
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicImage
4
+ class Format
5
+ attr_reader :name, :animated, :content_types, :extensions, :magic_bytes,
6
+ :save_options
7
+
8
+ def initialize(name, options)
9
+ options = default_options.merge(options)
10
+
11
+ @name = name
12
+ @animated = options[:animated]
13
+ @content_types = Array(options[:content_type])
14
+ @extensions = Array(options[:extension])
15
+ @magic_bytes = options[:magic_bytes].map do |s|
16
+ s.dup.force_encoding("binary")
17
+ end
18
+ @save_options = options[:save_options]
19
+ end
20
+
21
+ def animated?
22
+ animated
23
+ end
24
+
25
+ def content_type
26
+ content_types.first
27
+ end
28
+
29
+ def extension
30
+ extensions.first
31
+ end
32
+
33
+ class << self
34
+ def content_type(type)
35
+ formats.filter { |f| f.content_types.include?(type) }.first
36
+ end
37
+
38
+ def content_types
39
+ formats.flat_map(&:content_types)
40
+ end
41
+
42
+ def find(name)
43
+ key = name.to_s.upcase
44
+ key = "JPEG" if key == "JPG"
45
+ registered_formats[key]
46
+ end
47
+
48
+ def formats
49
+ registered_formats.map { |_, f| f }
50
+ end
51
+
52
+ def register(name, **opts)
53
+ registered_formats[name] = new(name, opts)
54
+ end
55
+
56
+ def sniff(bytes)
57
+ return unless bytes
58
+
59
+ formats.each do |format|
60
+ format.magic_bytes.each do |b|
61
+ return format if bytes.start_with?(b)
62
+ end
63
+ end
64
+ nil
65
+ end
66
+
67
+ private
68
+
69
+ def registered_formats
70
+ @registered_formats ||= {}
71
+ end
72
+ end
73
+
74
+ def default_options
75
+ { animated: false, content_type: [], extension: [], magic_bytes: [],
76
+ save_options: {} }
77
+ end
78
+
79
+ register(
80
+ "BMP",
81
+ content_type: %w[image/bmp],
82
+ extension: %w[.bmp],
83
+ magic_bytes: ["\x42\x4d"]
84
+ )
85
+
86
+ register(
87
+ "GIF",
88
+ animated: true,
89
+ content_type: %w[image/gif],
90
+ extension: %w[.gif],
91
+ magic_bytes: %w[GIF87a GIF89a],
92
+ save_options: { optimize_gif_frames: true,
93
+ optimize_gif_transparency: true }
94
+ )
95
+
96
+ register(
97
+ "JPEG",
98
+ content_type: %w[image/jpeg image/pjpeg],
99
+ extension: %w[.jpg .jpeg],
100
+ magic_bytes: ["\xff\xd8"],
101
+ save_options: { Q: 90, strip: true, background: [255.0, 255.0, 255.0] }
102
+ )
103
+
104
+ register(
105
+ "PNG",
106
+ content_type: %w[image/png],
107
+ extension: %w[.png],
108
+ magic_bytes: ["\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"]
109
+ )
110
+
111
+ register(
112
+ "TIFF",
113
+ content_type: %w[image/tiff],
114
+ extension: %w[.tiff .tif],
115
+ magic_bytes: ["\x49\x49\x2a\x00", "\x4d\x4d\x00\x2a"]
116
+ )
117
+
118
+ register(
119
+ "WEBP",
120
+ animated: true,
121
+ content_type: %w[image/webp],
122
+ extension: %w[.webp],
123
+ magic_bytes: ["\x52\x49\x46\x46"],
124
+ save_options: { Q: 90, strip: true }
125
+ )
126
+ end
127
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicImage
4
+ class ImageProcessor
5
+ # = ImageProcessor::Colors
6
+ #
7
+ # Performs the necessary profile conversions on the image. All
8
+ # images are converted to the sRGB colorspace using either the
9
+ # embedded profile, or the built-in generic profile. Grayscale
10
+ # images are converted back to grayscale after processing.
11
+ module Colors
12
+ private
13
+
14
+ def icc_profile?(image)
15
+ image.get_fields.include?("icc-profile-data")
16
+ end
17
+
18
+ def icc_transform_srgb(image)
19
+ return image unless icc_profile?(image)
20
+
21
+ image.icc_transform("srgb", embedded: true, intent: :perceptual)
22
+ end
23
+
24
+ def screen_profile(image)
25
+ if !icc_profile?(image) && %i[rgb b-w].include?(image.interpretation)
26
+ return image
27
+ end
28
+
29
+ target_space = image.interpretation == :"b-w" ? "b-w" : "srgb"
30
+ icc_transform_srgb(image).colourspace(target_space)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicImage
4
+ class ImageProcessor
5
+ module Frames
6
+ # Extracts a single frame from a multi-frame image.
7
+ def frame(index)
8
+ apply extract_frame(index)
9
+ end
10
+
11
+ # Returns the number of frames.
12
+ def frame_count
13
+ image.get("height") / size.y
14
+ end
15
+
16
+ private
17
+
18
+ def each_frame(&block)
19
+ return apply(block.call(image)) unless frame_count > 1
20
+
21
+ apply(replace_frames(frames.map { |f| block.call(f) }))
22
+ end
23
+
24
+ def extract_frame(index)
25
+ image.extract_area(0, (index * size.y), size.x, size.y)
26
+ end
27
+
28
+ def frames
29
+ frame_count.times.map { |i| extract_frame(i) }
30
+ end
31
+
32
+ def replace_frames(new_frames)
33
+ new_size = Vector2d(new_frames.first.size)
34
+ new_image = blank_image.insert(
35
+ Vips::Image.arrayjoin(new_frames, across: 1),
36
+ 0, 0, expand: true
37
+ ).extract_area(0, 0, new_size.x, new_size.y * frame_count).copy
38
+ new_image.set("page-height", new_size.y)
39
+ new_image
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicImage
4
+ class ImageProcessor
5
+ module Transform
6
+ # Crops the image
7
+ def crop(crop_size, crop_start)
8
+ return self if crop_start == Vector2d(0, 0) && crop_size == size
9
+
10
+ unless valid_crop?(crop_start, crop_size)
11
+ raise DynamicImage::Errors::InvalidTransformation,
12
+ "crop size is out of bounds"
13
+ end
14
+
15
+ each_frame do |frame|
16
+ frame.crop(crop_start.x, crop_start.y, crop_size.x, crop_size.y)
17
+ end
18
+ end
19
+
20
+ # Resize the image to a new size.
21
+ def resize(new_size)
22
+ new_size = Vector2d(new_size)
23
+ apply image.thumbnail_image(new_size.x.to_i,
24
+ height: new_size.y.to_i,
25
+ crop: :none,
26
+ size: :both)
27
+ end
28
+
29
+ # Rotates the image. The rotation must be a multiple of 90 degrees.
30
+ def rotate(degrees)
31
+ degrees = degrees.to_i % 360
32
+ return self if degrees.zero?
33
+
34
+ if (degrees % 90).nonzero?
35
+ raise DynamicImage::Errors::InvalidTransformation,
36
+ "angle must be a multiple of 90 degrees"
37
+ end
38
+
39
+ each_frame { |frame| frame.rotate(degrees) }
40
+ end
41
+
42
+ private
43
+
44
+ def valid_crop?(crop_start, crop_size)
45
+ bounds = crop_start + crop_size
46
+ bounds.x <= size.x && bounds.y <= size.y
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dynamic_image/image_processor/colors"
4
+ require "dynamic_image/image_processor/frames"
5
+ require "dynamic_image/image_processor/transform"
6
+
7
+ module DynamicImage
8
+ # = ImageProcessor
9
+ #
10
+ # This is the image processing pipeline.
11
+ #
12
+ # ==== Example:
13
+ #
14
+ # DynamicImage::ImageProcessor
15
+ # .new(file)
16
+ # .crop(crop_start, crop_size)
17
+ # .resize(size)
18
+ # .convert(:jpeg)
19
+ # .read
20
+ class ImageProcessor
21
+ include DynamicImage::ImageProcessor::Colors
22
+ include DynamicImage::ImageProcessor::Frames
23
+ include DynamicImage::ImageProcessor::Transform
24
+
25
+ attr_reader :image, :target_format
26
+
27
+ def initialize(image, target_format: nil)
28
+ if image.is_a?(Vips::Image)
29
+ @image = image
30
+ @target_format = target_format
31
+ else
32
+ reader = DynamicImage::ImageReader.new(image)
33
+ @image = screen_profile(reader.read.autorot)
34
+ @target_format = reader.format
35
+ end
36
+ end
37
+
38
+ # Convert the image to a different format.
39
+ def convert(new_format)
40
+ unless new_format.is_a?(DynamicImage::Format)
41
+ new_format = DynamicImage::Format.find(new_format)
42
+ end
43
+ if frame_count > 1 && !new_format.animated?
44
+ self.class.new(extract_frame(0), target_format: new_format)
45
+ else
46
+ self.class.new(image, target_format: new_format)
47
+ end
48
+ end
49
+
50
+ # Returns the image data as a binary string.
51
+ def read
52
+ tempfile = Tempfile.new(["dynamic_image", target_format.extension],
53
+ binmode: true)
54
+ tempfile.close
55
+ write(tempfile.path)
56
+ tempfile.open
57
+ tempfile.read
58
+ ensure
59
+ tempfile.close
60
+ end
61
+
62
+ # Returns the image size as a Vector2d.
63
+ def size
64
+ Vector2d.new(
65
+ image.get("width"),
66
+ image.get(
67
+ image.get_fields.include?("page-height") ? "page-height" : "height"
68
+ )
69
+ )
70
+ end
71
+
72
+ # Write the image to a file.
73
+ def write(path)
74
+ image.write_to_file(path, **target_format.save_options)
75
+ end
76
+
77
+ private
78
+
79
+ def apply(new_image)
80
+ self.class.new(new_image, target_format: target_format)
81
+ end
82
+
83
+ def blank_image
84
+ image.draw_rect([0.0, 0.0, 0.0, 0.0],
85
+ 0, 0, image.get("width"), image.get("height"),
86
+ fill: true)
87
+ end
88
+ end
89
+ end
@@ -2,54 +2,49 @@
2
2
 
3
3
  module DynamicImage
4
4
  class ImageReader
5
- class << self
6
- def magic_bytes
7
- @magic_bytes ||= [
8
- "\x47\x49\x46\x38\x37\x61", # GIF
9
- "\x47\x49\x46\x38\x39\x61",
10
- "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a", # PNG
11
- "\xff\xd8", # JPEG
12
- "\x49\x49\x2a\x00", # TIFF
13
- "\x4d\x4d\x00\x2a",
14
- "\x42\x4d", # BMP
15
- "\x52\x49\x46\x46" # WEBP
16
- ].map { |s| s.dup.force_encoding("binary") }
17
- end
18
- end
19
-
20
5
  def initialize(data)
21
6
  @data = data
22
7
  end
23
8
 
24
- def exif
25
- raise DynamicImage::Errors::InvalidHeader unless valid_header?
26
-
27
- MiniExiftool.new(string_io)
9
+ def format
10
+ DynamicImage::Format.sniff(file_header)
28
11
  end
29
12
 
30
13
  def read
31
14
  raise DynamicImage::Errors::InvalidHeader unless valid_header?
32
15
 
33
- MiniMagick::Image.read(@data)
16
+ if @data.is_a?(String)
17
+ Vips::Image.new_from_buffer(@data, option_string)
18
+ else
19
+ Vips::Image.new_from_file(@data.path + option_string, access: :random)
20
+ end
34
21
  end
35
22
 
36
23
  def valid_header?
37
- return false if file_header.blank?
38
-
39
- self.class.magic_bytes.each do |str|
40
- return true if file_header.start_with?(str)
41
- end
42
- false
24
+ format ? true : false
43
25
  end
44
26
 
45
27
  private
46
28
 
47
29
  def file_header
48
- @file_header ||= string_io.read(8)
30
+ @file_header ||= read_file_header
31
+ end
32
+
33
+ def option_string
34
+ format.animated? ? "[n=-1]" : ""
49
35
  end
50
36
 
51
- def string_io
52
- StringIO.new(@data, "rb")
37
+ def read_file_header
38
+ data_stream = stream
39
+ header = data_stream.read(8)
40
+ data_stream.seek((0 - header.length), IO::SEEK_CUR) if header
41
+ header
42
+ end
43
+
44
+ def stream
45
+ return StringIO.new(@data, "rb") if @data.is_a?(String)
46
+
47
+ @data
53
48
  end
54
49
  end
55
50
  end
@@ -36,19 +36,6 @@ module DynamicImage
36
36
  [crop_size, (start + crop_start)]
37
37
  end
38
38
 
39
- # Returns crop geometry as an ImageMagick compatible string.
40
- #
41
- # ==== Example
42
- #
43
- # image = Image.find(params[:id]) # 320x200 image
44
- # sizing = DynamicImage::ImageSizing.new(image)
45
- #
46
- # sizing.crop_geometry(Vector2d(100, 100)) # => "200x200+60+0"
47
- def crop_geometry_string(ratio_vector)
48
- crop_size, start = crop_geometry(ratio_vector)
49
- crop_size.floor.to_s + "+#{start.x.to_i}+#{start.y.to_i}!"
50
- end
51
-
52
39
  # Adjusts +fit_size+ to fit the image dimensions.
53
40
  # Any dimension set to zero will be ignored.
54
41
  #
@@ -15,39 +15,38 @@ module DynamicImage
15
15
  def colorspace
16
16
  return unless valid?
17
17
 
18
- case metadata[:colorspace]
18
+ case metadata[:colorspace].to_s
19
19
  when /rgb/i
20
20
  "rgb"
21
21
  when /cmyk/i
22
22
  "cmyk"
23
- when /gray/i
23
+ when /gray/i, /b-w/i
24
24
  "gray"
25
25
  end
26
26
  end
27
27
 
28
28
  # Returns the content type of the image.
29
29
  def content_type
30
- "image/#{format.downcase}" if valid?
30
+ reader.format.content_type if valid?
31
+ end
32
+
33
+ def format
34
+ reader.format.name if valid?
31
35
  end
32
36
 
33
37
  # Returns the dimensions of the image as a vector.
34
38
  def dimensions
35
- Vector2d.new(*metadata[:dimensions]) if valid?
39
+ Vector2d.new(metadata[:width], metadata[:height]) if valid?
36
40
  end
37
41
 
38
42
  # Returns the width of the image.
39
43
  def width
40
- dimensions.try(:x)
44
+ metadata[:width] if valid?
41
45
  end
42
46
 
43
47
  # Returns the height of the image.
44
48
  def height
45
- dimensions.try(:y)
46
- end
47
-
48
- # Returns the format of the image.
49
- def format
50
- metadata[:format] if valid?
49
+ metadata[:height] if valid?
51
50
  end
52
51
 
53
52
  # Returns true if the image is valid.
@@ -63,12 +62,8 @@ module DynamicImage
63
62
 
64
63
  def read_image
65
64
  image = reader.read
66
- image.auto_orient
67
- result = yield image
68
- image.destroy!
69
- result
70
- rescue MiniMagick::Invalid
71
- :invalid
65
+ image.autorot if image.respond_to?(:autorot)
66
+ yield image
72
67
  end
73
68
 
74
69
  def reader
@@ -77,11 +72,15 @@ module DynamicImage
77
72
 
78
73
  def read_metadata
79
74
  read_image do |image|
80
- {
81
- colorspace: image[:colorspace],
82
- dimensions: image[:dimensions],
83
- format: image[:format]
84
- }
75
+ height = if image.get_fields.include?("page-height")
76
+ image.get("page-height")
77
+ else
78
+ image.get("height")
79
+ end
80
+
81
+ { width: image.get("width"),
82
+ height: height,
83
+ colorspace: image.get("interpretation") }
85
84
  end
86
85
  end
87
86
  end
@@ -10,10 +10,10 @@ module DynamicImage
10
10
  transform_image do |image|
11
11
  new_size = real_size.constrain_both(max_size)
12
12
  scale = new_size.x / real_size.x
13
- image.resize(new_size)
14
13
  crop_attributes.each do |attr|
15
14
  self[attr] = self[attr] * scale if self[attr]
16
15
  end
16
+ image.resize(new_size)
17
17
  end
18
18
  end
19
19
 
@@ -29,8 +29,8 @@ module DynamicImage
29
29
  end
30
30
 
31
31
  transform_image do |image|
32
- image.rotate(degrees)
33
32
  rotate_dimensions(real_size.x, real_size.y, degrees)
33
+ image.rotate(degrees)
34
34
  end
35
35
  end
36
36
 
@@ -74,15 +74,9 @@ module DynamicImage
74
74
  [new_width - crop_gravity_y, crop_gravity_x]
75
75
  end
76
76
 
77
- def processed_image
78
- DynamicImage::ProcessedImage.new(self, uncropped: true)
79
- end
80
-
81
- def transform_image
77
+ def transform_image(&block)
82
78
  read_image_metadata if data_changed?
83
- self.data = processed_image.normalized do |image|
84
- yield(image) if block_given?
85
- end
79
+ self.data = block.call(DynamicImage::ImageProcessor.new(data)).read
86
80
  read_image_metadata
87
81
  self
88
82
  end
@@ -58,19 +58,11 @@ module DynamicImage
58
58
 
59
59
  module ClassMethods
60
60
  def allowed_colorspaces
61
- %w[rgb
62
- cmyk
63
- gray]
61
+ %w[rgb cmyk gray]
64
62
  end
65
63
 
66
64
  def allowed_content_types
67
- %w[image/bmp
68
- image/gif
69
- image/jpeg
70
- image/pjpeg
71
- image/png
72
- image/tiff
73
- image/webp]
65
+ DynamicImage::Format.content_types
74
66
  end
75
67
  end
76
68
 
@@ -100,10 +100,9 @@ module DynamicImage
100
100
  # Includes a timestamp fingerprint in the URL param, so
101
101
  # that rendered images can be cached indefinitely.
102
102
  def to_param
103
- [id, updated_at.utc.to_formatted_s(cache_timestamp_format)].join("-")
103
+ [id, updated_at.utc.to_s(cache_timestamp_format)].join("-")
104
104
  end
105
105
 
106
-
107
106
  private
108
107
 
109
108
  def read_image_metadata
@@ -11,21 +11,8 @@ module DynamicImage
11
11
  def initialize(record, options = {})
12
12
  @record = record
13
13
  @uncropped = options[:uncropped] ? true : false
14
- @format = options[:format].to_s.upcase if options[:format]
15
- @format = "JPEG" if defined?(@format) && @format == "JPG"
16
- end
17
-
18
- # Returns the content type of the processed image.
19
- #
20
- # ==== Example
21
- #
22
- # image = Image.find(params[:id])
23
- # DynamicImage::ProcessedImage.new(image).content_type
24
- # # => 'image/png'
25
- # DynamicImage::ProcessedImage.new(image, :jpeg).content_type
26
- # # => 'image/jpeg'
27
- def content_type
28
- "image/#{format}".downcase
14
+ @format_name = options[:format].to_s.upcase if options[:format]
15
+ @format_name = "JPEG" if defined?(@format_name) && @format_name == "JPG"
29
16
  end
30
17
 
31
18
  # Crops and resizes the image. Normalization is performed as well.
@@ -56,6 +43,10 @@ module DynamicImage
56
43
  record.variants.find_by(variant_params(size))
57
44
  end
58
45
 
46
+ def format
47
+ DynamicImage::Format.find(@format_name) || record_format
48
+ end
49
+
59
50
  # Normalizes the image.
60
51
  #
61
52
  # * Applies EXIF rotation
@@ -72,109 +63,48 @@ module DynamicImage
72
63
  # Returns a binary string.
73
64
  def normalized
74
65
  require_valid_image!
75
- process_data do |image|
76
- image.combine_options do |combined|
77
- combined.auto_orient
78
- convert_to_srgb(image, combined)
79
- yield(combined) if block_given?
80
- optimize(combined)
81
- end
82
- image.format(format) if needs_format_conversion?
83
- end
84
- end
85
-
86
- private
87
66
 
88
- def coalesced(image)
89
- gif? ? DynamicImage::ImageReader.new(image.coalesce.to_blob).read : image
67
+ image = DynamicImage::ImageProcessor.new(record.data)
68
+ image = yield(image) if block_given?
69
+ image.convert(format).read
90
70
  end
91
71
 
92
- def convert_to_srgb(image, combined)
93
- if image.data["profiles"].present? &&
94
- exif.colorspacedata&.strip&.downcase == record.colorspace
95
- combined.profile(srgb_profile)
96
- end
97
- combined.colorspace("sRGB") if record.cmyk?
98
- end
72
+ private
99
73
 
100
74
  def create_variant(size)
101
75
  record.variants.create(
102
76
  variant_params(size).merge(filename: record.filename,
103
- content_type: content_type,
77
+ content_type: format.content_type,
104
78
  data: crop_and_resize(size))
105
79
  )
106
80
  end
107
81
 
108
82
  def crop_and_resize(size)
109
83
  normalized do |image|
110
- next unless record.cropped? || size != record.size
111
-
112
- image.crop(image_sizing.crop_geometry_string(size))
113
- image.resize(size)
84
+ image.crop(*image_sizing.crop_geometry(size)).resize(size)
114
85
  end
115
86
  end
116
87
 
117
- def exif
118
- @exif ||= DynamicImage::ImageReader.new(record.data).exif
119
- end
120
-
121
- def format
122
- @format ||= record_format
123
- end
124
-
125
- def gif?
126
- content_type == "image/gif"
127
- end
128
-
129
- def jpeg?
130
- content_type == "image/jpeg"
131
- end
132
-
133
88
  def image_sizing
134
89
  @image_sizing ||=
135
90
  DynamicImage::ImageSizing.new(record, uncropped: @uncropped)
136
91
  end
137
92
 
138
- def needs_format_conversion?
139
- format != record_format
140
- end
141
-
142
- def optimize(image)
143
- image.layers "optimize" if gif?
144
- image.strip
145
- image.quality(85).sampling_factor("4:2:0").interlace("JPEG") if jpeg?
146
- image
147
- end
148
-
149
- def process_data
150
- image = coalesced(DynamicImage::ImageReader.new(record.data).read)
151
- yield(image)
152
- result = image.to_blob
153
- image.destroy!
154
- result
155
- end
156
-
157
93
  def record_format
158
- { "image/bmp" => "BMP", "image/png" => "PNG", "image/gif" => "GIF",
159
- "image/jpeg" => "JPEG", "image/pjpeg" => "JPEG", "image/tiff" => "TIFF",
160
- "image/webp" => "WEBP" }[record.content_type]
94
+ DynamicImage::Format.content_type(record.content_type)
161
95
  end
162
96
 
163
97
  def require_valid_image!
164
98
  raise DynamicImage::Errors::InvalidImage unless record.valid?
165
99
  end
166
100
 
167
- def srgb_profile
168
- File.join(File.dirname(__FILE__), "profiles/sRGB_ICC_v4_Appearance.icc")
169
- end
170
-
171
101
  def variant_params(size)
172
102
  crop_size, crop_start = image_sizing.crop_geometry(size)
173
103
 
174
104
  { width: size.x.round, height: size.y.round,
175
105
  crop_width: crop_size.x, crop_height: crop_size.y,
176
106
  crop_start_x: crop_start.x, crop_start_y: crop_start.y,
177
- format: format }
107
+ format: format.name }
178
108
  end
179
109
  end
180
110
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DynamicImage
4
- VERSION = "2.1.5"
4
+ VERSION = "3.0.0"
5
5
  end
data/lib/dynamic_image.rb CHANGED
@@ -1,16 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "mini_exiftool"
4
- require "mini_magick"
5
3
  require "dis"
6
4
  require "vector2d"
5
+ require "vips"
7
6
 
8
7
  require "dynamic_image/belongs_to"
9
8
  require "dynamic_image/controller"
10
9
  require "dynamic_image/digest_verifier"
11
10
  require "dynamic_image/engine"
12
11
  require "dynamic_image/errors"
12
+ require "dynamic_image/format"
13
13
  require "dynamic_image/helper"
14
+ require "dynamic_image/image_processor"
14
15
  require "dynamic_image/image_reader"
15
16
  require "dynamic_image/image_sizing"
16
17
  require "dynamic_image/jobs"
@@ -20,5 +21,5 @@ require "dynamic_image/processed_image"
20
21
  require "dynamic_image/routing"
21
22
 
22
23
  module DynamicImage
23
- cattr_accessor :digest_verifier, :process_later_limit
24
+ cattr_accessor :digest_verifier
24
25
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dynamic_image
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.5
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Inge Jørgensen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-03 00:00:00.000000000 Z
11
+ date: 2021-05-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dis
@@ -19,7 +19,7 @@ dependencies:
19
19
  version: '1.1'
20
20
  - - ">="
21
21
  - !ruby/object:Gem::Version
22
- version: 1.0.6
22
+ version: 1.1.11
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,49 +29,35 @@ dependencies:
29
29
  version: '1.1'
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: 1.0.6
32
+ version: 1.1.11
33
33
  - !ruby/object:Gem::Dependency
34
- name: mini_exiftool
34
+ name: rails
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - "~>"
37
+ - - ">"
38
38
  - !ruby/object:Gem::Version
39
- version: '2.10'
39
+ version: '5.0'
40
40
  type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - "~>"
44
+ - - ">"
45
45
  - !ruby/object:Gem::Version
46
- version: '2.10'
46
+ version: '5.0'
47
47
  - !ruby/object:Gem::Dependency
48
- name: mini_magick
48
+ name: ruby-vips
49
49
  requirement: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '4.9'
53
+ version: 2.1.0
54
54
  type: :runtime
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '4.9'
61
- - !ruby/object:Gem::Dependency
62
- name: rails
63
- requirement: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - ">"
66
- - !ruby/object:Gem::Version
67
- version: '5.0'
68
- type: :runtime
69
- prerelease: false
70
- version_requirements: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - ">"
73
- - !ruby/object:Gem::Version
74
- version: '5.0'
60
+ version: 2.1.0
75
61
  - !ruby/object:Gem::Dependency
76
62
  name: vector2d
77
63
  requirement: !ruby/object:Gem::Requirement
@@ -167,7 +153,12 @@ files:
167
153
  - lib/dynamic_image/digest_verifier.rb
168
154
  - lib/dynamic_image/engine.rb
169
155
  - lib/dynamic_image/errors.rb
156
+ - lib/dynamic_image/format.rb
170
157
  - lib/dynamic_image/helper.rb
158
+ - lib/dynamic_image/image_processor.rb
159
+ - lib/dynamic_image/image_processor/colors.rb
160
+ - lib/dynamic_image/image_processor/frames.rb
161
+ - lib/dynamic_image/image_processor/transform.rb
171
162
  - lib/dynamic_image/image_reader.rb
172
163
  - lib/dynamic_image/image_sizing.rb
173
164
  - lib/dynamic_image/jobs.rb
@@ -179,7 +170,6 @@ files:
179
170
  - lib/dynamic_image/model/validations.rb
180
171
  - lib/dynamic_image/model/variants.rb
181
172
  - lib/dynamic_image/processed_image.rb
182
- - lib/dynamic_image/profiles/sRGB_ICC_v4_Appearance.icc
183
173
  - lib/dynamic_image/routing.rb
184
174
  - lib/dynamic_image/version.rb
185
175
  - lib/rails/generators/dynamic_image/resource/resource_generator.rb
@@ -202,7 +192,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
202
192
  - !ruby/object:Gem::Version
203
193
  version: '0'
204
194
  requirements: []
205
- rubygems_version: 3.4.1
195
+ rubygems_version: 3.2.3
206
196
  signing_key:
207
197
  specification_version: 4
208
198
  summary: Rails plugin that simplifies image uploading and processing