/** * Minimal ZIP writer (stored, no compression). Images are already compressed, * so deflating them gains nothing, and this avoids a dependency. */ const CRC_TABLE = (() => { const table = new Uint32Array(256); for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; table[n] = c >>> 0; } return table; })(); function crc32(bytes) { let crc = 0xffffffff; for (let i = 0; i < bytes.length; i++) { crc = CRC_TABLE[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8); } return (crc ^ 0xffffffff) >>> 0; } function header(size, write) { const view = new DataView(new ArrayBuffer(size)); write(view); return new Uint8Array(view.buffer); } /** @param {{name: string, data: Uint8Array}[]} files @returns {Blob} */ export function createZip(files) { const encoder = new TextEncoder(); const now = new Date(); const time = (now.getHours() << 11) | (now.getMinutes() << 5) | (now.getSeconds() >> 1); const date = ((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate(); const parts = []; const central = []; let offset = 0; for (const { name, data } of files) { const nameBytes = encoder.encode(name); const crc = crc32(data); const local = header(30, (v) => { v.setUint32(0, 0x04034b50, true); v.setUint16(4, 20, true); v.setUint16(6, 0x0800, true); // UTF-8 names v.setUint16(8, 0, true); // stored v.setUint16(10, time, true); v.setUint16(12, date, true); v.setUint32(14, crc, true); v.setUint32(18, data.length, true); v.setUint32(22, data.length, true); v.setUint16(26, nameBytes.length, true); }); parts.push(local, nameBytes, data); central.push( header(46, (v) => { v.setUint32(0, 0x02014b50, true); v.setUint16(4, 20, true); v.setUint16(6, 20, true); v.setUint16(8, 0x0800, true); v.setUint16(10, 0, true); v.setUint16(12, time, true); v.setUint16(14, date, true); v.setUint32(16, crc, true); v.setUint32(20, data.length, true); v.setUint32(24, data.length, true); v.setUint16(28, nameBytes.length, true); v.setUint32(42, offset, true); }), nameBytes ); offset += local.length + nameBytes.length + data.length; } const centralSize = central.reduce((sum, part) => sum + part.length, 0); const end = header(22, (v) => { v.setUint32(0, 0x06054b50, true); v.setUint16(8, files.length, true); v.setUint16(10, files.length, true); v.setUint32(12, centralSize, true); v.setUint32(16, offset, true); }); return new Blob([...parts, ...central, end], { type: "application/zip" }); }