imageproxy

A image processing proxy server, written in Ruby as a Rack application.

There are many possible uses for this, but one major use is to resize images on-the-fly directly from HTML code, rather than processing the image when it is first uploaded or created. For example, if a user uploads a file to your site and it gets stored in http://example.com/uploads/39c0c11af.png, you could show a 50x50 px version like this:

<img src="http://example.com/convert?resize=50x50&source=http%3A%2F%2Fexample.com%2Fuploads%2F39c0c11af.png">

If you ever decided to change the size, you wouldn't have to re-encode anything, just change the HTML:

<img src="http://example.com/convert?resize=75x75&source=http%3A%2F%2Fexample.com%2Fuploads%2F39c0c11af.png">

See http://imageproxy.heroku.com/selftest for some examples (it's running on Heroku's free plan, so it will be a bit slow).

Status

imageproxy is being used in production on at least one commercial website, keeping two big EC2 servers busy. There are definitely some feature and performance improvements that can be/need to be made. Suggestions and pull requests are welcome.

Current Features

  • display information about an image
  • resize, flip, rotate, change format and change quality of images
  • standard query-parameter based URLs as well as Amazon CloudFront-compatibile URLs
  • tested on Heroku and Amazon EC2
  • use the requester's user agent string
  • signed requests (to stop unauthorized use)
  • obfuscated params

Future Features

Feel free to help out with some of these :)

  • specify crop size and offset
  • create rounded corners
  • Rails helper for generating image tags that use imageproxy
  • X-Sendfile / X-Accel-Redirect header
  • better documentation
  • signature generation testing tool
  • nice error messages for improper API use
  • performance

See also the release notes

Performance

imageproxy doesn't do any sort of caching. That kind of thing is better left up to CDNs (like Amazon CloudFront or VoxCast CDN) or to caching proxies such as Varnish.

Also, imageproxy itself isn't nearly as fast as it could be. It's written in an interpreted language, and it shells out to curl and ImageMagick to do its work. Presumably, it would be way faster written in C as an Apache module or something.

Requirements

Installing

gem install imageproxy

API

There are two major functions: identify and convert, plus a helpful selftest function.

Identify

identify spits out a lot of information about the provided image.

Parameters

source (Required) The URL of the image to identify.

signature To stop unauthorized use. See the "Signing Requests" section of this document.

Convert

convert converts an image.

Parameters

source (Required) The URL of the image to convert. (Also aliased to src.)

resize The new size of the image, in "WxH" format (e.g., 20x30).

thumbnail The new size of the image, in "WxH" format (e.g., 20x30). Thumbnailing assumes the resulting image will be pretty small and makes some optimizations.

shape The shape of the image, when resizeing or thumbnailing to a different aspect ratio. The value can be preserve which will preserve the original aspect ratio, pad which will add padding to keep the proper aspect ratio (you can supply a background parameter to choose the background color to pad with, or leave blank to pad with transparent color if the image format allows it), and cut which will cut the image to fit the new size. The default is preserve.

flip Flip the image. The value can be horizontal or vertical.

rotate Rotate the image. The value can be any number. When rotating to a non-right-angle, you can specify the background parameter to choose the color for the background.

format Change the format. Possible formats include gif, jpg, png, png8, etc.

quality Choose the compression quality for formats that support lossy compression. The value can be any number from 0 to 100. The default is to use Imagemagick's default quality settings.

progressive Choose whether a JPEG image should be a progressive JPEG or not. Possible values are true and false. The default is false.

background Some operations allow for a background color to be provided. The format is hex (e.g., #ff00ff) or rgba (e.g., rgba(20,30,19,0.4))

signature To stop unauthorized use. See the "Signing Requests" section of this document.

Request Format

The request must start with identify or convert (for backwards-compatibility, process is a synonym for convert).

The parameters can be query string parameters, like this:

http://example.com/convert?resize=100x100&shape=cut

Or, the parameters can be Amazon CloudFront-compatible URLs, like this:

http://example.com/convert/resize/100x100/shape/cut

(Note that CloudFront now optionally support query parameters.)

You can also mix the parameters if you like. This doesn't make much sense except for the case of the signature parameter which must be a query param:

http://example.com/convert/resize/100x100?signature=szFGj470w%2ByhJYJfTRryFLF9msA%3D

Important: Make sure to URL escape all query parameters. When using the CloudFront-compatible URL format, make sure to double-escape the source URL:

http://example.com/convert?resize=100x100&source=http://www.google.com/images/logos/ps_logo2.png # WRONG - not escaped
http://example.com/convert?resize=100x100&source=http%3A%2F%2Fwww.google.com%2Fimages%2Flogos%2Fps_logo2.png # RIGHT - escaped

http://exampe.com/convert/resize/100x100/source/http://www.google.com/images/logos/ps_logo2.png # WRONG - not escaped
http://exampe.com/convert/resize/100x100/source/http%3A%2F%2Fwww.google.com%2Fimages%2Flogos%2Fps_logo2.png # WRONG - only escaped once
http://exampe.com/convert/resize/100x100/source/http%253A%252F%252Fwww.google.com%252Fimages%252Flogos%252Fps_logo2.png # RIGHT - escaped and then escaped again

Signing Requests

To require that requests are signed, set the following two environment variables:

IMAGEPROXY_SIGNATURE_REQUIRED=true
IMAGEPROXY_SIGNATURE_SECRET=some secret key

Then add a signature parameter to your query string or path.

The signature is calculated with the following formula, which is the same formula that Amazon Web Services uses:

URLSafeBase64( HMAC-SHA1( UTF-8-Encoding-Of( YourSecretKey, StringToSign ) ) );

Where YourSecretKey is a secret key that you make up, and StringToSign is the full path of the request, excluding host name and the "signature" parameter, in the same order as in the query. For http://example.com/convert/resize/100x100?shape=cut&signature=szFGj470w%2ByhJYJfTRryFLF9msA%3D the StringToSign would be /convert/resize/100x100?shape=cut.

URL safe base64 is the normal encoding but with replacing the + with - and / with _.

Example Ruby code to generate the signature:

digest = OpenSSL::Digest::Digest.new("sha1")
Base64.encode64(OpenSSL::HMAC.digest(digest, your_secret_key, your_query_string)).strip.tr('+/', '-_')

Other Server Configuration

IMAGEPROXY_TIMEOUT maximum duration (in whole seconds) for downloading a source image (not recommended if you're using Ruby 1.8)

IMAGEPROXY_CACHE_TIME value of the max-age header for the converted image

IMAGEPROXY_VERBOSE enables full Ruby stacktraces in your error log

IMAGEPROXY_ALLOWED_DOMAINS A comma-separated list of second-level domains (e.g., "example.com, example.org") that are valid domains for the source parameter. If not specified, then the source parameter can reference any domain.

IMAGEPROXY_MAX_SIZE The maximum dimension allowed for a resize or thumbnail operation. Specifying 20 would cause a resize of 10x30 to fail because the maximum dimension of 20 is less than the largest requested dimension of 30.

IMAGEPROXY_WORLD_READABLE_TEMPFILE set to true if you want the generated tempfiles to be -rw-r--r-- instead of the default -rw-------

Obfuscating Requests

You may obfuscate your requests by Base64 encoding and then URL encoding your query string or path. The parameter name for this encoded value is _ if you're using a query string or - if you're using a path. Example:

http://example.com/convert?src=http://example.com/dog.jpg&resize=10x10
http://example.com/convert?_=c3JjPWh0dHA6Ly9leGFtcGxlLmNvbS9kb2cuanBnJnJlc2l6ZT0xMHgxMA%3D%3D
http://example.com/convert/-/c3JjPWh0dHA6Ly9leGFtcGxlLmNvbS9kb2cuanBnJnJlc2l6ZT0xMHgxMA%3D%3D

You can also replace the "=" characters in the Base64-encoded strings with "." characters which may make it possible to avoid having to percent-escape.

Example requests

CloudFront-compatible URLs:

http://example.com/convert/resize/100x100/source/http%253A%252F%252Fwww.google.com%252Fimages%252Flogos%252Fps_logo2.png
http://example.com/identify/source/http%253A%252F%252Fwww.google.com%252Fimages%252Flogos%252Fps_logo2.png

Regular query string URLs:

http://example.com/convert?resize=100x100&source=http%253A%252F%252Fwww.google.com%252Fimages%252Flogos%252Fps_logo2.png
http://example.com/identify?source=http%253A%252F%252Fwww.google.com%252Fimages%252Flogos%252Fps_logo2.png

Resize:

http://example.com/convert?resize=100x100&source=http%253A%252F%252Fwww.google.com%252Fimages%252Flogos%252Fps_logo2.png

Resize with padding:

http://example.com/convert?resize=100x100&shape=pad&source=http%253A%252F%252Fwww.google.com%252Fimages%252Flogos%252Fps_logo2.png
http://example.com/convert?resize=100x100&shape=pad&background=%23ff00ff&source=http%253A%252F%252Fwww.google.com%252Fimages%252Flogos%252Fps_logo2.png

Resize with cutting:

http://example.com/convert?resize=100x100&shape=cut&source=http%253A%252F%252Fwww.google.com%252Fimages%252Flogos%252Fps_logo2.png

Flipping:

http://example.com/convert?flip=horizontal&source=http%253A%252F%252Fwww.google.com%252Fimages%252Flogos%252Fps_logo2.png
http://example.com/convert?flip=vertical&source=http%253A%252F%252Fwww.google.com%252Fimages%252Flogos%252Fps_logo2.png

Rotating:

http://example.com/convert?rotate=90&source=http%253A%252F%252Fwww.google.com%252Fimages%252Flogos%252Fps_logo2.png
http://example.com/convert?rotate=120&source=http%253A%252F%252Fwww.google.com%252Fimages%252Flogos%252Fps_logo2.png
http://example.com/convert?rotate=120&background=%23ff00ff&source=http%253A%252F%252Fwww.google.com%252Fimages%252Flogos%252Fps_logo2.png

Combo:

http://example.com/convert?resize=100x100&shape=cut&rotate=45&background=%23ff00ff&source=http%253A%252F%252Fwww.google.com%252Fimages%252Flogos%252Fps_logo2.png

With signature (signed with secret key "SEEKRET"):

http://example.com/convert/resize/100x100/source/http%253A%252F%252Fwww.google.com%252Fimages%252Flogos%252Fps_logo2.png?signature=iNoljMh0kALsoxRLzJfr7Wcq%2BnY%3D
http://example.com/convert?resize=100x100&source=http%253A%252F%252Fwww.google.com%252Fimages%252Flogos%252Fps_logo2.png&signature=KLga1QNdCY8Xu4thsKdbTUjnYAk%3D

Selftest

You can go to the /selftest URL to see everything in action. For example: http://imageproxy.heroku.com/selftest.

Sample EC2 Installation Recipe

Create and boot an instance of an AWS Linux AMI using Amazon's EC2 console. In this example, I used a "micro" instance.

ssh into the instance as user ec2-user

Make a directory for the proxy:

sudo mkdir /opt/imageproxy
sudo chown ec2-user:ec2-user /opt/imageproxy

Install the Ruby HTTP stack + ImageMagick:

sudo yum -y install make gcc gcc-c++ http rubygems ruby-devel openssl-devel zlib-devel httpd-devel git curl-devel openssl ImageMagick ImageMagick-devel

Install passenger:

sudo gem install passenger
sudo passenger-install-apache2-module

Update the Apache config as suggested by the passenger installer

LoadModule passenger_module /usr/lib/ruby/gems/1.8/gems/passenger-3.0.5/ext/apache2/mod_passenger.so
PassengerRoot /usr/lib/ruby/gems/1.8/gems/passenger-3.0.5
PassengerRuby /usr/bin/ruby

Set up a VirtualHost as suggested by the passenger installer

<VirtualHost *:80>
    ServerName ec2-184-72-213-98.compute-1.amazonaws.com
    DocumentRoot /opt/imageproxy/public
    <Directory /opt/imageproxy/public>
        Allow from all
        Options -MultiViews
    </Directory>
</VirtualHost>

Clone the imageproxy code:

git clone git://github.com/eahanson/imageproxy.git

Install the gems:

bundle install

Start Apache:

sudo /etc/init.d/httpd start

Sample Heroku Installation Recipe

Clone the imageproxy code:

git clone git://github.com/eahanson/imageproxy.git

Set up Heroku:

As of Aug 2012 cedar does not work out of the box but bamboo does (specify bamboo as an option to the heroku create command).

http://devcenter.heroku.com/articles/quickstart

Deploy:

git push heroku master

If You Want To Modify The Code

Tun run the server locally:

rackup

Make sure everthing is working:

http://localhost:9292/selftest

To run the specs

rake spec
  • Thumbor is similar but has "smart crop" using various detection algorithms and some other features.

Thanks

Thanks to the following for code contributions:

License

Licensed under the MIT license. See LICENSE.txt.