dynamic_image 2.1.1 → 3.0.1

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: 814628e7438a06292a2a283be0c88b724a1449327276c2f7ca89656947c690d7
4
- data.tar.gz: 8ff9d551e13362514d102cae1c04e61d3374689282715911448019c425c12487
3
+ metadata.gz: 54a38cb5cf9c2a33ac95554bfed28331edeb0a5eefdf3fcaedeca0d24f277dc3
4
+ data.tar.gz: fde8b74767223348ba07437f017cebee8f75aa1e9e4f7c1093c5aecd13c3dbe7
5
5
  SHA512:
6
- metadata.gz: c52354d7f7100b367b402ea9be16a522872d423e8f1a21f49482bc86eb6223ac9d0c1194979e1d6b49fcfac2352922460aa44395e37adef1ca8cac0c6009e2b6
7
- data.tar.gz: 94c370ea4a7ab9fe575ae324f8d50844c6600c5a677efcfe03471703130b4eea7613c5e3ac48d894481a1520484efdae2843ef90e36486f114009aa168681d1e
6
+ metadata.gz: 1cc69dbf8518edf8a6050684a4431baddd0bc6b16248ea6bd0d88a4fa6dd5c80537364aa243162e7c82a332095ac07dba6d044546e04fbebb1a15bc83f605b0d
7
+ data.tar.gz: f68f63c569e6bc8d27b76079c05faa16805bbac75a37224eadf1f9073fc03578a01643cbc4ffa74f91a187d6fab670ea7654a6f2b3c2b87acc72b3f2d83a3512
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.
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
@@ -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,37 +44,31 @@ module DynamicImage
44
44
  private
45
45
 
46
46
  def cache_expiration_header
47
- expires_in 30.days, 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
- rescue MiniMagick::Error
63
- process_later(image, options, requested_size)
64
- raise
65
64
  end
66
65
 
67
- def process_later(image, options, requested_size)
68
- DynamicImage::Jobs::CreateVariant
69
- .perform_later(image, options, requested_size.to_s)
70
- end
71
-
72
- def process_later?(processed_image, size)
73
- return false unless DynamicImage.process_later_limit
74
-
75
- image_size = processed_image.record.size.x * processed_image.record.size.y
76
- image_size > DynamicImage.process_later_limit &&
77
- !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")
78
72
  end
79
73
 
80
74
  def render_image(options)
@@ -82,7 +76,7 @@ module DynamicImage
82
76
 
83
77
  respond_to do |format|
84
78
  format.html do
85
- render(file: File.join(File.dirname(__FILE__), "templates/show"),
79
+ render(template: "dynamic_image/images/show",
86
80
  layout: false, locals: { options: options })
87
81
  end
88
82
  format.any(:gif, :jpeg, :jpg, :png, :tiff, :webp) do
@@ -91,7 +85,7 @@ module DynamicImage
91
85
  end
92
86
  end
93
87
 
94
- def render_raw_image(disposition: "inline", filename: nil)
88
+ def render_raw_image(disposition: "inline")
95
89
  return unless stale?(@record)
96
90
 
97
91
  respond_to do |format|
@@ -108,12 +102,6 @@ module DynamicImage
108
102
  params[:format]
109
103
  end
110
104
 
111
- def send_image(processed_image, requested_size)
112
- send_data(processed_image.cropped_and_resized(requested_size),
113
- content_type: processed_image.content_type,
114
- disposition: "inline")
115
- end
116
-
117
105
  def verify_signed_params
118
106
  key = %i[action id size].map do |k|
119
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,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
@@ -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
@@ -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.
@@ -62,13 +61,7 @@ module DynamicImage
62
61
  end
63
62
 
64
63
  def read_image
65
- image = reader.read
66
- image.auto_orient
67
- result = yield image
68
- image.destroy!
69
- result
70
- rescue MiniMagick::Invalid
71
- :invalid
64
+ yield reader.read.autorot
72
65
  end
73
66
 
74
67
  def reader
@@ -77,11 +70,15 @@ module DynamicImage
77
70
 
78
71
  def read_metadata
79
72
  read_image do |image|
80
- {
81
- colorspace: image[:colorspace],
82
- dimensions: image[:dimensions],
83
- format: image[:format]
84
- }
73
+ height = if image.get_fields.include?("page-height")
74
+ image.get("page-height")
75
+ else
76
+ image.get("height")
77
+ end
78
+
79
+ { width: image.get("width"),
80
+ height: height,
81
+ colorspace: image.get("interpretation") }
85
82
  end
86
83
  end
87
84
  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
 
@@ -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.1"
4
+ VERSION = "3.0.1"
5
5
  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.1
4
+ version: 3.0.1
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: 2021-01-04 00:00:00.000000000 Z
11
+ date: 2021-05-14 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
@@ -110,16 +96,16 @@ dependencies:
110
96
  name: rspec-rails
111
97
  requirement: !ruby/object:Gem::Requirement
112
98
  requirements:
113
- - - "~>"
99
+ - - ">="
114
100
  - !ruby/object:Gem::Version
115
- version: 3.7.0
101
+ version: '0'
116
102
  type: :development
117
103
  prerelease: false
118
104
  version_requirements: !ruby/object:Gem::Requirement
119
105
  requirements:
120
- - - "~>"
106
+ - - ">="
121
107
  - !ruby/object:Gem::Version
122
- version: 3.7.0
108
+ version: '0'
123
109
  - !ruby/object:Gem::Dependency
124
110
  name: simplecov
125
111
  requirement: !ruby/object:Gem::Requirement
@@ -159,6 +145,7 @@ files:
159
145
  - README.md
160
146
  - Rakefile
161
147
  - app/models/dynamic_image/variant.rb
148
+ - app/views/dynamic_image/images/show.html.erb
162
149
  - db/migrate/20190620160500_create_dynamic_image_variants.rb
163
150
  - lib/dynamic_image.rb
164
151
  - lib/dynamic_image/belongs_to.rb
@@ -166,7 +153,12 @@ files:
166
153
  - lib/dynamic_image/digest_verifier.rb
167
154
  - lib/dynamic_image/engine.rb
168
155
  - lib/dynamic_image/errors.rb
156
+ - lib/dynamic_image/format.rb
169
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
170
162
  - lib/dynamic_image/image_reader.rb
171
163
  - lib/dynamic_image/image_sizing.rb
172
164
  - lib/dynamic_image/jobs.rb
@@ -178,9 +170,7 @@ files:
178
170
  - lib/dynamic_image/model/validations.rb
179
171
  - lib/dynamic_image/model/variants.rb
180
172
  - lib/dynamic_image/processed_image.rb
181
- - lib/dynamic_image/profiles/sRGB_ICC_v4_Appearance.icc
182
173
  - lib/dynamic_image/routing.rb
183
- - lib/dynamic_image/templates/show.html.erb
184
174
  - lib/dynamic_image/version.rb
185
175
  - lib/rails/generators/dynamic_image/resource/resource_generator.rb
186
176
  homepage: https://github.com/elektronaut/dynamic_image
@@ -195,14 +185,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
195
185
  requirements:
196
186
  - - ">="
197
187
  - !ruby/object:Gem::Version
198
- version: 2.4.0
188
+ version: 2.7.0
199
189
  required_rubygems_version: !ruby/object:Gem::Requirement
200
190
  requirements:
201
191
  - - ">="
202
192
  - !ruby/object:Gem::Version
203
193
  version: '0'
204
194
  requirements: []
205
- rubygems_version: 3.1.4
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