Image processing proxy that works via signed URLs
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
examples
lib
spec
.gitignore
.travis.yml
DEVELOPMENT.md
Gemfile
LICENSE.txt
README.md
Rakefile
SECURITY.md
image_vise.gemspec

README.md

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 with src_url and pipeline properties (the source URL of the image and processing steps to apply)
  • signature - the HMAC signature, computed over the JSON in q 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://' + URI.encode(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