image_vise 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- metadata +2 -36
- data/.gitignore +0 -8
- data/.travis.yml +0 -13
- data/DEVELOPMENT.md +0 -111
- data/Gemfile +0 -4
- data/LICENSE.txt +0 -29
- data/README.md +0 -213
- data/Rakefile +0 -6
- data/SECURITY.md +0 -57
- data/examples/config.ru +0 -17
- data/examples/custom_image_operator.rb +0 -27
- data/examples/error_handline_appsignal.rb +0 -23
- data/examples/error_handling_sentry.rb +0 -25
- data/image_vise.gemspec +0 -43
- data/lib/image_vise/fetchers/fetcher_file.rb +0 -27
- data/lib/image_vise/fetchers/fetcher_http.rb +0 -42
- data/lib/image_vise/file_response.rb +0 -22
- data/lib/image_vise/image_request.rb +0 -70
- data/lib/image_vise/operators/auto_orient.rb +0 -10
- data/lib/image_vise/operators/background_fill.rb +0 -18
- data/lib/image_vise/operators/crop.rb +0 -32
- data/lib/image_vise/operators/ellipse_stencil.rb +0 -70
- data/lib/image_vise/operators/fit_crop.rb +0 -33
- data/lib/image_vise/operators/force_jpg_out.rb +0 -17
- data/lib/image_vise/operators/geom.rb +0 -16
- data/lib/image_vise/operators/sRGB_v4_ICC_preference_displayclass.icc +0 -0
- data/lib/image_vise/operators/sharpen.rb +0 -21
- data/lib/image_vise/operators/srgb.rb +0 -30
- data/lib/image_vise/operators/strip_metadata.rb +0 -10
- data/lib/image_vise/pipeline.rb +0 -64
- data/lib/image_vise/render_engine.rb +0 -298
- data/lib/image_vise/version.rb +0 -3
- data/lib/image_vise/writers/auto_writer.rb +0 -23
- data/lib/image_vise/writers/jpeg_writer.rb +0 -9
- data/lib/image_vise.rb +0 -177
@@ -1,298 +0,0 @@
|
|
1
|
-
class ImageVise::RenderEngine
|
2
|
-
class UnsupportedInputFormat < StandardError; end
|
3
|
-
class EmptyRender < StandardError; end
|
4
|
-
|
5
|
-
DEFAULT_HEADERS = {
|
6
|
-
'Allow' => "GET"
|
7
|
-
}.freeze
|
8
|
-
|
9
|
-
# Headers for error responses that denote an invalid or
|
10
|
-
# an unsatisfiable request
|
11
|
-
JSON_ERROR_HEADERS_REQUEST = DEFAULT_HEADERS.merge({
|
12
|
-
'Content-Type' => 'application/json',
|
13
|
-
'Cache-Control' => 'public, max-age=600'
|
14
|
-
}).freeze
|
15
|
-
|
16
|
-
# Headers for error responses that denote
|
17
|
-
# an intermittent error (that permit retries)
|
18
|
-
JSON_ERROR_HEADERS_INTERMITTENT = DEFAULT_HEADERS.merge({
|
19
|
-
'Content-Type' => 'application/json',
|
20
|
-
'Cache-Control' => 'public, max-age=5'
|
21
|
-
}).freeze
|
22
|
-
|
23
|
-
# "public" of course. Add max-age so that there is _some_
|
24
|
-
# revalidation after a time (otherwise some proxies treat it
|
25
|
-
# as "must-revalidate" always), and "no-transform" so that
|
26
|
-
# various deflate schemes are not applied to it (does happen
|
27
|
-
# with Rack::Cache and leads Chrome to throw up on content
|
28
|
-
# decoding for example).
|
29
|
-
IMAGE_CACHE_CONTROL = 'public, no-transform, max-age=2592000'
|
30
|
-
|
31
|
-
# How long is a render (the ImageMagick/write part) is allowed to
|
32
|
-
# take before we kill it
|
33
|
-
RENDER_TIMEOUT_SECONDS = 10
|
34
|
-
|
35
|
-
# Which input files we permit (based on extensions stored in MagicBytes)
|
36
|
-
PERMITTED_SOURCE_FILE_EXTENSIONS = %w( gif png jpg psd tif)
|
37
|
-
|
38
|
-
# How long should we wait when fetching the image from the external host
|
39
|
-
EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS = 4
|
40
|
-
|
41
|
-
def bail(status, *errors_array)
|
42
|
-
headers = if (300...500).cover?(status)
|
43
|
-
JSON_ERROR_HEADERS_REQUEST.dup
|
44
|
-
else
|
45
|
-
JSON_ERROR_HEADERS_INTERMITTENT.dup
|
46
|
-
end
|
47
|
-
response = [status.to_i, headers, [JSON.pretty_generate({errors: errors_array})]]
|
48
|
-
throw :__bail, response
|
49
|
-
end
|
50
|
-
|
51
|
-
# The main entry point for the Rack app. Wraps a call to {#handle_request} in a `catch{}` block
|
52
|
-
# so that any method can abort the request by calling {#bail}
|
53
|
-
#
|
54
|
-
# @param env[Hash] the Rack env
|
55
|
-
# @return [Array] the Rack response
|
56
|
-
def call(env)
|
57
|
-
catch(:__bail) { handle_request(env) }
|
58
|
-
end
|
59
|
-
|
60
|
-
# Hadles the Rack request. If one of the steps calls {#bail} the `:__bail` symbol will be
|
61
|
-
# thrown and the execution will abort. Any errors will cause either an error response in
|
62
|
-
# JSON format or an Exception will be raised (depending on the return value of `raise_exceptions?`)
|
63
|
-
#
|
64
|
-
# @param env[Hash] the Rack env
|
65
|
-
# @return [Array] the Rack response
|
66
|
-
def handle_request(env)
|
67
|
-
setup_error_handling(env)
|
68
|
-
|
69
|
-
# Assume that if _any_ ETag is given the image is being requested anew as a refetch,
|
70
|
-
# and the client already has it. Just respond with a 304.
|
71
|
-
return [304, DEFAULT_HEADERS.dup, []] if env['HTTP_IF_NONE_MATCH']
|
72
|
-
|
73
|
-
req = parse_env_into_request(env)
|
74
|
-
bail(405, 'Only GET supported') unless req.get?
|
75
|
-
params = extract_params_from_request(req)
|
76
|
-
|
77
|
-
image_request = ImageVise::ImageRequest.from_params(qs_params: params, secrets: ImageVise.secret_keys)
|
78
|
-
render_destination_file, render_file_type, etag = process_image_request(image_request)
|
79
|
-
image_rack_response(render_destination_file, render_file_type, etag)
|
80
|
-
rescue *permanent_failures => e
|
81
|
-
handle_request_error(e)
|
82
|
-
http_status_code = e.respond_to?(:http_status) ? e.http_status : 400
|
83
|
-
raise_exception_or_error_response(e, http_status_code)
|
84
|
-
rescue Exception => e
|
85
|
-
if http_status_code = (e.respond_to?(:http_status) && e.http_status)
|
86
|
-
handle_request_error(e)
|
87
|
-
raise_exception_or_error_response(e, http_status_code)
|
88
|
-
else
|
89
|
-
handle_generic_error(e)
|
90
|
-
raise_exception_or_error_response(e, 500)
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
# Parses the Rack environment into a Rack::Reqest. The following methods
|
95
|
-
# are going to be called on it: `#get?` and `#params`. You can use this
|
96
|
-
# method to override path-to-parameter translation for example.
|
97
|
-
#
|
98
|
-
# @param rack_env[Hash] the Rack environment
|
99
|
-
# @return [#get?, #params] the Rack request or a compatible object
|
100
|
-
def parse_env_into_request(rack_env)
|
101
|
-
Rack::Request.new(rack_env)
|
102
|
-
end
|
103
|
-
|
104
|
-
# Extracts the image params from the Rack::Request
|
105
|
-
#
|
106
|
-
# @param rack_request[#path_info] an object that has a path info
|
107
|
-
# @return [Hash] the params hash with `:q` and `:sig` keys
|
108
|
-
def extract_params_from_request(rack_request)
|
109
|
-
# Prevent cache bypass DOS attacks by only permitting :sig and :q
|
110
|
-
bail(400, 'Query strings are not supported') if rack_request.params.any?
|
111
|
-
|
112
|
-
# Extract the tail (signature) and the front (the Base64-encoded request).
|
113
|
-
# Slashes within :q are masked by ImageRequest already, so we don't have
|
114
|
-
# to worry about them.
|
115
|
-
*, q_from_path, sig_from_path = rack_request.path_info.split('/')
|
116
|
-
|
117
|
-
# Raise if any of them are empty or blank
|
118
|
-
nothing_recovered = [q_from_path, sig_from_path].all?{|v| v.nil? || v.empty? }
|
119
|
-
bail(400, 'Need 2 usable path components') if nothing_recovered
|
120
|
-
|
121
|
-
{q: q_from_path, sig: sig_from_path}
|
122
|
-
end
|
123
|
-
|
124
|
-
# Processes the ImageRequest object created from the request parameters,
|
125
|
-
# and returns a triplet of the File object containing the rendered image,
|
126
|
-
# the MagicBytes::FileType object of the render, and the cache ETag value
|
127
|
-
# representing the processing pipeline
|
128
|
-
#
|
129
|
-
# @param image_request[ImageVise::ImageRequest] the request for the image
|
130
|
-
# @return [Array<File, MagicBytes::FileType, String]
|
131
|
-
def process_image_request(image_request)
|
132
|
-
# Recover the source image URL and the pipeline instructions (all the image ops)
|
133
|
-
source_image_uri, pipeline = image_request.src_url, image_request.pipeline
|
134
|
-
raise 'Image pipeline has no operators' if pipeline.empty?
|
135
|
-
|
136
|
-
# Compute an ETag which describes this image transform + image source location.
|
137
|
-
# Assume the image URL contents does _never_ change.
|
138
|
-
etag = image_request.cache_etag
|
139
|
-
|
140
|
-
# Download/copy the original into a Tempfile
|
141
|
-
fetcher = ImageVise.fetcher_for(source_image_uri.scheme)
|
142
|
-
source_file = fetcher.fetch_uri_to_tempfile(source_image_uri)
|
143
|
-
|
144
|
-
# Make sure we do not try to process something...questionable
|
145
|
-
source_file_type = detect_file_type(source_file)
|
146
|
-
unless source_file_type_permitted?(source_file_type)
|
147
|
-
raise UnsupportedInputFormat.new("Unsupported/unknown input file format .%s" % source_file_type.ext)
|
148
|
-
end
|
149
|
-
|
150
|
-
render_destination_file = Tempfile.new('imagevise-render').tap{|f| f.binmode }
|
151
|
-
|
152
|
-
# Do the actual imaging stuff
|
153
|
-
apply_pipeline(source_file.path, pipeline, source_file_type, render_destination_file.path)
|
154
|
-
|
155
|
-
# Catch this one early
|
156
|
-
render_destination_file.rewind
|
157
|
-
raise EmptyRender, "The rendered image was empty" if render_destination_file.size.zero?
|
158
|
-
|
159
|
-
render_file_type = detect_file_type(render_destination_file)
|
160
|
-
[render_destination_file, render_file_type, etag]
|
161
|
-
ensure
|
162
|
-
ImageVise.close_and_unlink(source_file)
|
163
|
-
end
|
164
|
-
|
165
|
-
# Returns a Rack response triplet. Accepts the return value of
|
166
|
-
# `process_image_request` unsplatted, and returns a triplet that
|
167
|
-
# can be returned as a Rack response. The Rack response will contain
|
168
|
-
# an iterable body object that is designed to automatically delete
|
169
|
-
# the Tempfile it wraps on close.
|
170
|
-
#
|
171
|
-
# @param render_destination_file[File] the File handle to the rendered image
|
172
|
-
# @param render_file_type[MagicBytes::FileType] the rendered file type
|
173
|
-
# @param etag[String] the ETag for the response
|
174
|
-
def image_rack_response(render_destination_file, render_file_type, etag)
|
175
|
-
response_headers = DEFAULT_HEADERS.merge({
|
176
|
-
'Content-Type' => render_file_type.mime,
|
177
|
-
'Content-Length' => '%d' % render_destination_file.size,
|
178
|
-
'Cache-Control' => IMAGE_CACHE_CONTROL,
|
179
|
-
'ETag' => etag
|
180
|
-
})
|
181
|
-
|
182
|
-
# Wrap the body Tempfile with a self-closing response.
|
183
|
-
# Once the response is read in full, the tempfile is going to be closed and unlinked.
|
184
|
-
[200, response_headers, ImageVise::FileResponse.new(render_destination_file)]
|
185
|
-
end
|
186
|
-
|
187
|
-
# Depending on `raise_exceptions?` will either raise the passed Exception,
|
188
|
-
# or force the application to return the error in the Rack response.
|
189
|
-
#
|
190
|
-
# @param exception[Exception] the error that has to be captured
|
191
|
-
# @param status_code[Fixnum] the HTTP status code
|
192
|
-
def raise_exception_or_error_response(exception, status_code)
|
193
|
-
if raise_exceptions?
|
194
|
-
raise exception
|
195
|
-
else
|
196
|
-
bail status_code, exception.message
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
# Detects the file type of the given File and returns
|
201
|
-
# a MagicBytes::FileType object that contains the extension and
|
202
|
-
# the MIME type.
|
203
|
-
#
|
204
|
-
# @param tempfile[File] the file to perform detection on
|
205
|
-
# @return [MagicBytes::FileType] the detected file type
|
206
|
-
def detect_file_type(tempfile)
|
207
|
-
tempfile.rewind
|
208
|
-
MagicBytes.read_and_detect(tempfile).tap { tempfile.rewind }
|
209
|
-
end
|
210
|
-
|
211
|
-
# Tells whether the given file type may be loaded into the image processor.
|
212
|
-
#
|
213
|
-
# @param magic_bytes_file_info[MagicBytes::FileType] the filetype
|
214
|
-
# @return [Boolean]
|
215
|
-
def source_file_type_permitted?(magic_bytes_file_info)
|
216
|
-
PERMITTED_SOURCE_FILE_EXTENSIONS.include?(magic_bytes_file_info.ext)
|
217
|
-
end
|
218
|
-
|
219
|
-
# Lists exceptions that should lead to the request being flagged
|
220
|
-
# as invalid (4xx as opposed to 5xx for a generic server error).
|
221
|
-
# Decent clients should _not_ retry those requests.
|
222
|
-
def permanent_failures
|
223
|
-
[
|
224
|
-
Magick::ImageMagickError,
|
225
|
-
UnsupportedInputFormat,
|
226
|
-
ImageVise::ImageRequest::InvalidRequest
|
227
|
-
]
|
228
|
-
end
|
229
|
-
|
230
|
-
# Is meant to be overridden by subclasses,
|
231
|
-
# will be called at the start of each request to set up the error handling
|
232
|
-
# library (Appsignal, Honeybadger, Sentry...)
|
233
|
-
#
|
234
|
-
# @param rack_env[Hash] the Rack env
|
235
|
-
# @return [void]
|
236
|
-
def setup_error_handling(rack_env)
|
237
|
-
end
|
238
|
-
|
239
|
-
# Is meant to be overridden by subclasses,
|
240
|
-
# will be called when a request fails due to a malformed query string,
|
241
|
-
# unrecognized signature or other client-induced problems. The method
|
242
|
-
# should _not_ re-raise the exception.
|
243
|
-
#
|
244
|
-
# @param exception[Exception] the exception to be handled
|
245
|
-
# @return [void]
|
246
|
-
def handle_request_error(exception)
|
247
|
-
end
|
248
|
-
|
249
|
-
# Is meant to be overridden by subclasses,
|
250
|
-
# will be called when a request fails due to an error on the server
|
251
|
-
# (like an unexpected error in an image operator). The method
|
252
|
-
# should _not_ re-raise the exception.
|
253
|
-
#
|
254
|
-
# @param exception[Exception] the exception to be handled
|
255
|
-
# @return [void]
|
256
|
-
def handle_generic_error(exception)
|
257
|
-
end
|
258
|
-
|
259
|
-
# Tells whether the engine must raise the exceptions further up the Rack stack,
|
260
|
-
# or they should be suppressed and a JSON response must be returned.
|
261
|
-
#
|
262
|
-
# @return [Boolean]
|
263
|
-
def raise_exceptions?
|
264
|
-
false
|
265
|
-
end
|
266
|
-
|
267
|
-
# Applies the given {ImageVise::Pipeline} to the image, and writes the render to
|
268
|
-
# the given path.
|
269
|
-
#
|
270
|
-
# @param source_file_path[String] the path to the file containing the source image
|
271
|
-
# @param pipeline[#apply!(Magick::Image)] the processing pipeline
|
272
|
-
# @param render_to_path[String] the path to write the rendered image to
|
273
|
-
# @return [void]
|
274
|
-
def apply_pipeline(source_file_path, pipeline, source_file_type, render_to_path)
|
275
|
-
render_file_type = source_file_type
|
276
|
-
|
277
|
-
# Load the first frame of the animated GIF _or_ the blended compatibility layer from Photoshop
|
278
|
-
image_list = Magick::Image.read(source_file_path)
|
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)
|
293
|
-
ensure
|
294
|
-
# destroy all the loaded images explicitly
|
295
|
-
(image_list || []).map {|img| ImageVise.destroy(img) }
|
296
|
-
end
|
297
|
-
|
298
|
-
end
|
data/lib/image_vise/version.rb
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
# Picks the most reasonable "default" output format for web resources. In practice, if the
|
2
|
-
# image contains transparency (an alpha channel) PNG will be chosen, and if not - JPEG will
|
3
|
-
# be chosen. Since ImageVise URLs do not contain a file extension we are free to pick
|
4
|
-
# the suitable format at render time
|
5
|
-
class ImageVise::AutoWriter
|
6
|
-
# The default file type for images with alpha
|
7
|
-
PNG_FILE_TYPE = MagicBytes::FileType.new('png','image/png').freeze
|
8
|
-
|
9
|
-
# Renders the file as a jpg if the custom output filetype operator is used
|
10
|
-
JPG_FILE_TYPE = MagicBytes::FileType.new('jpg','image/jpeg').freeze
|
11
|
-
|
12
|
-
def write_image!(magick_image, _, render_to_path)
|
13
|
-
# If processing the image has created an alpha channel, use PNG always.
|
14
|
-
# Otherwise, keep the original format for as far as the supported formats list goes.
|
15
|
-
render_file_type = if magick_image.alpha?
|
16
|
-
PNG_FILE_TYPE
|
17
|
-
else
|
18
|
-
JPG_FILE_TYPE
|
19
|
-
end
|
20
|
-
magick_image.format = render_file_type.ext
|
21
|
-
magick_image.write(render_to_path)
|
22
|
-
end
|
23
|
-
end
|
@@ -1,9 +0,0 @@
|
|
1
|
-
class ImageVise::JPGWriter < Ks.strict(:quality)
|
2
|
-
JPG_FILE_TYPE = MagicBytes::FileType.new('jpg','image/jpeg').freeze
|
3
|
-
|
4
|
-
def write_image!(magick_image, _, render_to_path)
|
5
|
-
q = self.quality # to avoid the changing "self" context
|
6
|
-
magick_image.format = JPG_FILE_TYPE.ext
|
7
|
-
magick_image.write(render_to_path) { self.quality = q }
|
8
|
-
end
|
9
|
-
end
|
data/lib/image_vise.rb
DELETED
@@ -1,177 +0,0 @@
|
|
1
|
-
require 'ks'
|
2
|
-
require 'json'
|
3
|
-
require 'patron'
|
4
|
-
require 'rmagick'
|
5
|
-
require 'magic_bytes'
|
6
|
-
require 'thread'
|
7
|
-
require 'base64'
|
8
|
-
require 'rack'
|
9
|
-
|
10
|
-
class ImageVise
|
11
|
-
require_relative 'image_vise/version'
|
12
|
-
S_MUTEX = Mutex.new
|
13
|
-
private_constant :S_MUTEX
|
14
|
-
|
15
|
-
@allowed_hosts = Set.new
|
16
|
-
@keys = Set.new
|
17
|
-
@operators = {}
|
18
|
-
@allowed_glob_patterns = Set.new
|
19
|
-
@fetchers = {}
|
20
|
-
|
21
|
-
class << self
|
22
|
-
# Resets all allowed hosts
|
23
|
-
def reset_allowed_hosts!
|
24
|
-
S_MUTEX.synchronize { @allowed_hosts.clear }
|
25
|
-
end
|
26
|
-
|
27
|
-
# Add an allowed host
|
28
|
-
def add_allowed_host!(hostname)
|
29
|
-
S_MUTEX.synchronize { @allowed_hosts << hostname }
|
30
|
-
end
|
31
|
-
|
32
|
-
# Returns both the allowed hosts added at runtime and the ones set in the constant
|
33
|
-
def allowed_hosts
|
34
|
-
S_MUTEX.synchronize { @allowed_hosts.to_a }
|
35
|
-
end
|
36
|
-
|
37
|
-
# Removes all set keys
|
38
|
-
def reset_secret_keys!
|
39
|
-
S_MUTEX.synchronize { @keys.clear }
|
40
|
-
end
|
41
|
-
|
42
|
-
def allow_filesystem_source!(glob_pattern)
|
43
|
-
S_MUTEX.synchronize { @allowed_glob_patterns << glob_pattern }
|
44
|
-
end
|
45
|
-
|
46
|
-
def allowed_filesystem_sources
|
47
|
-
S_MUTEX.synchronize { @allowed_glob_patterns.to_a }
|
48
|
-
end
|
49
|
-
|
50
|
-
def deny_filesystem_sources!
|
51
|
-
S_MUTEX.synchronize { @allowed_glob_patterns.clear }
|
52
|
-
end
|
53
|
-
|
54
|
-
# Adds a key against which the parameters are going to be verified.
|
55
|
-
# Multiple applications may have their own different keys,
|
56
|
-
# so we need to have multiple keys.
|
57
|
-
def add_secret_key!(key)
|
58
|
-
S_MUTEX.synchronize { @keys << key }
|
59
|
-
self
|
60
|
-
end
|
61
|
-
|
62
|
-
# Returns the array of defined keys or raises an exception if no keys have been set yet
|
63
|
-
def secret_keys
|
64
|
-
keys = S_MUTEX.synchronize { @keys.any? && @keys.to_a }
|
65
|
-
keys or raise "No keys set, add a key using `ImageVise.add_secret_key!(key)'"
|
66
|
-
end
|
67
|
-
|
68
|
-
# Generate a set of querystring params for a resized image. Yields a Pipeline object that
|
69
|
-
# will receive method calls for adding image operations to a stack.
|
70
|
-
#
|
71
|
-
# ImageVise.image_params(src_url: image_url_on_s3, secret: '...') do |p|
|
72
|
-
# p.center_fit width: 128, height: 128
|
73
|
-
# p.elliptic_stencil
|
74
|
-
# end #=> {q: '...', sig: '...'}
|
75
|
-
#
|
76
|
-
# The query string elements can be then passed on to RenderEngine for validation and execution.
|
77
|
-
#
|
78
|
-
# @yield {ImageVise::Pipeline}
|
79
|
-
# @return [Hash]
|
80
|
-
def image_params(src_url:, secret:)
|
81
|
-
p = Pipeline.new
|
82
|
-
yield(p)
|
83
|
-
raise ArgumentError, "Image pipeline has no steps defined" if p.empty?
|
84
|
-
ImageRequest.new(src_url: URI(src_url), pipeline: p).to_query_string_params(secret)
|
85
|
-
end
|
86
|
-
|
87
|
-
# Generate a path for a resized image. Yields a Pipeline object that
|
88
|
-
# will receive method calls for adding image operations to a stack.
|
89
|
-
#
|
90
|
-
# ImageVise.image_path(src_url: image_url_on_s3, secret: '...') do |p|
|
91
|
-
# p.center_fit width: 128, height: 128
|
92
|
-
# p.elliptic_stencil
|
93
|
-
# end #=> "/abcdef/xyz123"
|
94
|
-
#
|
95
|
-
# The query string elements can be then passed on to RenderEngine for validation and execution.
|
96
|
-
#
|
97
|
-
# @yield {ImageVise::Pipeline}
|
98
|
-
# @return [String]
|
99
|
-
def image_path(src_url:, secret:)
|
100
|
-
p = Pipeline.new
|
101
|
-
yield(p)
|
102
|
-
raise ArgumentError, "Image pipeline has no steps defined" if p.empty?
|
103
|
-
ImageRequest.new(src_url: URI(src_url), pipeline: p).to_path_params(secret)
|
104
|
-
end
|
105
|
-
|
106
|
-
# Adds an operator
|
107
|
-
def add_operator(operator_name, object_responding_to_new)
|
108
|
-
@operators[operator_name.to_s] = object_responding_to_new
|
109
|
-
end
|
110
|
-
|
111
|
-
# Gets an operator by name
|
112
|
-
def operator_from(operator_name)
|
113
|
-
@operators.fetch(operator_name.to_s)
|
114
|
-
end
|
115
|
-
|
116
|
-
def defined_operator_names
|
117
|
-
@operators.keys
|
118
|
-
end
|
119
|
-
|
120
|
-
def register_fetcher(scheme, fetcher)
|
121
|
-
S_MUTEX.synchronize { @fetchers[scheme.to_s] = fetcher }
|
122
|
-
end
|
123
|
-
|
124
|
-
def fetcher_for(scheme)
|
125
|
-
S_MUTEX.synchronize { @fetchers[scheme.to_s] or raise "No fetcher registered for #{scheme}" }
|
126
|
-
end
|
127
|
-
|
128
|
-
def operator_name_for(operator)
|
129
|
-
S_MUTEX.synchronize do
|
130
|
-
@operators.key(operator.class) or raise "Operator #{operator.inspect} not registered using ImageVise.add_operator"
|
131
|
-
end
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
# Made available since the object that is used with `mount()` in Rails
|
136
|
-
# has to, by itself, to respond to `call`.
|
137
|
-
#
|
138
|
-
# Thanks to this method you can do this:
|
139
|
-
#
|
140
|
-
# mount ImageVise => '/thumbnails'
|
141
|
-
#
|
142
|
-
# instead of having to do
|
143
|
-
#
|
144
|
-
# mount ImageVise.new => '/thumbnails'
|
145
|
-
#
|
146
|
-
def self.call(rack_env)
|
147
|
-
ImageVise::RenderEngine.new.call(rack_env)
|
148
|
-
end
|
149
|
-
|
150
|
-
def call(rack_env)
|
151
|
-
ImageVise::RenderEngine.new.call(rack_env)
|
152
|
-
end
|
153
|
-
|
154
|
-
# Used as a shorthand to force-destroy Magick images in ensure() blocks. Since
|
155
|
-
# ensure blocks sometimes deal with variables in inconsistent states (variable
|
156
|
-
# in scope but not yet set to an image) we take the possibility of nils into account.
|
157
|
-
# We also deal with Magick::Image objects that already have been destroyed in a clean manner.
|
158
|
-
def self.destroy(maybe_image)
|
159
|
-
return unless maybe_image
|
160
|
-
return unless maybe_image.respond_to?(:destroy!)
|
161
|
-
return if maybe_image.destroyed?
|
162
|
-
maybe_image.destroy!
|
163
|
-
end
|
164
|
-
|
165
|
-
# Used as a shorthand to force-dealloc Tempfiles in an ensure() blocks. Since
|
166
|
-
# ensure blocks sometimes deal with variables in inconsistent states (variable
|
167
|
-
# in scope but not yet set to an image) we take the possibility of nils into account.
|
168
|
-
def self.close_and_unlink(maybe_tempfile)
|
169
|
-
return unless maybe_tempfile
|
170
|
-
maybe_tempfile.close unless maybe_tempfile.closed?
|
171
|
-
maybe_tempfile.unlink
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
Dir.glob(__dir__ + '/**/*.rb').sort.each do |f|
|
176
|
-
require f unless f == File.expand_path(__FILE__)
|
177
|
-
end
|