https://github.com/g-wilson/wrender
Image compression and transformation reverse-proxy for Express apps
https://github.com/g-wilson/wrender
cdn compression express image-processing jpeg libvips nodejs responsive-images sharp
Last synced: 1 day ago
JSON representation
Image compression and transformation reverse-proxy for Express apps
- Host: GitHub
- URL: https://github.com/g-wilson/wrender
- Owner: g-wilson
- License: mit
- Archived: true
- Created: 2017-01-21T17:51:14.000Z (over 9 years ago)
- Default Branch: master
- Last Pushed: 2020-12-10T17:17:51.000Z (over 5 years ago)
- Last Synced: 2025-09-29T08:52:47.489Z (9 months ago)
- Topics: cdn, compression, express, image-processing, jpeg, libvips, nodejs, responsive-images, sharp
- Language: JavaScript
- Size: 3.81 MB
- Stars: 14
- Watchers: 2
- Forks: 1
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- awesome-github-projects - wrender - Image compression and transformation reverse-proxy for Express apps ⭐14 `JavaScript` (📦 Legacy & Inactive Projects)
README
# Wrender
[](https://www.npmjs.com/package/wrender)
[](https://github.com/g-wilson/wrender/actions)
High-performance image compression and transformation reverse-proxy for Node.js Express apps.
----
This _library_ can be used to serve up compressed and transformed images from a high-resolution origin (e.g. Amazon S3) suitable for caching and delivery by a CDN.
It provides features comparable to [Imgix](https://www.imgix.com/) and [Cloudinary](https://cloudinary.com/) for environments where you want much more customisation for how you source and handle your images. You will of course need your own CDN!
It is an open-source re-implementation of [Car Throttle](https://www.carthrottle.com/about/)'s image delivery service, which (running behind Cloudfront) handles hundreds of GBs and tens of millions of requests every day.
The image processing is extremely fast and is handled by [Sharp](https://github.com/lovell/sharp), which implements the [libvips](https://github.com/jcupitt/libvips) library as a native module. As such, Node.js [Streams](https://nodejs.org/api/stream.html) are used to abstract the handling of image data.
The recommended usage is part of a larger [express](https://expressjs.com)-based application although a simple server is provided for example, testing and non-production environments. Rate-limiting, authentication, logging, and other such features are best implemented alongside with relevant packages and therefore are not provided here, although we do present a few examples to better demonstrate certain use-cases.
----
Use version 3.x.x for Node 12+
Use version 2.x.x for Node 10
**Breaking changes:**
- Recipe handler functions are treated as `async` functions, so they can now return a promise. This lets you use the Sharp [metadata api](https://sharp.pixelplumbing.com/api-input) inside recipes.
- Recipe handler functions must return the Sharp `image` object for the pipeline to work.
----
## Compression Defaults
- All images are converted to JPEG and compressed at quality level 85.
- All EXIF data is stripped (including colour profiles).
- All images are converted to sRGB colour space.
- If include EXIF is set to true, all metadata is preserved, and an sRGB ICC colour profile is assigned.
- Cache headers are set expire 1 year in the future. Set your web server or CDN to respect the headers.
- Source images larger than 3000px in each dimension are not transformed and an error response is sent.
## Usage
```js
const express = require('express');
const wrender = require('wrender');
const app = express();
const instance = wrender({
quality: 90,
maxAge: 86400,
});
app.use('/images', instance);
```
For a complete example with full configuration object and defaults, see below.
## Recipes
Different strategies for image handling are defined as the first parameter of the URL path. All recipe paths contain `/:origin`, which refers to the specific origin the client wishes to use. Failure to end your recipe with `/:origin` will result in an error being thrown, so ideally you should configure recipes on boot. For example, a recipe of `/hello/:origin` would match:
```
/hello/https://static.carthrottle.com/workspace/uploads/articles/dsc_6267-56ead06f7fda8.jpg
# Note the protocol, that's important to allow HTTPS origins
# If the origin contains a query string, you must encode the URL first:
`/hello/http%3A%2F%2Fstatic.carthrottle.com%2Fworkspace%2Fuploads%2Farticles%2F%3Ffilename%3Ddsc_6267-56ead06f7fda8.jpg`
```
### Built-in recipes
These are the recipes that are attached if you omit `recipes` from the config object you supply to `wrender`.
- **Proxy**
- Exposed at `wrender.recipes.proxy`
- The default path is `/proxy/:origin`
- Applies compression to the source image, but no other transformations.
- **Resize**
- Exposed at `wrender.recipes.resize`
- The default path is `/resize/:width/:height/:origin`
- You can also resize by `:width` or `:height`, whilst maintaining the aspect ratio, by setting either `:width` or `:height` to `0`.
- **Crop**
- Exposed at `wrender.recipes.crop`
- The default path is `/crop/:width/:height/:origin`
- Resizes the source image to the desired dimensions (maintaining aspect ratio), then performs a crop from the centre.
## Origins
Origins describe where the original image content is coming from. They append the path in the recipe, replacing `/:origin` with their path, and can be used to obfuscate the original source of the images.
### Built-in origins
If you omit `origins` from the config object you supply to *wrender*, the default HTTP origin will be used.
- **HTTP**
- Fetch source images from an external HTTP(S) source
- Exposed at `wrender.origins.http()`
- Function taking entirely optional `opts`:
- `prefix` - add a prefix to the origin to avoid catch-all usage
- `defaults` - pass a set of default options to `request.defaults`
- `whitelist` - pass a whitelist in micromatch format for hostnames to allow (see examples)
- `blacklist` - pass a whitelist in micromatch format for hostnames to deny (see examples)
- The default path is `/:source`, which makes this origin act as a catch-all
- **If you require a query string** then you must url-encode the entire `:source`, otherwise Express will strip the query string
- **FS**
- Loads an image from the filesystem
- Exposed at `wrender.origins.fs()`
- Function taking opts
- `prefix` - optionally add a prefix to the origin to avoid catch-all usage
- `mount` - optionally define the start mount for the source, e.g. `/data`
- The default path is `/:source`, which makes this origin act as a catch-all
- **Identicon**
- Generates an image from the hash of an input token similar to GitHub
- Exposed at `wrender.origins.identicon()`
- URL params `/identicon/:token`
- `token` - input token which is hashed to generate the background colour (e.g. user id)
- Function taking opts
- `prefix` - optionally add a prefix to the origin to avoid catch-all usage (default 'identicon')
- `size` - size of the generated image (note: increasing this may impact memory usage). If `:size` or `:width` is used in the recipe params, the recipe params will overwrite the options here.
- `gridsize` - odd-numbered-integer to divide the image into pixels
- `saturation` - intensity of the foreground colour `[0, 1]`
- `lightness` - white/black level of the foreground colour `[0, 1]`
- `background` - rgb array for the background colour `[ r, g, b ] [0, 255]`
- `invert` - swaps the foreground and background colours (i.e. pixels are white on a coloured background)
- **Initials**
- Generates an plain background colour with a text overlay
- Exposed at `wrender.origins.initials()`
- URL params `/initials/:token/:text`
- `token` - input token which is hashed to generate the background colour (e.g. user id)
- `text` - text to overlay (keep to 1 or 2 characters for best results)
- Function taking opts
- `prefix` - optionally add a prefix to the origin to avoid catch-all usage (default 'initials')
- `size` - size of the generated image (note: increasing this may impact memory usage). If `:size` or `:width` is used in the recipe params, the recipe params will overwrite the options here.
- `saturation` - intensity of the background colour `[0, 1]`
- `lightness` - white/black level of the background colour `[0, 1]`
- `color` - text colour (default 'white')
- `font` - font family for the test (default 'sans-serif')
## API
```js
const wrender = require('wrender');
const instance = wrender({
// JPEG compression level to apply
quality: 85, // Default
// Optionally preserve original format
convertGIF: true, // Default
convertPNG: true, // Default
// Include source image metadata
includeEXIF: false, // Default
// Maximum output image dimensions allowed
maxWidth: 3000, // Default
maxHeight: 3000, // Default
// Response 'max age' cache header (seconds)
maxAge: 31536000, // Default
// Timeout for fetching source image
timeout: 10000, // Default
// Only allow specified UA
userAgent: 'Amazon CloudFront',
// Add a callback if an error if encountered
onError: e => { console.error(e) },
// You can specify your own recipes, or use the pre-defined ones, or both!
// Skip this property to use the default recipes
recipes: [
// You can pick recipes from wrender you want to use
wrender.recipes.proxy,
wrender.recipes.resize,
wrender.recipes.crop,
// Or you can attach custom recipes (see documentation below)
wrender.createRecipe('/mirror/:origin', async image => {
const resized = await wrender.invokeRecipe(wrender.recipes.resize, image, { width: 200, height: 200 });
return resized.flop();
}),
],
// If you want to use our recipes AND your own, that's easy to do too:
recipes: [
...wrender.recipes,
wrender.createRecipe('/tiny/:origin', image => {
return wrender.invokeRecipe(wrender.recipes.resize, image, { width: 100, height: 100 });
}),
wrender.createRecipe('/huge/:origin', image => {
return wrender.invokeRecipe(wrender.recipes.resize, image, { height: 1800, width: 2560 });
}),
]),
// Specify how images can be fetched from the origin.
origins: [
wrender.origins.http({
// Prefix the origin to allow multiple endpoints
prefix: '/http',
// Since the HTTP origin is based on Request, you can provide an object of defaults
// Underneath this will trigger `request.defaults` in an attempt to keep performance high
defaults: {
auth: { user: 'dan-kmemes-7312@hotmail.com', pass: 'correct-horse-battery-staple' },
},
}),
wrender.origins.fs({
// Prefix the origin as appropriate
prefix: '/fs',
// Pull data from a particular mount point
mount: '/data',
}),
// Custom origins (see documentation below)
wrender.createOrigin('/s3/:Bucket/:Key(*)', ({ source }) => {
// const s3 = new AWS.S3({ region: 'us-east-1' });
return s3.getObject({ Bucket, Key }).createReadStream();
});
// The default origin is HTTP, but without a prefix it acts as a catch-all
wrender.origins.http(),
],
});
app.use('/images', instance);
/**
* Available recipes:
* - /proxy/:origin
* - /resize/:width/:height/:origin
* - /crop/:width/:height/:origin
*
* Available origins:
* - /http/:url
* - /fs/:path
* - /s3/:Bucket/:Key
*
* All together, available routes are, noting that the instance is mounted to "/images":
* - /images/proxy/http/:url
* - /images/proxy/fs/:path
* - /images/proxy/s3/:Bucket/:Key
* - /images/resize/:width/:height/http/:url
* - /images/resize/:width/:height/fs/:path
* - /images/resize/:width/:height/s3/:Bucket/:Key
* - /images/crop/:width/:height/http/:url
* - /images/crop/:width/:height/fs/:path
* - /images/crop/:width/:height/s3/:Bucket/:Key
*/
```
## Error Handling
If an error is caught inside wrender's route handler, a blank 1x1 PNG is served as a response along with an appropriate error code (usually 404 or perhaps 500).
It is advised (but not required) to add a `onError` callback function to the constructor. This callback takes one argument (`error`) and is fired _after the response is sent_. You can use this callback to log errors wherever you like.
-----
## Custom Recipes
Custom recipes are designed to allow you complete customisation of how images are transformed before being served to the client, by using [the Sharp API](http://sharp.dimens.io/en/stable/api-operation).
Recipes are created using the `wrender.createRecipe` method with the following arguments:
```js
wrender.createRecipe(path, handler, config)
// Where `path` is a string defining the first part of the mount point, ending in /:origin
// Where `handler` is function, with the arguments (image, params)
// `image` is the Sharp instance, for you to instruct the transformation
// `params` is the req.params, which contain the variables in the route that you set with `path`, plus...
// `params.query` is req.query
// `params.path` is req.path
// `params.originalUrl` is req.originalUrl
// - it is always executed asynchronously (i.e. always treats the return value as a promise)
// - you *must* return the image (sharp object) at the end of the handler function
// Where `config` is a plain object containing overrides to the Wrender instance config scoped to the this recipe only. Useful for customising quality or GIF conversion on a URL-basis.
```
### Recipe examples
Asynchronous recipes are not supported. If you're looking to do an asynchronous operation with your recipe, consider using [the underlying `sharp` package](https://npm.im/sharp).
```js
wrender.createRecipe('/mirror/:origin', async image => {
const resized = await wrender.invokeRecipe(wrender.recipes.resize, image, { width: 200, height: 200 });
return rezized.flop();
})
```
This recipe will resize the image using the built-in resize recipe, to 200x200, then flop the image about the horizontal X axis, as [discussed in the Sharp API operation docs](https://sharp.pixelplumbing.com/api-operation#flop).
```js
wrender.createRecipe('/thumbnail/:source', image => wrender.invokeRecipe(wrender.recipes.resize, image, { width: 150 }))
```
By using `wrender.invokeRecipe(recipe, image, [params])` you can call existing recipes with pre-defined values. This is useful if you wish to hide options from the URLs to prevent undesired costs or DoS attacks:
```js
const watermark = new Buffer('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==', 'base64');
wrender.createRecipe('/watermark/:origin', async image => {
const resized = await wrender.invokeRecipe(wrender.recipes.resize, image, { width: 200, height: 200 });
return resized.composite([ { input: watermark, gravity: 'northeast' });
})
```
Following [the composite docs](https://sharp.pixelplumbing.com/api-composite), we can see how we would implement a watermark recipe.
-----
## Custom Origins
Not every use-case involves fetching information from a public-facing image endpoint. Therefore *wrender* support custom origins, which can also be used to obfuscate the source of your images.
Origins are created using the `wrender.createOrigin` method with the following arguments:
```js
wrender.createOrigin(path, handler)
// Where `path` is a string defining the last part of the mount point
// Where `handler` is a function, optionally async, with the arguments (params)
// `params` is the req.params, which contain the variables in the route that you set with `path`, plus...
// `params.query` is req.query
// `params.path` is req.path
// `params.originalUrl` is req.originalUrl
```
Ensure params in your origin paths are unique to your origin, as conflicting params with recipes will lead to unexpected behaviours. For example, a recipe with `/resize/:width/:height/:origin` and an origin with `/fb/:width/:profile_id` will lead to `/resize/:width/:height/fb/:width/:profile_id`. Not good!
`handler` expects [a readable stream](https://nodejs.org/api/stream.html#stream_readable_streams) to be returned. Origin functions can be async, allowing you to perform (hopefully) simple async operations, to a database or an external source.
### Origin examples
#### Whitelist/blacklist HTTP origins
It's likely you will want to run your HTTP(S) origins through a whitelist/blacklist, to ensure only origins you allow (or prevent origins you disallow) from being hit by your *wrender* instance. This is supported by default, and the micromatch syntax is supported:
```js
app.use('/images', wrender({
origins: [
wrender.origins.http({
// Only allow specified image hosts - uses micromatch syntax
whitelist: [ '**.giphy.com/**', 's3.amazonaws.com' ],
// Or blacklist specific image hosts - again, micromatch syntax
blacklist: [ 'hack.thepla.net' ],
}),
],
}));
// => /images/proxy/https://s3.amazonaws.com/user-uploads.someimportantcompany.com/profiles/1505c30c51bb93545db48919b3cce7f9.jpg
// => Will succeed, since s3.amazonaws.com is in the whitelist
// => /images/proxy/https://i.imgur.com/cl4Bu.gif
// => Will fail, since i.imgur.com isn't in the whitelist
// => /images/proxy/https://hack.thepla.net/evilcorp.exe
// => Hasn't got a chance, since it's not in the whitelist, and irrelevantly isn't in the blacklist
// => In this example, you would need to remove the whitelist array in order to only use the blacklist
```
#### Private S3 Buckets
```js
const wrender = require('wrender');
const AWS = require('aws-sdk');
const s3 = new AWS.S3({ region, secretAccessKey, accessKeyId }); // Load these from environment variables
module.exports = wrender.createOrigin('/s3/:Bucket/:Key(*)', ({ Bucket, Key }) => {
return s3.getObject({ Bucket, Key }).createReadStream()
});
// => /images/proxy/s3/user-uploads.someimportantcompany.com/profiles/1505c30c51bb93545db48919b3cce7f9.jpg
// => Streams from S3, as long as the s3 instance has the correct permissions
// => Super-effective with EC2 instance roles & ECS task roles
```
#### Facebook Profile Pictures
```js
const request = require('request');
const wrender = require('wrender');
module.exports = wrender.createOrigin('/fb/:profile_id', ({ profile_id }) => {
return request(`https://graph.facebook.com/${profile_id}/picture?width=1024&height=1024`);
});
// => /images/proxy/fb/113741208636938
// => https://graph.facebook.com/113741208636938/picture?width=1024&height=1024
```
This is also a good example for using custom origins to rewrite URLs.
#### Lookup image data from a model
```js
const wrender = require('wrender');
const AWS = require('aws-sdk');
const images = require('../models/images');
const s3 = new AWS.S3({ region, secretAccessKey, accessKeyId });
module.exports = wrender.createOrigin('/users/:image_id', async ({ image_id }) => {
const { bucket, key } = await images.findById(image_id);
return s3.getObject({ Bucket: bucket, Key: key }).createReadStream();
});
// => /images/resize/200/200/users/9ff4a3cf5fe1a735ec96f142a2081f3e
// => s3://user-uploads.someimportantcompany.com/profiles/9ff4a3cf5fe1a735ec96f142a2081f3e.jpg
```
Hopefully, `images.findById` will be nicely cached or easy to pull up.
-----
## Docker
```sh
$ docker build -t g-wilson/wrender:dev .
$ docker run -it -p 3010:3010 g-wilson/wrender:dev
```