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 +4 -4
- data/README.md +6 -7
- data/lib/dynamic_image/controller.rb +17 -26
- data/lib/dynamic_image/format.rb +127 -0
- data/lib/dynamic_image/image_processor/colors.rb +34 -0
- data/lib/dynamic_image/image_processor/frames.rb +43 -0
- data/lib/dynamic_image/image_processor/transform.rb +50 -0
- data/lib/dynamic_image/image_processor.rb +89 -0
- data/lib/dynamic_image/image_reader.rb +24 -29
- data/lib/dynamic_image/image_sizing.rb +0 -13
- data/lib/dynamic_image/metadata.rb +21 -22
- data/lib/dynamic_image/model/transformations.rb +4 -10
- data/lib/dynamic_image/model/validations.rb +2 -10
- data/lib/dynamic_image/model.rb +1 -2
- data/lib/dynamic_image/processed_image.rb +14 -84
- data/lib/dynamic_image/version.rb +1 -1
- data/lib/dynamic_image.rb +4 -3
- metadata +18 -28
- data/lib/dynamic_image/profiles/sRGB_ICC_v4_Appearance.icc +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d9d7c5e9f872a132439d46c725089651caee0b684a52b425c2e572026e733bd0
|
4
|
+
data.tar.gz: 787251fd6c8d096cb1a918bf8667b482d92a59aec19dd3b8420a2e22b3dcce69
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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/
|
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 [
|
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.
|
33
|
-
*
|
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", "~>
|
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"
|
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
|
-
|
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
|
55
|
-
|
56
|
-
|
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
|
-
|
61
|
+
filename(DynamicImage::Format.find(format) ||
|
62
|
+
DynamicImage::Format.content_type(@record.content_type))
|
61
63
|
end
|
62
64
|
end
|
63
65
|
|
64
|
-
def
|
65
|
-
DynamicImage::
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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"
|
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
|
25
|
-
|
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
|
-
|
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
|
-
|
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 ||=
|
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
|
52
|
-
|
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
|
-
|
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(
|
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
|
-
|
44
|
+
metadata[:width] if valid?
|
41
45
|
end
|
42
46
|
|
43
47
|
# Returns the height of the image.
|
44
48
|
def height
|
45
|
-
|
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.
|
67
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
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
|
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 =
|
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
|
-
|
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
|
|
data/lib/dynamic_image/model.rb
CHANGED
@@ -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.
|
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
|
-
@
|
15
|
-
@
|
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
|
-
|
89
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
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:
|
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:
|
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.
|
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.
|
32
|
+
version: 1.1.11
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
|
-
name:
|
34
|
+
name: rails
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
36
36
|
requirements:
|
37
|
-
- - "
|
37
|
+
- - ">"
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version: '
|
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: '
|
46
|
+
version: '5.0'
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
|
-
name:
|
48
|
+
name: ruby-vips
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
50
50
|
requirements:
|
51
51
|
- - "~>"
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version:
|
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:
|
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.
|
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
|
Binary file
|