mirror of
https://github.com/THU-MAIC/OpenMAIC.git
synced 2026-10-28 16:35:27 +08:00
fix(generation): contain-fit slide images inside the model's box (#2004)
When an assigned image's real aspect ratio differed from the model's image box by more than 10%, reconciliation kept the width and recomputed the height, so a box flatter than its image grew downward over the elements below it. Fit the image like object-fit: contain instead: the largest box with the image's ratio inside the model's box, centered there. @openmaic/generation 0.3.25. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ad6c6f72f4
commit
ee300e00c7
4 files changed
+85
-9
No files matched your search
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- A slide image whose box has a different shape from the image is now fitted inside the box the model laid out, centered, like `object-fit: contain`. Previously the box kept its width and changed its height, so a box flatter than its image grew downward and covered the text below it. Boxes within 10% of the image's ratio, and images with unknown dimensions, are unchanged. `@openmaic/generation` 0.3.25.
|
||||
|
||||
## [1.3.0-rc.1] - 2026-10-22
|
||||
|
||||
1.3.0-rc.1 is the first release candidate for 1.3.0. Its main theme is the **knowledge base**: every upload is kept in its owner's knowledge base until it is deleted, organized in folders and parsed once, with the result reused by every conversation. A new library page lists and organizes the materials, the composer offers them through `@` and a picker, and the agent gets tools to read, search and organize them. Interactive scenes also load their libraries from the deployment instead of a public CDN, the editor marks pages that have current problems, and malformed slide shapes no longer hang the renderer. This release contains breaking changes and upgrade conditions: the built-in Veo provider is removed, and the knowledge base adds schema changes that cannot run beside older instances or be rolled back. Read **Breaking Changes** before upgrading from 1.2.x.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openmaic/generation",
|
||||
"version": "0.3.24",
|
||||
"version": "0.3.25",
|
||||
"description": "Pure generation pipeline contracts and packaged prompt assets for MAIC consumers.",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -548,13 +548,19 @@ function fixElementDefaults(
|
||||
const curW = normalized.width || 400;
|
||||
const curH = normalized.height || 300;
|
||||
if (Math.abs(curW / curH - knownRatio) / knownRatio > 0.1) {
|
||||
// Keep width, correct height
|
||||
const newH = Math.round(curW / knownRatio);
|
||||
if (newH > 462) {
|
||||
// canvas 562.5 - margins 50×2
|
||||
return { ...normalized, width: Math.round(462 * knownRatio), height: 462 };
|
||||
}
|
||||
return { ...normalized, height: newH };
|
||||
// Contain-fit (like `object-fit: contain`): the largest box with the
|
||||
// real ratio inside the model's box, centered in it. Never grows past
|
||||
// the model's box, so it cannot cover the elements laid out around it.
|
||||
const fitByWidth = curW / curH < knownRatio;
|
||||
const newW = fitByWidth ? curW : Math.round(curH * knownRatio);
|
||||
const newH = fitByWidth ? Math.round(curW / knownRatio) : curH;
|
||||
return {
|
||||
...normalized,
|
||||
left: Math.round((normalized.left ?? 0) + (curW - newW) / 2),
|
||||
top: Math.round((normalized.top ?? 0) + (curH - newH) / 2),
|
||||
width: newW,
|
||||
height: newH,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { generateSceneContent, resolveImageIds } from '@openmaic/generation';
|
||||
import type { GeneratedSlideData, PdfImage } from '@openmaic/generation';
|
||||
import type { GeneratedSlideContent, GeneratedSlideData, PdfImage } from '@openmaic/generation';
|
||||
|
||||
import { slideOutline } from './scene-fixtures.js';
|
||||
|
||||
@@ -85,3 +85,69 @@ describe('generateSceneContent — non-vision text ordering with a mapping prese
|
||||
expect(availableMedia).not.toContain('[see attached]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateSceneContent — image aspect-ratio reconciliation (contain-fit)', () => {
|
||||
// 1189×479 → ratio ≈ 2.48.
|
||||
const wideImage: PdfImage = { id: 'img_1', src: '', pageNumber: 1, width: 1189, height: 479 };
|
||||
|
||||
async function fitImage(
|
||||
box: { left: number; top: number; width: number; height: number },
|
||||
assignedImages: PdfImage[] = [wideImage],
|
||||
) {
|
||||
const aiCall = vi.fn(async () =>
|
||||
JSON.stringify({
|
||||
elements: [{ ...imageElement('img_1'), ...box }],
|
||||
remark: '',
|
||||
}),
|
||||
);
|
||||
const content = (await generateSceneContent(slideOutline(), aiCall, {
|
||||
assignedImages,
|
||||
imageMapping: { img_1: 'ast_1' },
|
||||
})) as GeneratedSlideContent;
|
||||
expect(content.elements).toHaveLength(1);
|
||||
const [image] = content.elements;
|
||||
if (image.type !== 'image') throw new Error(`expected an image, got ${image.type}`);
|
||||
const { left, top, width, height } = image;
|
||||
return { left, top, width, height };
|
||||
}
|
||||
|
||||
test('a box flatter than the image shrinks the width and centers horizontally, never growing down', async () => {
|
||||
// Keeping the width would have made the box 363 tall and covered the
|
||||
// elements laid out below it.
|
||||
expect(await fitImage({ left: 50, top: 160, width: 900, height: 120 })).toEqual({
|
||||
left: 351,
|
||||
top: 160,
|
||||
width: 298,
|
||||
height: 120,
|
||||
});
|
||||
});
|
||||
|
||||
test('a box taller than the image keeps the width, shrinks the height and centers vertically', async () => {
|
||||
// 400 / 2.482 ≈ 161; (400 - 161) / 2 ≈ 119.5 → 120 offset.
|
||||
expect(await fitImage({ left: 100, top: 50, width: 400, height: 400 })).toEqual({
|
||||
left: 100,
|
||||
top: 170,
|
||||
width: 400,
|
||||
height: 161,
|
||||
});
|
||||
});
|
||||
|
||||
test('a box within the 10% ratio tolerance is left unchanged', async () => {
|
||||
expect(await fitImage({ left: 10, top: 20, width: 500, height: 210 })).toEqual({
|
||||
left: 10,
|
||||
top: 20,
|
||||
width: 500,
|
||||
height: 210,
|
||||
});
|
||||
});
|
||||
|
||||
test('an image with unknown dimensions is left unchanged', async () => {
|
||||
const unknown: PdfImage = { id: 'img_1', src: '', pageNumber: 1 };
|
||||
expect(await fitImage({ left: 50, top: 160, width: 900, height: 120 }, [unknown])).toEqual({
|
||||
left: 50,
|
||||
top: 160,
|
||||
width: 900,
|
||||
height: 120,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user