image_vise 0.1.6 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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) }
|