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.
- package/LICENSE +9 -0
- package/README.md +209 -0
- package/package.json +53 -0
- package/src/animation.js +817 -0
- package/src/charts.js +989 -0
- package/src/clipboard.js +416 -0
- package/src/colors.js +297 -0
- package/src/effects3d.js +312 -0
- package/src/extract.js +535 -0
- package/src/fntdata.js +265 -0
- package/src/fonts.js +676 -0
- package/src/index.js +751 -0
- package/src/pdf.js +298 -0
- package/src/render.js +1964 -0
- package/src/shapes.js +666 -0
- package/src/slideshow.js +492 -0
- package/src/smartart.js +696 -0
- package/src/svg.js +732 -0
- package/src/theme.js +88 -0
- package/src/utils.js +50 -0
- package/src/writer.js +1015 -0
- package/src/zip-writer.js +214 -0
- package/src/zip.js +194 -0
|
@@ -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
|
+
}
|