Resize, Crop and Compose Images with sharp in Node.js — Cover Sizes for Stores, Overlaying SVG Text, Cropping a Border, and Keeping JPEGs Small
Practical sharp recipes from a publishing pipeline: resizing a generated image to the exact sizes stores require, extracting a region to remove a border, compositing an SVG overlay with text and gradients, and JPEG settings that keep web images small without visible loss.
sharp is the fastest way to handle images in Node and covers almost everything a content pipeline needs. These are the recipes we use every week to turn one generated image into the various sizes and formats that web pages and book stores want.
Resize to an exact size for a store
Stores are strict: a Kindle cover wants a tall portrait, a Gumroad cover wants a wide landscape, a Thai ebook store wants a large portrait. fit: 'cover' fills the box and crops the overflow evenly:
const sharp = require('sharp');
await sharp('cover-source.jpg').resize(1600, 2560, { fit: 'cover' }).jpeg({ quality: 90 }).toFile('cover-kindle.jpg');
await sharp('cover-source.jpg').resize(1600, 900, { fit: 'cover' }).jpeg({ quality: 88 }).toFile('cover-gumroad.jpg');
Use position to bias the crop toward the part that matters ('top', 'attention', and so on).
Remove a border by extracting a region
Generated images sometimes come with a decorative frame. Cut the inside out, then resize:
await sharp('generated.jpg')
.extract({ left: 68, top: 68, width: 664, height: 664 })
.resize(800, 800)
.jpeg({ quality: 90 })
.toFile('clean.jpg');
Measure the border once by opening the image, then hard-code the numbers for that source.
Overlay text and gradients with an SVG
sharp composites SVG as a layer, which gives you text, gradients and shapes without a drawing library:
const W = 1600, H = 2560;
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}">
<defs><linearGradient id="v" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#0b4f58" stop-opacity=".8"/><stop offset=".6" stop-color="#0b4f58" stop-opacity="0"/>
</linearGradient></defs>
<rect width="${W}" height="${H}" fill="url(#v)"/>
<g font-family="Tahoma, sans-serif" fill="#f3e9d2" text-anchor="middle">
<text x="800" y="980" font-size="120" font-weight="700">Title line one</text>
<text x="800" y="1130" font-size="120" font-weight="700">Title line two</text>
</g>
</svg>`;
const base = await sharp('bg.jpg').resize(W, H, { fit: 'cover' }).toBuffer();
await sharp(base).composite([{ input: Buffer.from(svg), top: 0, left: 0 }]).jpeg({ quality: 90 }).toFile('cover.jpg');
Two details: escape &, < and > in text, and pick a font the machine has. Thai renders fine with Tahoma on Windows; on a Linux server install the fonts first, or render the text with headless Chrome instead (see PDF and screenshots with headless Chrome).
Keep web images small
For photos and illustrations on web pages, mozjpeg at moderate quality is hard to distinguish from the original and often halves the size:
await sharp(input).resize(1200, 900, { fit: 'cover' }).jpeg({ quality: 84, mozjpeg: true }).toFile('web.jpg');
Set width and height attributes on the <img> to match the intrinsic size so the browser reserves space, and let CSS handle display size with height: auto plus aspect-ratio so a stale HTML height attribute never stretches the image.
Reading metadata
const meta = await sharp('cover.jpg').metadata();
console.log(meta.width, meta.height, meta.format);
Handy in a build script to assert the output is exactly the size the store demands before you upload.
Common mistakes
- Using
fit: 'fill'. It distorts; usecoverorcontain. - Forgetting to escape text in the SVG. An ampersand breaks the whole overlay.
- Assuming fonts exist on the server. Check with a test render.
Summary
Resize with fit: 'cover' for store sizes, extract to remove borders, composite an SVG for text and gradients, and export with mozjpeg for the web. Assert dimensions with metadata() before uploading anywhere.