Version 2.0 of DADI CDN introduced support for plugins, which are modular pieces of user-defined logic with the power to extend the functionality of the core application. They’re analogous to API hooks and Web events, as they allow scripts at a project level to change the normal course of a request with custom behavior.

For maximum flexibility, we have introduced three types of plugin, each one triggered at a different point in the lifecycle of a request.

Let’s go through them and see what each one can be used for.

Pre-processing plugins

Pre-processing plugins are executed before any image processing begins. All they can do is change the set of manipulation options, which come from either the query parameters in the URL, from the settings block of a recipe, or both.

In reality, a pre-processing plugin is similar to a recipe, as they both allow a group of manipulation options to be applied to an image, but whilst a recipe defines its parameters as a static JSON object, a plugin applies them dynamically using arbitrary code. As an example, this allows a parameter to be applied only if certain conditions are met.

Let’s create a simple example plugin: when a request is made in hours of darkness, say between 7pm and 7am (or 3pm to 11am if you’re in London during winter), we want to bring the saturation down to make the output image black and white.

Pre-processing plugins are loaded from recipes, so the first step is to create one.

workspace/recipes/daylight.json

{

"recipe": "daylight",

"plugins": ["night-mode"]

}

When a recipe contains a plugins array, the corresponding plugins will be executed sequentially, from left to right. In our example, we’re loading a single plugin, but you can use as many as you want. Now, we need to create the night-mode plugin we’ve referenced.

workspace/plugins/night-mode.js

module.exports.pre = ({options, url}) => {

let hour = new Date().getHours()



// Are we between 7pm and 7am?

if ((hour < 7) || (hour >= 19)) {

options.saturate = -100

}

}

After reloading the application, requesting an image with the path /daylight/image.jpg will load the recipe and the plugin, so you should see the saturation going down depending on the time of the day. Cool, right?

Note that nowhere in the recipe file are we telling CDN that night-mode is a pre-processing plugin. This is because plugins themselves are responsible for declaring their type, by exporting the right functions from the module – you can see that our example above is exporting a single function named pre , which makes it a pre-processing plugin.

Post-processing plugins

The execution of post-processing plugins takes place after CDN’s image processor has finished applying all the manipulation parameters. At this point, plugins have the ability to apply additional transformations to the image before it’s delivered to the user.

Whilst pre-processing plugins only get the options object and the request URL, post-processing plugins receive a whole bunch of things, including direct access to the image processing engine. This is where things start to get interesting.

This article describes a super cool image filter called duotone, where two colors (one for the highlights, one for the shadows) replace all the others in an image. Let’s see how we can build that effect as a CDN plugin.

Post-processing plugins, too, are loaded via recipes, so let’s start by creating one.

workspace/recipes/duotone.json

{

"recipe": "duotone",

"plugins": ["duotone-plugin"]

}

Then, let’s create the plugin script.

workspace/plugins/duotone-plugin.js

module.exports.post = ({jsonData, options, processor, sharp, stream, url}) => {

const parsedUrl = urlParser.parse(url, true)

const colourHighlight = parsedUrl.query.highlight || '#f00e2e'

const colourShadow = parsedUrl.query.shadow || '#192550'

const duotoneGradient = createDuotoneGradient(

hexToRgb(colourHighlight),

hexToRgb(colourShadow)

) return processor

.raw()

.toBuffer({resolveWithObject: true})

.then(({data, info}) => {

for (let i = 0; i < data.length; i = i + info.channels) {

const r = data[i + 0]

const g = data[i + 1]

const b = data[i + 2] const avg = Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b) data[i + 0] = duotoneGradient[avg][0]

data[i + 1] = duotoneGradient[avg][1]

data[i + 2] = duotoneGradient[avg][2]

} return sharp(data, {

raw: info,

}).toFormat(options.format).toBuffer()

})

.then(buffer => {

let bufferStream = new PassThrough() bufferStream.end(buffer) return bufferStream

})

}

Note: additional functions used by the code above were ommitted for simplification purposes. You can find the full source code for the duotone plugin here.

We start by defining a post export to declare that this is a post-processing plugin. You can see that the function argument contains a lot more properties, such as the Sharp image processing module ( sharp ), the instance of the engine used to process the current image ( processor ) and a JSON payload with image metadata ( jsonData ).

Also worth noting is the stream object. A post-processing plugin can choose to simply add operations to the image processing instance that is about to be delivered to the user, or they can choose to perform more complex operations using a new instance of the engine or a different engine altogether. When this happens, the plugin must resolve with a Stream, and the next plugin in the chain (if any) will receive it in the stream property.

As a consequence of the above, it is a good practice for a post-processing plugin to check the value of the stream parameter before deciding what source to use as a starting point, as another plugin might have done some work upstream. We’re keeping this out of the example plugin for simplification purposes.

The plugin is parsing the request URL in order to look for the highlight and shadow query parameters, which it expects in the form of HEX colors. Here’s how it looks.

/kanye.png