Converting images with Node.js - Part 1: The out-of-the-box experience

This article will explain how to convert between image types with Node.js and Sharp. Only JPEG, PNG, WEBP, GIF, TIFF, AVIF are supported out-of-the-box. For other image types read one of the follow-up articles, as well as for file formats that aren’t technically images, but can be converted to images.

TL;DR

If you don’t care about theory, security, or best practices, run:

npm i sharp

Put the following in any javascript file:

example.js
import sharp from "sharp";

await sharp("image.png")
  .jpeg()
  .toFile("image.jpg");

Run with:

node example.js 

About Sharp

Sharp is a Node.js binding for libvips, an image transformation library written in C. Because Sharp is shipped with a prebuilt version of libvips it works out-of-the-box on most platforms and operating systems without requiring libvips itself to be installed.

n.b. the libvips build determines which codecs exist; Sharp cannot add a codec at runtime.

Due to its use of libvips, Sharp is typically four to five times faster at image transformations that other popular libraries such as ImageMagick and GraphicsMagick. For most transformations its speed is constrained by CPU instead of I/O, and parallelism is handled by Node worker threads under the hood instead of having to abuse Promise.all() in order to increase speed.

For general use cases such as thumbnail generation and image format conversion Sharp is fast enough that infrastructure decisions matter more than code optimizations.

Security Considerations

Even when using the out-of-the-box version of Sharp, user-supplied files are still untrusted input.

Codecs used by libvips are native open-source code, often written in C which is a low-level language without memory safety. Even though these core libraries and codecs are widely adopted and thoroughly tested by security researchers there is always potential for Common Vulnerabilities and Exposures (CVE) in their code.

Large or malformed images can cause excessive use of memory and be used as an attack vector for malicious actors to cause a denial-of-service (DoS) and image metadata can be abused to leak information.

Therefore it is recommended to set memory limits, timeouts, and pixel limits to user-supplied files and strip metadata before processing. Always treat processing of user-supplied files as unsafe computation, even when the file formats are common.

Deployment best practices

Never run image transformation software on web servers, or any other servers with access to databases, authentication, or other access to Personally Identifiable Information (PII) for security reasons mentioned above.

It is not recommended for most use-cases to run image transformations synchronous with end-user interaction in a blocking manner. The process is fast with Sharp, but not so fast that it makes sense to force the user to wait for it in real time. Furthermore, failed image processing in a blocking workflow might put the user interface in an unrecoverable state when not handled correctly. Asynchronous non-blocking image processing is generally the best solution.

For these reasons, and the previously mentioned potential for denial-of-service, either intentional or accidental, it is best to run image transformations on short-lived queue workers without persistent storage and with tight timeouts, memory limits, and minimal permissions.

Demonstration

Below is a simple script, which demonstrates all cross-format conversions between image types:

out-of-the-box.js
import sharp from "sharp";

const inputs = [
    {filename: 'test.avif', format: 'avif'},
    {filename: 'test.gif', format: 'gif'},
    {filename: 'test.jpeg', format: 'jpeg'},
    {filename: 'test.png', format: 'png'},
    {filename: 'test.tiff', format: 'tiff'},
    {filename: 'test.webp', format: 'webp'},
];

for (const file of inputs) {
    await sharp(`fixtures/${file.filename}`).avif().toFile(`output/from_${file.format}.avif`);
    await sharp(`fixtures/${file.filename}`).gif().toFile(`output/from_${file.format}.gif`);
    await sharp(`fixtures/${file.filename}`).jpeg().toFile(`output/from_${file.format}.jpeg`);
    await sharp(`fixtures/${file.filename}`).png().toFile(`output/from_${file.format}.png`);
    await sharp(`fixtures/${file.filename}`).tiff().toFile(`output/from_${file.format}.tiff`);
    await sharp(`fixtures/${file.filename}`).webp().toFile(`output/from_${file.format}.webp`);
}

Source: https://gitlab.com/minimg/diy/node/-/blob/master/examples/out-of-the-box.js

You can run this code as is, or check out the example from GitLab if you want to try this out yourself. This has been tested in docker containers with different Linux distributions; it worked the same every time.

Conclusion

This article showed the capabilities Sharp offers out-of-the-box as well as the security and infrastructure constraints you should consider when deploying this software. In the next article we will examine how to convert other image types that are not supported by the prebuilt version of Sharp.