ai-chatbot/lib/canvas/layout-replace-text-board.ts

46 lines
2.6 KiB
TypeScript

import sharp from "sharp";
import type { NormalizedReplacementBox } from "@/lib/ai/replacement-layout-lock";
export async function makeLayoutReplaceTextBoards(template: Buffer, result: Buffer, boxes: NormalizedReplacementBox[]) {
const metadata = await sharp(template).metadata();
const width = metadata.width!; const height = metadata.height!;
const generated = await sharp(result).resize(width, height, { fit: "fill" }).png().toBuffer();
const boards: Buffer[] = [];
let overlays: { input: Buffer; left: number; top: number }[] = [];
let boardHeight = 10;
async function flush() {
if (!overlays.length) return;
boards.push(await sharp({ create: { width: 1390, height: boardHeight,
channels: 3, background: "#d9d9d9" } }).composite(overlays).png().toBuffer());
overlays = []; boardHeight = 10;
}
for (const box of boxes) {
const left = Math.max(0, Math.floor(box[0] / 1000 * width) - 24);
const top = Math.max(0, Math.floor(box[1] / 1000 * height) - 24);
const right = Math.min(width, Math.ceil(box[2] / 1000 * width) + 24);
const bottom = Math.min(height, Math.ceil(box[3] / 1000 * height) + 24);
// A tall coupon panel must remain tall. A fixed 150px cell made its small
// print unreadable, causing valid text to be reported as missing/changed.
const rowHeight = Math.min(1100, Math.max(150, Math.ceil((bottom - top) / (right - left) * 680)));
if (boardHeight + rowHeight + 10 > 1320) await flush();
for (const [column, buffer] of [template, generated].entries()) {
const cropWidth = right - left; const cropHeight = bottom - top;
let cropped = await sharp(buffer).extract({ left, top, width: cropWidth, height: cropHeight }).png().toBuffer();
if (column === 0) {
const x = Math.max(1, box[0] / 1000 * width - left);
const y = Math.max(1, box[1] / 1000 * height - top);
const targetWidth = Math.max(1, Math.min(cropWidth - x - 1, (box[2] - box[0]) / 1000 * width));
const targetHeight = Math.max(1, Math.min(cropHeight - y - 1, (box[3] - box[1]) / 1000 * height));
const outline = Buffer.from(`<svg width="${cropWidth}" height="${cropHeight}"><rect x="${x}" y="${y}" width="${targetWidth}" height="${targetHeight}" fill="none" stroke="#0088ff" stroke-width="1"/></svg>`);
cropped = await sharp(cropped).composite([{ input: outline }]).png().toBuffer();
}
const input = await sharp(cropped)
.resize(680, rowHeight, { fit: "contain", background: "#ffffff" }).png().toBuffer();
overlays.push({ input, left: 10 + column * 690, top: boardHeight });
}
boardHeight += rowHeight + 10;
}
await flush();
return boards;
}