/** * zip-store.js — a minimal, dependency-free STORED (uncompressed) ZIP writer. * * Extracted from analysis/xlsx.js so the shapefile writer can share it. Both * produce small archives (a few analysis rows, a handful of drawn features) * where DEFLATE would save nothing worth a compression dependency, and every * reader — Excel, QGIS, GDAL, unzip — accepts stored entries. * * zipStore({ 'a/b.txt': 'hello', 'a/c.bin': new Uint8Array([1, 2]) }) * * Values may be strings (encoded UTF-8), Uint8Array, or ArrayBuffer. */ const CRC_TABLE = (() => { const t = 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); t[n] = c >>> 0; } return t; })(); function crc32(bytes) { let c = 0xFFFFFFFF; for (let i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 0xFF] ^ (c >>> 8); return (c ^ 0xFFFFFFFF) >>> 0; } function toBytes(enc, value) { if (value instanceof Uint8Array) return value; if (value instanceof ArrayBuffer) return new Uint8Array(value); if (ArrayBuffer.isView(value)) { return new Uint8Array(value.buffer, value.byteOffset, value.byteLength); } return enc.encode(String(value)); } /** * Pack files (path → string | Uint8Array | ArrayBuffer) into an uncompressed * ZIP Blob. * * @param {Record} files * @param {string} [mimeType] Blob type for the result. * @returns {Blob} */ export function zipStore(files, mimeType = 'application/zip') { const enc = new TextEncoder(); const entries = Object.entries(files).map(([name, value]) => { const nameBytes = enc.encode(name); return { nameBytes, data: toBytes(enc, value), // Bit 11 declares the name is UTF-8. Only set when it matters, so // pure-ASCII archives keep byte-for-byte the output xlsx.js had before. flags: nameBytes.length === name.length ? 0 : 0x0800, }; }); // DOS time/date — a fixed timestamp keeps output deterministic. const time = 0, date = ((2020 - 1980) << 9) | (1 << 5) | 1; const chunks = []; const central = []; let offset = 0; const u16 = (v) => [v & 0xFF, (v >>> 8) & 0xFF]; const u32 = (v) => [v & 0xFF, (v >>> 8) & 0xFF, (v >>> 16) & 0xFF, (v >>> 24) & 0xFF]; for (const e of entries) { const crc = crc32(e.data); const size = e.data.length; const local = [ ...u32(0x04034b50), ...u16(20), ...u16(e.flags), ...u16(0), ...u16(time), ...u16(date), ...u32(crc), ...u32(size), ...u32(size), ...u16(e.nameBytes.length), ...u16(0), ]; chunks.push(new Uint8Array(local), e.nameBytes, e.data); central.push([ ...u32(0x02014b50), ...u16(20), ...u16(20), ...u16(e.flags), ...u16(0), ...u16(time), ...u16(date), ...u32(crc), ...u32(size), ...u32(size), ...u16(e.nameBytes.length), ...u16(0), ...u16(0), ...u16(0), ...u16(0), ...u32(0), ...u32(offset), ]); central.push(e.nameBytes); offset += local.length + e.nameBytes.length + size; } const cdParts = []; let cdSize = 0; for (const part of central) { const arr = part instanceof Uint8Array ? part : new Uint8Array(part); cdParts.push(arr); cdSize += arr.length; } const end = new Uint8Array([ ...u32(0x06054b50), ...u16(0), ...u16(0), ...u16(entries.length), ...u16(entries.length), ...u32(cdSize), ...u32(offset), ...u16(0), ]); return new Blob([...chunks, ...cdParts, end], { type: mimeType }); } /** Trigger a browser download of a Blob. */ export function downloadBlob(blob, filename) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000); }