94 lines
3.3 KiB
TypeScript
94 lines
3.3 KiB
TypeScript
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<Blob>((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;
|
||
}
|