Sharp is one of the most popular image processing libraries in Node.js. It is actually packaged with the LIBvips library written in C language, so its high performance is also a big selling point of Sharp. Sharp makes it easy to perform common image editing operations such as cropping, formatting, rotation, adding filters, and more. Of course, there are many articles on the web and Sharp’s official documentation is quite detailed, so this is not the focus of this article. The purpose of this article is to record some of the solutions I have encountered in using Sharp for complex image processing requirements. I hope to share them with you to help you.

Sharp basis

Sharp adopts streaming processing mode as a whole. After reading image data, it goes through a series of processing and then outputs the results. To understand this, look at a simple example:

const sharp = require('sharp');
sharp('input.jpg')
  .rotate()
  .resize(200)
  .toBuffer()
  .then( data= > ... )
  .catch( err= >...). ;Copy the code

Almost all of sharp’s function interfaces are mounted on sharp instances, so the first step in image processing must be to read the image data (the Sharp function takes the image local path or the image Buffer data as arguments) and convert it to a Sharp instance, followed by pipeline-like processing. Therefore, a preprocessor should be provided to convert the image received by the server to a Sharp instance:

/ * * * * @ param {String | Buffer} inputImg image local path or Buffer data * @ return {Sharp} * /
async convert2Sharp(inputImg) {
    return sharp(inputImg)
}
Copy the code

Then specific image processing can be carried out.

Add a watermark

The backend implementation

Add watermark function should be a relatively common image processing requirements. Sharp provides only one function for image composition: overlayWith, which takes an image parameter (also the image local path string or image Buffer data) and an optional options configuration object (to configure the location of the watermark image and other information) and then overwrites the image on the original image. The logic is also relatively simple. Our code looks like this:

@param {Sharp} @param {String} watermarkRaw watermarking @param {top} watermarkRaw watermarking @param {left} Distance between watermark and left edge of image */
async watermark(img, { watermarkRaw, top, left }) {
    const watermarkImg = await watermarkRaw.toBuffer()
    return img
        .overlayWith(watermarkImg, { top, left })
}
Copy the code

For simplicity, only the location of watermark images can be configured. Sharp also supports more complex configuration parameters, such as whether to paste multiple watermark images repeatedly and whether to paste only watermark images in the α channel. For details, please refer to the documents of overlayWith.

The front-end implementation

The front-end implementation needs to be mentioned in passing. Of course, if the server is to add watermarks to the image according to fixed rules (such as sina Weibo image watermarks placed in a fixed position), the front end does not have to do anything. But in some scenarios, such as online photo editing tools, users expect a WYSIWYG experience on the front end when adding watermarks. At this time, if the user adds the watermark and selects the location, the data must be sent to the server for processing and then get the processing result, which is bound to affect the smoothness of the whole service. Fortunately, the powerful HTML5 makes front-end functions more and more rich, and we can realize the function of adding watermarks in the front-end with the help of Canvas. The specific implementation details are not difficult, mainly using the drawImage method provided by Canvas, take a look at the example:

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext('2d');

/ / img: reproduction
// watermarkImg: watermark image
// x, y are the coordinates on the canvas where img is placed
ctx.drawImage(img, x, y);
ctx.drawImage(watermarkImg, x, y);
Copy the code

In fact, the whole function of adding watermark (select the original image, select the watermark image, set the watermark image position, get the image after adding watermark) can be completely completed by the front end. Of course, in order to pursue the integrity of the server function, it is recommended to use the model of front-end display + back-end processing.

Paste the text

The need to paste text is actually similar to adding a watermark. The only difference is that the watermarked image is replaced by text, and we may need to adjust the size and font of text. Ideas are also easier to think of, the text into the form of pictures can be. Here we use the text-to-SVG library to convert text to SVG. Using the features of SVG, we can easily set the font size, color and so on. Then call buffer. from to convert the SVG to Buffer data that Sharp can use. The last step is to add the same watermark as above.

const Text2SVG = require('text-to-svg')

@param {Sharp} img @param {String} text @param {Number} fontSize @param {String} Color Text color * @param {Number} left Distance from the left edge of the picture * @param {Number} top Distance from the top edge of the picture */
async pasteText(img, {
    text, fontSize, color, left, top,
}) {
    const text2SVG = Text2SVG.loadSync()
    const attributes = { fill: color }
    const options = {
        fontSize,
        anchor: 'top',
        attributes,
    }
    const svg = Buffer.from(text2SVG.getSVG(text, options))
    return img
        .overlayWith(svg, { left, top })
}
Copy the code

Stitching images

The operation of splicing images is the most complicated. Here we provide two configuration items: Mosaic mode (horizontal/vertical) and background color. The Mosaic mode is easy to understand. It simply arranges images horizontally or vertically. The background color is used to fill in the white space. When stitching pictures, the pictures should be centered according to the axis. Take the horizontal arrangement of pictures as an example, the schematic diagram is as follows:

There are no ready-made functions provided by Sharp, and everything is handled with a unique overlayWith. The use of overlayWith is to paste one image onto another, which is slightly different from our need for Mosaic images. We need to change the thinking: you can create a base map in advance, the background color can be determined according to the configuration value, and then paste all the images to be spliced on it, you can meet the requirements.

First we need to read the length and width of all images to be spliced. Assume that the stitching mode is horizontal stitching, then the width of the final generated picture is the sum of the width of all pictures, and the height is the maximum height of all pictures (for vertical stitching, the reverse is true) :

let totalWidth = 0
let totalHeight = 0
let maxWidth = 0
let maxHeight = 0
const imgMetadataList = []
// Get the width and height of all images, calculate the sum and the maximum value
for (let i = 0, j = imgList.length; i < j; i += i) {
    const { width, height } = await imgList[i].metadata()
    imgMetadataList.push({ width, height })
    totalHeight += height
    totalWidth += width
    maxHeight = Math.max(maxHeight, height)
    maxWidth = Math.max(maxWidth, width)
}
Copy the code

Then we use the resulting width and height data to create a new base image with a background color for the incoming configuration (or white by default) :

const baseOpt = {
    width: mode === 'horizontal' ? totalWidth : maxWidth,
    height: mode === 'vertical' ? totalHeight : maxHeight,
    channels: 4.background: background || {
        r: 255.g: 255.b: 255.alpha: 1,}}const base = sharp({
    create: baseOpt,
}).jpeg().toBuffer()
Copy the code

Then, on the basis of the base images, the overlayWith function is repeatedly called to paste the images to be spliced onto the base images one by one. Note here is the location of the picture, the front also mentioned that we will be center aligned images according to the main axis, so every time you put the pictures to the calculation of the top and left (one is in the calculation, is calculated as the picture display order offset), of course, to understand the principle followed by primary school math problem, There’s not much to say. Another thing to note is that overlayWith can only compose two images at a time, so we use the Reduce method to continuously paste images onto the base image and use the results as input for the next time.

imgMetadataList.unshift({ width: 0.height: 0 })
let imgIndex = 0
const result = await imgList.reduce(async (input, overlay) => {
    const offsetOpt = {}
    if (mode === 'horizontal') {
        offsetOpt.left = imgMetadataList[imgIndex++].width
        offsetOpt.top = (maxHeight - imgMetadataList[imgIndex].height) / 2
    } else {
        offsetOpt.top = imgMetadataList[imgIndex++].height
        offsetOpt.left = (maxWidth - imgMetadataList[imgIndex].width) / 2
    }
    overlay = await overlay.toBuffer()
    return input.then(data= > sharp(data).overlayWith(overlay, offsetOpt).jpeg().toBuffer())
}, base)
return result
Copy the code

Here is the complete implementation of the Mosaic function:

@param {Array
      
       } imgList * @param {String} mode Horizontal (horizontal)/vertical(vertical) * @param {Object} background {r: 0-255, g: 0-255, b: 0-255, alpha: 0-1} 255, g: 255, b: 255, alpha: 1} */
      
async joinImage(imgList, { mode, background }) {
    let totalWidth = 0
    let totalHeight = 0
    let maxWidth = 0
    let maxHeight = 0
    const imgMetadataList = []
    // Get the width and height of all images, calculate the sum and the maximum value
    for (let i = 0, j = imgList.length; i < j; i += i) {
        const { width, height } = await imgList[i].metadata()
        imgMetadataList.push({ width, height })
        totalHeight += height
        totalWidth += width
        maxHeight = Math.max(maxHeight, height)
        maxWidth = Math.max(maxWidth, width)
    }

    const baseOpt = {
        width: mode === 'horizontal' ? totalWidth : maxWidth,
        height: mode === 'vertical' ? totalHeight : maxHeight,
        channels: 4.background: background || {
            r: 255.g: 255.b: 255.alpha: 1,}}const base = sharp({
        create: baseOpt,
    }).jpeg().toBuffer()

    // Get the original size of the image for offset
    imgMetadataList.unshift({ width: 0.height: 0 })
    let imgIndex = 0
    const result = await imgList.reduce(async (input, overlay) => {
        const offsetOpt = {}
        if (mode === 'horizontal') {
            offsetOpt.left = imgMetadataList[imgIndex++].width
            offsetOpt.top = (maxHeight - imgMetadataList[imgIndex].height) / 2
        } else {
            offsetOpt.top = imgMetadataList[imgIndex++].height
            offsetOpt.left = (maxWidth - imgMetadataList[imgIndex].width) / 2
        }
        overlay = await overlay.toBuffer()
        return input.then(data= > sharp(data).overlayWith(overlay, offsetOpt).jpeg().toBuffer())
    }, base)
    return result
},
Copy the code

These are some of the practical actions I have taken using Sharp. In fact, there are many advanced features of Sharp that I don’t use, which is the “80/20 rule” : 80% of requirements are usually done with 20% of features. I would like to share with you more examples of sharp in the future

This article was first published on my blog (click here to view it).