208 lines
6.2 KiB
TypeScript
208 lines
6.2 KiB
TypeScript
import JSZip from 'jszip';
|
|
import { useCallback, useState } from 'react';
|
|
|
|
import type {
|
|
CanvasAssetExportImage,
|
|
CanvasAssetExportMetadata,
|
|
CanvasLayer,
|
|
} from './ImageCanvasEditorTypes';
|
|
import {
|
|
blobToUint8Array,
|
|
buildLayerExportMetadata,
|
|
formatExportDate,
|
|
getImageExtensionFromTypeOrSrc,
|
|
getLayerExportKey,
|
|
readLayerImageBlob,
|
|
sanitizeExportFilePart,
|
|
} from './ImageCanvasExportModel';
|
|
|
|
type AssetExportStatus = {
|
|
tone: 'info' | 'success' | 'error';
|
|
message: string;
|
|
};
|
|
|
|
type UseImageCanvasAssetExportWorkflowOptions = {
|
|
layers: CanvasLayer[];
|
|
projectId: string | null;
|
|
projectTitle: string;
|
|
};
|
|
|
|
export function useImageCanvasAssetExportWorkflow({
|
|
layers,
|
|
projectId,
|
|
projectTitle,
|
|
}: UseImageCanvasAssetExportWorkflowOptions) {
|
|
const [assetExportStatus, setAssetExportStatus] =
|
|
useState<AssetExportStatus | null>(null);
|
|
const [isExportingAssets, setIsExportingAssets] = useState(false);
|
|
|
|
const exportLayerImage = useCallback((layer: CanvasLayer | null) => {
|
|
if (!layer) {
|
|
return;
|
|
}
|
|
const link = document.createElement('a');
|
|
link.href = layer.src;
|
|
link.download = `${sanitizeExportFilePart(layer.title, 'canvas-layer')}.png`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
link.remove();
|
|
}, []);
|
|
|
|
const exportCanvasAssets = useCallback(async () => {
|
|
if (isExportingAssets) {
|
|
return;
|
|
}
|
|
const exportableLayers = layers
|
|
.filter((layer) => layer.src.trim().length > 0)
|
|
.sort((left, right) => left.zIndex - right.zIndex);
|
|
if (!exportableLayers.length) {
|
|
setAssetExportStatus({
|
|
tone: 'info',
|
|
message: '当前画布没有可导出的素材',
|
|
});
|
|
return;
|
|
}
|
|
|
|
setIsExportingAssets(true);
|
|
setAssetExportStatus(null);
|
|
|
|
try {
|
|
const exportedAt = new Date();
|
|
const projectName = sanitizeExportFilePart(projectTitle, '未命名画布');
|
|
const rootFolderName = `${projectName}-画布素材`;
|
|
const zip = new JSZip();
|
|
const rootFolder = zip.folder(rootFolderName) ?? zip;
|
|
const imagesFolder = rootFolder.folder('images') ?? rootFolder;
|
|
const imageByKey = new Map<string, CanvasAssetExportImage>();
|
|
const usedFileNames = new Map<string, number>();
|
|
|
|
for (const layer of exportableLayers) {
|
|
const key = getLayerExportKey(layer);
|
|
if (imageByKey.has(key)) {
|
|
continue;
|
|
}
|
|
const index = imageByKey.size + 1;
|
|
const safeTitle = sanitizeExportFilePart(layer.title, '画布素材');
|
|
const baseFileName = `${String(index).padStart(3, '0')}-${safeTitle}`;
|
|
const duplicateCount = usedFileNames.get(baseFileName) ?? 0;
|
|
usedFileNames.set(baseFileName, duplicateCount + 1);
|
|
const indexedFileName =
|
|
duplicateCount > 0
|
|
? `${baseFileName}-${duplicateCount + 1}`
|
|
: baseFileName;
|
|
|
|
try {
|
|
const blob = await readLayerImageBlob(layer);
|
|
const extension = getImageExtensionFromTypeOrSrc(
|
|
blob.type,
|
|
layer.src,
|
|
);
|
|
const file = `images/${indexedFileName}.${extension}`;
|
|
imageByKey.set(key, {
|
|
key,
|
|
file,
|
|
layer,
|
|
blob,
|
|
});
|
|
imagesFolder.file(
|
|
`${indexedFileName}.${extension}`,
|
|
await blobToUint8Array(blob),
|
|
);
|
|
} catch (error) {
|
|
imageByKey.set(key, {
|
|
key,
|
|
file: `images/${indexedFileName}.png`,
|
|
layer,
|
|
error: error instanceof Error ? error.message : '图片读取失败',
|
|
});
|
|
}
|
|
}
|
|
|
|
const failedImages = [...imageByKey.values()].filter(
|
|
(image) => image.error,
|
|
);
|
|
const successfulImages = [...imageByKey.values()].filter(
|
|
(image) => image.blob,
|
|
);
|
|
if (!successfulImages.length) {
|
|
setAssetExportStatus({
|
|
tone: 'error',
|
|
message: '素材导出失败',
|
|
});
|
|
return;
|
|
}
|
|
|
|
const metadata: CanvasAssetExportMetadata = {
|
|
projectId,
|
|
projectTitle,
|
|
exportedAt: exportedAt.toISOString(),
|
|
layers: exportableLayers.map((layer) => {
|
|
const image = imageByKey.get(getLayerExportKey(layer));
|
|
return buildLayerExportMetadata(
|
|
layer,
|
|
image?.blob ? image.file : null,
|
|
image?.error,
|
|
);
|
|
}),
|
|
failedImages: failedImages.map((image) => ({
|
|
key: image.key,
|
|
title: image.layer.title,
|
|
src: image.layer.src,
|
|
error: image.error ?? '图片读取失败',
|
|
})),
|
|
};
|
|
const manifest = [
|
|
`项目:${projectTitle}`,
|
|
`导出时间:${metadata.exportedAt}`,
|
|
`素材数量:${successfulImages.length}`,
|
|
`图层数量:${exportableLayers.length}`,
|
|
failedImages.length ? `失败素材数量:${failedImages.length}` : null,
|
|
]
|
|
.filter(Boolean)
|
|
.join('\n');
|
|
|
|
rootFolder.file('metadata.json', JSON.stringify(metadata, null, 2));
|
|
rootFolder.file('manifest.txt', manifest);
|
|
|
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
|
if (
|
|
typeof URL.createObjectURL !== 'function' ||
|
|
typeof URL.revokeObjectURL !== 'function'
|
|
) {
|
|
setAssetExportStatus({
|
|
tone: 'error',
|
|
message: '当前浏览器不支持素材下载',
|
|
});
|
|
return;
|
|
}
|
|
|
|
const downloadUrl = URL.createObjectURL(zipBlob);
|
|
const link = document.createElement('a');
|
|
link.href = downloadUrl;
|
|
link.download = `${rootFolderName}-${formatExportDate(exportedAt)}.zip`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
link.remove();
|
|
URL.revokeObjectURL(downloadUrl);
|
|
setAssetExportStatus({
|
|
tone: failedImages.length ? 'error' : 'success',
|
|
message: failedImages.length ? '部分素材未能导出' : '画布素材已导出',
|
|
});
|
|
} catch {
|
|
setAssetExportStatus({
|
|
tone: 'error',
|
|
message: '素材导出失败',
|
|
});
|
|
} finally {
|
|
setIsExportingAssets(false);
|
|
}
|
|
}, [isExportingAssets, layers, projectId, projectTitle]);
|
|
|
|
return {
|
|
assetExportStatus,
|
|
isExportingAssets,
|
|
exportCanvasAssets,
|
|
exportLayerImage,
|
|
};
|
|
}
|