Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Netlify Function/AWS Lambda to generate a social media image with dynamic text using pureimage
[functions]
included_files = ["public/font.ttf", "public/logo.jpg"]
import { Handler, HandlerContext, HandlerEvent } from '@netlify/functions';
import PImage from 'pureimage';
import { Bitmap } from 'pureimage/types/bitmap';
import { PassThrough } from 'stream';
import fs from 'fs';
const SOCIAL_IMAGE_WIDTH = 1000;
const SOCIAL_IMAGE_HEIGHT = 600;
const BACKGROUND_COLOR = '#fff';
const LOGO_IMAGE_PATH = 'public/logo.jpg';
const LOGO_IMAGE_WIDTH = 300;
const LOGO_IMAGE_HEIGHT = 300;
const LOGO_IMAGE_Y_WHEN_TEXT = 70;
const FONT_PATH = 'public/font.ttf';
const FONT_NAME = 'Source Sans Pro';
const FONT_SIZE = '48pt';
const TEXT_Y = 420;
const TEXT_COLOR = '#222';
const OUTPUT_FILE_TYPE: 'jpeg' | 'png' = 'jpeg';
/**
* Function to output Bitmap to a buffer using stream.PassThrough. The default
* examples of pureimage use writeFile to persist images on disk. In a serverless
* function we want to keep it in memory.
*/
const imageToBuffer = (image: Bitmap): Promise<Buffer> => {
return new Promise((resolve) => {
const stream = new PassThrough();
const imageData: Uint8Array[] = [];
stream.on('data', (chunk) => {
imageData.push(chunk);
});
stream.on('end', () => {
resolve(Buffer.concat(imageData));
});
// @ts-ignore No overlap error from TypeScript
if (OUTPUT_FILE_TYPE === 'png') {
PImage.encodePNGToStream(image, stream);
return;
}
PImage.encodeJPEGToStream(image, stream);
});
};
const loadFont = (fontPath: string, fontName: string): Promise<void> => {
return new Promise((resolve, reject) => {
let font;
// 3rd, 4th and 5th parameters are required by TypeScript...
try {
font = PImage.registerFont(fontPath, fontName, 400, 'normal', 'normal');
font.load(() => {
resolve();
});
} catch (err) {
reject(err);
}
});
};
const handler: Handler = async ({ queryStringParameters }: HandlerEvent, context: HandlerContext) => {
const image = PImage.make(SOCIAL_IMAGE_WIDTH, SOCIAL_IMAGE_HEIGHT, {});
const ctx = image.getContext('2d');
const text = queryStringParameters?.text;
const hasText = text !== undefined && typeof text === 'string';
ctx.fillStyle = BACKGROUND_COLOR;
ctx.fillRect(0, 0, SOCIAL_IMAGE_WIDTH, SOCIAL_IMAGE_HEIGHT);
try {
const logo = await PImage.decodeJPEGFromStream(fs.createReadStream(LOGO_IMAGE_PATH));
const logoX = (SOCIAL_IMAGE_WIDTH / 2) - (LOGO_IMAGE_WIDTH / 2);
const defaultLogoY = (SOCIAL_IMAGE_HEIGHT / 2) - (LOGO_IMAGE_HEIGHT / 2);
ctx.drawImage(logo, logoX, hasText ? LOGO_IMAGE_Y_WHEN_TEXT : defaultLogoY, LOGO_IMAGE_WIDTH, LOGO_IMAGE_HEIGHT);
} catch (err) {
console.error(err);
}
if (hasText) {
const truncatedText = text.substring(0, 50);
try {
await loadFont(FONT_PATH, FONT_NAME);
ctx.fillStyle = TEXT_COLOR;
// @ts-ignore Definition of .font is incorrect in pureimage
ctx.font = `${FONT_SIZE} ${FONT_NAME}`;
ctx.textAlign = 'center';
ctx.fillText(truncatedText, SOCIAL_IMAGE_WIDTH / 2, TEXT_Y);
} catch (err) {
//
}
}
const buffer = await imageToBuffer(image);
return {
statusCode: 200,
headers: {
'Content-Type': OUTPUT_FILE_TYPE === 'jpeg' ? 'image/jpeg' : 'image/png',
'Cache-Control': 'public, max-age=604800, immutable', // 7 days
},
body: buffer.toString('base64'),
isBase64Encoded: true,
};
};
export { handler };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment