A thumbnailing server
ImageVise
is an image-from-url-as-a-service server for use either standalone or within a larger Rails/Rack
framework. The main uses are:
- Image resizing on request
- Applying image filters
It is implemented as a Rack application that responds to any URL and accepts the following two last path
compnents, internally named request
and signature
:
-
request
- Base64 encoded JSON object withsrc_url
andpipeline
properties (the source URL of the image and processing steps to apply) -
signature
- the HMAC signature, computed over the JSON inq
before it gets Base64-encoded
A request to ImageVise
might look like this:
/acbhGyfhyYErghff/acfgheg123
The URL that gets generated is best composed with the included ImageVise.image_params
method. This method will
take care of encoding the source URL and the commands in the right way, as well as signing.
ImageMagick version workaround
As specified in this StackOverflow answer you need to install ImageMagick 6 from keg on OSX since RMagick cannot yet cope with ImageMagick 7.
$ brew rm imagemagick
$ brew install imagemagick@6 --with-little-cms --with-little-cms2
$ brew link imagemagick@6 --force
$ export PATH=$PATH:$(brew --prefix imagemagick@6)/bin
bundle install
Using ImageVise within a Rails application
Mount ImageVise in your routes.rb
:
mount '/images' => ImageVise
and add an initializer (like config/initializers/image_vise_config.rb
) to set up the permitted hosts
ImageVise.add_allowed_host! your_application_hostname
ImageVise.add_secret_key! ENV.fetch('IMAGE_VISE_SECRET')
You might want to define a helper method for generating signed URLs as well, which will look something like this:
def thumb_url(source_image_url)
path = ImageVise.image_path(src_url: source_image_url, secret: ENV.fetch('IMAGE_VISE_SECRET')) do |pipeline|
# For example, you can also yield `pipeline` to the caller
pipeline.fit_crop width: 128, height: 128, gravity: 'c'
end
'/images' + path
end
To preserve your sanity, make the route to the ImageVise engine terminal and do not perform rewrites on it in your webserver configuration - for instance, Base64 permits slashes.
Using ImageVise within a Rack application
Mount ImageVise under a script name in your config.ru
:
map '/images' do
run ImageVise
end
and add the initialization code either to config.ru
proper or to some file in your application:
ImageVise.add_allowed_host! your_application_hostname
ImageVise.add_secret_key! ENV.fetch('IMAGE_VISE_SECRET')
You might want to define a helper method for generating signed URLs as well, which will look something like this:
def thumb_url(source_image_url)
path_param = ImageVise.image_path(src_url: source_image_url, secret: ENV.fetch('IMAGE_VISE_SECRET')) do |pipe|
pipe.fit_crop width: 256, height: 256, gravity: 'c'
pipe.sharpen sigma: 0.5, radius: 2
pipe.ellipse_stencil
end
# Output a URL to the app
'/images' + path
end
Processing files on the local filesystem instead of remote ones
If you want to grab a local file, compose a file://
URL (mind the endcoding!)
src_url = 'file://' + ImageVise::FetcherFile.encode_file_uri_path(File.expand_path(my_pic))
Note that you need to permit certain glob patterns as sources before this will work, see below.
Operators and pipelining
ImageVise processes an image using operators. Each operator is just like an adjustment layer in Photoshop, except that it can also resize the canvas. If you are familiar with node-based compositing systems like Shake, Nuke or Fusion the pipeline is a node DAG with only one connection arrow going all the way. The operations are always applied in a destructive way, so that the additional intermediate versions don't have to be deallocated manually after processing.
Each Operator is described in the pipeline using a tuple (Array) of roughly this structure:
[<operator_name>, {"<operator_param1>": <operator_param1_value>}]
You can have an unlimited number of such Operators per thumbnail, and they all get encoded in the URL (well, technically, you are limited - by the URL length supported by your web server).
For example, you can use the pipeline to apply a sharpening operator after resising an image (for the lack of decent image filtering choices in ImageMagick proper).
Here is an example pipeline, JSON-encoded (this is what is passed in the URL):
[
["auto_orient", {}],
["geom", {"geometry_string": "512x512"}],
["fit_crop", {"width": 32, "height": 32, "gravity": "se"}],
["sharpen", {"radius": 0.75, "sigma": 0.5}],
["ellipse_stencil", {}]
]
The same pipeline can be created using the Pipeline
DSL:
pipe = Pipeline.new.
auto_orient.
geom(geometry_string: '512x512').
fit_crop(width: 32, height: 32, gravity: 'se').
sharpen(radius: 0.75, sigma: 0.5).
ellipse_stencil
and can then be applied to a Magick::Image
object:
image = Magick::Image.read(my_image_path)[0]
pipe.apply!(image)
Caching
The app is designed to be run behind a frontline HTTP cache. The easiest is to use Rack::Cache
, but this might
be instance-local depending on the storage backend used. A much better idea is to run ImageVise behind a long-caching
CDN.
Shared HMAC keys for signed URLs
To allow ImageVise
to recognize the signature when the signature is going to be received, add it to the list
of the shared keys on the ImageVise
server:
ImageVise.add_secret_key!('ahoy! this is a secret!')
A single ImageVise
server can maintain multiple signature keys, so that you will be able to generate thumbnails from
multiple applications all using different keys for their signatures. Every request will be validated against
each key and if at least one key generates the same signature for the same given parameters, it is going to be
accepted and the request will be allowed to go through.
Hostname and filesystem validation
By default, ImageVise
will refuse to process images from URLs on "unknown" hosts. To mark a host as "known"
tell ImageVise
to
ImageVise.add_allowed_host!('my-image-store.ourcompany.co.uk')
If you want to permit images from the local server filesystem to be accessed, add the glob pattern to the set of allowed filesystem patterns:
ImageVise.allow_filesystem_source!(Rails.root + '/public/*.jpg')
Note that these are glob patterns. The image path will be checked against them using File.fnmatch
.
Handling errors within the rendering Rack app
By default, the Rack app within ImageVise swallows all exceptions and returns the error message
within a machine-readable JSON payload. If that doesn't work for you, or you want to add error
handling using some error tracking provider, either subclass ImageVise::RenderEngine
or prepend
a module into it that will intercept the errors. See error handling in examples/
for more.
State
Except for the HTTP cache no state is stored (ImageVise
does not care whether you store
your images using Dragonfly, CarrierWave or some custom handling code). All the app needs is the full URL.
Running the tests, versioning, contributing
By default, bundle exec rake
will run RSpec and will also open the generated images using the $ open
command available
on your CLI. If you want to skip viewing those images, set the SKIP_INTERACTIVE
environment variable to any value.
The gem version is specified in image_vise.rb
. When contributing, please follow:
- Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
- Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
- Fork the project.
- Start a feature/bugfix branch.
- Commit and push until you are happy with your contribution.
- Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
- 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.
Copyright
Copyright (c) 2016 WeTransfer. See LICENSE.txt for further details.
The licensing terms also apply to the waterside_magic_hour.jpg
test image.
The worker_in_tube.jpg
is used with permission from Arcadis Nederland B.V.
The sRGB color profiles are downloaded from the ICC and it's use is governed by the terms present in the LICENSE.txt