Files
ttstd-infinite-canvas/web/src/lib/canvas/canvas-export.ts

79 lines
4.6 KiB
TypeScript

import { saveAs } from "file-saver";
import { createZip } from "@/lib/zip";
import { getMediaBlob } from "@/services/file-storage";
import { getImageBlob } from "@/services/image-storage";
import type { CanvasExportAsset, CanvasExportFile } from "@/types/canvas-export";
import type { CanvasProject } from "@/stores/canvas/use-canvas-store";
import { loadCanvasDrawing, loadCanvasDrawingPreview, loadCanvasDrawingRender } from "@/lib/canvas/canvas-drawing-storage";
import type { CanvasDrawingExport } from "@/types/canvas-export";
export async function exportCanvasProjects(projects: CanvasProject[], fileName = "影策画布") {
const zipFiles: { name: string; data: BlobPart }[] = [];
const exportedProjects = await Promise.all(
projects.map(async (project) => {
const files: CanvasExportAsset[] = [];
await Promise.all(
collectStorageKeys(project).map(async (storageKey) => {
const blob = storageKey.startsWith("image:") ? await getImageBlob(storageKey) : await getMediaBlob(storageKey);
if (!blob) return;
const path = `projects/${project.id}/files/${safeFileName(storageKey)}.${fileExtension(blob.type, storageKey)}`;
files.push({ storageKey, path, mimeType: blob.type || "application/octet-stream", bytes: blob.size });
zipFiles.push({ name: path, data: blob });
}),
);
const drawingDocuments = (await Promise.all(project.nodes.filter((node) => node.type === "drawing" && node.metadata?.drawingId).map(async (node): Promise<CanvasDrawingExport | null> => {
const drawingId = node.metadata?.drawingId;
if (!drawingId) return null;
const [saved, preview, render] = await Promise.all([
loadCanvasDrawing(project.id, drawingId),
loadCanvasDrawingPreview(project.id, drawingId),
loadCanvasDrawingRender(project.id, drawingId),
]);
if (!saved) return null;
const previewPath = preview ? `projects/${project.id}/drawings/${safeFileName(drawingId)}.png` : undefined;
if (preview && previewPath) zipFiles.push({ name: previewPath, data: preview });
const generationRenderPath = render ? `projects/${project.id}/drawings/${safeFileName(drawingId)}.generation.png` : undefined;
if (render && generationRenderPath) zipFiles.push({ name: generationRenderPath, data: render.blob });
return {
drawingId,
...saved,
previewPath,
generationRender: render && generationRenderPath
? { path: generationRenderPath, pageId: render.pageId, width: render.width, height: render.height, mimeType: render.mimeType, background: render.background }
: undefined,
} satisfies CanvasDrawingExport;
}))).filter((item): item is CanvasDrawingExport => item !== null);
drawingDocuments.forEach((document) => zipFiles.push({ name: `projects/${project.id}/drawings/${safeFileName(document.drawingId)}.json`, data: JSON.stringify(document) }));
return { project, files, drawingDocuments };
}),
);
const data: CanvasExportFile = { app: "infinite-canvas", version: 4, exportedAt: new Date().toISOString(), projects: exportedProjects };
const zip = await createZip([{ name: "projects.json", data: JSON.stringify(data, null, 2) }, ...zipFiles]);
saveAs(zip, `${safeFileName(fileName)}.zip`);
}
function collectStorageKeys(value: unknown, keys = new Set<string>()) {
if (!value || typeof value !== "object") return [...keys];
if ("storageKey" in value && typeof value.storageKey === "string" && value.storageKey.includes(":")) keys.add(value.storageKey);
Object.values(value).forEach((item) => (Array.isArray(item) ? item.forEach((child) => collectStorageKeys(child, keys)) : collectStorageKeys(item, keys)));
return [...keys];
}
function safeFileName(value: string) {
return value.replace(/[\\/:*?"<>|]/g, "_");
}
function fileExtension(mimeType: string, storageKey: string) {
if (mimeType.includes("png")) return "png";
if (mimeType.includes("jpeg")) return "jpg";
if (mimeType.includes("webp")) return "webp";
if (mimeType.includes("gif")) return "gif";
if (mimeType.includes("mp4")) return "mp4";
if (mimeType.includes("webm")) return "webm";
if (mimeType.includes("gltf-binary")) return "glb";
if (mimeType.includes("gltf+json")) return "gltf";
return storageKey.startsWith("image:") ? "png" : "bin";
}