75 lines
2.6 KiB
JavaScript
75 lines
2.6 KiB
JavaScript
import sharp from "sharp";
|
|
|
|
const productDimensions = Object.freeze({
|
|
"3:4": Object.freeze({ pixelHeight: 1440, pixelWidth: 1080 }),
|
|
"1:1": Object.freeze({ pixelHeight: 1080, pixelWidth: 1080 }),
|
|
"4:3": Object.freeze({ pixelHeight: 1080, pixelWidth: 1440 }),
|
|
"9:16": Object.freeze({ pixelHeight: 1920, pixelWidth: 1080 }),
|
|
});
|
|
|
|
const gptImageRequestSizes = Object.freeze({
|
|
"3:4": "1056x1408",
|
|
"1:1": "1088x1088",
|
|
"4:3": "1408x1056",
|
|
"9:16": "1008x1792",
|
|
});
|
|
|
|
const allowedMimeTypes = new Set(["image/jpeg", "image/png", "image/webp"]);
|
|
const maximumInputBytes = 20 * 1024 * 1024;
|
|
|
|
function assertRatio(ratio) {
|
|
if (!(ratio in productDimensions)) throw new Error("image_output_ratio_unsupported");
|
|
return ratio;
|
|
}
|
|
|
|
export function productDimensionsForRatio(ratio) {
|
|
return { ...productDimensions[assertRatio(ratio)] };
|
|
}
|
|
|
|
export function gptImageRequestSizeForRatio(ratio) {
|
|
return gptImageRequestSizes[assertRatio(ratio)];
|
|
}
|
|
|
|
export async function normalizeImageOutputToRatio(input) {
|
|
const ratio = assertRatio(input?.ratio);
|
|
if (!Buffer.isBuffer(input?.bytes) || input.bytes.length === 0 || input.bytes.length > maximumInputBytes
|
|
|| !allowedMimeTypes.has(input?.mimeType)) {
|
|
throw new Error("image_output_media_invalid");
|
|
}
|
|
const target = productDimensions[ratio];
|
|
if (input.pixelWidth === target.pixelWidth && input.pixelHeight === target.pixelHeight) {
|
|
return {
|
|
bytes: Buffer.from(input.bytes),
|
|
mimeType: input.mimeType,
|
|
normalized: false,
|
|
...target,
|
|
upstreamPixelHeight: input.pixelHeight,
|
|
upstreamPixelWidth: input.pixelWidth,
|
|
};
|
|
}
|
|
|
|
const image = sharp(input.bytes, { failOn: "error", limitInputPixels: 40_000_000 });
|
|
const metadata = await image.metadata();
|
|
if (!metadata.width || !metadata.height) throw new Error("image_output_dimensions_missing");
|
|
const requestedRatio = target.pixelWidth / target.pixelHeight;
|
|
const upstreamRatio = metadata.width / metadata.height;
|
|
if (Math.abs(upstreamRatio - requestedRatio) / requestedRatio > 0.02) {
|
|
throw new Error("image_output_aspect_ratio_mismatch");
|
|
}
|
|
const { data, info } = await image
|
|
.resize(target.pixelWidth, target.pixelHeight, { fit: "fill", kernel: sharp.kernel.lanczos3 })
|
|
.png({ compressionLevel: 9 })
|
|
.toBuffer({ resolveWithObject: true });
|
|
if (info.width !== target.pixelWidth || info.height !== target.pixelHeight || info.format !== "png") {
|
|
throw new Error("image_output_normalization_failed");
|
|
}
|
|
return {
|
|
bytes: data,
|
|
mimeType: "image/png",
|
|
normalized: true,
|
|
...target,
|
|
upstreamPixelHeight: metadata.height,
|
|
upstreamPixelWidth: metadata.width,
|
|
};
|
|
}
|