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
package/src/fntdata.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fntdata.js — Embedded PPTX font decoder.
|
|
3
|
+
*
|
|
4
|
+
* PowerPoint embeds fonts as .fntdata files inside ppt/fonts/.
|
|
5
|
+
* These are standard TrueType/OpenType fonts XOR-obfuscated with a key
|
|
6
|
+
* derived from the relationship ID, as specified in ECMA-376 §15.2.12.
|
|
7
|
+
*
|
|
8
|
+
* The 32-byte XOR key is constructed from the relationship GUID and applied
|
|
9
|
+
* to the first 32 bytes of the font data. The rest of the file is plain TTF.
|
|
10
|
+
*
|
|
11
|
+
* After decoding, the font is loaded via the FontFace API so it becomes
|
|
12
|
+
* available for canvas text rendering automatically.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* await loadEmbeddedFonts(renderer._files, renderer._allRels);
|
|
16
|
+
* // fonts are now available — renderer will use them automatically
|
|
17
|
+
*
|
|
18
|
+
* Or via the renderer instance:
|
|
19
|
+
* await renderer.loadEmbeddedFonts();
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// ── XOR key derivation (ECMA-376 §15.2.12) ────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Derive the 32-byte obfuscation key from a relationship ID string.
|
|
26
|
+
*
|
|
27
|
+
* The relationship ID is a GUID in the form:
|
|
28
|
+
* {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}
|
|
29
|
+
*
|
|
30
|
+
* The key is constructed by parsing the GUID into its binary representation
|
|
31
|
+
* and XOR-interleaving according to the spec.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} rId — relationship GUID (from fontRef in presentation.xml)
|
|
34
|
+
* @returns {Uint8Array} 32-byte key
|
|
35
|
+
*/
|
|
36
|
+
function deriveObfuscationKey(rId) {
|
|
37
|
+
// Extract hex digits from GUID, stripping braces and dashes
|
|
38
|
+
const hex = rId.replace(/[{}\-]/g, '');
|
|
39
|
+
if (hex.length < 32) {
|
|
40
|
+
// Pad or repeat if too short (non-standard GUID)
|
|
41
|
+
const padded = hex.padEnd(32, '0');
|
|
42
|
+
return hexToBytes(padded.slice(0, 32));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ECMA-376 specifies the key as the GUID bytes in a specific byte order:
|
|
46
|
+
// Data1 (4 bytes, little-endian), Data2 (2 bytes, little-endian),
|
|
47
|
+
// Data3 (2 bytes, little-endian), Data4 (8 bytes, big-endian)
|
|
48
|
+
const data1 = hex.slice(0, 8); // 4 bytes
|
|
49
|
+
const data2 = hex.slice(8, 12); // 2 bytes
|
|
50
|
+
const data3 = hex.slice(12, 16); // 2 bytes
|
|
51
|
+
const data4 = hex.slice(16, 32); // 8 bytes
|
|
52
|
+
|
|
53
|
+
const key = new Uint8Array(16);
|
|
54
|
+
// Data1: little-endian
|
|
55
|
+
key[0] = parseInt(data1.slice(6, 8), 16);
|
|
56
|
+
key[1] = parseInt(data1.slice(4, 6), 16);
|
|
57
|
+
key[2] = parseInt(data1.slice(2, 4), 16);
|
|
58
|
+
key[3] = parseInt(data1.slice(0, 2), 16);
|
|
59
|
+
// Data2: little-endian
|
|
60
|
+
key[4] = parseInt(data2.slice(2, 4), 16);
|
|
61
|
+
key[5] = parseInt(data2.slice(0, 2), 16);
|
|
62
|
+
// Data3: little-endian
|
|
63
|
+
key[6] = parseInt(data3.slice(2, 4), 16);
|
|
64
|
+
key[7] = parseInt(data3.slice(0, 2), 16);
|
|
65
|
+
// Data4: big-endian
|
|
66
|
+
for (let i = 0; i < 8; i++) {
|
|
67
|
+
key[8 + i] = parseInt(data4.slice(i * 2, i * 2 + 2), 16);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The spec says the 32-byte key is formed by repeating the 16-byte GUID key twice
|
|
71
|
+
const key32 = new Uint8Array(32);
|
|
72
|
+
key32.set(key);
|
|
73
|
+
key32.set(key, 16);
|
|
74
|
+
return key32;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function hexToBytes(hex) {
|
|
78
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
79
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
80
|
+
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
81
|
+
}
|
|
82
|
+
return bytes;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Decode font data ──────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Decode an .fntdata buffer by XOR-ing the first 32 bytes with the key.
|
|
89
|
+
* Returns a Uint8Array containing valid TTF/OTF font data.
|
|
90
|
+
*
|
|
91
|
+
* @param {Uint8Array} data — raw .fntdata file contents
|
|
92
|
+
* @param {Uint8Array} key — 32-byte XOR key from deriveObfuscationKey()
|
|
93
|
+
* @returns {Uint8Array} — decoded font bytes
|
|
94
|
+
*/
|
|
95
|
+
function decodeFontData(data, key) {
|
|
96
|
+
const decoded = new Uint8Array(data);
|
|
97
|
+
// XOR only the first 32 bytes
|
|
98
|
+
for (let i = 0; i < Math.min(32, decoded.length); i++) {
|
|
99
|
+
decoded[i] ^= key[i];
|
|
100
|
+
}
|
|
101
|
+
return decoded;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Detect if decoded bytes look like a valid font.
|
|
106
|
+
* TTF starts with 0x00010000, OTF with 'OTTO', woff with 'wOFF'.
|
|
107
|
+
*/
|
|
108
|
+
function isValidFont(bytes) {
|
|
109
|
+
if (bytes.length < 4) return false;
|
|
110
|
+
const sig = (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3];
|
|
111
|
+
return sig === 0x00010000 // TTF
|
|
112
|
+
|| sig === 0x4F54544F // 'OTTO' - OTF CFF
|
|
113
|
+
|| sig === 0x74727565 // 'true'
|
|
114
|
+
|| sig === 0x774F4646 // 'wOFF'
|
|
115
|
+
|| sig === 0x774F4632; // 'wOF2'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Font registry ─────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
/** Tracks which embedded fonts have been loaded to avoid double-loading. */
|
|
121
|
+
const _loadedEmbedded = new Set();
|
|
122
|
+
|
|
123
|
+
// ── Main decoder ──────────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Parse presentation.xml's <p:embeddedFontLst> to find embedded fonts,
|
|
127
|
+
* decode them, and load them via FontFace API.
|
|
128
|
+
*
|
|
129
|
+
* @param {Record<string,Uint8Array>} files — ZIP file map
|
|
130
|
+
* @param {object} presRels — rels for presentation.xml
|
|
131
|
+
* @returns {Promise<EmbeddedFontResult[]>}
|
|
132
|
+
*/
|
|
133
|
+
export async function loadEmbeddedFonts(files, presRels) {
|
|
134
|
+
const results = [];
|
|
135
|
+
|
|
136
|
+
// Find presentation.xml
|
|
137
|
+
const presXml = files['ppt/presentation.xml'];
|
|
138
|
+
if (!presXml) return results;
|
|
139
|
+
|
|
140
|
+
const parser = new DOMParser();
|
|
141
|
+
const presDoc = parser.parseFromString(new TextDecoder().decode(presXml), 'application/xml');
|
|
142
|
+
|
|
143
|
+
const embeddedFontLst = presDoc.querySelector('embeddedFontLst') ||
|
|
144
|
+
[...presDoc.getElementsByTagName('*')].find(el => el.localName === 'embeddedFontLst');
|
|
145
|
+
|
|
146
|
+
if (!embeddedFontLst) return results;
|
|
147
|
+
|
|
148
|
+
// Each <p:embeddedFont> has a <p:font typeface="..."/> and <p:regular/bold/italic/boldItalic r:id="rId..."/>
|
|
149
|
+
const embeddedFonts = [...embeddedFontLst.children].filter(el => el.localName === 'embeddedFont');
|
|
150
|
+
|
|
151
|
+
for (const fontEl of embeddedFonts) {
|
|
152
|
+
const fontDescEl = [...fontEl.children].find(el => el.localName === 'font');
|
|
153
|
+
if (!fontDescEl) continue;
|
|
154
|
+
|
|
155
|
+
const typeface = fontDescEl.getAttribute('typeface') || fontDescEl.getAttribute('t');
|
|
156
|
+
if (!typeface) continue;
|
|
157
|
+
|
|
158
|
+
const variants = [
|
|
159
|
+
{ el: [...fontEl.children].find(e => e.localName === 'regular'), weight: '400', style: 'normal' },
|
|
160
|
+
{ el: [...fontEl.children].find(e => e.localName === 'bold'), weight: '700', style: 'normal' },
|
|
161
|
+
{ el: [...fontEl.children].find(e => e.localName === 'italic'), weight: '400', style: 'italic' },
|
|
162
|
+
{ el: [...fontEl.children].find(e => e.localName === 'boldItalic'),weight: '700', style: 'italic' },
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
for (const { el, weight, style } of variants) {
|
|
166
|
+
if (!el) continue;
|
|
167
|
+
|
|
168
|
+
// Get rId — attribute may be r:id or just id
|
|
169
|
+
const rId = el.getAttribute('r:id') || el.getAttribute('id') || el.getAttributeNS(
|
|
170
|
+
'http://schemas.openxmlformats.org/officeDocument/2006/relationships', 'id'
|
|
171
|
+
);
|
|
172
|
+
if (!rId) continue;
|
|
173
|
+
|
|
174
|
+
const cacheKey = `${typeface}:${weight}:${style}`;
|
|
175
|
+
if (_loadedEmbedded.has(cacheKey)) {
|
|
176
|
+
results.push({ typeface, weight, style, status: 'already-loaded' });
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Resolve relationship to file path
|
|
181
|
+
const rel = presRels ? presRels[rId] : null;
|
|
182
|
+
if (!rel) {
|
|
183
|
+
results.push({ typeface, weight, style, status: 'rel-not-found', rId });
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const rawData = files[rel.fullPath];
|
|
188
|
+
if (!rawData) {
|
|
189
|
+
results.push({ typeface, weight, style, status: 'file-not-found', path: rel.fullPath });
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Derive decryption key from the rId
|
|
194
|
+
// The rId used for key derivation is the GUID from the relationship target, not the Id
|
|
195
|
+
// In practice many tools use the rId string directly; we try both approaches.
|
|
196
|
+
let decoded = null;
|
|
197
|
+
const key = deriveObfuscationKey(rId);
|
|
198
|
+
const attempt1 = decodeFontData(rawData, key);
|
|
199
|
+
|
|
200
|
+
if (isValidFont(attempt1)) {
|
|
201
|
+
decoded = attempt1;
|
|
202
|
+
} else {
|
|
203
|
+
// Try with the full target path's basename as GUID
|
|
204
|
+
const basename = rel.fullPath.split('/').pop().replace('.fntdata', '');
|
|
205
|
+
const key2 = deriveObfuscationKey(basename);
|
|
206
|
+
const attempt2 = decodeFontData(rawData, key2);
|
|
207
|
+
if (isValidFont(attempt2)) {
|
|
208
|
+
decoded = attempt2;
|
|
209
|
+
} else {
|
|
210
|
+
// Try raw (some .fntdata files aren't actually obfuscated)
|
|
211
|
+
if (isValidFont(rawData)) {
|
|
212
|
+
decoded = rawData;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!decoded) {
|
|
218
|
+
results.push({ typeface, weight, style, status: 'decode-failed', path: rel.fullPath });
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Load via FontFace API
|
|
223
|
+
try {
|
|
224
|
+
const fontFace = new FontFace(typeface, decoded.buffer, { weight, style });
|
|
225
|
+
await fontFace.load();
|
|
226
|
+
document.fonts.add(fontFace);
|
|
227
|
+
_loadedEmbedded.add(cacheKey);
|
|
228
|
+
results.push({ typeface, weight, style, status: 'loaded', path: rel.fullPath });
|
|
229
|
+
} catch (err) {
|
|
230
|
+
results.push({ typeface, weight, style, status: 'load-failed', error: err.message });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return results;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get info about embedded fonts without loading them.
|
|
240
|
+
* Useful for displaying what fonts are embedded in a PPTX.
|
|
241
|
+
*
|
|
242
|
+
* @param {Record<string,Uint8Array>} files
|
|
243
|
+
* @returns {EmbeddedFontInfo[]}
|
|
244
|
+
*/
|
|
245
|
+
export function listEmbeddedFonts(files) {
|
|
246
|
+
const presXml = files['ppt/presentation.xml'];
|
|
247
|
+
if (!presXml) return [];
|
|
248
|
+
|
|
249
|
+
const presDoc = new DOMParser().parseFromString(
|
|
250
|
+
new TextDecoder().decode(presXml), 'application/xml'
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const embeddedFonts = [...presDoc.getElementsByTagName('*')]
|
|
254
|
+
.filter(el => el.localName === 'embeddedFont');
|
|
255
|
+
|
|
256
|
+
return embeddedFonts.map(fontEl => {
|
|
257
|
+
const fontDescEl = [...fontEl.children].find(el => el.localName === 'font');
|
|
258
|
+
const typeface = fontDescEl ? (fontDescEl.getAttribute('typeface') || fontDescEl.getAttribute('t') || '') : '';
|
|
259
|
+
const variants = ['regular', 'bold', 'italic', 'boldItalic']
|
|
260
|
+
.filter(v => [...fontEl.children].some(el => el.localName === v));
|
|
261
|
+
return { typeface, variants, loaded: variants.some(v => _loadedEmbedded.has(
|
|
262
|
+
`${typeface}:${v === 'bold' || v === 'boldItalic' ? '700' : '400'}:${v.includes('talic') ? 'italic' : 'normal'}`
|
|
263
|
+
))};
|
|
264
|
+
}).filter(f => f.typeface);
|
|
265
|
+
}
|