|
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 }; |