image_vise 0.1.6 → 0.2.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/.gitignore +8 -0
- data/.travis.yml +13 -0
- data/DEVELOPMENT.md +0 -11
- data/Gemfile +2 -20
- data/Rakefile +3 -26
- data/image_vise.gemspec +37 -132
- data/lib/image_vise/file_response.rb +2 -2
- data/lib/image_vise/image_request.rb +2 -0
- data/lib/image_vise/operators/background_fill.rb +18 -0
- data/lib/image_vise/operators/ellipse_stencil.rb +7 -5
- data/lib/image_vise/operators/force_jpg_out.rb +17 -0
- data/lib/image_vise/pipeline.rb +13 -3
- data/lib/image_vise/render_engine.rb +36 -64
- data/lib/image_vise/version.rb +3 -0
- data/lib/image_vise/writers/auto_writer.rb +23 -0
- data/lib/image_vise/writers/jpeg_writer.rb +9 -0
- data/lib/image_vise.rb +19 -19
- metadata +43 -135
- data/spec/image_vise/auto_orient_spec.rb +0 -10
- data/spec/image_vise/crop_spec.rb +0 -20
- data/spec/image_vise/ellipse_stencil_spec.rb +0 -19
- data/spec/image_vise/fetcher_file_spec.rb +0 -48
- data/spec/image_vise/fetcher_http_spec.rb +0 -44
- data/spec/image_vise/file_response_spec.rb +0 -45
- data/spec/image_vise/fit_crop_spec.rb +0 -20
- data/spec/image_vise/geom_spec.rb +0 -33
- data/spec/image_vise/image_request_spec.rb +0 -62
- data/spec/image_vise/pipeline_spec.rb +0 -72
- data/spec/image_vise/render_engine_spec.rb +0 -325
- data/spec/image_vise/sharpen_spec.rb +0 -17
- data/spec/image_vise/srgb_spec.rb +0 -28
- data/spec/image_vise/strip_metadata_spec.rb +0 -14
- data/spec/image_vise_spec.rb +0 -110
- data/spec/layers-with-blending.psd +0 -0
- data/spec/spec_helper.rb +0 -103
- data/spec/test_server.rb +0 -61
- data/spec/waterside_magic_hour.jpg +0 -0
- data/spec/waterside_magic_hour.psd +0 -0
- data/spec/waterside_magic_hour_adobergb.jpg +0 -0
- data/spec/waterside_magic_hour_gray.tif +0 -0
- data/spec/waterside_magic_hour_transp.png +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fe3aff4ba407443d636d9b23b4d7a2a55d74fc8d
|
4
|
+
data.tar.gz: 34a54619e0a29cb45594975dcf8b74b43742bb49
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8db53ad9d51d8a367cc316472a54fcb9eaa54e6d0c666fbe16de26ad00c336a8796c390bc343677fee635cf455352414bee43782899a162bb30089d79bc82f72
|
7
|
+
data.tar.gz: 22bc8553e556428e459dc794c123a001dce16f4df527336ecaec8cafa7f9c8f6fd91cad80d20414c0bbc2e03afb28e4c08a507decfcf2d9d4c22348cd91d863d
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/DEVELOPMENT.md
CHANGED
@@ -109,14 +109,3 @@ actually using them. If you are using a library it is a one-time cost, with very
|
|
109
109
|
Also note that image operators are not per definition Imagemagick-specific - it's completely possible to not only use
|
110
110
|
a different library for processing them, but even to use a different image processing server complying to the
|
111
111
|
same protocol (a signed JSON-encodded waybill of HTTP(S) source-URL + pipeline instructions).
|
112
|
-
|
113
|
-
## Using forked child processes for RMagick tasks
|
114
|
-
|
115
|
-
You can optionally set the `IMAGE_VISE_ENABLE_FORK` environment variable to `yes` to enable forking. When this
|
116
|
-
variable is set, ImageVise will fork a child process and perform the image processing task within that process,
|
117
|
-
killing it afterwards and deallocating all the memory. This can be extremely efficient for dealing with potential
|
118
|
-
memory bloat issues in ImageMagick/RMagick. However, loading images into RMagick may hang in a forked child. This
|
119
|
-
will lead to the child being timeout-terminated, and no image is going to be rendered. This issue is known and
|
120
|
-
also platform-dependent (it does not happen on OSX but does happen on Docker within Ubuntu Trusty for instance).
|
121
|
-
|
122
|
-
So, this feature _does_ exist but your mileage may vary with regards to it's use.
|
data/Gemfile
CHANGED
@@ -1,22 +1,4 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
gem 'exceptional_fork', '~> 1.2'
|
6
|
-
gem 'ks'
|
7
|
-
gem 'magic_bytes'
|
8
|
-
|
9
|
-
group :development do
|
10
|
-
gem 'bundler'
|
11
|
-
gem 'yard'
|
12
|
-
gem 'simplecov'
|
13
|
-
gem 'rack-cache'
|
14
|
-
gem 'strenv'
|
15
|
-
gem 'addressable', :require => %w( addressable/uri )
|
16
|
-
gem 'rack', '~> 1'
|
17
|
-
gem 'rack-test'
|
18
|
-
gem 'foreman'
|
19
|
-
gem 'rspec', '~> 3.2', '< 3.3'
|
20
|
-
gem 'rake', '~> 10'
|
21
|
-
gem "jeweler"
|
22
|
-
end
|
3
|
+
# Specify your gem's dependencies in image_vise.gemspec
|
4
|
+
gemspec
|
data/Rakefile
CHANGED
@@ -1,29 +1,6 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require_relative 'lib/image_vise'
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rspec/core/rake_task"
|
4
3
|
|
5
|
-
|
6
|
-
gem.version = ImageVise::VERSION
|
7
|
-
gem.name = "image_vise"
|
8
|
-
gem.summary = "Runtime thumbnailing proxy"
|
9
|
-
gem.description = "Image processing via URLs"
|
10
|
-
gem.email = "me@julik.nl"
|
11
|
-
gem.homepage = "https://github.com/WeTransfer/image_vise"
|
12
|
-
gem.authors = ["Julik Tarkhanov"]
|
13
|
-
gem.license = 'MIT'
|
4
|
+
RSpec::Core::RakeTask.new(:spec)
|
14
5
|
|
15
|
-
# Do not package invisibles
|
16
|
-
gem.files.exclude ".*"
|
17
|
-
|
18
|
-
# When running as a gem, do not lock all of our versions
|
19
|
-
# even though the lockfile is in the repo for running standalone
|
20
|
-
gem.files.exclude "Gemfile.lock"
|
21
|
-
end
|
22
|
-
|
23
|
-
Jeweler::RubygemsDotOrgTasks.new
|
24
|
-
|
25
|
-
RSpec::Core::RakeTask.new(:spec) do |t|
|
26
|
-
t.rspec_opts = ["-c", "-f progress", "-r ./spec/spec_helper.rb"]
|
27
|
-
t.pattern = 'spec/**/*_spec.rb'
|
28
|
-
end
|
29
6
|
task :default => :spec
|
data/image_vise.gemspec
CHANGED
@@ -1,138 +1,43 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
# stub: image_vise 0.1.6 ruby lib
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'image_vise/version'
|
6
5
|
|
7
|
-
Gem::Specification.new do |
|
8
|
-
|
9
|
-
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "image_vise"
|
8
|
+
spec.version = ImageVise::VERSION
|
9
|
+
spec.authors = ["Julik Tarkhanov"]
|
10
|
+
spec.email = ["me@julik.nl"]
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
s.description = "Image processing via URLs"
|
16
|
-
s.email = "me@julik.nl"
|
17
|
-
s.extra_rdoc_files = [
|
18
|
-
"LICENSE.txt",
|
19
|
-
"README.md"
|
20
|
-
]
|
21
|
-
s.files = [
|
22
|
-
"DEVELOPMENT.md",
|
23
|
-
"Gemfile",
|
24
|
-
"LICENSE.txt",
|
25
|
-
"README.md",
|
26
|
-
"Rakefile",
|
27
|
-
"SECURITY.md",
|
28
|
-
"examples/config.ru",
|
29
|
-
"examples/custom_image_operator.rb",
|
30
|
-
"examples/error_handline_appsignal.rb",
|
31
|
-
"examples/error_handling_sentry.rb",
|
32
|
-
"image_vise.gemspec",
|
33
|
-
"lib/image_vise.rb",
|
34
|
-
"lib/image_vise/fetchers/fetcher_file.rb",
|
35
|
-
"lib/image_vise/fetchers/fetcher_http.rb",
|
36
|
-
"lib/image_vise/file_response.rb",
|
37
|
-
"lib/image_vise/image_request.rb",
|
38
|
-
"lib/image_vise/operators/auto_orient.rb",
|
39
|
-
"lib/image_vise/operators/crop.rb",
|
40
|
-
"lib/image_vise/operators/ellipse_stencil.rb",
|
41
|
-
"lib/image_vise/operators/fit_crop.rb",
|
42
|
-
"lib/image_vise/operators/geom.rb",
|
43
|
-
"lib/image_vise/operators/sRGB_v4_ICC_preference_displayclass.icc",
|
44
|
-
"lib/image_vise/operators/sharpen.rb",
|
45
|
-
"lib/image_vise/operators/srgb.rb",
|
46
|
-
"lib/image_vise/operators/strip_metadata.rb",
|
47
|
-
"lib/image_vise/pipeline.rb",
|
48
|
-
"lib/image_vise/render_engine.rb",
|
49
|
-
"spec/image_vise/auto_orient_spec.rb",
|
50
|
-
"spec/image_vise/crop_spec.rb",
|
51
|
-
"spec/image_vise/ellipse_stencil_spec.rb",
|
52
|
-
"spec/image_vise/fetcher_file_spec.rb",
|
53
|
-
"spec/image_vise/fetcher_http_spec.rb",
|
54
|
-
"spec/image_vise/file_response_spec.rb",
|
55
|
-
"spec/image_vise/fit_crop_spec.rb",
|
56
|
-
"spec/image_vise/geom_spec.rb",
|
57
|
-
"spec/image_vise/image_request_spec.rb",
|
58
|
-
"spec/image_vise/pipeline_spec.rb",
|
59
|
-
"spec/image_vise/render_engine_spec.rb",
|
60
|
-
"spec/image_vise/sharpen_spec.rb",
|
61
|
-
"spec/image_vise/srgb_spec.rb",
|
62
|
-
"spec/image_vise/strip_metadata_spec.rb",
|
63
|
-
"spec/image_vise_spec.rb",
|
64
|
-
"spec/layers-with-blending.psd",
|
65
|
-
"spec/spec_helper.rb",
|
66
|
-
"spec/test_server.rb",
|
67
|
-
"spec/waterside_magic_hour.jpg",
|
68
|
-
"spec/waterside_magic_hour.psd",
|
69
|
-
"spec/waterside_magic_hour_adobergb.jpg",
|
70
|
-
"spec/waterside_magic_hour_gray.tif",
|
71
|
-
"spec/waterside_magic_hour_transp.png"
|
72
|
-
]
|
73
|
-
s.homepage = "https://github.com/WeTransfer/image_vise"
|
74
|
-
s.licenses = ["MIT"]
|
75
|
-
s.rubygems_version = "2.4.5.1"
|
76
|
-
s.summary = "Runtime thumbnailing proxy"
|
12
|
+
spec.summary = "Runtime thumbnailing proxy"
|
13
|
+
spec.description = "Image processing via URLs"
|
14
|
+
spec.homepage = "https://github.com/WeTransfer/image_vise"
|
15
|
+
spec.license = "MIT"
|
77
16
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
s.add_runtime_dependency(%q<patron>, ["~> 0.6"])
|
83
|
-
s.add_runtime_dependency(%q<rmagick>, ["~> 2.15"])
|
84
|
-
s.add_runtime_dependency(%q<exceptional_fork>, ["~> 1.2"])
|
85
|
-
s.add_runtime_dependency(%q<ks>, [">= 0"])
|
86
|
-
s.add_runtime_dependency(%q<magic_bytes>, [">= 0"])
|
87
|
-
s.add_development_dependency(%q<bundler>, [">= 0"])
|
88
|
-
s.add_development_dependency(%q<yard>, [">= 0"])
|
89
|
-
s.add_development_dependency(%q<simplecov>, [">= 0"])
|
90
|
-
s.add_development_dependency(%q<rack-cache>, [">= 0"])
|
91
|
-
s.add_development_dependency(%q<strenv>, [">= 0"])
|
92
|
-
s.add_development_dependency(%q<addressable>, [">= 0"])
|
93
|
-
s.add_development_dependency(%q<rack>, ["~> 1"])
|
94
|
-
s.add_development_dependency(%q<rack-test>, [">= 0"])
|
95
|
-
s.add_development_dependency(%q<foreman>, [">= 0"])
|
96
|
-
s.add_development_dependency(%q<rspec>, ["< 3.3", "~> 3.2"])
|
97
|
-
s.add_development_dependency(%q<rake>, ["~> 10"])
|
98
|
-
s.add_development_dependency(%q<jeweler>, [">= 0"])
|
99
|
-
else
|
100
|
-
s.add_dependency(%q<patron>, ["~> 0.6"])
|
101
|
-
s.add_dependency(%q<rmagick>, ["~> 2.15"])
|
102
|
-
s.add_dependency(%q<exceptional_fork>, ["~> 1.2"])
|
103
|
-
s.add_dependency(%q<ks>, [">= 0"])
|
104
|
-
s.add_dependency(%q<magic_bytes>, [">= 0"])
|
105
|
-
s.add_dependency(%q<bundler>, [">= 0"])
|
106
|
-
s.add_dependency(%q<yard>, [">= 0"])
|
107
|
-
s.add_dependency(%q<simplecov>, [">= 0"])
|
108
|
-
s.add_dependency(%q<rack-cache>, [">= 0"])
|
109
|
-
s.add_dependency(%q<strenv>, [">= 0"])
|
110
|
-
s.add_dependency(%q<addressable>, [">= 0"])
|
111
|
-
s.add_dependency(%q<rack>, ["~> 1"])
|
112
|
-
s.add_dependency(%q<rack-test>, [">= 0"])
|
113
|
-
s.add_dependency(%q<foreman>, [">= 0"])
|
114
|
-
s.add_dependency(%q<rspec>, ["< 3.3", "~> 3.2"])
|
115
|
-
s.add_dependency(%q<rake>, ["~> 10"])
|
116
|
-
s.add_dependency(%q<jeweler>, [">= 0"])
|
117
|
-
end
|
17
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
18
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata['allowed_push_host'] = "https://rubygems.org"
|
118
21
|
else
|
119
|
-
|
120
|
-
s.add_dependency(%q<rmagick>, ["~> 2.15"])
|
121
|
-
s.add_dependency(%q<exceptional_fork>, ["~> 1.2"])
|
122
|
-
s.add_dependency(%q<ks>, [">= 0"])
|
123
|
-
s.add_dependency(%q<magic_bytes>, [">= 0"])
|
124
|
-
s.add_dependency(%q<bundler>, [">= 0"])
|
125
|
-
s.add_dependency(%q<yard>, [">= 0"])
|
126
|
-
s.add_dependency(%q<simplecov>, [">= 0"])
|
127
|
-
s.add_dependency(%q<rack-cache>, [">= 0"])
|
128
|
-
s.add_dependency(%q<strenv>, [">= 0"])
|
129
|
-
s.add_dependency(%q<addressable>, [">= 0"])
|
130
|
-
s.add_dependency(%q<rack>, ["~> 1"])
|
131
|
-
s.add_dependency(%q<rack-test>, [">= 0"])
|
132
|
-
s.add_dependency(%q<foreman>, [">= 0"])
|
133
|
-
s.add_dependency(%q<rspec>, ["< 3.3", "~> 3.2"])
|
134
|
-
s.add_dependency(%q<rake>, ["~> 10"])
|
135
|
-
s.add_dependency(%q<jeweler>, [">= 0"])
|
22
|
+
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
136
23
|
end
|
137
|
-
end
|
138
24
|
|
25
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
spec.add_dependency 'patron', '~> 0.6'
|
31
|
+
spec.add_dependency 'rmagick', '~> 2.15'
|
32
|
+
spec.add_dependency 'ks'
|
33
|
+
spec.add_dependency 'magic_bytes', '~> 1'
|
34
|
+
spec.add_dependency 'rack', '~> 1'
|
35
|
+
|
36
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
37
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
38
|
+
spec.add_development_dependency "rack-test"
|
39
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
40
|
+
spec.add_development_dependency "addressable"
|
41
|
+
spec.add_development_dependency "strenv"
|
42
|
+
spec.add_development_dependency "simplecov"
|
43
|
+
end
|
@@ -5,7 +5,7 @@ class ImageVise::FileResponse
|
|
5
5
|
def initialize(file)
|
6
6
|
@file = file
|
7
7
|
end
|
8
|
-
|
8
|
+
|
9
9
|
def each
|
10
10
|
@file.flush # Make sure all the writes have been synchronized
|
11
11
|
# We can easily open another file descriptor
|
@@ -15,7 +15,7 @@ class ImageVise::FileResponse
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
18
|
-
|
18
|
+
|
19
19
|
def close
|
20
20
|
ImageVise.close_and_unlink(@file)
|
21
21
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Applies a background fill color.
|
2
|
+
# Can handle most 'word' colors and hex color codes but not RGB values.
|
3
|
+
#
|
4
|
+
# The corresponding Pipeline method is `background_fill`.
|
5
|
+
class ImageVise::BackgroundFill < Ks.strict(:color)
|
6
|
+
def initialize(*)
|
7
|
+
super
|
8
|
+
self.color = color.to_s
|
9
|
+
raise ArgumentError, "the :color parameter must be present and not empty" if self.color.empty?
|
10
|
+
end
|
11
|
+
|
12
|
+
def apply!(image)
|
13
|
+
image.border!(0, 0, color)
|
14
|
+
image.alpha(Magick::DeactivateAlphaChannel)
|
15
|
+
end
|
16
|
+
|
17
|
+
ImageVise.add_operator 'background_fill', self
|
18
|
+
end
|
@@ -9,7 +9,8 @@
|
|
9
9
|
# The corresponding Pipeline method is `ellipse_stencil`.
|
10
10
|
class ImageVise::EllipseStencil
|
11
11
|
C_black = 'black'.freeze
|
12
|
-
|
12
|
+
C_white = 'white'.freeze
|
13
|
+
private_constant :C_white, :C_black
|
13
14
|
|
14
15
|
def apply!(magick_image)
|
15
16
|
width, height = magick_image.columns, magick_image.rows
|
@@ -25,9 +26,8 @@ class ImageVise::EllipseStencil
|
|
25
26
|
# must be taken not to overmult or overdivide.
|
26
27
|
#
|
27
28
|
# To begin,generate a black and white image for the stencil
|
28
|
-
|
29
|
-
draw_circle(
|
30
|
-
mask = circle_img.negate
|
29
|
+
mask = Magick::Image.new(width, height)
|
30
|
+
draw_circle(mask, width, height)
|
31
31
|
|
32
32
|
# At this stage the mask contains a B/W image of the circle, black outside, white inside.
|
33
33
|
# Retain the alpha of the original in a separate image
|
@@ -44,7 +44,7 @@ class ImageVise::EllipseStencil
|
|
44
44
|
# And perform the operation (set gray(RGB) of mask as the A of magick_image)
|
45
45
|
magick_image.composite!(mask, Magick::CenterGravity, Magick::CopyOpacityCompositeOp)
|
46
46
|
ensure
|
47
|
-
[mask, only_alpha
|
47
|
+
[mask, only_alpha].each do |maybe_image|
|
48
48
|
ImageVise.destroy(maybe_image)
|
49
49
|
end
|
50
50
|
end
|
@@ -58,6 +58,8 @@ class ImageVise::EllipseStencil
|
|
58
58
|
|
59
59
|
gc = Magick::Draw.new
|
60
60
|
gc.fill C_black
|
61
|
+
gc.rectangle(0, 0, width, height)
|
62
|
+
gc.fill C_white
|
61
63
|
gc.ellipse(center_x, center_y, radius_width, radius_height, deg_start=0, deg_end=360)
|
62
64
|
gc.draw(into_image)
|
63
65
|
ensure
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# Forces the output format to be JPEG and specifies the quality factor to use when saving
|
2
|
+
#
|
3
|
+
# The corresponding Pipeline method is `force_jpg_out`.
|
4
|
+
class ImageVise::ForceJPGOut < Ks.strict(:quality)
|
5
|
+
def initialize(quality:)
|
6
|
+
unless (0..100).cover?(quality)
|
7
|
+
raise ArgumentError, "the :quality setting must be within 0..100, but was %d" % quality
|
8
|
+
end
|
9
|
+
self.quality = quality
|
10
|
+
end
|
11
|
+
|
12
|
+
def apply!(_, metadata)
|
13
|
+
metadata[:writer] = ImageVise::JPGWriter.new(quality: quality)
|
14
|
+
end
|
15
|
+
|
16
|
+
ImageVise.add_operator 'force_jpg_out', self
|
17
|
+
end
|
data/lib/image_vise/pipeline.rb
CHANGED
@@ -44,10 +44,20 @@ class ImageVise::Pipeline
|
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
|
-
def apply!(magick_image)
|
48
|
-
@ops.each
|
47
|
+
def apply!(magick_image, image_metadata)
|
48
|
+
@ops.each do |operator|
|
49
|
+
apply_operator_passing_metadata(magick_image, operator, image_metadata)
|
50
|
+
end
|
49
51
|
end
|
50
|
-
|
52
|
+
|
53
|
+
def apply_operator_passing_metadata(magick_image, operator, image_metadata)
|
54
|
+
if operator.method(:apply!).arity == 1
|
55
|
+
operator.apply!(magick_image)
|
56
|
+
else
|
57
|
+
operator.apply!(magick_image, image_metadata)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
51
61
|
def each(&b)
|
52
62
|
@ops.each(&b)
|
53
63
|
end
|
@@ -1,11 +1,11 @@
|
|
1
|
-
class ImageVise::RenderEngine
|
1
|
+
class ImageVise::RenderEngine
|
2
2
|
class UnsupportedInputFormat < StandardError; end
|
3
3
|
class EmptyRender < StandardError; end
|
4
4
|
|
5
5
|
DEFAULT_HEADERS = {
|
6
6
|
'Allow' => "GET"
|
7
7
|
}.freeze
|
8
|
-
|
8
|
+
|
9
9
|
# Headers for error responses that denote an invalid or
|
10
10
|
# an unsatisfiable request
|
11
11
|
JSON_ERROR_HEADERS_REQUEST = DEFAULT_HEADERS.merge({
|
@@ -19,7 +19,7 @@ class ImageVise::RenderEngine
|
|
19
19
|
'Content-Type' => 'application/json',
|
20
20
|
'Cache-Control' => 'public, max-age=5'
|
21
21
|
}).freeze
|
22
|
-
|
22
|
+
|
23
23
|
# "public" of course. Add max-age so that there is _some_
|
24
24
|
# revalidation after a time (otherwise some proxies treat it
|
25
25
|
# as "must-revalidate" always), and "no-transform" so that
|
@@ -27,7 +27,7 @@ class ImageVise::RenderEngine
|
|
27
27
|
# with Rack::Cache and leads Chrome to throw up on content
|
28
28
|
# decoding for example).
|
29
29
|
IMAGE_CACHE_CONTROL = 'public, no-transform, max-age=2592000'
|
30
|
-
|
30
|
+
|
31
31
|
# How long is a render (the ImageMagick/write part) is allowed to
|
32
32
|
# take before we kill it
|
33
33
|
RENDER_TIMEOUT_SECONDS = 10
|
@@ -35,16 +35,9 @@ class ImageVise::RenderEngine
|
|
35
35
|
# Which input files we permit (based on extensions stored in MagicBytes)
|
36
36
|
PERMITTED_SOURCE_FILE_EXTENSIONS = %w( gif png jpg psd tif)
|
37
37
|
|
38
|
-
# Which output files are permitted (regardless of the input format
|
39
|
-
# the processed images will be converted to one of these types)
|
40
|
-
PERMITTED_OUTPUT_FILE_EXTENSIONS = %W( gif png jpg)
|
41
|
-
|
42
38
|
# How long should we wait when fetching the image from the external host
|
43
39
|
EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS = 4
|
44
|
-
|
45
|
-
# The default file type for images with alpha
|
46
|
-
PNG_FILE_TYPE = MagicBytes::FileType.new('png','image/png').freeze
|
47
|
-
|
40
|
+
|
48
41
|
def bail(status, *errors_array)
|
49
42
|
headers = if (300...500).cover?(status)
|
50
43
|
JSON_ERROR_HEADERS_REQUEST.dup
|
@@ -54,7 +47,7 @@ class ImageVise::RenderEngine
|
|
54
47
|
response = [status.to_i, headers, [JSON.pretty_generate({errors: errors_array})]]
|
55
48
|
throw :__bail, response
|
56
49
|
end
|
57
|
-
|
50
|
+
|
58
51
|
# The main entry point for the Rack app. Wraps a call to {#handle_request} in a `catch{}` block
|
59
52
|
# so that any method can abort the request by calling {#bail}
|
60
53
|
#
|
@@ -63,7 +56,7 @@ class ImageVise::RenderEngine
|
|
63
56
|
def call(env)
|
64
57
|
catch(:__bail) { handle_request(env) }
|
65
58
|
end
|
66
|
-
|
59
|
+
|
67
60
|
# Hadles the Rack request. If one of the steps calls {#bail} the `:__bail` symbol will be
|
68
61
|
# thrown and the execution will abort. Any errors will cause either an error response in
|
69
62
|
# JSON format or an Exception will be raised (depending on the return value of `raise_exceptions?`)
|
@@ -80,9 +73,9 @@ class ImageVise::RenderEngine
|
|
80
73
|
req = parse_env_into_request(env)
|
81
74
|
bail(405, 'Only GET supported') unless req.get?
|
82
75
|
params = extract_params_from_request(req)
|
83
|
-
|
76
|
+
|
84
77
|
image_request = ImageVise::ImageRequest.from_params(qs_params: params, secrets: ImageVise.secret_keys)
|
85
|
-
render_destination_file, render_file_type, etag = process_image_request(image_request)
|
78
|
+
render_destination_file, render_file_type, etag = process_image_request(image_request)
|
86
79
|
image_rack_response(render_destination_file, render_file_type, etag)
|
87
80
|
rescue *permanent_failures => e
|
88
81
|
handle_request_error(e)
|
@@ -97,7 +90,7 @@ class ImageVise::RenderEngine
|
|
97
90
|
raise_exception_or_error_response(e, 500)
|
98
91
|
end
|
99
92
|
end
|
100
|
-
|
93
|
+
|
101
94
|
# Parses the Rack environment into a Rack::Reqest. The following methods
|
102
95
|
# are going to be called on it: `#get?` and `#params`. You can use this
|
103
96
|
# method to override path-to-parameter translation for example.
|
@@ -115,7 +108,7 @@ class ImageVise::RenderEngine
|
|
115
108
|
def extract_params_from_request(rack_request)
|
116
109
|
# Prevent cache bypass DOS attacks by only permitting :sig and :q
|
117
110
|
bail(400, 'Query strings are not supported') if rack_request.params.any?
|
118
|
-
|
111
|
+
|
119
112
|
# Extract the tail (signature) and the front (the Base64-encoded request).
|
120
113
|
# Slashes within :q are masked by ImageRequest already, so we don't have
|
121
114
|
# to worry about them.
|
@@ -143,11 +136,11 @@ class ImageVise::RenderEngine
|
|
143
136
|
# Compute an ETag which describes this image transform + image source location.
|
144
137
|
# Assume the image URL contents does _never_ change.
|
145
138
|
etag = image_request.cache_etag
|
146
|
-
|
139
|
+
|
147
140
|
# Download/copy the original into a Tempfile
|
148
141
|
fetcher = ImageVise.fetcher_for(source_image_uri.scheme)
|
149
142
|
source_file = fetcher.fetch_uri_to_tempfile(source_image_uri)
|
150
|
-
|
143
|
+
|
151
144
|
# Make sure we do not try to process something...questionable
|
152
145
|
source_file_type = detect_file_type(source_file)
|
153
146
|
unless source_file_type_permitted?(source_file_type)
|
@@ -156,15 +149,8 @@ class ImageVise::RenderEngine
|
|
156
149
|
|
157
150
|
render_destination_file = Tempfile.new('imagevise-render').tap{|f| f.binmode }
|
158
151
|
|
159
|
-
#
|
160
|
-
|
161
|
-
require 'exceptional_fork'
|
162
|
-
ExceptionalFork.fork_and_wait do
|
163
|
-
apply_pipeline(source_file.path, pipeline, source_file_type, render_destination_file.path)
|
164
|
-
end
|
165
|
-
else
|
166
|
-
apply_pipeline(source_file.path, pipeline, source_file_type, render_destination_file.path)
|
167
|
-
end
|
152
|
+
# Do the actual imaging stuff
|
153
|
+
apply_pipeline(source_file.path, pipeline, source_file_type, render_destination_file.path)
|
168
154
|
|
169
155
|
# Catch this one early
|
170
156
|
render_destination_file.rewind
|
@@ -175,7 +161,7 @@ class ImageVise::RenderEngine
|
|
175
161
|
ensure
|
176
162
|
ImageVise.close_and_unlink(source_file)
|
177
163
|
end
|
178
|
-
|
164
|
+
|
179
165
|
# Returns a Rack response triplet. Accepts the return value of
|
180
166
|
# `process_image_request` unsplatted, and returns a triplet that
|
181
167
|
# can be returned as a Rack response. The Rack response will contain
|
@@ -204,13 +190,13 @@ class ImageVise::RenderEngine
|
|
204
190
|
# @param exception[Exception] the error that has to be captured
|
205
191
|
# @param status_code[Fixnum] the HTTP status code
|
206
192
|
def raise_exception_or_error_response(exception, status_code)
|
207
|
-
if raise_exceptions?
|
193
|
+
if raise_exceptions?
|
208
194
|
raise exception
|
209
195
|
else
|
210
196
|
bail status_code, exception.message
|
211
197
|
end
|
212
198
|
end
|
213
|
-
|
199
|
+
|
214
200
|
# Detects the file type of the given File and returns
|
215
201
|
# a MagicBytes::FileType object that contains the extension and
|
216
202
|
# the MIME type.
|
@@ -230,15 +216,6 @@ class ImageVise::RenderEngine
|
|
230
216
|
PERMITTED_SOURCE_FILE_EXTENSIONS.include?(magic_bytes_file_info.ext)
|
231
217
|
end
|
232
218
|
|
233
|
-
# Tells whether the given file type may be returned
|
234
|
-
# as the result of the render
|
235
|
-
#
|
236
|
-
# @param magic_bytes_file_info[MagicBytes::FileType] the filetype
|
237
|
-
# @return [Boolean]
|
238
|
-
def output_file_type_permitted?(magic_bytes_file_info)
|
239
|
-
PERMITTED_OUTPUT_FILE_EXTENSIONS.include?(magic_bytes_file_info.ext)
|
240
|
-
end
|
241
|
-
|
242
219
|
# Lists exceptions that should lead to the request being flagged
|
243
220
|
# as invalid (4xx as opposed to 5xx for a generic server error).
|
244
221
|
# Decent clients should _not_ retry those requests.
|
@@ -249,7 +226,7 @@ class ImageVise::RenderEngine
|
|
249
226
|
ImageVise::ImageRequest::InvalidRequest
|
250
227
|
]
|
251
228
|
end
|
252
|
-
|
229
|
+
|
253
230
|
# Is meant to be overridden by subclasses,
|
254
231
|
# will be called at the start of each request to set up the error handling
|
255
232
|
# library (Appsignal, Honeybadger, Sentry...)
|
@@ -278,7 +255,7 @@ class ImageVise::RenderEngine
|
|
278
255
|
# @return [void]
|
279
256
|
def handle_generic_error(exception)
|
280
257
|
end
|
281
|
-
|
258
|
+
|
282
259
|
# Tells whether the engine must raise the exceptions further up the Rack stack,
|
283
260
|
# or they should be suppressed and a JSON response must be returned.
|
284
261
|
#
|
@@ -286,14 +263,7 @@ class ImageVise::RenderEngine
|
|
286
263
|
def raise_exceptions?
|
287
264
|
false
|
288
265
|
end
|
289
|
-
|
290
|
-
# Tells whether image processing in a forked subproces should be turned on
|
291
|
-
#
|
292
|
-
# @return [Boolean]
|
293
|
-
def enable_forking?
|
294
|
-
ENV['IMAGE_VISE_ENABLE_FORK'] == 'yes'
|
295
|
-
end
|
296
|
-
|
266
|
+
|
297
267
|
# Applies the given {ImageVise::Pipeline} to the image, and writes the render to
|
298
268
|
# the given path.
|
299
269
|
#
|
@@ -303,21 +273,23 @@ class ImageVise::RenderEngine
|
|
303
273
|
# @return [void]
|
304
274
|
def apply_pipeline(source_file_path, pipeline, source_file_type, render_to_path)
|
305
275
|
render_file_type = source_file_type
|
306
|
-
|
276
|
+
|
307
277
|
# Load the first frame of the animated GIF _or_ the blended compatibility layer from Photoshop
|
308
278
|
image_list = Magick::Image.read(source_file_path)
|
309
|
-
magick_image = image_list.first
|
310
|
-
|
311
|
-
#
|
312
|
-
|
313
|
-
|
314
|
-
#
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
279
|
+
magick_image = image_list.first # Picks up the "precomp" PSD layer in compatibility mode, or the first frame of a GIF
|
280
|
+
|
281
|
+
# If any operators want to stash some data for downstream use we use this Hash
|
282
|
+
metadata = {}
|
283
|
+
|
284
|
+
# Apply the pipeline (all the image operators)
|
285
|
+
pipeline.apply!(magick_image, metadata)
|
286
|
+
|
287
|
+
# Write out the file honoring the possible injected metadata. One of the metadata
|
288
|
+
# elements (that an operator might want to alter) is the :writer, we forcibly #fetch
|
289
|
+
# it so that we get a KeyError if some operator has deleted it without providing a replacement.
|
290
|
+
# If no operators touched the writer we are going to use the automatic format selection
|
291
|
+
writer = metadata.fetch(:writer, ImageVise::AutoWriter.new)
|
292
|
+
writer.write_image!(magick_image, metadata, render_to_path)
|
321
293
|
ensure
|
322
294
|
# destroy all the loaded images explicitly
|
323
295
|
(image_list || []).map {|img| ImageVise.destroy(img) }
|