type DownloadableFile = { name: string; url: string; }; type ZipEntry = { name: Uint8Array; data: Uint8Array; crc: number; offset: number; }; export async function downloadFilesAsZip(files: DownloadableFile[], archiveName: string) { if (!files.length) return; const usedNames = new Set(); const entries: ZipEntry[] = []; for (let index = 0; index < files.length; index += 1) { const file = files[index]; const response = await fetch(file.url, { credentials: "same-origin" }); if (!response.ok) throw new Error(`下载第 ${index + 1} 个图层失败`); const data = new Uint8Array(await response.arrayBuffer()); const safeName = uniqueFileName(sanitizeFileName(file.name) || `layer-${index}.png`, usedNames); entries.push({ name: new TextEncoder().encode(safeName), data, crc: crc32(data), offset: 0 }); } const blob = zipBlob(entries); const href = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = href; anchor.download = sanitizeFileName(archiveName) || "seedream-layers.zip"; document.body.appendChild(anchor); anchor.click(); anchor.remove(); window.setTimeout(() => URL.revokeObjectURL(href), 1000); } function zipBlob(entries: ZipEntry[]) { const localParts: Uint8Array[] = []; let offset = 0; for (const entry of entries) { entry.offset = offset; const header = new Uint8Array(30 + entry.name.length); const view = new DataView(header.buffer); view.setUint32(0, 0x04034b50, true); view.setUint16(4, 20, true); view.setUint16(6, 0x0800, true); view.setUint16(8, 0, true); view.setUint32(14, entry.crc, true); view.setUint32(18, entry.data.length, true); view.setUint32(22, entry.data.length, true); view.setUint16(26, entry.name.length, true); header.set(entry.name, 30); localParts.push(header, entry.data); offset += header.length + entry.data.length; } const centralOffset = offset; const centralParts: Uint8Array[] = []; for (const entry of entries) { const header = new Uint8Array(46 + entry.name.length); const view = new DataView(header.buffer); view.setUint32(0, 0x02014b50, true); view.setUint16(4, 20, true); view.setUint16(6, 20, true); view.setUint16(8, 0x0800, true); view.setUint16(10, 0, true); view.setUint32(16, entry.crc, true); view.setUint32(20, entry.data.length, true); view.setUint32(24, entry.data.length, true); view.setUint16(28, entry.name.length, true); view.setUint32(42, entry.offset, true); header.set(entry.name, 46); centralParts.push(header); offset += header.length; } const end = new Uint8Array(22); const endView = new DataView(end.buffer); endView.setUint32(0, 0x06054b50, true); endView.setUint16(8, entries.length, true); endView.setUint16(10, entries.length, true); endView.setUint32(12, offset - centralOffset, true); endView.setUint32(16, centralOffset, true); return new Blob([...localParts, ...centralParts, end].map(arrayBufferCopy), { type: "application/zip" }); } function arrayBufferCopy(value: Uint8Array) { const copy = new Uint8Array(value.byteLength); copy.set(value); return copy.buffer; } function sanitizeFileName(value: string) { return value.trim().replace(/[\\/:*?"<>|\u0000-\u001f]/g, "-").replace(/^\.+/, "").slice(0, 120); } function uniqueFileName(name: string, used: Set) { if (!used.has(name)) { used.add(name); return name; } const dot = name.lastIndexOf("."); const base = dot > 0 ? name.slice(0, dot) : name; const extension = dot > 0 ? name.slice(dot) : ""; let index = 2; while (used.has(`${base}-${index}${extension}`)) index += 1; const result = `${base}-${index}${extension}`; used.add(result); return result; } const crcTable = Array.from({ length: 256 }, (_, index) => { let value = index; for (let bit = 0; bit < 8; bit += 1) value = (value & 1) ? 0xedb88320 ^ (value >>> 1) : value >>> 1; return value >>> 0; }); function crc32(data: Uint8Array) { let crc = 0xffffffff; for (const value of data) crc = crcTable[(crc ^ value) & 0xff] ^ (crc >>> 8); return (crc ^ 0xffffffff) >>> 0; }