image_vise 0.0.16
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +203 -0
- data/Rakefile +29 -0
- data/examples/config.ru +17 -0
- data/image_vise.gemspec +116 -0
- data/lib/image_vise/auto_orient.rb +10 -0
- data/lib/image_vise/crop.rb +32 -0
- data/lib/image_vise/ellipse_stencil.rb +43 -0
- data/lib/image_vise/file_response.rb +23 -0
- data/lib/image_vise/fit_crop.rb +33 -0
- data/lib/image_vise/geom.rb +16 -0
- data/lib/image_vise/image_request.rb +68 -0
- data/lib/image_vise/pipeline.rb +54 -0
- data/lib/image_vise/render_engine.rb +210 -0
- data/lib/image_vise/sharpen.rb +21 -0
- data/lib/image_vise.rb +118 -0
- data/spec/image_vise/auto_orient_spec.rb +10 -0
- data/spec/image_vise/crop_spec.rb +20 -0
- data/spec/image_vise/ellipse_stencil_spec.rb +10 -0
- data/spec/image_vise/file_response_spec.rb +45 -0
- data/spec/image_vise/fit_crop_spec.rb +20 -0
- data/spec/image_vise/geom_spec.rb +17 -0
- data/spec/image_vise/image_request_spec.rb +53 -0
- data/spec/image_vise/pipeline_spec.rb +70 -0
- data/spec/image_vise/render_engine_spec.rb +167 -0
- data/spec/image_vise/sharpen_spec.rb +17 -0
- data/spec/image_vise_spec.rb +89 -0
- data/spec/spec_helper.rb +74 -0
- data/spec/test_server.rb +61 -0
- data/spec/waterside_magic_hour.jpg +0 -0
- metadata +306 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a1fd49220c9d775bba0d8f8a5646b2e78a4412c5
|
4
|
+
data.tar.gz: 8b0033eed381e0c135bf8b109cc9013f63e58bdd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a46027f576b9237097f7813d76399b9bfddd23e6c2d948416cee63e758236d56791ec1b33cef0a3ab9322e1817424c5ac4b167216cd210bfeba2cf54b6fde4da
|
7
|
+
data.tar.gz: 376b9f9efe967201740f83f4778df4eff720dc8525c7a62d2d36e326acc16d18e5c2e10cf85c974bcc5db956dcb063d4a33b8536a74d33c88f888da3be734f15
|
data/Gemfile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gem 'bundler'
|
4
|
+
gem 'patron', '~> 0.6'
|
5
|
+
gem 'rmagick', '~> 2.15', :require => 'rmagick'
|
6
|
+
gem 'exceptional_fork', '~> 1.2'
|
7
|
+
gem 'ks'
|
8
|
+
gem 'magic_bytes'
|
9
|
+
|
10
|
+
group :development do
|
11
|
+
gem 'simplecov'
|
12
|
+
gem 'rack-cache'
|
13
|
+
gem 'strenv'
|
14
|
+
gem 'addressable', :require => %w( addressable/uri )
|
15
|
+
gem 'rack', '~> 1'
|
16
|
+
gem 'rack-test'
|
17
|
+
gem 'foreman'
|
18
|
+
gem 'rspec', '~> 3.2', '< 3.3'
|
19
|
+
gem 'rake', '~> 10'
|
20
|
+
gem "jeweler"
|
21
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) 2016 WeTransfer
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
data/README.md
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
# A thumbnailing server
|
2
|
+
|
3
|
+
`ImageVise` is an image-from-url-as-a-service server for use either standalone or within a larger Rails/Rack
|
4
|
+
framework. The main uses are:
|
5
|
+
|
6
|
+
* Image resizing on request
|
7
|
+
* Applying image filters
|
8
|
+
|
9
|
+
It is implemented as a Rack application that responds to any URL and accepts the following query string parameters:
|
10
|
+
|
11
|
+
* `q` - Bese-64 encoded JSON object with `src_url` and `pipeline` properties
|
12
|
+
* `sig` - the HMAC signature of the hash with `url`, `w` and `h` computed on a query-string encode of them
|
13
|
+
|
14
|
+
A request to `ImageVise` might look like this:
|
15
|
+
|
16
|
+
/?q=acbhGyfhyYErghff&sig=acfgheg123
|
17
|
+
|
18
|
+
The URL that gets generated is best composed with the included `ImageVise.image_params` method. This method will
|
19
|
+
take care of encoding the source URL and the commands in the right way, as well as signing.
|
20
|
+
|
21
|
+
## Using ImageVise within a Rails application
|
22
|
+
|
23
|
+
Mount ImageVise in your `routes.rb`:
|
24
|
+
|
25
|
+
mount '/images' => ImageVise
|
26
|
+
|
27
|
+
and add an initializer (like `config/initializers/image_vise_config.rb`) to set up the permitted hosts
|
28
|
+
|
29
|
+
ImageVise.add_allowed_host! your_application_hostname
|
30
|
+
ImageVise.add_secret_key! ENV.fetch('IMAGE_VISE_SECRET')
|
31
|
+
|
32
|
+
You might want to define a helper method for generating signed URLs as well, which will look something like this:
|
33
|
+
|
34
|
+
def thumb_url(source_image_url)
|
35
|
+
qs_params = ImageVise.image_params(src_url: source_image_url, secret: ENV.fetch('IMAGE_VISE_SECRET')) do |pipeline|
|
36
|
+
# For example, you can also yield `pipeline` to the caller
|
37
|
+
pipeline.fit_crop width: 128, height: 128, gravity: 'c'
|
38
|
+
end
|
39
|
+
'/images?' + Rack::Utils.build_query(qs_params) # or use url_for...
|
40
|
+
end
|
41
|
+
|
42
|
+
## Using ImageVise within a Rack application
|
43
|
+
|
44
|
+
Mount ImageVise under a script name in your `config.ru`:
|
45
|
+
|
46
|
+
map '/images' do
|
47
|
+
run ImageVise
|
48
|
+
end
|
49
|
+
|
50
|
+
and add the initialization code either to `config.ru` proper or to some file in your application:
|
51
|
+
|
52
|
+
ImageVise.add_allowed_host! your_application_hostname
|
53
|
+
ImageVise.add_secret_key! ENV.fetch('IMAGE_VISE_SECRET')
|
54
|
+
|
55
|
+
You might want to define a helper method for generating signed URLs as well, which will look something like this:
|
56
|
+
|
57
|
+
def thumb_url(source_image_url)
|
58
|
+
qs_params = ImageVise.image_params(src_url: source_image_url, secret: ENV.fetch('IMAGE_VISE_SECRET')) do |pipe|
|
59
|
+
# For example, you can also yield `pipeline` to the caller
|
60
|
+
pipe.fit_crop width: 256, height: 256, gravity: 'c'
|
61
|
+
pipe.sharpen sigma: 0.5, radius: 2
|
62
|
+
pipe.ellipse_stencil
|
63
|
+
end
|
64
|
+
# Output a URL to the app
|
65
|
+
'/images?' + Rack::Utils.build_query(image_request)
|
66
|
+
end
|
67
|
+
|
68
|
+
## Operators and pipelining
|
69
|
+
|
70
|
+
ImageVise processes an image using _operators_. Each operator is just like an adjustment layer in Photoshop, except
|
71
|
+
that it can also resize the canvas. If you are familiar with node-based compositing systems like Shake, Nuke or Fusion
|
72
|
+
the pipeline is a node DAG with only one connection arrow going all the way. The operations are always applied in a
|
73
|
+
destructive way, so that the additional intermediate versions don't have to be deallocated manually after processing.
|
74
|
+
|
75
|
+
Each Operator is described in the pipeline using a tuple (Array) of roughly this structure:
|
76
|
+
|
77
|
+
[<operator_name>, {"<operator_param1>": <operator_param1_value>}]
|
78
|
+
|
79
|
+
You can have an unlimited number of such Operators per thumbnail, and they all get encoded in the URL (well,
|
80
|
+
technically, you _are_ limited - by the URL length supported by your web server).
|
81
|
+
|
82
|
+
For example, you can use the pipeline to apply a sharpening operator _after_ resising an image (for the lack
|
83
|
+
of decent image filtering choices in ImageMagick proper).
|
84
|
+
|
85
|
+
Here is an example pipeline, JSON-encoded (this is what is passed in the URL):
|
86
|
+
|
87
|
+
[
|
88
|
+
["auto_orient", {}],
|
89
|
+
["geom", {"geometry_string": "512x512"}],
|
90
|
+
["fit_crop", {"width": 32, "height": 32, "gravity": "se"}],
|
91
|
+
["sharpen", {"radius": 0.75, "sigma": 0.5}],
|
92
|
+
["ellipse_stencil", {}]
|
93
|
+
]
|
94
|
+
|
95
|
+
The same pipeline can be created using the `Pipeline` DSL:
|
96
|
+
|
97
|
+
pipe = Pipeline.new.
|
98
|
+
auto_orient.
|
99
|
+
geom(geometry_string: '512x512').
|
100
|
+
fit_crop(width: 32, height: 32, gravity: 'se').
|
101
|
+
sharpen(radius: 0.75, sigma: 0.5).
|
102
|
+
ellipse_stencil
|
103
|
+
|
104
|
+
and can then be applied to a `Magick::Image` object:
|
105
|
+
|
106
|
+
image = Magick::Image.read(my_image_path)[0]
|
107
|
+
pipe.apply!(image)
|
108
|
+
|
109
|
+
## Performance and memory
|
110
|
+
|
111
|
+
ImageVise uses ImageMagick and RMagick. It _does not_ shell out to `convert` or `mogrify`, because shelling out
|
112
|
+
is _expensive_ in terms of wall clock. It _does_ do it's best to deallocate (`#destroy!`) the image it works on,
|
113
|
+
but it is not 100% bullet proof.
|
114
|
+
|
115
|
+
Additionally, in contrast to `convert` and `mogrify` ImageVise supports _stackable_ operations, and these operations
|
116
|
+
might be repeated with different parameters. Unfortunately, `convert` is _not_ Shake, so we cannot pass a DAG of
|
117
|
+
image operators to it and just expect it to work. If we want to do processing of multiple steps that `convert` is
|
118
|
+
unable to execute in one call, we have to do
|
119
|
+
|
120
|
+
[fork+exec read + convert + write] -> [fork+exec read + convert + write] + ...
|
121
|
+
|
122
|
+
for each operator we want to apply in a consistent fashion. We cannot stuff all the operators into one `convert`
|
123
|
+
command because the order the operators get applied within `convert` is not clear, whereas we need a reproducible
|
124
|
+
deterministic order of operations (as set in the pipeline). A much better solution is - load the image into memory
|
125
|
+
**once**, do all the transformations, save. Additionally, if you use things like OpenCL with ImageMagick, the overhead
|
126
|
+
of loading the library and compiling the compute kernels will outweigh _any_ performance gains you might get when
|
127
|
+
actually using them. If you are using a library it is a one-time cost, with very fast processing afterwards.
|
128
|
+
|
129
|
+
Also note that image operators are not per definition Imagemagick-specific - it's completely possible to not only use
|
130
|
+
a different library for processing them, but even to use a different image processing server complying to the
|
131
|
+
same protocol (a signed JSON-encodded waybill of HTTP(S) source-URL + pipeline instructions).
|
132
|
+
|
133
|
+
## Using forked child processes for RMagick tasks
|
134
|
+
|
135
|
+
You can optionally set the `IMAGE_VISE_ENABLE_FORK` environment variable to any value to enable forking. When this
|
136
|
+
variable is set, ImageVise will fork a child process and perform the image processing task within that process,
|
137
|
+
killing it afterwards and deallocating all the memory. This can be extremely efficient for dealing with potential
|
138
|
+
memory bloat issues in ImageMagick/RMagick. However, loading images into RMagick may hang in a forked child. This
|
139
|
+
will lead to the child being timeout-terminated, and no image is going to be rendered. This issue is known and
|
140
|
+
also platform-dependent (it does not happen on OSX but does happen on Docker within Ubuntu Trusty for instance).
|
141
|
+
|
142
|
+
So, this feature _does_ exist but your mileage may vary with regards to it's use.
|
143
|
+
|
144
|
+
## Caching
|
145
|
+
|
146
|
+
The app is _designed_ to be run behind a frontline HTTP cache. The easiest is to use `Rack::Cache`, but this might
|
147
|
+
be instance-local depending on the storage backend used. A much better idea is to run ImageVise behind a long-caching
|
148
|
+
CDN.
|
149
|
+
|
150
|
+
## Shared HMAC keys for signed URLs
|
151
|
+
|
152
|
+
To allow `ImageVise` to recognize the signature when the signature is going to be received, add it to the list
|
153
|
+
of the shared keys on the `ImageVise` server:
|
154
|
+
|
155
|
+
ImageVise.add_secret_key!('ahoy! this is a secret!')
|
156
|
+
|
157
|
+
A single `ImageVise` server can maintain multiple signature keys, so that you will be able to generate thumbnails from
|
158
|
+
multiple applications all using different keys for their signatures. Every request will be validated against
|
159
|
+
each key and if at least one key generates the same signature for the same given parameters, it is going to be
|
160
|
+
accepted and the request will be allowed to go through.
|
161
|
+
|
162
|
+
When running `ImageVise` as a standalone application you can add set the `VISE_SECRET_KEYS` environment
|
163
|
+
variable to a comma-separated list of keys you are willing to accept (no spaces after the commas).
|
164
|
+
|
165
|
+
## Hostname validation
|
166
|
+
|
167
|
+
By default, `ImageVise` will refuse to process images from URLs on "unknown" hosts. To mark a host as "known"
|
168
|
+
tell `ImageVise` to
|
169
|
+
|
170
|
+
ImageVise.add_allowed_host!('my-image-store.ourcompany.co.uk')
|
171
|
+
|
172
|
+
## State
|
173
|
+
|
174
|
+
Except for the HTTP cache for redirects et.al no state is stored (`ImageVise` does not care whether you store
|
175
|
+
your images using Dragonfly, CarrierWave or some custom handling code). All the app needs is the full URL.
|
176
|
+
|
177
|
+
## FAQ
|
178
|
+
|
179
|
+
* _Yo dawg, I thought you like URLs so I have put encoded URL in your URL so you can..._ - well, the only alternative
|
180
|
+
is also managing image storage, and this something we want to avoid to keep `ImageVise` stateless
|
181
|
+
* _But the URLs can be exploited_ - this is highly unlikely if you pick strong keys for the HMAC signatures
|
182
|
+
* _I can load any image into the thumbnailer_ - in fact, no. First you have the URL checks, and then - all the URLs
|
183
|
+
are supposed to be coming from the sources you trust since they are signed.
|
184
|
+
|
185
|
+
## Running the tests, versioning, contributing
|
186
|
+
|
187
|
+
By default, `bundle exec rake` will run RSpec and will also open the generated images using the `$ open` command available
|
188
|
+
on your CLI. If you want to skip viewing those images, set the `SKIP_INTERACTIVE` environment variable to any value.
|
189
|
+
|
190
|
+
The gem version is specified in `image_vise.rb`. When contributing, please follow:
|
191
|
+
|
192
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
193
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
194
|
+
* Fork the project.
|
195
|
+
* Start a feature/bugfix branch.
|
196
|
+
* Commit and push until you are happy with your contribution.
|
197
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
198
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
199
|
+
|
200
|
+
### Copyright
|
201
|
+
|
202
|
+
Copyright (c) 2016 WeTransfer. See LICENSE.txt for further details.
|
203
|
+
The licensing terms also apply to the `waterside_magic_hour.jpg` test image.
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
require 'jeweler'
|
3
|
+
require_relative 'lib/image_vise'
|
4
|
+
|
5
|
+
Jeweler::Tasks.new do |gem|
|
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'
|
14
|
+
|
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
|
+
task :default => :spec
|
data/examples/config.ru
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/lib/image_vise'
|
2
|
+
|
3
|
+
# Add all the secret keys specified in the environment separated by a comma
|
4
|
+
if ENV['VISE_SECRET_KEYS']
|
5
|
+
ENV['VISE_SECRET_KEYS'].split(',').map do | key |
|
6
|
+
ImageVise.add_secret_key!(key.strip)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
# Cover things with caching, even redirects (since we do not want to ask S3 too often)
|
11
|
+
use Rack::Cache, :metastore => 'file:tmp/cache/rack/meta', :entitystore => 'file:tmp/cache/rack/entity',
|
12
|
+
:verbose => true, :allow_reload => false, :allow_revalidate => false
|
13
|
+
|
14
|
+
# Serve runtime thumbnails, under a specific URL
|
15
|
+
map '/images'do
|
16
|
+
run ImageVise
|
17
|
+
end
|
data/image_vise.gemspec
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
# stub: image_vise 0.0.16 ruby lib
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = "image_vise"
|
9
|
+
s.version = "0.0.16"
|
10
|
+
|
11
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
|
+
s.require_paths = ["lib"]
|
13
|
+
s.authors = ["Julik Tarkhanov"]
|
14
|
+
s.date = "2016-10-15"
|
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
|
+
"Gemfile",
|
23
|
+
"LICENSE.txt",
|
24
|
+
"README.md",
|
25
|
+
"Rakefile",
|
26
|
+
"examples/config.ru",
|
27
|
+
"image_vise.gemspec",
|
28
|
+
"lib/image_vise.rb",
|
29
|
+
"lib/image_vise/auto_orient.rb",
|
30
|
+
"lib/image_vise/crop.rb",
|
31
|
+
"lib/image_vise/ellipse_stencil.rb",
|
32
|
+
"lib/image_vise/file_response.rb",
|
33
|
+
"lib/image_vise/fit_crop.rb",
|
34
|
+
"lib/image_vise/geom.rb",
|
35
|
+
"lib/image_vise/image_request.rb",
|
36
|
+
"lib/image_vise/pipeline.rb",
|
37
|
+
"lib/image_vise/render_engine.rb",
|
38
|
+
"lib/image_vise/sharpen.rb",
|
39
|
+
"spec/image_vise/auto_orient_spec.rb",
|
40
|
+
"spec/image_vise/crop_spec.rb",
|
41
|
+
"spec/image_vise/ellipse_stencil_spec.rb",
|
42
|
+
"spec/image_vise/file_response_spec.rb",
|
43
|
+
"spec/image_vise/fit_crop_spec.rb",
|
44
|
+
"spec/image_vise/geom_spec.rb",
|
45
|
+
"spec/image_vise/image_request_spec.rb",
|
46
|
+
"spec/image_vise/pipeline_spec.rb",
|
47
|
+
"spec/image_vise/render_engine_spec.rb",
|
48
|
+
"spec/image_vise/sharpen_spec.rb",
|
49
|
+
"spec/image_vise_spec.rb",
|
50
|
+
"spec/spec_helper.rb",
|
51
|
+
"spec/test_server.rb",
|
52
|
+
"spec/waterside_magic_hour.jpg"
|
53
|
+
]
|
54
|
+
s.homepage = "https://github.com/WeTransfer/image_vise"
|
55
|
+
s.licenses = ["MIT"]
|
56
|
+
s.rubygems_version = "2.4.5.1"
|
57
|
+
s.summary = "Runtime thumbnailing proxy"
|
58
|
+
|
59
|
+
if s.respond_to? :specification_version then
|
60
|
+
s.specification_version = 4
|
61
|
+
|
62
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
63
|
+
s.add_runtime_dependency(%q<bundler>, [">= 0"])
|
64
|
+
s.add_runtime_dependency(%q<patron>, ["~> 0.6"])
|
65
|
+
s.add_runtime_dependency(%q<rmagick>, ["~> 2.15"])
|
66
|
+
s.add_runtime_dependency(%q<exceptional_fork>, ["~> 1.2"])
|
67
|
+
s.add_runtime_dependency(%q<ks>, [">= 0"])
|
68
|
+
s.add_runtime_dependency(%q<magic_bytes>, [">= 0"])
|
69
|
+
s.add_development_dependency(%q<simplecov>, [">= 0"])
|
70
|
+
s.add_development_dependency(%q<rack-cache>, [">= 0"])
|
71
|
+
s.add_development_dependency(%q<strenv>, [">= 0"])
|
72
|
+
s.add_development_dependency(%q<addressable>, [">= 0"])
|
73
|
+
s.add_development_dependency(%q<rack>, ["~> 1"])
|
74
|
+
s.add_development_dependency(%q<rack-test>, [">= 0"])
|
75
|
+
s.add_development_dependency(%q<foreman>, [">= 0"])
|
76
|
+
s.add_development_dependency(%q<rspec>, ["< 3.3", "~> 3.2"])
|
77
|
+
s.add_development_dependency(%q<rake>, ["~> 10"])
|
78
|
+
s.add_development_dependency(%q<jeweler>, [">= 0"])
|
79
|
+
else
|
80
|
+
s.add_dependency(%q<bundler>, [">= 0"])
|
81
|
+
s.add_dependency(%q<patron>, ["~> 0.6"])
|
82
|
+
s.add_dependency(%q<rmagick>, ["~> 2.15"])
|
83
|
+
s.add_dependency(%q<exceptional_fork>, ["~> 1.2"])
|
84
|
+
s.add_dependency(%q<ks>, [">= 0"])
|
85
|
+
s.add_dependency(%q<magic_bytes>, [">= 0"])
|
86
|
+
s.add_dependency(%q<simplecov>, [">= 0"])
|
87
|
+
s.add_dependency(%q<rack-cache>, [">= 0"])
|
88
|
+
s.add_dependency(%q<strenv>, [">= 0"])
|
89
|
+
s.add_dependency(%q<addressable>, [">= 0"])
|
90
|
+
s.add_dependency(%q<rack>, ["~> 1"])
|
91
|
+
s.add_dependency(%q<rack-test>, [">= 0"])
|
92
|
+
s.add_dependency(%q<foreman>, [">= 0"])
|
93
|
+
s.add_dependency(%q<rspec>, ["< 3.3", "~> 3.2"])
|
94
|
+
s.add_dependency(%q<rake>, ["~> 10"])
|
95
|
+
s.add_dependency(%q<jeweler>, [">= 0"])
|
96
|
+
end
|
97
|
+
else
|
98
|
+
s.add_dependency(%q<bundler>, [">= 0"])
|
99
|
+
s.add_dependency(%q<patron>, ["~> 0.6"])
|
100
|
+
s.add_dependency(%q<rmagick>, ["~> 2.15"])
|
101
|
+
s.add_dependency(%q<exceptional_fork>, ["~> 1.2"])
|
102
|
+
s.add_dependency(%q<ks>, [">= 0"])
|
103
|
+
s.add_dependency(%q<magic_bytes>, [">= 0"])
|
104
|
+
s.add_dependency(%q<simplecov>, [">= 0"])
|
105
|
+
s.add_dependency(%q<rack-cache>, [">= 0"])
|
106
|
+
s.add_dependency(%q<strenv>, [">= 0"])
|
107
|
+
s.add_dependency(%q<addressable>, [">= 0"])
|
108
|
+
s.add_dependency(%q<rack>, ["~> 1"])
|
109
|
+
s.add_dependency(%q<rack-test>, [">= 0"])
|
110
|
+
s.add_dependency(%q<foreman>, [">= 0"])
|
111
|
+
s.add_dependency(%q<rspec>, ["< 3.3", "~> 3.2"])
|
112
|
+
s.add_dependency(%q<rake>, ["~> 10"])
|
113
|
+
s.add_dependency(%q<jeweler>, [">= 0"])
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# Applies ImageMagick auto_orient to the image, so that i.e. mobile photos
|
2
|
+
# can be oriented correctly. The operation is applied destructively (changes actual pixel data)
|
3
|
+
#
|
4
|
+
# The corresponding Pipeline method is `auto_orient`.
|
5
|
+
class ImageVise::AutoOrient
|
6
|
+
def apply!(magick_image)
|
7
|
+
magick_image.auto_orient!
|
8
|
+
end
|
9
|
+
ImageVise.add_operator 'auto_orient', self
|
10
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# Crops the image to the given dimensions with a given gravity. Gravities are shorthand versions
|
2
|
+
# of ImageMagick gravity parameters (see GRAVITY_PARAMS)
|
3
|
+
#
|
4
|
+
# The corresponding Pipeline method is `crop`.
|
5
|
+
class ImageVise::Crop < Ks.strict(:width, :height, :gravity)
|
6
|
+
GRAVITY_PARAMS = {
|
7
|
+
'nw' => Magick::NorthWestGravity,
|
8
|
+
'n' => Magick::NorthGravity,
|
9
|
+
'ne' => Magick::NorthEastGravity,
|
10
|
+
'w' => Magick::WestGravity,
|
11
|
+
'c' => Magick::CenterGravity,
|
12
|
+
'e' => Magick::EastGravity,
|
13
|
+
'sw' => Magick::SouthWestGravity,
|
14
|
+
's' => Magick::SouthGravity,
|
15
|
+
'se' => Magick::SouthEastGravity,
|
16
|
+
}
|
17
|
+
|
18
|
+
def initialize(*)
|
19
|
+
super
|
20
|
+
self.width = width.to_i
|
21
|
+
self.height = height.to_i
|
22
|
+
raise ArgumentError, ":width must positive" unless width > 0
|
23
|
+
raise ArgumentError, ":height must positive" unless height > 0
|
24
|
+
raise ArgumentError, ":gravity must be within the permitted values" unless GRAVITY_PARAMS.key? gravity
|
25
|
+
end
|
26
|
+
|
27
|
+
def apply!(image)
|
28
|
+
image.crop!(GRAVITY_PARAMS.fetch(gravity), width, height, remove_padding_data_outside_window = true)
|
29
|
+
end
|
30
|
+
|
31
|
+
ImageVise.add_operator 'crop', self
|
32
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# Applies an elliptic stencil around the entire image. The stencil will fit inside the image boundaries,
|
2
|
+
# with about 1 pixel cushion on each side to provide smooth anti-aliased edges. If the input image to be
|
3
|
+
# provessed is square, the ellipse will turn into a neat circle.
|
4
|
+
#
|
5
|
+
# This adds an alpha channel to the image being processed (and premultiplies the RGB channels by it). This
|
6
|
+
# will force the RenderEngine to return the processed image as a PNG in all cases, instead of keeping it
|
7
|
+
# in the original format.
|
8
|
+
#
|
9
|
+
# The corresponding Pipeline method is `ellipse_stencil`.
|
10
|
+
class ImageVise::EllipseStencil
|
11
|
+
C_black = 'black'.freeze
|
12
|
+
private_constant :C_black
|
13
|
+
|
14
|
+
def apply!(magick_image)
|
15
|
+
# http://stackoverflow.com/a/13329959/153886
|
16
|
+
width, height = magick_image.columns, magick_image.rows
|
17
|
+
|
18
|
+
center_x = (width / 2.0)
|
19
|
+
center_y = (height / 2.0)
|
20
|
+
# Make sure all the edges are anti-aliased
|
21
|
+
radius_width = center_x - 1.5
|
22
|
+
radius_height = center_y - 1.5
|
23
|
+
|
24
|
+
gc = Magick::Draw.new
|
25
|
+
gc.fill C_black
|
26
|
+
gc.ellipse(center_x, center_y, radius_width, radius_height, deg_start=0, deg_end=360)
|
27
|
+
|
28
|
+
circle_img = Magick::Image.new(width, height)
|
29
|
+
gc.draw(circle_img)
|
30
|
+
|
31
|
+
mask = circle_img.negate
|
32
|
+
mask.matte = false
|
33
|
+
|
34
|
+
magick_image.matte = true
|
35
|
+
magick_image.composite!(mask, Magick::CenterGravity, Magick::CopyOpacityCompositeOp)
|
36
|
+
ensure
|
37
|
+
[mask, gc, circle_img].each do |maybe_image|
|
38
|
+
ImageVise.destroy(maybe_image)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
ImageVise.add_operator 'ellipse_stencil', self
|
43
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Wrappers a given Tempfile for a Rack response.
|
2
|
+
# Will close _and_ unlink the Tempfile it contains.
|
3
|
+
class ImageVise::FileResponse
|
4
|
+
ONE_CHUNK_BYTES = 1024 * 512
|
5
|
+
def initialize(file)
|
6
|
+
@file = file
|
7
|
+
end
|
8
|
+
|
9
|
+
def each
|
10
|
+
@file.flush # Make sure all the writes have been synchronized
|
11
|
+
# We can easily open another file descriptor
|
12
|
+
File.open(@file.path, 'rb') do |my_file_descriptor|
|
13
|
+
while data = my_file_descriptor.read(ONE_CHUNK_BYTES)
|
14
|
+
yield(data)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def close
|
20
|
+
@file.close
|
21
|
+
@file.unlink
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Fits the image based on the smaller-side fit. This means that the image is going to be fit
|
2
|
+
# into the requested rectangle so that all of the pixels of the rectangle are filled. The
|
3
|
+
# gravity parameter defines the crop gravity (on corners, sides, or in the middle).
|
4
|
+
#
|
5
|
+
# The corresponding Pipeline method is `fit_crop`.
|
6
|
+
class ImageVise::FitCrop < Ks.strict(:width, :height, :gravity)
|
7
|
+
GRAVITY_PARAMS = {
|
8
|
+
'nw' => Magick::NorthWestGravity,
|
9
|
+
'n' => Magick::NorthGravity,
|
10
|
+
'ne' => Magick::NorthEastGravity,
|
11
|
+
'w' => Magick::WestGravity,
|
12
|
+
'c' => Magick::CenterGravity,
|
13
|
+
'e' => Magick::EastGravity,
|
14
|
+
'sw' => Magick::SouthWestGravity,
|
15
|
+
's' => Magick::SouthGravity,
|
16
|
+
'se' => Magick::SouthEastGravity,
|
17
|
+
}
|
18
|
+
|
19
|
+
def initialize(*)
|
20
|
+
super
|
21
|
+
self.width = width.to_i
|
22
|
+
self.height = height.to_i
|
23
|
+
raise ArgumentError, ":width must positive" unless width > 0
|
24
|
+
raise ArgumentError, ":height must positive" unless height > 0
|
25
|
+
raise ArgumentError, ":gravity must be within the permitted values" unless GRAVITY_PARAMS.key? gravity
|
26
|
+
end
|
27
|
+
|
28
|
+
def apply!(magick_image)
|
29
|
+
magick_image.resize_to_fill! width, height, GRAVITY_PARAMS.fetch(gravity)
|
30
|
+
end
|
31
|
+
|
32
|
+
ImageVise.add_operator 'fit_crop', self
|
33
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# Applies a transformation using an ImageMagick geometry string
|
2
|
+
#
|
3
|
+
# The corresponding Pipeline method is `geom`.
|
4
|
+
class ImageVise::Geom < Ks.strict(:geometry_string)
|
5
|
+
def initialize(*)
|
6
|
+
super
|
7
|
+
self.geometry_string = geometry_string.to_s
|
8
|
+
raise ArgumentError, "the :geom parameter must be present and not empty" if self.geometry_string.empty?
|
9
|
+
end
|
10
|
+
|
11
|
+
def apply!(image)
|
12
|
+
image.change_geometry(geometry_string) { |cols, rows, _| image.resize!(cols,rows) }
|
13
|
+
end
|
14
|
+
|
15
|
+
ImageVise.add_operator 'geom', self
|
16
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
class ImageVise::ImageRequest < Ks.strict(:src_url, :pipeline)
|
4
|
+
class InvalidRequest < ArgumentError; end
|
5
|
+
class SignatureError < InvalidRequest; end
|
6
|
+
class URLError < InvalidRequest; end
|
7
|
+
class MissingParameter < InvalidRequest; end
|
8
|
+
|
9
|
+
# Initializes a new ParamsChecker from given HTTP server framework
|
10
|
+
# params. The params can be symbol- or string-keyed, does not matter.
|
11
|
+
def self.to_request(qs_params:, secrets:, permitted_source_hosts:)
|
12
|
+
base64_encoded_params = qs_params.fetch(:q) rescue qs_params.fetch('q')
|
13
|
+
given_signature = qs_params.fetch(:sig) rescue qs_params.fetch('sig')
|
14
|
+
|
15
|
+
# Decode Base64 first - this gives us a stable serialized form of the request parameters
|
16
|
+
decoded_json = Base64.decode64(base64_encoded_params)
|
17
|
+
|
18
|
+
# Check the signature before decoding JSON (since we will be creating symbols and stuff)
|
19
|
+
raise SignatureError, "Invalid or missing signature" unless valid_signature?(decoded_json, given_signature, secrets)
|
20
|
+
|
21
|
+
# Decode the JSON
|
22
|
+
params = JSON.parse(decoded_json, symbolize_names: true)
|
23
|
+
|
24
|
+
# Pick up the URL and validate it
|
25
|
+
src_url = params.fetch(:src_url).to_s
|
26
|
+
raise URLError, "the :src_url parameter must be non-empty" if src_url.empty?
|
27
|
+
raise URLError, "#{src_url} is not permitted as source" unless valid_host?(src_url, permitted_source_hosts)
|
28
|
+
|
29
|
+
# Build out the processing pipeline
|
30
|
+
pipeline_definition = params.fetch(:pipeline)
|
31
|
+
|
32
|
+
new(src_url: src_url, pipeline: ImageVise::Pipeline.from_param(pipeline_definition))
|
33
|
+
rescue KeyError => e
|
34
|
+
raise InvalidRequest.new(e.message)
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_query_string_params(signed_with_secret)
|
38
|
+
payload = JSON.dump(to_h)
|
39
|
+
{q: Base64.strict_encode64(payload), sig: OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, signed_with_secret, payload)}
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_h
|
43
|
+
{pipeline: pipeline.to_params, src_url: src_url}
|
44
|
+
end
|
45
|
+
|
46
|
+
def cache_etag
|
47
|
+
Digest::SHA1.hexdigest(JSON.dump(to_h))
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def self.valid_signature?(for_payload, given_signature, secrets)
|
53
|
+
# Check the signature against every key that we have,
|
54
|
+
# since different apps might be using different keys
|
55
|
+
seen_valid_signature = false
|
56
|
+
secrets.each do | stored_secret |
|
57
|
+
expected_signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, stored_secret, for_payload)
|
58
|
+
result_for_this_key = Rack::Utils.secure_compare(expected_signature, given_signature)
|
59
|
+
seen_valid_signature ||= result_for_this_key
|
60
|
+
end
|
61
|
+
seen_valid_signature
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.valid_host?(src_url, permitted_hosts)
|
65
|
+
parsed_url = URI.parse(src_url)
|
66
|
+
permitted_hosts.include?(parsed_url.host)
|
67
|
+
end
|
68
|
+
end
|