Files
NianAIGC/lib/client/seedream-layer-compose.ts

94 lines
3.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}