export type SeedreamLayerCompositionItem = { url: string; visible: boolean; box: [number, number, number, number]; rotation?: number; }; export async function downloadSeedreamLayerComposition(options: { items: SeedreamLayerCompositionItem[]; width?: number; height?: number; fileName: string; }) { if (!options.items.some((item) => item.visible)) throw new Error("请至少显示一个图层后再导出"); const decoded: Array<{ item: SeedreamLayerCompositionItem; bitmap: ImageBitmap }> = []; try { for (const item of options.items) { if (item.visible) decoded.push({ item, bitmap: await fetchBitmap(item.url) }); } const reference = decoded[0].bitmap; const width = positiveDimension(options.width) || reference.width; const height = positiveDimension(options.height) || reference.height; const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const context = canvas.getContext("2d"); if (!context) throw new Error("当前浏览器无法创建合成画布"); context.clearRect(0, 0, width, height); for (const { item, bitmap } of decoded) { const [left, top, right, bottom] = item.box; const x = left / 1000 * width; const y = top / 1000 * height; const itemWidth = Math.max(1, (right - left) / 1000 * width); const itemHeight = Math.max(1, (bottom - top) / 1000 * height); const rotation = Number.isFinite(item.rotation) ? item.rotation || 0 : 0; if (!rotation) { context.drawImage(bitmap, x, y, itemWidth, itemHeight); continue; } context.save(); context.translate(x + itemWidth / 2, y + itemHeight / 2); context.rotate(rotation * Math.PI / 180); context.drawImage(bitmap, -itemWidth / 2, -itemHeight / 2, itemWidth, itemHeight); context.restore(); } const blob = await canvasBlob(canvas, "image/png"); downloadBlob(blob, options.fileName); } finally { for (const { bitmap } of decoded) bitmap.close(); } } async function fetchBitmap(url: string) { const response = await fetch(sameOriginBinaryURL(url), { credentials: "same-origin" }); if (!response.ok) throw new Error(`读取图层失败(HTTP ${response.status})`); return createImageBitmap(await response.blob()); } function sameOriginBinaryURL(url: string) { if (!url.startsWith("/api/assets/") || !url.includes("/download")) return url; const [path, rawQuery = ""] = url.split("?", 2); const query = new URLSearchParams(rawQuery); query.delete("inline"); const suffix = query.toString(); return suffix ? `${path}?${suffix}` : path; } function canvasBlob(canvas: HTMLCanvasElement, type: string) { return new Promise((resolve, reject) => { canvas.toBlob((blob) => { if (blob) resolve(blob); else reject(new Error("生成合成图片失败")); }, type); }); } function downloadBlob(blob: Blob, fileName: string) { const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = fileName; document.body.appendChild(anchor); anchor.click(); anchor.remove(); window.setTimeout(() => URL.revokeObjectURL(url), 0); } function positiveDimension(value?: number) { return value && Number.isFinite(value) && value > 1 ? Math.round(value) : undefined; }