让照片编辑器的预览和导出保持一致:TypeScript 渲染管线的版本化设计
作者开发 Photocard.ai(自定义 photo card 制作工具),Maker 管线把用户上传的照片和模板美术组合,涉及照片摆放、背景移除、装饰层和多种输出尺寸。最有用的架构决策是:让构图决策显式化,并保存成版本化的渲染计划(render plan),预览和高分辨率渲染消费同一份决策。
目标是跨分辨率的构图一致。轻量浏览器预览在头发细节、滤镜、边缘质量上和最终渲染有差异——这些是独立要验证的东西,但"布局决策"必须一致。
保存产生图片的决策
模板缩略图显示预期外观,但它无法告诉渲染器:每个槽位放哪张源图、裁哪里、哪层装饰在人前面。Maker 实现里,模板 manifest 描述这些规则,渲染计划为特定的一组上传解析它们:
type Rect = { x: number; y: number; width: number; height: number };
type RenderSlot = {
sourceIndex: number;
sourceCrop: Rect; // 相对旋转后的源图
destination: Rect; // 相对输出画布
zIndex: number;
subjectAlphaKey?: string;
refinedForegroundKey?: string;
effectSeed: number;
};
type RenderPlan = {
version: 2;
templateVersionId: string;
assetPackageVersion: string;
outputVariant: string;
slots: RenderSlot[];
};
原则很小:保存解释这个构图所需的信息。导出开始时加载已保存的裁剪——再跑一次自动裁剪检测器就多一次选错人脸位置的机会。装饰效果同理:如果纸张边缘处理用随机性,保存它的 seed,避免另一次渲染选不同图案。版本号背后要有具体的东西:只要旧卡仍可编辑,对应的美术和处理行为就必须仍可获得——版本字符串救不回被覆盖的资源。
让每个模板槽位自己选源
人像放在相框里可能需要完整原图,贴纸构图可能需要透明人像。manifest 让这个选择显式:sourceComposition: "original_photo" | "cutout_subject"。这防止一种微妙失败:仅仅因为缓存里恰好存在 mask,就对槽位应用背景移除。全图路径保留场景;cutout 槽位缺必需 mask 是校验错误。显式编辑功能(如轮廓模式)可以引入自己的规则,但可选资源的存在性绝不该意外决定视觉策略。群组合照尤其如此:单人 mask 和原群组合照包含不同主体,选错就改变卡片内容。
分离源坐标与画布坐标
裁剪操作里有两个矩形:源裁剪选择上传图的区域,目标框把它放到输出画布上。两者都可以用归一化坐标——x=0.1 表示相关图像宽度的 10%。源矩形相对旋转后的上传图,目标矩形相对画布。
同样的 {x:0.1, y:0.1, width:0.8, height:0.8} 在 600×900 画布上是 480×720,在 2400×3600 上是 1920×2880——布局无需新决策就能缩放。在渲染边界转成像素,并做完整的矩形合法性校验(数值有限、坐标非负、尺寸非零、不越界),圆整边界让矩形边缘显式,比较不同输出分辨率时允许圆整差异。
工程要点
这套设计的核心:构图是数据,不是过程。把"选源、裁剪、分层、随机种子"固化成版本化文档,预览与导出共享同一决策源,就消除了"预览批准了、导出变了"这类最让用户恼火的 bug 类别。对任何有渲染链的产品(图片、文档、视频),把布局决策与渲染执行解耦、给决策加版本,是性价比极高的架构投资。
来源:Keeping Photo Editor Previews and Exports in Sync with TypeScript and Sharp - DEV Community