import { expect, test } from "@playwright/test"; import sharp from "sharp"; import { buildReplacementInputBoundingBoxList, compositeReplacementWithReferenceLocks, expandReplacementSubjectBoundingBoxForGeneration, parseReplacementLayoutLockRegions, } from "@/lib/ai/replacement-layout-lock"; test("parses normalized subject and protected text regions", () => { const result = parseReplacementLayoutLockRegions(` \`\`\`json { "subjectBoundingBox": [200, 250, 800, 900], "textBoundingBoxes": [[100, 80, 700, 160], [20, 20, 20, 40], [-20, 900, 1100, 980]] } \`\`\` `); expect(result).toEqual({ subjectBoundingBox: [200, 250, 800, 900], textBoundingBoxes: [ [100, 80, 700, 160], [0, 900, 1000, 980], ], analyzed: true, }); }); test("marks incomplete layout analysis for a safe fallback", () => { expect( parseReplacementLayoutLockRegions({ subjectBoundingBox: [200, 250, 800, 900], }), ).toEqual({ subjectBoundingBox: [200, 250, 800, 900], textBoundingBoxes: [], analyzed: false, }); }); test("expands the replacement subject horizontally and aligns it with the layout input", () => { expect( expandReplacementSubjectBoundingBoxForGeneration([379, 240, 628, 917]), ).toEqual([354, 230, 653, 927]); expect( buildReplacementInputBoundingBoxList({ imageUrls: [ "https://example.com/product.png", "https://example.com/layout.png", ], referenceImageUrl: "https://example.com/layout.png", subjectBoundingBox: [379, 240, 628, 917], }), ).toEqual([[], [[354, 230, 653, 927]]]); }); test("keeps the reference outside the subject and inside protected text", async () => { const width = 100; const height = 100; const reference = await sharp({ create: { width, height, channels: 3, background: { r: 255, g: 0, b: 0 }, }, }) .png() .toBuffer(); const generated = await sharp({ create: { width, height, channels: 3, background: { r: 0, g: 0, b: 255 }, }, }) .png() .toBuffer(); const result = await compositeReplacementWithReferenceLocks({ generatedBuffer: generated, referenceBuffer: reference, width, height, preserveReferenceOutsideSubject: true, subjectBoundingBox: [300, 300, 700, 700], textBoundingBoxes: [[450, 450, 550, 550]], }); const { data, info } = await sharp(result) .removeAlpha() .raw() .toBuffer({ resolveWithObject: true }); const pixel = (x: number, y: number) => Array.from( data.subarray( (y * width + x) * info.channels, (y * width + x) * info.channels + 3, ), ); expect(pixel(5, 5)).toEqual([255, 0, 0]); expect(pixel(35, 50)).toEqual([0, 0, 255]); expect(pixel(50, 50)).toEqual([255, 0, 0]); }); test("restores original text pixels while retaining a regenerated scene", async () => { const width = 100; const height = 100; const reference = await sharp({ create: { width, height, channels: 3, background: { r: 255, g: 0, b: 0 }, }, }) .png() .toBuffer(); const generated = await sharp({ create: { width, height, channels: 3, background: { r: 0, g: 0, b: 255 }, }, }) .png() .toBuffer(); const result = await compositeReplacementWithReferenceLocks({ generatedBuffer: generated, referenceBuffer: reference, width, height, preserveReferenceOutsideSubject: false, textBoundingBoxes: [[400, 400, 600, 600]], }); const { data, info } = await sharp(result) .removeAlpha() .raw() .toBuffer({ resolveWithObject: true }); const pixel = (x: number, y: number) => Array.from( data.subarray( (y * width + x) * info.channels, (y * width + x) * info.channels + 3, ), ); expect(pixel(5, 5)).toEqual([0, 0, 255]); expect(pixel(50, 50)).toEqual([255, 0, 0]); });