pptx-browser 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,214 @@
1
+ /**
2
+ * zip-writer.js — ZIP archive serializer. Zero dependencies.
3
+ *
4
+ * Uses the native CompressionStream('deflate-raw') API for compression.
5
+ * Browser support: Chrome 80+, Firefox 113+, Safari 16.4+, Node 18+
6
+ * (same as the existing zip.js reader).
7
+ *
8
+ * Usage:
9
+ * const w = new ZipWriter();
10
+ * w.addText('hello.txt', 'Hello world');
11
+ * w.addBytes('data.bin', uint8array);
12
+ * const zipBytes = await w.finalize(); // → Uint8Array
13
+ */
14
+
15
+ // ── DEFLATE via CompressionStream ─────────────────────────────────────────────
16
+
17
+ async function deflateRaw(data) {
18
+ const cs = new CompressionStream('deflate-raw');
19
+ const writer = cs.writable.getWriter();
20
+ const reader = cs.readable.getReader();
21
+
22
+ writer.write(data);
23
+ writer.close();
24
+
25
+ const chunks = [];
26
+ let total = 0;
27
+ for (;;) {
28
+ const { value, done } = await reader.read();
29
+ if (done) break;
30
+ chunks.push(value);
31
+ total += value.length;
32
+ }
33
+ const out = new Uint8Array(total);
34
+ let off = 0;
35
+ for (const c of chunks) { out.set(c, off); off += c.length; }
36
+ return out;
37
+ }
38
+
39
+ // ── CRC-32 ────────────────────────────────────────────────────────────────────
40
+
41
+ const CRC_TABLE = (() => {
42
+ const t = new Uint32Array(256);
43
+ for (let i = 0; i < 256; i++) {
44
+ let c = i;
45
+ for (let j = 0; j < 8; j++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
46
+ t[i] = c;
47
+ }
48
+ return t;
49
+ })();
50
+
51
+ function crc32(data) {
52
+ let crc = 0xFFFFFFFF;
53
+ for (let i = 0; i < data.length; i++) crc = CRC_TABLE[(crc ^ data[i]) & 0xFF] ^ (crc >>> 8);
54
+ return (crc ^ 0xFFFFFFFF) >>> 0;
55
+ }
56
+
57
+ // ── Binary helpers ────────────────────────────────────────────────────────────
58
+
59
+ class BufWriter {
60
+ constructor() { this._chunks = []; this._size = 0; }
61
+ append(u8) { this._chunks.push(u8); this._size += u8.length; }
62
+ get size() { return this._size; }
63
+ concat() {
64
+ const out = new Uint8Array(this._size);
65
+ let off = 0;
66
+ for (const c of this._chunks) { out.set(c, off); off += c.length; }
67
+ return out;
68
+ }
69
+ }
70
+
71
+ function u16le(n) { return new Uint8Array([(n) & 0xFF, (n >> 8) & 0xFF]); }
72
+ function u32le(n) {
73
+ return new Uint8Array([(n) & 0xFF, (n >> 8) & 0xFF, (n >> 16) & 0xFF, (n >> 24) & 0xFF]);
74
+ }
75
+
76
+ const enc = new TextEncoder();
77
+ function utf8bytes(s) { return enc.encode(s); }
78
+
79
+ // MS-DOS epoch: 1980-01-01 00:00:00
80
+ const DOS_DATE = 0x4A21; // 2025-01-01
81
+ const DOS_TIME = 0x0000;
82
+
83
+ // ── ZipWriter ────────────────────────────────────────────────────────────────
84
+
85
+ export class ZipWriter {
86
+ constructor() {
87
+ /** @type {Array<{name, nameBytes, compData, uncompSize, crc, method, localOffset}>} */
88
+ this._entries = [];
89
+ }
90
+
91
+ /**
92
+ * Add a file from a UTF-8 string.
93
+ * @param {string} name
94
+ * @param {string} text
95
+ */
96
+ async addText(name, text) {
97
+ return this.addBytes(name, enc.encode(text));
98
+ }
99
+
100
+ /**
101
+ * Add a file from raw bytes, with optional DEFLATE compression.
102
+ * @param {string} name
103
+ * @param {Uint8Array} data
104
+ * @param {boolean} [compress=true] false for already-compressed data
105
+ */
106
+ async addBytes(name, data, compress = true) {
107
+ const nameBytes = utf8bytes(name);
108
+ const crc = crc32(data);
109
+ const uncompSize = data.length;
110
+
111
+ let compData, method;
112
+ if (compress && data.length > 32) {
113
+ const deflated = await deflateRaw(data);
114
+ // Only use deflated if it's actually smaller
115
+ if (deflated.length < data.length) {
116
+ compData = deflated;
117
+ method = 8; // DEFLATE
118
+ } else {
119
+ compData = data;
120
+ method = 0; // STORED
121
+ }
122
+ } else {
123
+ compData = data;
124
+ method = 0; // STORED
125
+ }
126
+
127
+ this._entries.push({ name, nameBytes, compData, uncompSize, crc, method, localOffset: 0 });
128
+ }
129
+
130
+ /**
131
+ * Serialize the archive and return the ZIP bytes.
132
+ * @returns {Promise<Uint8Array>}
133
+ */
134
+ async finalize() {
135
+ const body = new BufWriter();
136
+ const cdEntries = [];
137
+
138
+ // ── Local file headers + data ─────────────────────────────────────────
139
+ for (const entry of this._entries) {
140
+ entry.localOffset = body.size;
141
+
142
+ // Local file header (signature 0x04034b50)
143
+ body.append(new Uint8Array([0x50, 0x4B, 0x03, 0x04])); // signature
144
+ body.append(u16le(20)); // version needed: 2.0
145
+ body.append(u16le(0x800)); // general purpose bit flag: UTF-8 name
146
+ body.append(u16le(entry.method)); // compression method
147
+ body.append(u16le(DOS_TIME)); // last mod time
148
+ body.append(u16le(DOS_DATE)); // last mod date
149
+ body.append(u32le(entry.crc)); // CRC-32
150
+ body.append(u32le(entry.compData.length)); // compressed size
151
+ body.append(u32le(entry.uncompSize)); // uncompressed size
152
+ body.append(u16le(entry.nameBytes.length)); // filename length
153
+ body.append(u16le(0)); // extra field length
154
+ body.append(entry.nameBytes);
155
+ body.append(entry.compData);
156
+ }
157
+
158
+ const cdOffset = body.size;
159
+
160
+ // ── Central directory ─────────────────────────────────────────────────
161
+ for (const entry of this._entries) {
162
+ body.append(new Uint8Array([0x50, 0x4B, 0x01, 0x02])); // CD signature
163
+ body.append(u16le(0x031E)); // version made by: Unix, 3.0
164
+ body.append(u16le(20)); // version needed
165
+ body.append(u16le(0x800)); // general purpose bit flag
166
+ body.append(u16le(entry.method));
167
+ body.append(u16le(DOS_TIME));
168
+ body.append(u16le(DOS_DATE));
169
+ body.append(u32le(entry.crc));
170
+ body.append(u32le(entry.compData.length));
171
+ body.append(u32le(entry.uncompSize));
172
+ body.append(u16le(entry.nameBytes.length));
173
+ body.append(u16le(0)); // extra field length
174
+ body.append(u16le(0)); // file comment length
175
+ body.append(u16le(0)); // disk number start
176
+ body.append(u16le(0)); // internal file attributes
177
+ body.append(u32le(0)); // external file attributes
178
+ body.append(u32le(entry.localOffset));
179
+ body.append(entry.nameBytes);
180
+ cdEntries.push(entry);
181
+ }
182
+
183
+ const cdSize = body.size - cdOffset;
184
+
185
+ // ── End of central directory ──────────────────────────────────────────
186
+ body.append(new Uint8Array([0x50, 0x4B, 0x05, 0x06])); // EOCD signature
187
+ body.append(u16le(0)); // disk number
188
+ body.append(u16le(0)); // start disk
189
+ body.append(u16le(this._entries.length)); // entries on this disk
190
+ body.append(u16le(this._entries.length)); // total entries
191
+ body.append(u32le(cdSize)); // central dir size
192
+ body.append(u32le(cdOffset)); // central dir offset
193
+ body.append(u16le(0)); // comment length
194
+
195
+ return body.concat();
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Convenience: create a ZIP from a files map (path → Uint8Array).
201
+ * @param {Record<string, Uint8Array|string>} files
202
+ * @returns {Promise<Uint8Array>}
203
+ */
204
+ export async function writeZip(files) {
205
+ const w = new ZipWriter();
206
+ for (const [path, data] of Object.entries(files)) {
207
+ if (typeof data === 'string') {
208
+ await w.addText(path, data);
209
+ } else {
210
+ await w.addBytes(path, data);
211
+ }
212
+ }
213
+ return w.finalize();
214
+ }
package/src/zip.js ADDED
@@ -0,0 +1,194 @@
1
+ /**
2
+ * zip.js — Lightweight ZIP/ZIP64 reader, zero dependencies.
3
+ *
4
+ * Uses the browser/Node built-in DecompressionStream API for DEFLATE.
5
+ * Browser support: Chrome 80+, Firefox 113+, Safari 16.4+, Node 18+
6
+ *
7
+ * Supported compression methods:
8
+ * 0 = STORED (no compression)
9
+ * 8 = DEFLATE (standard)
10
+ */
11
+
12
+ /**
13
+ * Parse a ZIP archive.
14
+ * @param {ArrayBuffer|Uint8Array} input
15
+ * @returns {Promise<Record<string, Uint8Array>>} path → bytes map
16
+ */
17
+ export async function readZip(input) {
18
+ const data = input instanceof Uint8Array ? input : new Uint8Array(input);
19
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
20
+
21
+ // ── Locate EOCD (End of Central Directory) ──────────────────────────────────
22
+ // Signature 0x06054b50. The comment field can be up to 65535 bytes, so we
23
+ // search backwards that far. Stop as soon as we find a valid signature that
24
+ // yields a sane central-directory offset.
25
+ const EOCD_SIG = 0x06054b50;
26
+ const EOCD64_SIG = 0x06064b50; // ZIP64 end record
27
+ const EOCD64_LOC = 0x07064b50; // ZIP64 end locator
28
+
29
+ let eocdOff = -1;
30
+ const searchStart = Math.max(0, data.length - 65557);
31
+ for (let i = data.length - 22; i >= searchStart; i--) {
32
+ if (view.getUint32(i, true) === EOCD_SIG) {
33
+ eocdOff = i;
34
+ break;
35
+ }
36
+ }
37
+ if (eocdOff === -1) throw new Error('Not a valid ZIP file: EOCD not found');
38
+
39
+ // ── Check for ZIP64 ─────────────────────────────────────────────────────────
40
+ // If the EOCD64 locator sits just before the EOCD, use the ZIP64 end record.
41
+ let cdOffset, cdCount;
42
+
43
+ const locatorOff = eocdOff - 20;
44
+ if (locatorOff >= 0 && view.getUint32(locatorOff, true) === EOCD64_LOC) {
45
+ // ZIP64: offset of EOCD64 record is an 8-byte value at locator+8
46
+ const eocd64Off = Number(view.getBigUint64(locatorOff + 8, true));
47
+ if (view.getUint32(eocd64Off, true) !== EOCD64_SIG) {
48
+ throw new Error('Invalid ZIP64 end record');
49
+ }
50
+ cdOffset = Number(view.getBigUint64(eocd64Off + 48, true));
51
+ cdCount = Number(view.getBigUint64(eocd64Off + 32, true));
52
+ } else {
53
+ // Standard ZIP32
54
+ cdOffset = view.getUint32(eocdOff + 16, true);
55
+ cdCount = view.getUint16(eocdOff + 8, true);
56
+ }
57
+
58
+ if (cdOffset + 4 > data.length) {
59
+ throw new Error('ZIP central directory offset is outside the file');
60
+ }
61
+
62
+ // ── Walk Central Directory ──────────────────────────────────────────────────
63
+ const CD_SIG = 0x02014b50;
64
+ const files = {};
65
+ let pos = cdOffset;
66
+
67
+ for (let i = 0; i < cdCount; i++) {
68
+ if (pos + 46 > data.length) break;
69
+ if (view.getUint32(pos, true) !== CD_SIG) break;
70
+
71
+ const method = view.getUint16(pos + 10, true);
72
+ let compSize = view.getUint32(pos + 20, true);
73
+ let uncompSize = view.getUint32(pos + 24, true);
74
+ const nameLen = view.getUint16(pos + 28, true);
75
+ const extraLen = view.getUint16(pos + 30, true);
76
+ const commentLen = view.getUint16(pos + 32, true);
77
+ let localOffset = view.getUint32(pos + 42, true);
78
+ const name = utf8(data, pos + 46, nameLen);
79
+
80
+ // ── ZIP64 extra field ────────────────────────────────────────────────────
81
+ // If any 32-bit size fields are 0xFFFFFFFF, read true values from extra.
82
+ if (compSize === 0xFFFFFFFF || uncompSize === 0xFFFFFFFF || localOffset === 0xFFFFFFFF) {
83
+ const extraStart = pos + 46 + nameLen;
84
+ const extraEnd = extraStart + extraLen;
85
+ let ep = extraStart;
86
+ while (ep + 4 <= extraEnd) {
87
+ const tag = view.getUint16(ep, true);
88
+ const size = view.getUint16(ep + 2, true);
89
+ if (tag === 0x0001) { // ZIP64 extended info
90
+ let off = ep + 4;
91
+ if (uncompSize === 0xFFFFFFFF && off + 8 <= extraEnd) { uncompSize = Number(view.getBigUint64(off, true)); off += 8; }
92
+ if (compSize === 0xFFFFFFFF && off + 8 <= extraEnd) { compSize = Number(view.getBigUint64(off, true)); off += 8; }
93
+ if (localOffset === 0xFFFFFFFF && off + 8 <= extraEnd) { localOffset = Number(view.getBigUint64(off, true)); }
94
+ break;
95
+ }
96
+ ep += 4 + size;
97
+ }
98
+ }
99
+
100
+ pos += 46 + nameLen + extraLen + commentLen;
101
+
102
+ if (name.endsWith('/')) continue; // directory entry — skip
103
+ if (method !== 0 && method !== 8) continue; // unsupported compression
104
+
105
+ // ── Local File Header ────────────────────────────────────────────────────
106
+ if (localOffset + 30 > data.length) continue;
107
+ const lhNameLen = view.getUint16(localOffset + 26, true);
108
+ const lhExtraLen = view.getUint16(localOffset + 28, true);
109
+ const dataStart = localOffset + 30 + lhNameLen + lhExtraLen;
110
+ if (dataStart + compSize > data.length) continue;
111
+
112
+ const compData = data.subarray(dataStart, dataStart + compSize);
113
+
114
+ try {
115
+ files[name] = method === 0
116
+ ? compData.slice() // STORED
117
+ : await inflateRaw(compData); // DEFLATE
118
+ } catch (e) {
119
+ console.warn(`[zip.js] Failed to decompress "${name}":`, e.message);
120
+ }
121
+ }
122
+
123
+ return files;
124
+ }
125
+
126
+ // ── Decompression ─────────────────────────────────────────────────────────────
127
+
128
+ /**
129
+ * Decompress raw DEFLATE bytes using the native DecompressionStream API.
130
+ * Much faster than a JS implementation and ~0 bundle cost.
131
+ */
132
+ async function inflateRaw(compData) {
133
+ const ds = new DecompressionStream('deflate-raw');
134
+ const writer = ds.writable.getWriter();
135
+ const reader = ds.readable.getReader();
136
+
137
+ writer.write(compData);
138
+ writer.close();
139
+
140
+ const chunks = [];
141
+ let totalLen = 0;
142
+
143
+ for (;;) {
144
+ const { value, done } = await reader.read();
145
+ if (done) break;
146
+ chunks.push(value);
147
+ totalLen += value.length;
148
+ }
149
+
150
+ // Concatenate all output chunks into a single typed array
151
+ const out = new Uint8Array(totalLen);
152
+ let off = 0;
153
+ for (const c of chunks) { out.set(c, off); off += c.length; }
154
+ return out;
155
+ }
156
+
157
+ // ── Helpers ───────────────────────────────────────────────────────────────────
158
+
159
+ /** Decode a UTF-8 slice of a Uint8Array. */
160
+ function utf8(data, start, len) {
161
+ return new TextDecoder().decode(data.subarray(start, start + len));
162
+ }
163
+
164
+ // ── Public API ────────────────────────────────────────────────────────────────
165
+
166
+ /**
167
+ * Read a file from the ZIP as a UTF-8 string.
168
+ * @param {Record<string,Uint8Array>} files
169
+ * @param {string} path
170
+ * @returns {string|null}
171
+ */
172
+ export function getFileText(files, path) {
173
+ const d = files[path];
174
+ return d ? new TextDecoder().decode(d) : null;
175
+ }
176
+
177
+ /**
178
+ * Get raw bytes for a file in the ZIP.
179
+ * @param {Record<string,Uint8Array>} files
180
+ * @param {string} path
181
+ * @returns {Uint8Array|null}
182
+ */
183
+ export function getFileBytes(files, path) {
184
+ return files[path] ?? null;
185
+ }
186
+
187
+ /**
188
+ * List all file paths in the ZIP (excluding directories).
189
+ * @param {Record<string,Uint8Array>} files
190
+ * @returns {string[]}
191
+ */
192
+ export function listFiles(files) {
193
+ return Object.keys(files);
194
+ }