In this blog post, we'll cover how to convert images into WebP format and create blur hash images to store in your database to show as placeholders. WebP is used to create images that only need to be used in applications.
Resizing to Fit WebP
First, there's an important thing to note when using WebP. There is a maximum width and height of 16383 (according to Google, the creators of WebP - documented here).
If you forget to implement this you'll get a sharp error saying:
Processed image is too large for the WebP format
So we need to implement code to resize this if necessary before converting:
import sharp, { Sharp } from "sharp"; | |
const resizeImage = async (sharpImage: Sharp): Promise<Sharp> => { | |
const metadata = await sharpImage.metadata(); | |
// Calculate the maximum dimensions | |
const maxWidth = 16383; | |
const maxHeight = 16383; | |
if (!metadata.width) { | |
throw new Error("No metadata width found for image"); | |
} | |
if (!metadata.height) { | |
throw new Error("No metadata height found for image"); | |
} | |
// Determine whether resizing is necessary | |
const needsResize = metadata.width > maxWidth || metadata.height > maxHeight; | |
let resizedImage = sharpImage; | |
if (needsResize) { | |
// Calculate the new size maintaining the aspect ratio | |
const aspectRatio = metadata.width / metadata.height; | |
let newWidth = maxWidth; | |
let newHeight = maxHeight; | |
if (metadata.width > metadata.height) { | |
// Landscape or square image: scale by width | |
newHeight = Math.round(newWidth / aspectRatio); | |
} else { | |
// Portrait image: scale by height | |
newWidth = Math.round(newHeight * aspectRatio); | |
} | |
// Resize the image before converting to WebP | |
resizedImage = sharpImage.resize(newWidth, newHeight); | |
} | |
return resizedImage; | |
} |
This code just does a quick check to see if it needs to be resized and returns the sharp instance if needed.
Notice that width and height are optional on the metadata. I haven't seen these values returned as undefined, but in the event an image is corrupted ensure to handle the errors.
Converting To WebP
Next, we convert the image to WebP. We'll make use of the function above to resize if needed.
export const createWebP = async (imageArray: Uint8Array) => { | |
const sharpImage = sharp(imageArray); | |
const resizedImage = await resizeImage(sharpImage); | |
const webP = await resizedImage.webp().toBuffer(); | |
return webP; | |
}; |
This returns our converted image to WebP.
For more configuration read more about using Sharp here
Creating the BlurHash
We also want to create a BlurHash to store in our database for showing a placeholder while the image loads over the network.
import sharp, { Sharp } from "sharp"; | |
import { encode } from "blurhash"; | |
export const createHash = async (imageArray: Uint8Array) => { | |
const sharpImage = sharp(imageArray); | |
const { data, info } = await sharpImage.ensureAlpha().raw().toBuffer({ | |
resolveWithObject: true, | |
}); | |
const encoded = encode( | |
new Uint8ClampedArray(data), | |
info.width, | |
info.height, | |
4, | |
4 | |
); | |
return { | |
hash: encoded, | |
height: info.height, | |
width: info.width, | |
}; | |
}; |
This returns us the 36-character hashed string that can be used with BlurHash on the front end.
In our react front end we can use it like this:
import { useEffect, useRef, useState } from 'react' | |
import { Blurhash } from 'react-blurhash' | |
export function BlurhashImage({ | |
imageUrl, | |
blurHash, | |
blurHashHeight, | |
blurHashWidth, | |
}: { | |
imageUrl?: string | null | |
blurHash?: string | null | |
blurHashWidth?: number | null | |
blurHashHeight?: number | null | |
}) { | |
const [isLoading, setIsLoading] = useState(true) | |
const containerRef = useRef<HTMLDivElement>(null) | |
const [width, setDimensions] = useState(0) | |
useEffect(() => { | |
const updateSize = () => { | |
if (containerRef.current) { | |
const containerWidth = containerRef.current.offsetWidth | |
setDimensions(containerWidth) | |
} | |
} | |
updateSize() | |
window.addEventListener('resize', updateSize) | |
return () => window.removeEventListener('resize', updateSize) | |
}, []) | |
if (!imageUrl || !blurHash || !blurHashHeight || !blurHashWidth) { | |
return null | |
} | |
return ( | |
<div className="w-full" ref={containerRef}> | |
{isLoading ? <Blurhash hash={blurHash} height={blurHashHeight} width={width} /> : null} | |
<img | |
src={imageUrl} | |
alt="Loaded Image" | |
style={{ display: isLoading ? 'none' : 'block' }} | |
className="w-full h-auto rounded-t-xl" | |
onLoad={() => setIsLoading(false)} | |
/> | |
</div> | |
) | |
} |
Convert to fit your use case, but that's all you need to show blurred placeholders before your already optimised WebP image loads on the front end.
Read more about BlurHash usage here
Thanks for reading
Top comments (0)