/**
|
* 极简 ZIP 打包(store 存储,不压缩)。
|
*
|
* OOXML 容器(xlsx / docx)本质都是 zip 包,这里只实现写文件所需的最小集合:
|
* 本地文件头 + 中央目录 + 目录结束记录。不压缩可以省掉 deflate 依赖,
|
* 体积对导出场景可以接受,且所有解析器都兼容。
|
*/
|
|
const CRC_TABLE = (() => {
|
const table = new Uint32Array(256);
|
for (let i = 0; i < 256; i += 1) {
|
let c = i;
|
for (let k = 0; k < 8; k += 1) {
|
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
}
|
table[i] = c >>> 0;
|
}
|
return table;
|
})();
|
|
const crc32 = (bytes) => {
|
let c = 0xffffffff;
|
for (let i = 0; i < bytes.length; i += 1) {
|
c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8);
|
}
|
return (c ^ 0xffffffff) >>> 0;
|
};
|
|
const encoder = new TextEncoder();
|
|
export const utf8 = (text) => encoder.encode(text);
|
|
/**
|
* @param {Array<{name: string, data: Uint8Array}>} files
|
* @param {string} mimeType
|
* @returns {Blob}
|
*/
|
export function createZip(files, mimeType) {
|
const now = new Date();
|
const dosTime =
|
(now.getHours() << 11) | (now.getMinutes() << 5) | Math.floor(now.getSeconds() / 2);
|
const dosDate =
|
((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate();
|
|
const localParts = [];
|
const centralParts = [];
|
let offset = 0;
|
|
files.forEach((file) => {
|
const nameBytes = utf8(file.name);
|
const data = file.data;
|
const crc = crc32(data);
|
|
const local = new Uint8Array(30 + nameBytes.length);
|
const lv = new DataView(local.buffer);
|
lv.setUint32(0, 0x04034b50, true);
|
lv.setUint16(4, 20, true);
|
lv.setUint16(6, 0x0800, true);
|
lv.setUint16(8, 0, true);
|
lv.setUint16(10, dosTime, true);
|
lv.setUint16(12, dosDate, true);
|
lv.setUint32(14, crc, true);
|
lv.setUint32(18, data.length, true);
|
lv.setUint32(22, data.length, true);
|
lv.setUint16(26, nameBytes.length, true);
|
lv.setUint16(28, 0, true);
|
local.set(nameBytes, 30);
|
|
const central = new Uint8Array(46 + nameBytes.length);
|
const cv = new DataView(central.buffer);
|
cv.setUint32(0, 0x02014b50, true);
|
cv.setUint16(4, 20, true);
|
cv.setUint16(6, 20, true);
|
cv.setUint16(8, 0x0800, true);
|
cv.setUint16(10, 0, true);
|
cv.setUint16(12, dosTime, true);
|
cv.setUint16(14, dosDate, true);
|
cv.setUint32(16, crc, true);
|
cv.setUint32(20, data.length, true);
|
cv.setUint32(24, data.length, true);
|
cv.setUint16(28, nameBytes.length, true);
|
cv.setUint16(30, 0, true);
|
cv.setUint16(32, 0, true);
|
cv.setUint16(34, 0, true);
|
cv.setUint16(36, 0, true);
|
cv.setUint32(38, 0, true);
|
cv.setUint32(42, offset, true);
|
central.set(nameBytes, 46);
|
|
localParts.push(local, data);
|
centralParts.push(central);
|
offset += local.length + data.length;
|
});
|
|
const centralSize = centralParts.reduce((sum, part) => sum + part.length, 0);
|
const end = new Uint8Array(22);
|
const ev = new DataView(end.buffer);
|
ev.setUint32(0, 0x06054b50, true);
|
ev.setUint16(4, 0, true);
|
ev.setUint16(6, 0, true);
|
ev.setUint16(8, files.length, true);
|
ev.setUint16(10, files.length, true);
|
ev.setUint32(12, centralSize, true);
|
ev.setUint32(16, offset, true);
|
ev.setUint16(20, 0, true);
|
|
return new Blob([...localParts, ...centralParts, end], { type: mimeType });
|
}
|
|
export default createZip;
|