dynamic_image 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +6 -7
- data/{lib/dynamic_image/templates → app/views/dynamic_image/images}/show.html.erb +0 -0
- data/lib/dynamic_image.rb +4 -3
- data/lib/dynamic_image/controller.rb +22 -23
- data/lib/dynamic_image/errors.rb +6 -0
- data/lib/dynamic_image/format.rb +127 -0
- data/lib/dynamic_image/image_processor.rb +89 -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_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/processed_image.rb +14 -84
- data/lib/dynamic_image/version.rb +1 -1
- metadata +24 -34
- 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
|
[](https://codeclimate.com/github/elektronaut/dynamic_image)
|
4
4
|
[](https://codeclimate.com/github/elektronaut/dynamic_image)
|
5
5
|
[](http://inch-ci.org/github/elektronaut/dynamic_image)
|
6
|
-
[](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.
|
File without changes
|
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
|
@@ -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,19 +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
|
-
|
57
|
+
def filename(format = nil)
|
58
|
+
if format.is_a?(DynamicImage::Format)
|
59
|
+
File.basename(@record.filename, ".*") + format.extension
|
60
|
+
else
|
61
|
+
filename(DynamicImage::Format.find(format) ||
|
62
|
+
DynamicImage::Format.content_type(@record.content_type))
|
63
|
+
end
|
64
|
+
end
|
56
65
|
|
57
|
-
|
58
|
-
|
59
|
-
|
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")
|
60
72
|
end
|
61
73
|
|
62
74
|
def render_image(options)
|
@@ -64,16 +76,16 @@ module DynamicImage
|
|
64
76
|
|
65
77
|
respond_to do |format|
|
66
78
|
format.html do
|
67
|
-
render(
|
79
|
+
render(template: "dynamic_image/images/show",
|
68
80
|
layout: false, locals: { options: options })
|
69
81
|
end
|
70
82
|
format.any(:gif, :jpeg, :jpg, :png, :tiff, :webp) do
|
71
|
-
|
83
|
+
process_and_send(@record, options)
|
72
84
|
end
|
73
85
|
end
|
74
86
|
end
|
75
87
|
|
76
|
-
def render_raw_image(disposition: "inline"
|
88
|
+
def render_raw_image(disposition: "inline")
|
77
89
|
return unless stale?(@record)
|
78
90
|
|
79
91
|
respond_to do |format|
|
@@ -90,19 +102,6 @@ module DynamicImage
|
|
90
102
|
params[:format]
|
91
103
|
end
|
92
104
|
|
93
|
-
def send_image(image, options)
|
94
|
-
processed_image = DynamicImage::ProcessedImage.new(image, options)
|
95
|
-
if process_later?(processed_image, requested_size)
|
96
|
-
DynamicImage::Jobs::CreateVariant
|
97
|
-
.perform_later(image, options, requested_size.to_s)
|
98
|
-
head 503, retry_after: 10
|
99
|
-
else
|
100
|
-
send_data(processed_image.cropped_and_resized(requested_size),
|
101
|
-
content_type: processed_image.content_type,
|
102
|
-
disposition: "inline")
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
105
|
def verify_signed_params
|
107
106
|
key = %i[action id size].map do |k|
|
108
107
|
k == :id ? params.require(k).to_i : params.require(k)
|
data/lib/dynamic_image/errors.rb
CHANGED
@@ -3,11 +3,17 @@
|
|
3
3
|
module DynamicImage
|
4
4
|
module Errors
|
5
5
|
class Error < StandardError; end
|
6
|
+
|
6
7
|
class InvalidImage < DynamicImage::Errors::Error; end
|
8
|
+
|
7
9
|
class InvalidHeader < DynamicImage::Errors::Error; end
|
10
|
+
|
8
11
|
class InvalidSignature < DynamicImage::Errors::Error; end
|
12
|
+
|
9
13
|
class InvalidSizeOptions < DynamicImage::Errors::Error; end
|
14
|
+
|
10
15
|
class InvalidTransformation < DynamicImage::Errors::Error; end
|
16
|
+
|
11
17
|
class ParameterMissing < DynamicImage::Errors::Error; end
|
12
18
|
end
|
13
19
|
end
|
@@ -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
|
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
|
|
@@ -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
|
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
|
@@ -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:
|
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:
|
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.
|
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.
|
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
|