image_vise 0.0.16

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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
@@ -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