src/utils/image.ts (138 lines of code) (raw):
import { Color, SharpOptions, OverlayOptions } from "sharp";
import sharp from "sharp";
import {DEFAULT_IMAGE_SIZE, EXPAND_ACTION_PADDING} from "./constants";
const LOGO_SIZE = 10;
function createLogoConfig(colors: Color): SharpOptions {
return {
create: {
width: LOGO_SIZE,
height: LOGO_SIZE,
channels: 4,
background: colors,
},
};
}
export async function createLogo(): Promise<Buffer> {
const logoParts = [
{
input: await sharp(createLogoConfig({ r: 255, g: 255, b: 102, alpha: 1 }))
.png()
.toBuffer(),
left: 0,
top: 0,
},
{
input: await sharp(createLogoConfig({ r: 66, g: 255, b: 255, alpha: 1 }))
.png()
.toBuffer(),
left: LOGO_SIZE,
top: 0,
},
{
input: await sharp(createLogoConfig({ r: 81, g: 218, b: 76, alpha: 1 }))
.png()
.toBuffer(),
left: LOGO_SIZE * 2,
top: 0,
},
{
input: await sharp(createLogoConfig({ r: 255, g: 110, b: 60, alpha: 1 }))
.png()
.toBuffer(),
left: LOGO_SIZE * 3,
top: 0,
},
{
input: await sharp(createLogoConfig({ r: 60, g: 70, b: 255, alpha: 1 }))
.png()
.toBuffer(),
left: LOGO_SIZE * 4,
top: 0,
},
];
return await sharp({
create: {
width: LOGO_SIZE * 5,
height: LOGO_SIZE,
channels: 4,
background: { r: 255, g: 255, b: 255, alpha: 0 },
},
})
.composite(logoParts)
.png()
.toBuffer();
}
export async function createTiledComposite(
imageBuffers: Buffer[],
imageWidth: number = DEFAULT_IMAGE_SIZE,
imageHeight: number = DEFAULT_IMAGE_SIZE
): Promise<Buffer> {
let canvasWidth = imageWidth;
let canvasHeight = imageHeight;
if (imageBuffers.length === 2) {
canvasWidth = imageWidth * 2; // Double the width for two images side by side
canvasHeight = imageHeight; // Height remains the same
} else if (imageBuffers.length === 3 || imageBuffers.length === 4) {
canvasWidth = imageWidth * 2; // Double the width for a 2x2 grid
canvasHeight = imageHeight * 2; // Double the height for a 2x2 grid
}
const images: OverlayOptions[] = imageBuffers.map((buffer, i) => {
let left = (i % 2) * imageWidth; // 0 for quadrant 1 and 3, imageWidth for quadrant 2 and 4
let top = i < 2 ? 0 : imageHeight; // 0 for quadrants 1 and 2, imageHeight for quadrants 3 and 4
return {
input: buffer,
left: left,
top: top,
};
});
// If there are 3 images, the last quadrant should be empty, so no need to put any image there.
return await sharp({
create: {
width: canvasWidth,
height: canvasHeight,
channels: 4,
background: { r: 255, g: 255, b: 255, alpha: 0 },
},
})
.composite(images)
.png()
.toBuffer();
}
// This is no longer in use for dall-e-3.
export async function expandImage(buffer: Buffer): Promise<Buffer> {
const result = await sharp(buffer)
.extend({
top: EXPAND_ACTION_PADDING,
bottom: EXPAND_ACTION_PADDING,
left: EXPAND_ACTION_PADDING,
right: EXPAND_ACTION_PADDING,
background: { r: 0, g: 0, b: 0, alpha: 0 },
})
.resize(DEFAULT_IMAGE_SIZE)
.png()
.toBuffer();
return result;
}
// We need the numberOfImages because a composite may have empty spaces when the number of images
// doesn't fit neatly into the composite dimensions.
export async function extractImagesFromComposite(
composite: Buffer,
compositeWidth: number,
compositeHeight: number,
numberOfImages: number,
imageWidth: number = DEFAULT_IMAGE_SIZE,
imageHeight: number = DEFAULT_IMAGE_SIZE
): Promise<Buffer[]> {
const images = [];
var i = 0;
for (let y = 0; y <= compositeHeight - imageHeight; y += imageHeight) {
for (let x = 0; x <= compositeWidth - imageWidth; x += imageWidth) {
const image = await sharp(composite)
.extract({ left: x, top: y, width: imageWidth, height: imageHeight })
.png()
.toBuffer();
images.push(image);
i += 1;
if (i == numberOfImages) {
return images;
}
}
}
return images;
}