122 lines
4.1 KiB
TypeScript
122 lines
4.1 KiB
TypeScript
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<string>();
|
|
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<string>) {
|
|
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;
|
|
}
|