@file-viewer/renderer-iwork 0.0.1 → 2.3.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 +5 -158
- package/README.en.md +2 -15
- package/README.md +2 -15
- package/THIRD_PARTY_NOTICES.md +6 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +7 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +17 -72
- package/dist/iwork.d.ts +2 -0
- package/dist/iwork.js +368 -0
- package/dist/iwork.parser.d.ts +1 -0
- package/dist/iwork.parser.js +100 -0
- package/dist/iwork.worker.d.ts +1 -0
- package/dist/iwork.worker.js +100 -0
- package/dist/limits.d.ts +2 -0
- package/dist/limits.js +7 -0
- package/dist/model.d.ts +122 -0
- package/dist/model.js +1 -0
- package/dist/parser.d.ts +17 -2
- package/dist/parser.js +1387 -1
- package/dist/snappy.d.ts +2 -0
- package/dist/snappy.js +109 -0
- package/dist/workerClient.d.ts +3 -0
- package/dist/workerClient.js +60 -0
- package/package.json +37 -22
package/dist/parser.js
CHANGED
|
@@ -1 +1,1387 @@
|
|
|
1
|
-
|
|
1
|
+
import { DOMParser as XmlDomParser } from '@xmldom/xmldom';
|
|
2
|
+
import JSZip, {} from 'jszip';
|
|
3
|
+
import { ungzip } from 'pako';
|
|
4
|
+
import { read, utils } from 'styled-exceljs';
|
|
5
|
+
import * as keynoteArchivesNamespace from 'keynote-archives';
|
|
6
|
+
import { IworkContainerMismatchError } from './errors.js';
|
|
7
|
+
import { DEFAULT_IWORK_PARSE_LIMITS } from './limits.js';
|
|
8
|
+
import { decompressIwaFile } from './snappy.js';
|
|
9
|
+
// keynote-archives 2.x publishes CommonJS only. A named ESM import happens to
|
|
10
|
+
// work in the workspace build, but fails when a cold Vite project consumes the
|
|
11
|
+
// published renderer. Resolve the CommonJS namespace once and keep the parser
|
|
12
|
+
// independent of the bundler's named-export interop policy.
|
|
13
|
+
const keynoteArchivesModule = ('default' in keynoteArchivesNamespace && keynoteArchivesNamespace.default
|
|
14
|
+
? keynoteArchivesNamespace.default
|
|
15
|
+
: keynoteArchivesNamespace);
|
|
16
|
+
const { KeynoteArchives, TSPArchiveMessages, TSCHArchives } = keynoteArchivesModule;
|
|
17
|
+
export { IworkContainerMismatchError } from './errors.js';
|
|
18
|
+
export { DEFAULT_IWORK_PARSE_LIMITS } from './limits.js';
|
|
19
|
+
const createXmlParser = () => new XmlDomParser();
|
|
20
|
+
const localName = (node) => (node.localName || node.nodeName).split(':').pop().toLowerCase();
|
|
21
|
+
const childElements = (node, name) => Array.from(node.childNodes)
|
|
22
|
+
.filter((child) => child.nodeType === 1)
|
|
23
|
+
.filter(child => !name || localName(child) === name);
|
|
24
|
+
const descendants = (node, names) => {
|
|
25
|
+
const wanted = new Set(names.map(name => name.toLowerCase()));
|
|
26
|
+
const result = [];
|
|
27
|
+
const visit = (current, depth) => {
|
|
28
|
+
if (depth > 256)
|
|
29
|
+
return;
|
|
30
|
+
childElements(current).forEach(child => {
|
|
31
|
+
if (wanted.has(localName(child)))
|
|
32
|
+
result.push(child);
|
|
33
|
+
visit(child, depth + 1);
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
visit(node, 0);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
const cleanText = (value) => value.replace(/[\t\r ]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim();
|
|
40
|
+
const nodeText = (node) => cleanText((node === null || node === void 0 ? void 0 : node.textContent) || '');
|
|
41
|
+
const attribute = (node, ...names) => {
|
|
42
|
+
if (!node)
|
|
43
|
+
return undefined;
|
|
44
|
+
for (const name of names) {
|
|
45
|
+
const value = node.getAttribute(name);
|
|
46
|
+
if (value != null && value !== '')
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
};
|
|
51
|
+
const numericAttribute = (node, ...names) => {
|
|
52
|
+
const value = Number(attribute(node, ...names));
|
|
53
|
+
return Number.isFinite(value) ? value : undefined;
|
|
54
|
+
};
|
|
55
|
+
const firstDescendant = (node, names) => descendants(node, names)[0];
|
|
56
|
+
const hasAncestor = (node, names, boundary) => {
|
|
57
|
+
const wanted = new Set(names.map(name => name.toLowerCase()));
|
|
58
|
+
for (let current = node.parentNode; current && current !== boundary; current = current.parentNode) {
|
|
59
|
+
if (current.nodeType === 1 && wanted.has(localName(current)))
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
};
|
|
64
|
+
const kindFromType = (type) => {
|
|
65
|
+
switch ((type || '').toLowerCase()) {
|
|
66
|
+
case 'numbers': return 'numbers';
|
|
67
|
+
case 'key':
|
|
68
|
+
case 'keynote': return 'keynote';
|
|
69
|
+
default: return 'pages';
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const sceneSize = (kind) => kind === 'keynote'
|
|
73
|
+
? { width: 1280, height: 720 }
|
|
74
|
+
: kind === 'numbers'
|
|
75
|
+
? { width: 1440, height: 900 }
|
|
76
|
+
: { width: 794, height: 1123 };
|
|
77
|
+
const textBlocks = (values, kind) => {
|
|
78
|
+
const size = sceneSize(kind);
|
|
79
|
+
const margin = kind === 'keynote' ? 80 : 64;
|
|
80
|
+
const lineHeight = kind === 'keynote' ? 48 : 28;
|
|
81
|
+
return values.filter(Boolean).slice(0, 500).map((text, index) => ({
|
|
82
|
+
id: `text-${index + 1}`,
|
|
83
|
+
text,
|
|
84
|
+
x: margin,
|
|
85
|
+
y: margin + index * lineHeight,
|
|
86
|
+
width: Math.max(120, size.width - margin * 2),
|
|
87
|
+
height: Math.max(lineHeight, Math.ceil(text.length / 55) * lineHeight),
|
|
88
|
+
fontSize: index === 0 && kind === 'keynote' ? 34 : kind === 'keynote' ? 24 : 16,
|
|
89
|
+
bold: index === 0,
|
|
90
|
+
}));
|
|
91
|
+
};
|
|
92
|
+
const createScene = (kind, id, name, values, tables = [], notes = []) => ({ id, name, ...sceneSize(kind), blocks: textBlocks(values, kind), tables, objects: [], notes });
|
|
93
|
+
const entrySizes = (entry) => {
|
|
94
|
+
const data = entry._data;
|
|
95
|
+
return { compressed: Number((data === null || data === void 0 ? void 0 : data.compressedSize) || 0), uncompressed: Number((data === null || data === void 0 ? void 0 : data.uncompressedSize) || 0) };
|
|
96
|
+
};
|
|
97
|
+
const validateZipDirectory = (zip, limits) => {
|
|
98
|
+
let total = 0;
|
|
99
|
+
let objects = 0;
|
|
100
|
+
for (const entry of Object.values(zip.files)) {
|
|
101
|
+
objects += 1;
|
|
102
|
+
if (objects > limits.maxObjects)
|
|
103
|
+
throw new Error('iWork ZIP entry count exceeds the configured safety limit.');
|
|
104
|
+
if (entry.dir)
|
|
105
|
+
continue;
|
|
106
|
+
const { compressed, uncompressed } = entrySizes(entry);
|
|
107
|
+
total += uncompressed;
|
|
108
|
+
if (total > limits.maxUncompressedBytes)
|
|
109
|
+
throw new Error('iWork ZIP declares more uncompressed data than allowed.');
|
|
110
|
+
if (compressed > 0 && uncompressed / compressed > limits.maxCompressionRatio)
|
|
111
|
+
throw new Error(`Unsafe ZIP compression ratio in ${entry.name}.`);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const throwIfSafetyBoundaryError = (error) => {
|
|
115
|
+
if (error instanceof Error && /(?:safety|safe|unsafe|configured).*limit|compression ratio|object count|decompression exceeds|image-pixel/i.test(error.message)) {
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const imageDimensions = (bytes) => {
|
|
120
|
+
if (bytes.length >= 24 && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) {
|
|
121
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
122
|
+
return { width: view.getUint32(16), height: view.getUint32(20) };
|
|
123
|
+
}
|
|
124
|
+
if (bytes.length >= 4 && bytes[0] === 0xff && bytes[1] === 0xd8) {
|
|
125
|
+
let offset = 2;
|
|
126
|
+
while (offset + 9 < bytes.length) {
|
|
127
|
+
if (bytes[offset] !== 0xff) {
|
|
128
|
+
offset += 1;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const marker = bytes[offset + 1];
|
|
132
|
+
const length = (bytes[offset + 2] << 8) | bytes[offset + 3];
|
|
133
|
+
if ([0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf].includes(marker)) {
|
|
134
|
+
return { height: (bytes[offset + 5] << 8) | bytes[offset + 6], width: (bytes[offset + 7] << 8) | bytes[offset + 8] };
|
|
135
|
+
}
|
|
136
|
+
offset += Math.max(2 + length, 2);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (bytes.length >= 30 && new TextDecoder('ascii').decode(bytes.slice(0, 4)) === 'RIFF' && new TextDecoder('ascii').decode(bytes.slice(8, 12)) === 'WEBP') {
|
|
140
|
+
const chunk = new TextDecoder('ascii').decode(bytes.slice(12, 16));
|
|
141
|
+
if (chunk === 'VP8X') {
|
|
142
|
+
const width = 1 + bytes[24] + bytes[25] * 256 + bytes[26] * 65536;
|
|
143
|
+
const height = 1 + bytes[27] + bytes[28] * 256 + bytes[29] * 65536;
|
|
144
|
+
return { width, height };
|
|
145
|
+
}
|
|
146
|
+
if (chunk === 'VP8L' && bytes[20] === 0x2f) {
|
|
147
|
+
const width = 1 + bytes[21] + ((bytes[22] & 0x3f) << 8);
|
|
148
|
+
const height = 1 + (bytes[22] >> 6) + (bytes[23] << 2) + ((bytes[24] & 0x0f) << 10);
|
|
149
|
+
return { width, height };
|
|
150
|
+
}
|
|
151
|
+
if (chunk === 'VP8 ' && bytes[23] === 0x9d && bytes[24] === 0x01 && bytes[25] === 0x2a) {
|
|
152
|
+
return {
|
|
153
|
+
width: bytes[26] + ((bytes[27] & 0x3f) << 8),
|
|
154
|
+
height: bytes[28] + ((bytes[29] & 0x3f) << 8),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
};
|
|
160
|
+
const readBoundedImage = async (entry, name, limits) => {
|
|
161
|
+
const bytes = await entry.async('uint8array');
|
|
162
|
+
const dimensions = imageDimensions(bytes);
|
|
163
|
+
if (dimensions && dimensions.width * dimensions.height > limits.maxImagePixels) {
|
|
164
|
+
throw new Error(`Embedded image ${name} exceeds the image-pixel safety limit.`);
|
|
165
|
+
}
|
|
166
|
+
return bytes;
|
|
167
|
+
};
|
|
168
|
+
const findPreview = async (zip, limits) => {
|
|
169
|
+
const names = [
|
|
170
|
+
'preview-web.jpg', 'preview.jpg', 'QuickLook/Preview.jpg', 'QuickLook/Thumbnail.jpg',
|
|
171
|
+
'QuickLook/Preview.png', 'QuickLook/Thumbnail.png',
|
|
172
|
+
];
|
|
173
|
+
const lower = new Map(Object.keys(zip.files).map(name => [name.toLowerCase(), name]));
|
|
174
|
+
for (const candidate of names) {
|
|
175
|
+
const actual = lower.get(candidate.toLowerCase());
|
|
176
|
+
if (!actual)
|
|
177
|
+
continue;
|
|
178
|
+
const entry = zip.file(actual);
|
|
179
|
+
if (!entry)
|
|
180
|
+
continue;
|
|
181
|
+
const bytes = await entry.async('uint8array');
|
|
182
|
+
if (bytes.length > Math.min(limits.maxUncompressedBytes, 32 * 1024 * 1024))
|
|
183
|
+
continue;
|
|
184
|
+
const dimensions = imageDimensions(bytes);
|
|
185
|
+
if (dimensions && dimensions.width * dimensions.height > limits.maxImagePixels)
|
|
186
|
+
throw new Error(`Embedded preview ${actual} exceeds the image-pixel safety limit.`);
|
|
187
|
+
return { name: actual, mimeType: actual.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg', bytes };
|
|
188
|
+
}
|
|
189
|
+
return undefined;
|
|
190
|
+
};
|
|
191
|
+
const parseXml = (source, createParser) => {
|
|
192
|
+
const document = createParser().parseFromString(source, 'application/xml');
|
|
193
|
+
if (document.getElementsByTagName('parsererror').length)
|
|
194
|
+
throw new Error('iWork XML/APXL could not be parsed.');
|
|
195
|
+
return document;
|
|
196
|
+
};
|
|
197
|
+
const legacyText = (node) => {
|
|
198
|
+
const paragraphs = descendants(node, ['p']).map(nodeText).filter(Boolean);
|
|
199
|
+
const values = paragraphs.length ? paragraphs : descendants(node, ['text', 'string']).map(element => (attribute(element, 'sfa:string', 'sf:string', 'string') || nodeText(element))).filter(Boolean);
|
|
200
|
+
return values.filter((value, index) => values.indexOf(value) === index);
|
|
201
|
+
};
|
|
202
|
+
const xmlGeometry = (node) => {
|
|
203
|
+
const geometry = localName(node) === 'geometry' ? node : firstDescendant(node, ['geometry']);
|
|
204
|
+
const position = geometry && firstDescendant(geometry, ['position']);
|
|
205
|
+
const size = geometry && firstDescendant(geometry, ['size']);
|
|
206
|
+
if (!position || !size)
|
|
207
|
+
return undefined;
|
|
208
|
+
const x = numericAttribute(position, 'sfa:x', 'sf:x', 'x') || 0;
|
|
209
|
+
const y = numericAttribute(position, 'sfa:y', 'sf:y', 'y') || 0;
|
|
210
|
+
const width = numericAttribute(size, 'sfa:w', 'sf:w', 'w');
|
|
211
|
+
const height = numericAttribute(size, 'sfa:h', 'sf:h', 'h');
|
|
212
|
+
return width != null && height != null && width >= 0 && height >= 0 ? { x, y, width, height } : undefined;
|
|
213
|
+
};
|
|
214
|
+
const indexedLegacyElements = (root) => {
|
|
215
|
+
const index = new Map();
|
|
216
|
+
const candidates = [root, ...descendants(root, [
|
|
217
|
+
'master-slide', 'placeholder-style', 'paragraphstyle', 'characterstyle',
|
|
218
|
+
])];
|
|
219
|
+
candidates.forEach(element => {
|
|
220
|
+
const id = attribute(element, 'sfa:ID', 'sf:id', 'id');
|
|
221
|
+
if (id)
|
|
222
|
+
index.set(id, element);
|
|
223
|
+
});
|
|
224
|
+
return index;
|
|
225
|
+
};
|
|
226
|
+
const styleNumber = (style, property) => {
|
|
227
|
+
if (!style)
|
|
228
|
+
return undefined;
|
|
229
|
+
const properties = descendants(style, [property]);
|
|
230
|
+
const value = properties[properties.length - 1];
|
|
231
|
+
return numericAttribute(value && firstDescendant(value, ['number']), 'sfa:number', 'sf:number', 'number');
|
|
232
|
+
};
|
|
233
|
+
const styleString = (style, property) => {
|
|
234
|
+
if (!style)
|
|
235
|
+
return undefined;
|
|
236
|
+
const properties = descendants(style, [property]);
|
|
237
|
+
const value = properties[properties.length - 1];
|
|
238
|
+
const string = value && firstDescendant(value, ['string']);
|
|
239
|
+
return attribute(string, 'sfa:string', 'sf:string', 'string') || nodeText(string);
|
|
240
|
+
};
|
|
241
|
+
const styleColor = (style) => {
|
|
242
|
+
if (!style)
|
|
243
|
+
return undefined;
|
|
244
|
+
const colors = descendants(style, ['fontcolor']).flatMap(property => descendants(property, ['color']));
|
|
245
|
+
const color = colors[colors.length - 1];
|
|
246
|
+
if (!color)
|
|
247
|
+
return undefined;
|
|
248
|
+
const r = numericAttribute(color, 'sfa:r', 'sf:r', 'r');
|
|
249
|
+
const g = numericAttribute(color, 'sfa:g', 'sf:g', 'g');
|
|
250
|
+
const b = numericAttribute(color, 'sfa:b', 'sf:b', 'b');
|
|
251
|
+
if (r == null || g == null || b == null)
|
|
252
|
+
return undefined;
|
|
253
|
+
return `rgb(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)})`;
|
|
254
|
+
};
|
|
255
|
+
const legacyTextRuns = (paragraph, elementIndex) => {
|
|
256
|
+
const runs = [];
|
|
257
|
+
const append = (text, style) => {
|
|
258
|
+
if (!text)
|
|
259
|
+
return;
|
|
260
|
+
runs.push({
|
|
261
|
+
text,
|
|
262
|
+
color: styleColor(style),
|
|
263
|
+
bold: styleNumber(style, 'bold') === 1 || undefined,
|
|
264
|
+
italic: styleNumber(style, 'italic') === 1 || undefined,
|
|
265
|
+
});
|
|
266
|
+
};
|
|
267
|
+
const visit = (node, inheritedStyle) => {
|
|
268
|
+
if (node.nodeType === 3) {
|
|
269
|
+
append(node.nodeValue || '', inheritedStyle);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (node.nodeType !== 1)
|
|
273
|
+
return;
|
|
274
|
+
const element = node;
|
|
275
|
+
if (localName(element) === 'br') {
|
|
276
|
+
append('\n', inheritedStyle);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const styleId = attribute(element, 'sf:style', 'sfa:style', 'style');
|
|
280
|
+
const style = styleId ? elementIndex.get(styleId) || inheritedStyle : inheritedStyle;
|
|
281
|
+
Array.from(element.childNodes).forEach(child => visit(child, style));
|
|
282
|
+
};
|
|
283
|
+
Array.from(paragraph.childNodes).forEach(child => visit(child));
|
|
284
|
+
return runs;
|
|
285
|
+
};
|
|
286
|
+
const legacyKeynoteParagraphs = (storage, baseStyle, elementIndex, title) => descendants(storage, ['p']).map(paragraph => {
|
|
287
|
+
var _a, _b;
|
|
288
|
+
const styleId = attribute(paragraph, 'sf:style', 'sfa:style', 'style');
|
|
289
|
+
const localStyle = styleId ? elementIndex.get(styleId) : undefined;
|
|
290
|
+
let runs = legacyTextRuns(paragraph, elementIndex);
|
|
291
|
+
if (title && styleNumber(localStyle, 'capitalization') === 3) {
|
|
292
|
+
runs = runs.map(run => ({ ...run, text: run.text.replace(/\b\p{L}/gu, value => value.toLocaleUpperCase()) }));
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
runs,
|
|
296
|
+
bullet: numericAttribute(paragraph, 'sf:list-level', 'list-level') != null || undefined,
|
|
297
|
+
color: styleColor(localStyle) || styleColor(baseStyle),
|
|
298
|
+
bold: styleNumber(localStyle, 'bold') === 1 || undefined,
|
|
299
|
+
italic: styleNumber(localStyle, 'italic') === 1 || undefined,
|
|
300
|
+
spaceBefore: (_a = styleNumber(localStyle, 'spacebefore')) !== null && _a !== void 0 ? _a : styleNumber(baseStyle, 'spacebefore'),
|
|
301
|
+
spaceAfter: (_b = styleNumber(localStyle, 'spaceafter')) !== null && _b !== void 0 ? _b : styleNumber(baseStyle, 'spaceafter'),
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
const legacyTableCellValue = (cell) => {
|
|
305
|
+
const string = firstDescendant(cell, ['string']);
|
|
306
|
+
if (string)
|
|
307
|
+
return attribute(string, 'sfa:string', 'sf:string', 'string') || nodeText(string);
|
|
308
|
+
for (const candidate of [cell, ...descendants(cell, ['t', 'n', 'd', 'b', 'f', 'v'])]) {
|
|
309
|
+
const value = attribute(candidate, 'sfa:string', 'sfa:number', 'sf:v', 'sf:value', 'value');
|
|
310
|
+
if (value != null)
|
|
311
|
+
return value;
|
|
312
|
+
const text = nodeText(candidate);
|
|
313
|
+
if (text)
|
|
314
|
+
return text;
|
|
315
|
+
}
|
|
316
|
+
return '';
|
|
317
|
+
};
|
|
318
|
+
const legacyTables = (node, limits) => {
|
|
319
|
+
const simple = descendants(node, ['table']).map((table, tableIndex) => {
|
|
320
|
+
const rows = descendants(table, ['row']).map(row => {
|
|
321
|
+
const cells = descendants(row, ['cell', 'text-cell', 'number-cell', 'date-cell']);
|
|
322
|
+
return cells.map(legacyTableCellValue);
|
|
323
|
+
}).filter(row => row.length);
|
|
324
|
+
const geometry = xmlGeometry(table);
|
|
325
|
+
return { id: `table-${tableIndex + 1}`, x: (geometry === null || geometry === void 0 ? void 0 : geometry.x) || 64, y: (geometry === null || geometry === void 0 ? void 0 : geometry.y) || 160, width: geometry === null || geometry === void 0 ? void 0 : geometry.width, height: geometry === null || geometry === void 0 ? void 0 : geometry.height, rows };
|
|
326
|
+
}).filter(table => table.rows.length);
|
|
327
|
+
const tabular = descendants(node, ['tabular-info']).map((table, tableIndex) => {
|
|
328
|
+
const model = firstDescendant(table, ['tabular-model']);
|
|
329
|
+
const grid = model && firstDescendant(model, ['grid']);
|
|
330
|
+
if (!model || !grid)
|
|
331
|
+
return undefined;
|
|
332
|
+
const rowCount = Math.max(0, Math.floor(numericAttribute(grid, 'sf:numrows', 'numrows') || 0));
|
|
333
|
+
const columnCount = Math.max(0, Math.floor(numericAttribute(grid, 'sf:numcols', 'numcols') || 0));
|
|
334
|
+
if (!rowCount || !columnCount || rowCount * columnCount > limits.maxObjects)
|
|
335
|
+
return undefined;
|
|
336
|
+
const rows = Array.from({ length: rowCount }, () => Array(columnCount).fill(''));
|
|
337
|
+
const datasource = firstDescendant(grid, ['datasource']);
|
|
338
|
+
const cells = datasource ? childElements(datasource, 'g') : [];
|
|
339
|
+
cells.slice(0, rowCount * columnCount).forEach((cell, index) => {
|
|
340
|
+
rows[Math.floor(index / columnCount)][index % columnCount] = legacyTableCellValue(cell);
|
|
341
|
+
});
|
|
342
|
+
const geometry = xmlGeometry(table);
|
|
343
|
+
const columnWidths = childElements(firstDescendant(grid, ['columns']) || grid, 'grid-column')
|
|
344
|
+
.map(column => numericAttribute(column, 'sf:width', 'width') || 0).slice(0, columnCount);
|
|
345
|
+
const rowHeights = childElements(firstDescendant(grid, ['rows']) || grid, 'grid-row')
|
|
346
|
+
.map(row => numericAttribute(row, 'sf:height', 'height') || 0).slice(0, rowCount);
|
|
347
|
+
return {
|
|
348
|
+
id: attribute(model, 'sfa:ID', 'sf:id', 'id') || `table-${simple.length + tableIndex + 1}`,
|
|
349
|
+
x: (geometry === null || geometry === void 0 ? void 0 : geometry.x) || 0,
|
|
350
|
+
y: (geometry === null || geometry === void 0 ? void 0 : geometry.y) || 0,
|
|
351
|
+
width: geometry === null || geometry === void 0 ? void 0 : geometry.width,
|
|
352
|
+
height: geometry === null || geometry === void 0 ? void 0 : geometry.height,
|
|
353
|
+
rows,
|
|
354
|
+
columnWidths: columnWidths.length === columnCount ? columnWidths : undefined,
|
|
355
|
+
rowHeights: rowHeights.length === rowCount ? rowHeights : undefined,
|
|
356
|
+
};
|
|
357
|
+
}).filter((table) => Boolean(table));
|
|
358
|
+
return [...simple, ...tabular];
|
|
359
|
+
};
|
|
360
|
+
const legacyKeynoteBlocks = (scene, elementIndex) => {
|
|
361
|
+
const masterId = attribute(firstDescendant(scene, ['master-ref']), 'sfa:IDREF', 'sf:IDREF', 'IDREF');
|
|
362
|
+
const master = masterId ? elementIndex.get(masterId) : undefined;
|
|
363
|
+
return descendants(scene, ['text-storage'])
|
|
364
|
+
.filter(storage => !hasAncestor(storage, ['notes'], scene))
|
|
365
|
+
.flatMap((storage, index) => {
|
|
366
|
+
const text = legacyText(storage).join('\n');
|
|
367
|
+
if (!text)
|
|
368
|
+
return [];
|
|
369
|
+
let container = storage;
|
|
370
|
+
let placeholder;
|
|
371
|
+
let title = false;
|
|
372
|
+
while (container.parentNode && container !== scene) {
|
|
373
|
+
container = container.parentNode;
|
|
374
|
+
const name = container.nodeType === 1 ? localName(container) : '';
|
|
375
|
+
title || (title = name.includes('title'));
|
|
376
|
+
if (name.includes('placeholder')) {
|
|
377
|
+
placeholder = container;
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const ownGeometry = placeholder && xmlGeometry(placeholder);
|
|
382
|
+
const sentinelGeometry = (ownGeometry === null || ownGeometry === void 0 ? void 0 : ownGeometry.width) === 100 && ownGeometry.height === 100 && ownGeometry.x === 0 && ownGeometry.y === 0;
|
|
383
|
+
const masterPlaceholder = master && firstDescendant(master, [title ? 'title-placeholder' : 'body-placeholder']);
|
|
384
|
+
const placeholderStyleRef = firstDescendant(masterPlaceholder || placeholder || scene, ['placeholder-style-ref']);
|
|
385
|
+
const placeholderStyleId = attribute(placeholderStyleRef, 'sfa:IDREF', 'sf:IDREF', 'IDREF');
|
|
386
|
+
const placeholderStyle = placeholderStyleId ? elementIndex.get(placeholderStyleId) : undefined;
|
|
387
|
+
const inheritedGeometry = placeholderStyle && xmlGeometry(placeholderStyle);
|
|
388
|
+
const geometry = sentinelGeometry ? inheritedGeometry : ownGeometry || inheritedGeometry;
|
|
389
|
+
const inlineLayoutStyle = firstDescendant(placeholderStyle || placeholder || scene, ['layoutstyle']);
|
|
390
|
+
const layoutStyleRef = firstDescendant(placeholderStyle || placeholder || scene, ['layoutstyle-ref']);
|
|
391
|
+
const layoutStyleId = attribute(layoutStyleRef, 'sfa:IDREF', 'sf:IDREF', 'IDREF');
|
|
392
|
+
const layoutStyle = inlineLayoutStyle || (layoutStyleId ? elementIndex.get(layoutStyleId) : undefined);
|
|
393
|
+
const baseParagraphStyle = layoutStyle && firstDescendant(layoutStyle, ['paragraphstyle']);
|
|
394
|
+
const fallbackParagraphStyleRef = layoutStyle && firstDescendant(layoutStyle, ['paragraphstyle-ref']);
|
|
395
|
+
const fallbackParagraphStyleId = attribute(fallbackParagraphStyleRef, 'sfa:IDREF', 'sf:IDREF', 'IDREF');
|
|
396
|
+
const paragraphStyle = baseParagraphStyle || (fallbackParagraphStyleId ? elementIndex.get(fallbackParagraphStyleId) : undefined);
|
|
397
|
+
const fontSize = styleNumber(paragraphStyle, 'fontsize') || (title ? 64 : 28);
|
|
398
|
+
const alignment = styleNumber(paragraphStyle, 'alignment');
|
|
399
|
+
const verticalAlignment = styleNumber(layoutStyle, 'verticalalignment');
|
|
400
|
+
const paragraphs = legacyKeynoteParagraphs(storage, paragraphStyle, elementIndex, title);
|
|
401
|
+
return [{
|
|
402
|
+
id: attribute(storage, 'sfa:ID', 'id') || `text-${index + 1}`,
|
|
403
|
+
text,
|
|
404
|
+
x: (geometry === null || geometry === void 0 ? void 0 : geometry.x) || 64,
|
|
405
|
+
y: (geometry === null || geometry === void 0 ? void 0 : geometry.y) || (64 + index * 80),
|
|
406
|
+
width: (geometry === null || geometry === void 0 ? void 0 : geometry.width) || 672,
|
|
407
|
+
height: (geometry === null || geometry === void 0 ? void 0 : geometry.height) || 64,
|
|
408
|
+
fontSize,
|
|
409
|
+
fontFamily: styleString(paragraphStyle, 'fontname') || 'Gill Sans',
|
|
410
|
+
color: styleColor(paragraphStyle),
|
|
411
|
+
bold: styleNumber(paragraphStyle, 'bold') === 1 || undefined,
|
|
412
|
+
italic: styleNumber(paragraphStyle, 'italic') === 1 || undefined,
|
|
413
|
+
align: alignment === 2 ? 'center' : alignment === 3 ? 'right' : 'left',
|
|
414
|
+
verticalAlign: verticalAlignment === 1 ? 'middle' : verticalAlignment === 2 ? 'bottom' : 'top',
|
|
415
|
+
padding: { top: 3, right: 3, bottom: 3, left: 3 },
|
|
416
|
+
paragraphs,
|
|
417
|
+
}];
|
|
418
|
+
});
|
|
419
|
+
};
|
|
420
|
+
const legacySceneDimensions = (root, kind, tables) => {
|
|
421
|
+
if (kind === 'keynote') {
|
|
422
|
+
const size = childElements(root, 'size')[0];
|
|
423
|
+
return { width: numericAttribute(size, 'sfa:w', 'sf:w', 'w') || 800, height: numericAttribute(size, 'sfa:h', 'sf:h', 'h') || 600 };
|
|
424
|
+
}
|
|
425
|
+
if (kind === 'pages') {
|
|
426
|
+
const printInfo = firstDescendant(root, ['slprint-info']);
|
|
427
|
+
return { width: numericAttribute(printInfo, 'sl:page-width', 'page-width') || 595, height: numericAttribute(printInfo, 'sl:page-height', 'page-height') || 842 };
|
|
428
|
+
}
|
|
429
|
+
const width = Math.max(720, ...tables.map(table => table.x + (table.width || 0) + 74));
|
|
430
|
+
const height = Math.max(540, ...tables.map(table => table.y + (table.height || 0) + 73));
|
|
431
|
+
return { width, height };
|
|
432
|
+
};
|
|
433
|
+
const legacyPagesValues = (scene, root) => {
|
|
434
|
+
if (scene !== root)
|
|
435
|
+
return legacyText(scene);
|
|
436
|
+
// Pages '09 embeds complete section prototypes in the document root. Those
|
|
437
|
+
// prototypes are editing templates, not visible document content. Reading
|
|
438
|
+
// every <p> from the root made an empty Apple document render pages of
|
|
439
|
+
// overlapping template text. Only the body storage outside a prototype is
|
|
440
|
+
// the document's live word-processing flow.
|
|
441
|
+
return descendants(root, ['text-storage'])
|
|
442
|
+
.filter(storage => attribute(storage, 'sf:kind', 'kind') === 'body')
|
|
443
|
+
.filter(storage => !hasAncestor(storage, ['prototype'], root))
|
|
444
|
+
.flatMap(legacyText);
|
|
445
|
+
};
|
|
446
|
+
const parseLegacy = async (zip, kind, limits, createParser) => {
|
|
447
|
+
var _a;
|
|
448
|
+
const indexName = Object.keys(zip.files).find(name => /(?:^|\/)(index\.xml(?:\.gz)?|index\.apxl)$/i.test(name));
|
|
449
|
+
if (!indexName)
|
|
450
|
+
throw new Error("The iWork '09 index.xml/index.apxl entry was not found.");
|
|
451
|
+
const entry = zip.file(indexName);
|
|
452
|
+
if (!entry)
|
|
453
|
+
throw new Error(`Missing ${indexName}.`);
|
|
454
|
+
const raw = await entry.async('uint8array');
|
|
455
|
+
const bytes = indexName.toLowerCase().endsWith('.gz') ? ungzip(raw) : raw;
|
|
456
|
+
if (bytes.length > limits.maxUncompressedBytes)
|
|
457
|
+
throw new Error("The iWork '09 XML exceeds the configured safety limit.");
|
|
458
|
+
const document = parseXml(new TextDecoder().decode(bytes), createParser);
|
|
459
|
+
const root = document.documentElement;
|
|
460
|
+
const elementIndex = indexedLegacyElements(root);
|
|
461
|
+
let sceneElements;
|
|
462
|
+
if (kind === 'keynote')
|
|
463
|
+
sceneElements = descendants(root, ['slide']).filter(slide => localName(slide.parentNode) === 'slide-list');
|
|
464
|
+
else if (kind === 'numbers')
|
|
465
|
+
sceneElements = descendants(root, ['workspace', 'sheet']);
|
|
466
|
+
else
|
|
467
|
+
sceneElements = descendants(root, ['page']);
|
|
468
|
+
if (!sceneElements.length)
|
|
469
|
+
sceneElements = [root];
|
|
470
|
+
if (sceneElements.length > limits.maxObjects)
|
|
471
|
+
throw new Error('iWork scene count exceeds the configured safety limit.');
|
|
472
|
+
const scenes = sceneElements.map((scene, index) => {
|
|
473
|
+
var _a;
|
|
474
|
+
// Numbers workspaces contain implementation layer names such as
|
|
475
|
+
// LSWorkspaceCommentLayer. They are not document text and must not leak
|
|
476
|
+
// into search or export. Cell text is represented by the table model.
|
|
477
|
+
const values = kind === 'numbers' ? [] : kind === 'pages' ? legacyPagesValues(scene, root) : legacyText(scene);
|
|
478
|
+
const notes = kind === 'keynote' ? descendants(scene, ['notes', 'speaker-notes']).map(nodeText).filter(Boolean) : [];
|
|
479
|
+
const tables = legacyTables(scene, limits).map(table => {
|
|
480
|
+
var _a, _b;
|
|
481
|
+
return kind === 'numbers' ? {
|
|
482
|
+
...table,
|
|
483
|
+
x: table.x + 71,
|
|
484
|
+
y: table.y + 71,
|
|
485
|
+
// Numbers '09 stores the usable row content height. Apple includes the
|
|
486
|
+
// half-point grid stroke for every rendered row when calculating the
|
|
487
|
+
// table frame. Preserve that distinction so dense legacy sheets do not
|
|
488
|
+
// progressively drift vertically from the native export.
|
|
489
|
+
height: table.height == null ? table.height : table.height + table.rows.length * 0.5,
|
|
490
|
+
rowHeights: (_a = table.rowHeights) === null || _a === void 0 ? void 0 : _a.map(height => height + 0.5),
|
|
491
|
+
headerRows: Math.min(1, table.rows.length),
|
|
492
|
+
headerColumns: Math.min(1, ((_b = table.rows[0]) === null || _b === void 0 ? void 0 : _b.length) || 0),
|
|
493
|
+
headerRowBackground: '#ececec',
|
|
494
|
+
headerColumnBackground: '#ececec',
|
|
495
|
+
borderColor: '#d6d6d6',
|
|
496
|
+
} : table;
|
|
497
|
+
});
|
|
498
|
+
const blocks = kind === 'keynote' ? legacyKeynoteBlocks(scene, elementIndex) : textBlocks(values, kind);
|
|
499
|
+
const name = attribute(scene, 'ls:workspace-name', 'key:name', 'name', 'title') || ((_a = blocks[0]) === null || _a === void 0 ? void 0 : _a.text) || values[0] || `${kind === 'keynote' ? 'Slide' : kind === 'numbers' ? 'Sheet' : 'Page'} ${index + 1}`;
|
|
500
|
+
return {
|
|
501
|
+
id: `scene-${index + 1}`,
|
|
502
|
+
name,
|
|
503
|
+
...legacySceneDimensions(root, kind, tables),
|
|
504
|
+
blocks,
|
|
505
|
+
tables,
|
|
506
|
+
objects: tables.map(table => ({ id: `${table.id}-visual`, kind: 'table', x: table.x, y: table.y, width: table.width || 0, height: table.height || 0 })),
|
|
507
|
+
notes,
|
|
508
|
+
};
|
|
509
|
+
});
|
|
510
|
+
const objectCount = scenes.reduce((sum, scene) => sum + scene.blocks.length + scene.tables.length, scenes.length);
|
|
511
|
+
return {
|
|
512
|
+
kind,
|
|
513
|
+
generation: 'iwork-09',
|
|
514
|
+
title: ((_a = scenes[0]) === null || _a === void 0 ? void 0 : _a.name) || `Apple ${kind}`,
|
|
515
|
+
scenes,
|
|
516
|
+
preview: await findPreview(zip, limits),
|
|
517
|
+
diagnostics: ["Parsed the iWork '09 XML/APXL container."],
|
|
518
|
+
limits: kind === 'keynote' ? ['Animations and transitions are not executed.'] : kind === 'numbers' ? ['Formula values are read from the saved file.'] : [],
|
|
519
|
+
objectCount,
|
|
520
|
+
limitedPreview: false,
|
|
521
|
+
};
|
|
522
|
+
};
|
|
523
|
+
const readVarint = (bytes, pointer) => {
|
|
524
|
+
let value = 0;
|
|
525
|
+
let shift = 0;
|
|
526
|
+
for (let count = 0; count < 10; count += 1) {
|
|
527
|
+
if (pointer.offset >= bytes.length)
|
|
528
|
+
throw new Error('Malformed protobuf varint.');
|
|
529
|
+
const byte = bytes[pointer.offset++];
|
|
530
|
+
value += (byte & 0x7f) * 2 ** shift;
|
|
531
|
+
if (!(byte & 0x80))
|
|
532
|
+
return value;
|
|
533
|
+
shift += 7;
|
|
534
|
+
}
|
|
535
|
+
throw new Error('Protobuf varint exceeds 10 bytes.');
|
|
536
|
+
};
|
|
537
|
+
const isUsefulString = (value) => {
|
|
538
|
+
const cleaned = cleanText(value);
|
|
539
|
+
if (cleaned.length < 2 || cleaned.length > 2000)
|
|
540
|
+
return false;
|
|
541
|
+
if (/^[\d.\-_/]+$/.test(cleaned))
|
|
542
|
+
return false;
|
|
543
|
+
const printable = Array.from(cleaned).filter(character => !/[\u0000-\u001f\u007f]/.test(character)).length;
|
|
544
|
+
return printable / cleaned.length > 0.92 && /[\p{L}\p{N}]/u.test(cleaned);
|
|
545
|
+
};
|
|
546
|
+
const extractProtobufStrings = (bytes, limits) => {
|
|
547
|
+
const strings = new Set();
|
|
548
|
+
let objects = 0;
|
|
549
|
+
const decoder = new TextDecoder('utf-8', { fatal: false });
|
|
550
|
+
const scan = (payload, depth) => {
|
|
551
|
+
if (depth > Math.min(limits.maxNestingDepth, 24) || strings.size >= 20000)
|
|
552
|
+
return;
|
|
553
|
+
const pointer = { offset: 0 };
|
|
554
|
+
while (pointer.offset < payload.length && objects < limits.maxObjects) {
|
|
555
|
+
const start = pointer.offset;
|
|
556
|
+
try {
|
|
557
|
+
const key = readVarint(payload, pointer);
|
|
558
|
+
const field = Math.floor(key / 8);
|
|
559
|
+
const wire = key & 7;
|
|
560
|
+
if (!field || field > 1000000)
|
|
561
|
+
throw new Error('invalid field');
|
|
562
|
+
objects += 1;
|
|
563
|
+
if (wire === 0)
|
|
564
|
+
readVarint(payload, pointer);
|
|
565
|
+
else if (wire === 1)
|
|
566
|
+
pointer.offset += 8;
|
|
567
|
+
else if (wire === 5)
|
|
568
|
+
pointer.offset += 4;
|
|
569
|
+
else if (wire === 2) {
|
|
570
|
+
const length = readVarint(payload, pointer);
|
|
571
|
+
if (length < 0 || pointer.offset + length > payload.length)
|
|
572
|
+
throw new Error('invalid length');
|
|
573
|
+
const nested = payload.slice(pointer.offset, pointer.offset + length);
|
|
574
|
+
const value = cleanText(decoder.decode(nested));
|
|
575
|
+
if (isUsefulString(value))
|
|
576
|
+
strings.add(value);
|
|
577
|
+
if (length >= 2)
|
|
578
|
+
scan(nested, depth + 1);
|
|
579
|
+
pointer.offset += length;
|
|
580
|
+
}
|
|
581
|
+
else
|
|
582
|
+
throw new Error('unsupported wire');
|
|
583
|
+
if (pointer.offset > payload.length)
|
|
584
|
+
throw new Error('overflow');
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
pointer.offset = start + 1;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
scan(bytes, 0);
|
|
592
|
+
const decoded = decoder.decode(bytes);
|
|
593
|
+
for (const match of decoded.matchAll(/[\p{L}\p{N}][\p{L}\p{N}\p{P}\p{Zs}]{2,240}/gu)) {
|
|
594
|
+
const value = cleanText(match[0]);
|
|
595
|
+
if (isUsefulString(value))
|
|
596
|
+
strings.add(value);
|
|
597
|
+
if (strings.size >= 20000)
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
return { strings: [...strings], objects };
|
|
601
|
+
};
|
|
602
|
+
const loadModernIwaEntries = async (zip, limits) => {
|
|
603
|
+
const entries = [];
|
|
604
|
+
for (const entry of Object.values(zip.files).filter(item => !item.dir && /(?:^|\/)index\/.*\.iwa$/i.test(item.name))) {
|
|
605
|
+
entries.push({ name: entry.name, bytes: await entry.async('uint8array') });
|
|
606
|
+
}
|
|
607
|
+
const nestedEntry = Object.values(zip.files).find(item => !item.dir && /(?:^|\/)index\.zip$/i.test(item.name));
|
|
608
|
+
if (nestedEntry) {
|
|
609
|
+
const nestedBytes = await nestedEntry.async('uint8array');
|
|
610
|
+
const nestedZip = await JSZip.loadAsync(nestedBytes);
|
|
611
|
+
validateZipDirectory(nestedZip, limits);
|
|
612
|
+
for (const entry of Object.values(nestedZip.files).filter(item => !item.dir && /\.iwa$/i.test(item.name))) {
|
|
613
|
+
entries.push({ name: `Index.zip/${entry.name}`, bytes: await entry.async('uint8array') });
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (!entries.length)
|
|
617
|
+
throw new Error('No modern IWA object streams were found.');
|
|
618
|
+
return entries;
|
|
619
|
+
};
|
|
620
|
+
const worksheetRows = (workbook, name) => {
|
|
621
|
+
const sheet = workbook.Sheets[name];
|
|
622
|
+
const range = sheet['!ref'] ? utils.decode_range(sheet['!ref']) : undefined;
|
|
623
|
+
const width = range ? range.e.c - range.s.c + 1 : undefined;
|
|
624
|
+
const height = range ? range.e.r - range.s.r + 1 : undefined;
|
|
625
|
+
const rows = utils.sheet_to_json(sheet, { header: 1, raw: false, defval: '', blankrows: true })
|
|
626
|
+
.map(row => row.map(value => String(value !== null && value !== void 0 ? value : '')));
|
|
627
|
+
if (height != null) {
|
|
628
|
+
while (rows.length < height)
|
|
629
|
+
rows.push([]);
|
|
630
|
+
rows.length = height;
|
|
631
|
+
}
|
|
632
|
+
if (width != null) {
|
|
633
|
+
for (const row of rows) {
|
|
634
|
+
while (row.length < width)
|
|
635
|
+
row.push('');
|
|
636
|
+
row.length = width;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return rows;
|
|
640
|
+
};
|
|
641
|
+
const parseModernNumbers = async (buffer, entries, limits) => {
|
|
642
|
+
const workbook = read(buffer, { type: 'array', dense: true, cellDates: true, cellStyles: true, browserPixels: true, drawings: true });
|
|
643
|
+
const decoded = decodeModernObjectMap(entries, limits);
|
|
644
|
+
const tableInfos = [...decoded.objects.entries()].flatMap(([id, object]) => object.messages
|
|
645
|
+
.filter(message => message.info.type === 6000)
|
|
646
|
+
.map(message => {
|
|
647
|
+
var _a, _b, _c, _d;
|
|
648
|
+
const modelId = referenceId((_a = message.data) === null || _a === void 0 ? void 0 : _a.tableModel);
|
|
649
|
+
const model = messageData(modelId ? (_b = decoded.objects.get(modelId)) === null || _b === void 0 ? void 0 : _b.messages.find(candidate => candidate.info.type === 6001) : undefined);
|
|
650
|
+
return { id, info: message.data, model, parentId: referenceId((_d = (_c = message.data) === null || _c === void 0 ? void 0 : _c.super) === null || _d === void 0 ? void 0 : _d.parent) };
|
|
651
|
+
})
|
|
652
|
+
.filter(entry => { var _a, _b; return entry.parentId && ((_a = entry.model) === null || _a === void 0 ? void 0 : _a.numberOfRows) && ((_b = entry.model) === null || _b === void 0 ? void 0 : _b.numberOfColumns); }));
|
|
653
|
+
const sheetParents = [...new Set(tableInfos.map(entry => entry.parentId))];
|
|
654
|
+
const charts = [...decoded.objects.entries()].flatMap(([id, object]) => object.messages
|
|
655
|
+
.filter(message => message.info.type === 5021)
|
|
656
|
+
.map(message => { var _a, _b; return ({ object: chartGeometry(id, message.data, geometryOf(message.data)), parentId: referenceId((_b = (_a = message.data) === null || _a === void 0 ? void 0 : _a.super) === null || _b === void 0 ? void 0 : _b.parent) }); })
|
|
657
|
+
.filter((entry) => Boolean(entry.object)));
|
|
658
|
+
const scenes = workbook.SheetNames.map((name, index) => {
|
|
659
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
660
|
+
const rows = worksheetRows(workbook, name);
|
|
661
|
+
const sheet = workbook.Sheets[name];
|
|
662
|
+
const parentId = sheetParents[index];
|
|
663
|
+
const tableInfo = tableInfos.find(entry => entry.parentId === parentId) || tableInfos[index];
|
|
664
|
+
const model = (tableInfo === null || tableInfo === void 0 ? void 0 : tableInfo.model) || {};
|
|
665
|
+
const geometry = geometryOf(tableInfo === null || tableInfo === void 0 ? void 0 : tableInfo.info);
|
|
666
|
+
const columnCount = Math.max(Number(model.numberOfColumns || 0), ...rows.map(row => row.length));
|
|
667
|
+
const rowCount = Math.max(Number(model.numberOfRows || 0), rows.length);
|
|
668
|
+
const defaultColumnWidth = Number(model.defaultColumnWidth || 98);
|
|
669
|
+
const defaultRowHeight = Number(model.defaultRowHeight || 20);
|
|
670
|
+
const fittingRows = ((_c = (_b = (_a = tableInfo === null || tableInfo === void 0 ? void 0 : tableInfo.info) === null || _a === void 0 ? void 0 : _a.layoutEngine) === null || _b === void 0 ? void 0 : _b.widthHeightCache) === null || _c === void 0 ? void 0 : _c.rowsFittingEntries) || [];
|
|
671
|
+
const columnWidths = Array.from({ length: columnCount }, () => defaultColumnWidth);
|
|
672
|
+
const rowHeights = Array.from({ length: rowCount }, (_, row) => { var _a; return Number(((_a = fittingRows[row]) === null || _a === void 0 ? void 0 : _a.fittingSize) || defaultRowHeight); });
|
|
673
|
+
const canvasMargin = 72;
|
|
674
|
+
const tableNameHeight = model.tableNameEnabled === false ? 0 : 30;
|
|
675
|
+
const tableX = canvasMargin + Number(((_d = geometry === null || geometry === void 0 ? void 0 : geometry.position) === null || _d === void 0 ? void 0 : _d.x) || 0);
|
|
676
|
+
const tableY = canvasMargin + Number(((_e = geometry === null || geometry === void 0 ? void 0 : geometry.position) === null || _e === void 0 ? void 0 : _e.y) || 0) + tableNameHeight;
|
|
677
|
+
const tableWidth = columnWidths.reduce((sum, width) => sum + width, 0);
|
|
678
|
+
const tableHeight = rowHeights.reduce((sum, height) => sum + height, 0);
|
|
679
|
+
const table = {
|
|
680
|
+
id: `table-${(tableInfo === null || tableInfo === void 0 ? void 0 : tableInfo.id) || index + 1}`,
|
|
681
|
+
x: tableX,
|
|
682
|
+
y: tableY,
|
|
683
|
+
width: tableWidth,
|
|
684
|
+
height: tableHeight,
|
|
685
|
+
rows,
|
|
686
|
+
columnWidths,
|
|
687
|
+
rowHeights,
|
|
688
|
+
headerRows: Number(model.numberOfHeaderRows || 0),
|
|
689
|
+
headerColumns: Number(model.numberOfHeaderColumns || 0),
|
|
690
|
+
fontSize: 13.333,
|
|
691
|
+
fontFamily: 'Helvetica Neue',
|
|
692
|
+
headerRowBackground: '#bec0c0',
|
|
693
|
+
headerColumnBackground: '#d4d4d4',
|
|
694
|
+
merges: (_f = sheet['!merges']) === null || _f === void 0 ? void 0 : _f.map(merge => ({
|
|
695
|
+
row: merge.s.r,
|
|
696
|
+
col: merge.s.c,
|
|
697
|
+
rowspan: merge.e.r - merge.s.r + 1,
|
|
698
|
+
colspan: merge.e.c - merge.s.c + 1,
|
|
699
|
+
})),
|
|
700
|
+
};
|
|
701
|
+
const blocks = model.tableNameEnabled === false ? [] : [{
|
|
702
|
+
id: `table-name-${(tableInfo === null || tableInfo === void 0 ? void 0 : tableInfo.id) || index + 1}`,
|
|
703
|
+
text: String(model.tableName || name),
|
|
704
|
+
x: tableX,
|
|
705
|
+
y: canvasMargin + Number(((_g = geometry === null || geometry === void 0 ? void 0 : geometry.position) === null || _g === void 0 ? void 0 : _g.y) || 0) + 5,
|
|
706
|
+
width: tableWidth,
|
|
707
|
+
height: 22,
|
|
708
|
+
fontSize: 14,
|
|
709
|
+
fontFamily: 'Helvetica Neue',
|
|
710
|
+
align: 'center',
|
|
711
|
+
}];
|
|
712
|
+
const visualObjects = charts.filter(chart => chart.parentId === parentId).map(chart => ({
|
|
713
|
+
...chart.object,
|
|
714
|
+
x: chart.object.x + canvasMargin,
|
|
715
|
+
y: chart.object.y + canvasMargin,
|
|
716
|
+
}));
|
|
717
|
+
const contentRight = Math.max(tableX + tableWidth, ...visualObjects.map(object => object.x + object.width));
|
|
718
|
+
const contentBottom = Math.max(tableY + tableHeight, ...visualObjects.map(object => object.y + object.height));
|
|
719
|
+
return {
|
|
720
|
+
id: `sheet-${index + 1}`,
|
|
721
|
+
name,
|
|
722
|
+
width: Math.ceil((contentRight + canvasMargin) / 4) * 4,
|
|
723
|
+
height: Math.max(792, Math.ceil(contentBottom + canvasMargin)),
|
|
724
|
+
blocks,
|
|
725
|
+
tables: rows.length ? [table] : [],
|
|
726
|
+
objects: visualObjects,
|
|
727
|
+
notes: [],
|
|
728
|
+
};
|
|
729
|
+
});
|
|
730
|
+
return { workbook, scenes, decodedMessages: decoded.decodedMessages, skippedFrames: decoded.skippedFrames };
|
|
731
|
+
};
|
|
732
|
+
const messageData = (message) => (message === null || message === void 0 ? void 0 : message.data) || {};
|
|
733
|
+
const referenceId = (value) => (value === null || value === void 0 ? void 0 : value.identifier) == null ? undefined : String(value.identifier);
|
|
734
|
+
const geometryOf = (value) => {
|
|
735
|
+
var _a, _b, _c, _d, _e, _f;
|
|
736
|
+
return ((_a = value === null || value === void 0 ? void 0 : value.super) === null || _a === void 0 ? void 0 : _a.geometry)
|
|
737
|
+
|| ((_c = (_b = value === null || value === void 0 ? void 0 : value.super) === null || _b === void 0 ? void 0 : _b.super) === null || _c === void 0 ? void 0 : _c.geometry)
|
|
738
|
+
|| ((_f = (_e = (_d = value === null || value === void 0 ? void 0 : value.super) === null || _d === void 0 ? void 0 : _d.super) === null || _e === void 0 ? void 0 : _e.super) === null || _f === void 0 ? void 0 : _f.geometry);
|
|
739
|
+
};
|
|
740
|
+
const storageIdOf = (value) => {
|
|
741
|
+
var _a, _b;
|
|
742
|
+
return referenceId(value === null || value === void 0 ? void 0 : value.ownedStorage)
|
|
743
|
+
|| referenceId(value === null || value === void 0 ? void 0 : value.deprecatedStorage)
|
|
744
|
+
|| referenceId((_a = value === null || value === void 0 ? void 0 : value.super) === null || _a === void 0 ? void 0 : _a.ownedStorage)
|
|
745
|
+
|| referenceId((_b = value === null || value === void 0 ? void 0 : value.super) === null || _b === void 0 ? void 0 : _b.deprecatedStorage);
|
|
746
|
+
};
|
|
747
|
+
const storageText = (objects, id) => {
|
|
748
|
+
var _a, _b;
|
|
749
|
+
if (!id)
|
|
750
|
+
return '';
|
|
751
|
+
const storage = (_b = (_a = objects.get(id)) === null || _a === void 0 ? void 0 : _a.messages.find(message => message.info.type === 2001)) === null || _b === void 0 ? void 0 : _b.data;
|
|
752
|
+
return cleanText(Array.isArray(storage === null || storage === void 0 ? void 0 : storage.text) ? storage.text.join('') : String((storage === null || storage === void 0 ? void 0 : storage.text) || ''));
|
|
753
|
+
};
|
|
754
|
+
const styleParentId = (value) => {
|
|
755
|
+
var _a, _b, _c;
|
|
756
|
+
return referenceId((_a = value === null || value === void 0 ? void 0 : value.super) === null || _a === void 0 ? void 0 : _a.parent)
|
|
757
|
+
|| referenceId((_c = (_b = value === null || value === void 0 ? void 0 : value.super) === null || _b === void 0 ? void 0 : _b.super) === null || _c === void 0 ? void 0 : _c.parent);
|
|
758
|
+
};
|
|
759
|
+
const firstStyleId = (table) => referenceId(((table === null || table === void 0 ? void 0 : table.entries) || [])
|
|
760
|
+
.filter((entry) => Number((entry === null || entry === void 0 ? void 0 : entry.characterIndex) || 0) === 0)
|
|
761
|
+
.map((entry) => entry === null || entry === void 0 ? void 0 : entry.object)
|
|
762
|
+
.find(Boolean));
|
|
763
|
+
const colorCss = (color) => {
|
|
764
|
+
if (!color || ![color.r, color.g, color.b].every(Number.isFinite))
|
|
765
|
+
return undefined;
|
|
766
|
+
const component = (value) => Math.round(Math.min(1, Math.max(0, value)) * 255);
|
|
767
|
+
const alpha = Number.isFinite(color.a) ? Math.min(1, Math.max(0, Number(color.a))) : 1;
|
|
768
|
+
return `rgba(${component(color.r)}, ${component(color.g)}, ${component(color.b)}, ${alpha})`;
|
|
769
|
+
};
|
|
770
|
+
const fontFamilyCss = (name) => {
|
|
771
|
+
if (!name)
|
|
772
|
+
return undefined;
|
|
773
|
+
const normalized = name.replace(/-(?:Bold|Medium|Regular|Light|Italic|Oblique).*$/i, '');
|
|
774
|
+
if (/^HelveticaNeue$/i.test(normalized))
|
|
775
|
+
return 'Helvetica Neue';
|
|
776
|
+
return normalized;
|
|
777
|
+
};
|
|
778
|
+
const paragraphAlignment = (alignment) => alignment === 2
|
|
779
|
+
? 'center'
|
|
780
|
+
: alignment === 1
|
|
781
|
+
? 'right'
|
|
782
|
+
: 'left';
|
|
783
|
+
const resolveTextStyle = (objects, id, visited = new Set()) => {
|
|
784
|
+
var _a, _b, _c, _d;
|
|
785
|
+
if (!id || visited.has(id))
|
|
786
|
+
return {};
|
|
787
|
+
visited.add(id);
|
|
788
|
+
const message = (_a = objects.get(id)) === null || _a === void 0 ? void 0 : _a.messages.find(candidate => candidate.info.type === 2021 || candidate.info.type === 2022);
|
|
789
|
+
if (!message)
|
|
790
|
+
return {};
|
|
791
|
+
const inherited = resolveTextStyle(objects, styleParentId(message.data), visited);
|
|
792
|
+
const character = ((_b = message.data) === null || _b === void 0 ? void 0 : _b.charProperties) || {};
|
|
793
|
+
const paragraph = ((_c = message.data) === null || _c === void 0 ? void 0 : _c.paraProperties) || {};
|
|
794
|
+
const lineHeight = Number((_d = paragraph === null || paragraph === void 0 ? void 0 : paragraph.lineSpacing) === null || _d === void 0 ? void 0 : _d.amount);
|
|
795
|
+
return {
|
|
796
|
+
...inherited,
|
|
797
|
+
...(Number.isFinite(character.fontSize) ? { fontSize: Number(character.fontSize) } : {}),
|
|
798
|
+
...(character.fontName ? { fontFamily: fontFamilyCss(String(character.fontName)) } : {}),
|
|
799
|
+
...(character.fontColor ? { color: colorCss(character.fontColor) } : {}),
|
|
800
|
+
...(typeof character.bold === 'boolean' ? { bold: character.bold } : {}),
|
|
801
|
+
...(typeof character.italic === 'boolean' ? { italic: character.italic } : {}),
|
|
802
|
+
...(Number.isFinite(character.tracking) ? { letterSpacing: Number(character.tracking) } : {}),
|
|
803
|
+
// Apple stores proportional line spacing relative to its font metrics.
|
|
804
|
+
// CSS line-height uses the full em box, which is about 0.2 larger for the
|
|
805
|
+
// Helvetica Neue metrics used by current Keynote documents.
|
|
806
|
+
...(Number.isFinite(lineHeight) && lineHeight > 0 ? { lineHeight: lineHeight < 1 ? lineHeight + 0.2 : lineHeight } : {}),
|
|
807
|
+
...(Number.isFinite(paragraph.alignment) ? { align: paragraphAlignment(paragraph.alignment) } : {}),
|
|
808
|
+
};
|
|
809
|
+
};
|
|
810
|
+
const resolveStorageTextStyle = (objects, storageId) => {
|
|
811
|
+
var _a, _b;
|
|
812
|
+
if (!storageId)
|
|
813
|
+
return {};
|
|
814
|
+
const storage = (_b = (_a = objects.get(storageId)) === null || _a === void 0 ? void 0 : _a.messages.find(message => message.info.type === 2001)) === null || _b === void 0 ? void 0 : _b.data;
|
|
815
|
+
const paragraph = resolveTextStyle(objects, firstStyleId(storage === null || storage === void 0 ? void 0 : storage.tableParaStyle));
|
|
816
|
+
const character = resolveTextStyle(objects, firstStyleId(storage === null || storage === void 0 ? void 0 : storage.tableCharStyle));
|
|
817
|
+
return { ...paragraph, ...character };
|
|
818
|
+
};
|
|
819
|
+
const shapeStyleIdOf = (value) => {
|
|
820
|
+
var _a, _b, _c, _d, _e, _f;
|
|
821
|
+
return referenceId(value === null || value === void 0 ? void 0 : value.style)
|
|
822
|
+
|| referenceId((_a = value === null || value === void 0 ? void 0 : value.super) === null || _a === void 0 ? void 0 : _a.style)
|
|
823
|
+
|| referenceId((_c = (_b = value === null || value === void 0 ? void 0 : value.super) === null || _b === void 0 ? void 0 : _b.super) === null || _c === void 0 ? void 0 : _c.style)
|
|
824
|
+
|| referenceId((_f = (_e = (_d = value === null || value === void 0 ? void 0 : value.super) === null || _d === void 0 ? void 0 : _d.super) === null || _e === void 0 ? void 0 : _e.super) === null || _f === void 0 ? void 0 : _f.style);
|
|
825
|
+
};
|
|
826
|
+
const resolveShapeTextStyle = (objects, id, visited = new Set()) => {
|
|
827
|
+
var _a, _b;
|
|
828
|
+
if (!id || visited.has(id))
|
|
829
|
+
return {};
|
|
830
|
+
visited.add(id);
|
|
831
|
+
const message = (_a = objects.get(id)) === null || _a === void 0 ? void 0 : _a.messages.find(candidate => candidate.info.type === 2025);
|
|
832
|
+
if (!message)
|
|
833
|
+
return {};
|
|
834
|
+
const inherited = resolveShapeTextStyle(objects, styleParentId(message.data), visited);
|
|
835
|
+
const shape = ((_b = message.data) === null || _b === void 0 ? void 0 : _b.shapeProperties) || {};
|
|
836
|
+
const padding = shape.padding;
|
|
837
|
+
const verticalAlign = shape.verticalAlignment === 1 ? 'middle' : shape.verticalAlignment === 2 ? 'bottom' : 'top';
|
|
838
|
+
return {
|
|
839
|
+
...inherited,
|
|
840
|
+
...(Number.isFinite(shape.verticalAlignment) ? { verticalAlign } : {}),
|
|
841
|
+
...(padding ? { padding: {
|
|
842
|
+
top: Number(padding.top || 0), right: Number(padding.right || 0),
|
|
843
|
+
bottom: Number(padding.bottom || 0), left: Number(padding.left || 0),
|
|
844
|
+
} } : {}),
|
|
845
|
+
};
|
|
846
|
+
};
|
|
847
|
+
const visualGeometry = (id, kind, geometry, text) => {
|
|
848
|
+
const position = geometry === null || geometry === void 0 ? void 0 : geometry.position;
|
|
849
|
+
const size = geometry === null || geometry === void 0 ? void 0 : geometry.size;
|
|
850
|
+
if (!position || !size || !Number.isFinite(size.width) || !Number.isFinite(size.height))
|
|
851
|
+
return undefined;
|
|
852
|
+
return { id, kind, x: Number(position.x || 0), y: Number(position.y || 0), width: Number(size.width), height: Number(size.height), angle: Number(geometry.angle || 0), text };
|
|
853
|
+
};
|
|
854
|
+
const protobufUnknownFields = (value) => Object.getOwnPropertySymbols(value || {})
|
|
855
|
+
.filter(symbol => String(symbol).includes('protobuf-ts/unknown'))
|
|
856
|
+
.flatMap(symbol => Array.isArray(value[symbol]) ? value[symbol] : []);
|
|
857
|
+
const lengthDelimitedPayload = (bytes) => {
|
|
858
|
+
const pointer = { offset: 0 };
|
|
859
|
+
const length = readVarint(bytes, pointer);
|
|
860
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > bytes.length - pointer.offset)
|
|
861
|
+
return undefined;
|
|
862
|
+
return bytes.slice(pointer.offset, pointer.offset + length);
|
|
863
|
+
};
|
|
864
|
+
const decodeChartData = (drawable) => {
|
|
865
|
+
var _a, _b;
|
|
866
|
+
const extension = protobufUnknownFields(drawable).find(field => field.no === 10000 && field.wireType === 2);
|
|
867
|
+
const payload = extension && lengthDelimitedPayload(extension.data);
|
|
868
|
+
if (!payload)
|
|
869
|
+
return undefined;
|
|
870
|
+
try {
|
|
871
|
+
const archive = TSCHArchives.ChartArchive.fromBinary(payload);
|
|
872
|
+
const categories = (((_a = archive.grid) === null || _a === void 0 ? void 0 : _a.columnName) || []).map(value => String(value));
|
|
873
|
+
const series = (((_b = archive.grid) === null || _b === void 0 ? void 0 : _b.gridRow) || []).map((row, index) => {
|
|
874
|
+
var _a, _b;
|
|
875
|
+
return ({
|
|
876
|
+
name: String(((_b = (_a = archive.grid) === null || _a === void 0 ? void 0 : _a.rowName) === null || _b === void 0 ? void 0 : _b[index]) || `Series ${index + 1}`),
|
|
877
|
+
values: (row.value || []).map(value => Number(value.numericValue || 0)),
|
|
878
|
+
});
|
|
879
|
+
});
|
|
880
|
+
return categories.length && series.length ? { type: archive.chartType === 1 ? 'bar' : 'line', categories, series } : undefined;
|
|
881
|
+
}
|
|
882
|
+
catch {
|
|
883
|
+
return undefined;
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
const chartGeometry = (id, drawable, geometry) => {
|
|
887
|
+
const visual = visualGeometry(id, 'chart', geometry);
|
|
888
|
+
if (visual)
|
|
889
|
+
visual.chart = decodeChartData(drawable);
|
|
890
|
+
return visual;
|
|
891
|
+
};
|
|
892
|
+
const modernTableStrings = (objects, id) => {
|
|
893
|
+
var _a, _b, _c;
|
|
894
|
+
const entries = id && ((_c = (_b = (_a = objects.get(id)) === null || _a === void 0 ? void 0 : _a.messages.find(message => message.info.type === 6005)) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.entries);
|
|
895
|
+
const strings = new Map();
|
|
896
|
+
for (const entry of entries || []) {
|
|
897
|
+
if (Number.isFinite(entry === null || entry === void 0 ? void 0 : entry.key) && typeof (entry === null || entry === void 0 ? void 0 : entry.string) === 'string')
|
|
898
|
+
strings.set(Number(entry.key), entry.string);
|
|
899
|
+
}
|
|
900
|
+
return strings;
|
|
901
|
+
};
|
|
902
|
+
const decimal128 = (bytes, offset) => {
|
|
903
|
+
if (offset < 0 || offset + 16 > bytes.length)
|
|
904
|
+
return NaN;
|
|
905
|
+
const exponent = (bytes[offset + 15] & 0x7f) << 7 | bytes[offset + 14] >> 1;
|
|
906
|
+
let mantissa = bytes[offset + 14] & 1;
|
|
907
|
+
for (let index = offset + 13; index >= offset; index -= 1)
|
|
908
|
+
mantissa = mantissa * 256 + bytes[index];
|
|
909
|
+
return (bytes[offset + 15] & 0x80 ? -mantissa : mantissa) * 10 ** (exponent - 6176);
|
|
910
|
+
};
|
|
911
|
+
const modernCellValue = (bytes, strings) => {
|
|
912
|
+
if (bytes.length < 12 || bytes[0] !== 5)
|
|
913
|
+
return '';
|
|
914
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
915
|
+
const fields = view.getUint32(8, true);
|
|
916
|
+
let offset = 12;
|
|
917
|
+
let decimal = NaN;
|
|
918
|
+
let number = NaN;
|
|
919
|
+
let stringIndex = -1;
|
|
920
|
+
if (fields & 1) {
|
|
921
|
+
decimal = decimal128(bytes, offset);
|
|
922
|
+
offset += 16;
|
|
923
|
+
}
|
|
924
|
+
if (fields & 2 && offset + 8 <= bytes.length) {
|
|
925
|
+
number = view.getFloat64(offset, true);
|
|
926
|
+
offset += 8;
|
|
927
|
+
}
|
|
928
|
+
if (fields & 4)
|
|
929
|
+
offset += 8;
|
|
930
|
+
if (fields & 8 && offset + 4 <= bytes.length) {
|
|
931
|
+
stringIndex = view.getUint32(offset, true);
|
|
932
|
+
offset += 4;
|
|
933
|
+
}
|
|
934
|
+
const type = bytes[1];
|
|
935
|
+
if (type === 3)
|
|
936
|
+
return strings.get(stringIndex) || '';
|
|
937
|
+
if (type === 2 || type === 10)
|
|
938
|
+
return Number.isFinite(decimal) ? String(decimal) : '';
|
|
939
|
+
if (type === 7)
|
|
940
|
+
return Number.isFinite(number) ? String(number / 86400) : '';
|
|
941
|
+
if (type === 6)
|
|
942
|
+
return Number.isFinite(number) ? String(number > 0) : '';
|
|
943
|
+
return '';
|
|
944
|
+
};
|
|
945
|
+
const modernTableRows = (objects, model) => {
|
|
946
|
+
var _a, _b, _c, _d;
|
|
947
|
+
const rowCount = Math.max(0, Number((model === null || model === void 0 ? void 0 : model.numberOfRows) || 0));
|
|
948
|
+
const columnCount = Math.max(0, Number((model === null || model === void 0 ? void 0 : model.numberOfColumns) || 0));
|
|
949
|
+
const rows = Array.from({ length: rowCount }, () => Array(columnCount).fill(''));
|
|
950
|
+
const store = model === null || model === void 0 ? void 0 : model.baseDataStore;
|
|
951
|
+
const strings = modernTableStrings(objects, referenceId(store === null || store === void 0 ? void 0 : store.stringTable));
|
|
952
|
+
const tileSize = Math.max(1, Number(((_a = store === null || store === void 0 ? void 0 : store.tiles) === null || _a === void 0 ? void 0 : _a.tileSize) || 256));
|
|
953
|
+
for (const tileEntry of ((_b = store === null || store === void 0 ? void 0 : store.tiles) === null || _b === void 0 ? void 0 : _b.tiles) || []) {
|
|
954
|
+
const tileId = referenceId(tileEntry === null || tileEntry === void 0 ? void 0 : tileEntry.tile);
|
|
955
|
+
const tile = tileId ? (_d = (_c = objects.get(tileId)) === null || _c === void 0 ? void 0 : _c.messages.find(message => message.info.type === 6002)) === null || _d === void 0 ? void 0 : _d.data : undefined;
|
|
956
|
+
const rowOffset = Number((tileEntry === null || tileEntry === void 0 ? void 0 : tileEntry.tileid) || 0) * tileSize;
|
|
957
|
+
for (const rowInfo of (tile === null || tile === void 0 ? void 0 : tile.rowInfos) || []) {
|
|
958
|
+
const rowIndex = rowOffset + Number((rowInfo === null || rowInfo === void 0 ? void 0 : rowInfo.tileRowIndex) || 0);
|
|
959
|
+
if (!rows[rowIndex])
|
|
960
|
+
continue;
|
|
961
|
+
const storage = rowInfo === null || rowInfo === void 0 ? void 0 : rowInfo.cellStorageBuffer;
|
|
962
|
+
const offsets = rowInfo === null || rowInfo === void 0 ? void 0 : rowInfo.cellOffsets;
|
|
963
|
+
if (!(storage === null || storage === void 0 ? void 0 : storage.length) || !(offsets === null || offsets === void 0 ? void 0 : offsets.length))
|
|
964
|
+
continue;
|
|
965
|
+
const view = new DataView(offsets.buffer, offsets.byteOffset, offsets.byteLength);
|
|
966
|
+
const cells = [];
|
|
967
|
+
for (let column = 0; column < Math.min(columnCount, Math.floor(offsets.length / 2)); column += 1) {
|
|
968
|
+
const offset = view.getUint16(column * 2, true);
|
|
969
|
+
if (offset < 0xffff)
|
|
970
|
+
cells.push({ column, offset });
|
|
971
|
+
}
|
|
972
|
+
cells.forEach((cell, index) => {
|
|
973
|
+
var _a, _b;
|
|
974
|
+
const end = (_b = (_a = cells[index + 1]) === null || _a === void 0 ? void 0 : _a.offset) !== null && _b !== void 0 ? _b : storage.length;
|
|
975
|
+
if (cell.offset <= end && end <= storage.length)
|
|
976
|
+
rows[rowIndex][cell.column] = modernCellValue(storage.slice(cell.offset, end), strings);
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
return rows;
|
|
981
|
+
};
|
|
982
|
+
const imageMimeType = (name) => name.toLowerCase().endsWith('.png') ? 'image/png' : name.toLowerCase().endsWith('.webp') ? 'image/webp' : 'image/jpeg';
|
|
983
|
+
const decodeKeynoteFrame = (frame) => {
|
|
984
|
+
const pointer = { offset: 0 };
|
|
985
|
+
const objects = [];
|
|
986
|
+
while (pointer.offset < frame.length) {
|
|
987
|
+
const archiveLength = readVarint(frame, pointer);
|
|
988
|
+
if (archiveLength > frame.length - pointer.offset)
|
|
989
|
+
throw new Error('Keynote ArchiveInfo exceeds its Snappy frame.');
|
|
990
|
+
const archive = TSPArchiveMessages.ArchiveInfo.fromBinary(frame.slice(pointer.offset, pointer.offset + archiveLength));
|
|
991
|
+
pointer.offset += archiveLength;
|
|
992
|
+
const messages = [];
|
|
993
|
+
for (const info of archive.messageInfos) {
|
|
994
|
+
const length = Number(info.length);
|
|
995
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > frame.length - pointer.offset)
|
|
996
|
+
throw new Error('Keynote message exceeds its Snappy frame.');
|
|
997
|
+
const payload = frame.slice(pointer.offset, pointer.offset + length);
|
|
998
|
+
pointer.offset += length;
|
|
999
|
+
const messageType = KeynoteArchives[Number(info.type)];
|
|
1000
|
+
if (!messageType)
|
|
1001
|
+
continue;
|
|
1002
|
+
try {
|
|
1003
|
+
messages.push({ info: { type: Number(info.type) }, data: messageType.fromBinary(payload) });
|
|
1004
|
+
}
|
|
1005
|
+
catch {
|
|
1006
|
+
// Newer Keynote releases can add fields or message variants. Other typed
|
|
1007
|
+
// messages in the same frame remain useful, so isolate this one payload.
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
objects.push({ identifier: archive.identifier, messages });
|
|
1011
|
+
}
|
|
1012
|
+
return objects;
|
|
1013
|
+
};
|
|
1014
|
+
const decodeModernObjectMap = (entries, limits) => {
|
|
1015
|
+
const objects = new Map();
|
|
1016
|
+
let decodedMessages = 0;
|
|
1017
|
+
let skippedFrames = 0;
|
|
1018
|
+
let decompressedBytes = 0;
|
|
1019
|
+
for (const entry of entries) {
|
|
1020
|
+
const remaining = limits.maxUncompressedBytes - decompressedBytes;
|
|
1021
|
+
if (remaining <= 0)
|
|
1022
|
+
throw new Error('IWA decompression exceeds the configured safety limit.');
|
|
1023
|
+
const stream = decompressIwaFile(entry.bytes, remaining);
|
|
1024
|
+
decompressedBytes += stream.length;
|
|
1025
|
+
if (decompressedBytes > limits.maxUncompressedBytes)
|
|
1026
|
+
throw new Error('IWA decompression exceeds the configured safety limit.');
|
|
1027
|
+
let decoded;
|
|
1028
|
+
try {
|
|
1029
|
+
decoded = decodeKeynoteFrame(stream);
|
|
1030
|
+
}
|
|
1031
|
+
catch {
|
|
1032
|
+
skippedFrames += 1;
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
for (const object of decoded) {
|
|
1036
|
+
if (object.identifier == null)
|
|
1037
|
+
continue;
|
|
1038
|
+
decodedMessages += object.messages.length;
|
|
1039
|
+
if (decodedMessages > limits.maxObjects)
|
|
1040
|
+
throw new Error('Keynote object count exceeds the configured safety limit.');
|
|
1041
|
+
const id = String(object.identifier);
|
|
1042
|
+
const previous = objects.get(id);
|
|
1043
|
+
objects.set(id, previous ? { identifier: object.identifier, messages: [...previous.messages, ...object.messages] } : object);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
return { objects, decodedMessages, skippedFrames };
|
|
1047
|
+
};
|
|
1048
|
+
const findAssetName = (names, dataId) => dataId && names.find(name => {
|
|
1049
|
+
const escaped = dataId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1050
|
+
return new RegExp(`(?:^|[-/])${escaped}(?:-[^/]*)?\\.[^.]+$`, 'i').test(name);
|
|
1051
|
+
});
|
|
1052
|
+
const parseModernPages = async (zip, entries, limits) => {
|
|
1053
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1054
|
+
const decoded = decodeModernObjectMap(entries, limits);
|
|
1055
|
+
const bodyStorages = [...decoded.objects.values()].flatMap(object => object.messages)
|
|
1056
|
+
.filter(message => { var _a; return message.info.type === 2001 && ((_a = message.data) === null || _a === void 0 ? void 0 : _a.kind) === 0; })
|
|
1057
|
+
.map(message => { var _a, _b; return Array.isArray((_a = message.data) === null || _a === void 0 ? void 0 : _a.text) ? message.data.text.join('') : String(((_b = message.data) === null || _b === void 0 ? void 0 : _b.text) || ''); })
|
|
1058
|
+
.filter(Boolean)
|
|
1059
|
+
.sort((left, right) => right.length - left.length);
|
|
1060
|
+
const body = bodyStorages[0];
|
|
1061
|
+
if (!body)
|
|
1062
|
+
return undefined;
|
|
1063
|
+
const pageTexts = body.split('\u0005').map(value => cleanText(value.replace(/\ufffc/g, '')));
|
|
1064
|
+
const dataEntries = Object.keys(zip.files).filter(name => /(?:^|\/)data\//i.test(name));
|
|
1065
|
+
const objects = [];
|
|
1066
|
+
const tables = [];
|
|
1067
|
+
for (const [id, object] of decoded.objects) {
|
|
1068
|
+
for (const drawable of object.messages) {
|
|
1069
|
+
const geometry = geometryOf(drawable.data);
|
|
1070
|
+
if (drawable.info.type === 3005) {
|
|
1071
|
+
const visual = visualGeometry(id, 'image', geometry);
|
|
1072
|
+
const assetName = findAssetName(dataEntries, referenceId((_a = drawable.data) === null || _a === void 0 ? void 0 : _a.data));
|
|
1073
|
+
if (visual && assetName) {
|
|
1074
|
+
const asset = zip.file(assetName);
|
|
1075
|
+
if (asset) {
|
|
1076
|
+
visual.bytes = await readBoundedImage(asset, assetName, limits);
|
|
1077
|
+
visual.mimeType = imageMimeType(assetName);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
if (visual)
|
|
1081
|
+
objects.push(visual);
|
|
1082
|
+
}
|
|
1083
|
+
else if (drawable.info.type === 5021) {
|
|
1084
|
+
const visual = chartGeometry(id, drawable.data, geometry);
|
|
1085
|
+
if (visual)
|
|
1086
|
+
objects.push(visual);
|
|
1087
|
+
}
|
|
1088
|
+
else if (drawable.info.type === 6000) {
|
|
1089
|
+
const modelId = referenceId((_b = drawable.data) === null || _b === void 0 ? void 0 : _b.tableModel);
|
|
1090
|
+
const model = messageData(modelId ? (_c = decoded.objects.get(modelId)) === null || _c === void 0 ? void 0 : _c.messages.find(message => message.info.type === 6001) : undefined);
|
|
1091
|
+
const rows = Math.max(0, Number((model === null || model === void 0 ? void 0 : model.numberOfRows) || 0));
|
|
1092
|
+
const columns = Math.max(0, Number((model === null || model === void 0 ? void 0 : model.numberOfColumns) || 0));
|
|
1093
|
+
if (geometry && rows && columns) {
|
|
1094
|
+
const width = Number(((_d = geometry.size) === null || _d === void 0 ? void 0 : _d.width) || 0);
|
|
1095
|
+
const height = Number(((_e = geometry.size) === null || _e === void 0 ? void 0 : _e.height) || 0);
|
|
1096
|
+
tables.push({
|
|
1097
|
+
id: `table-${id}`,
|
|
1098
|
+
x: Number(((_f = geometry.position) === null || _f === void 0 ? void 0 : _f.x) || 0),
|
|
1099
|
+
y: Number(((_g = geometry.position) === null || _g === void 0 ? void 0 : _g.y) || 0),
|
|
1100
|
+
width: width || undefined,
|
|
1101
|
+
height: height || undefined,
|
|
1102
|
+
rows: modernTableRows(decoded.objects, model),
|
|
1103
|
+
columnWidths: Array.from({ length: columns }, () => width ? width / columns : Number(model.defaultColumnWidth || 98)),
|
|
1104
|
+
rowHeights: Array.from({ length: rows }, () => height ? height / rows : Number(model.defaultRowHeight || 22)),
|
|
1105
|
+
headerRows: Number(model.numberOfHeaderRows || 0),
|
|
1106
|
+
headerColumns: Number(model.numberOfHeaderColumns || 0),
|
|
1107
|
+
fontSize: 10,
|
|
1108
|
+
fontFamily: 'Helvetica Neue',
|
|
1109
|
+
headerRowBackground: '#bec0c0',
|
|
1110
|
+
headerColumnBackground: '#d4d4d4',
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
const visual = visualGeometry(id, 'table', geometry);
|
|
1114
|
+
if (visual)
|
|
1115
|
+
objects.push(visual);
|
|
1116
|
+
}
|
|
1117
|
+
else if (drawable.info.type === 2011 && !((_h = drawable.data) === null || _h === void 0 ? void 0 : _h.isTextBox)) {
|
|
1118
|
+
const visual = visualGeometry(id, 'shape', geometry, storageText(decoded.objects, storageIdOf(drawable.data)));
|
|
1119
|
+
if (visual)
|
|
1120
|
+
objects.push(visual);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
const pageWidth = 595.28;
|
|
1125
|
+
for (const table of tables) {
|
|
1126
|
+
if (table.width)
|
|
1127
|
+
table.x = Math.max(0, (pageWidth - table.width) / 2);
|
|
1128
|
+
const precedingBottom = Math.max(0, ...objects
|
|
1129
|
+
.filter(object => object.kind !== 'table' && object.y <= table.y)
|
|
1130
|
+
.map(object => object.y + object.height));
|
|
1131
|
+
table.y = Math.max(table.y, precedingBottom + 33);
|
|
1132
|
+
}
|
|
1133
|
+
const scenes = pageTexts.map((value, index) => ({
|
|
1134
|
+
id: `page-${index + 1}`,
|
|
1135
|
+
name: value.split('\n').find(Boolean) || `Page ${index + 1}`,
|
|
1136
|
+
width: pageWidth,
|
|
1137
|
+
height: 841.89,
|
|
1138
|
+
blocks: value ? textBlocks([value], 'pages') : [],
|
|
1139
|
+
tables: index === 0 ? tables : [],
|
|
1140
|
+
objects: index === 0 ? objects : [],
|
|
1141
|
+
notes: [],
|
|
1142
|
+
}));
|
|
1143
|
+
return { scenes, ...decoded };
|
|
1144
|
+
};
|
|
1145
|
+
const parseModernKeynote = async (zip, entries, limits) => {
|
|
1146
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2;
|
|
1147
|
+
const decoded = decodeModernObjectMap(entries, limits);
|
|
1148
|
+
const { objects } = decoded;
|
|
1149
|
+
const show = (_a = [...objects.values()].flatMap(object => object.messages).find(message => message.info.type === 2)) === null || _a === void 0 ? void 0 : _a.data;
|
|
1150
|
+
const nodeIds = [];
|
|
1151
|
+
const visited = new Set();
|
|
1152
|
+
const visitNode = (id) => {
|
|
1153
|
+
var _a, _b;
|
|
1154
|
+
if (!id || visited.has(id))
|
|
1155
|
+
return;
|
|
1156
|
+
visited.add(id);
|
|
1157
|
+
const node = (_b = (_a = objects.get(id)) === null || _a === void 0 ? void 0 : _a.messages.find(message => message.info.type === 4)) === null || _b === void 0 ? void 0 : _b.data;
|
|
1158
|
+
if (!node)
|
|
1159
|
+
return;
|
|
1160
|
+
if (referenceId(node.slide))
|
|
1161
|
+
nodeIds.push(id);
|
|
1162
|
+
for (const child of node.children || [])
|
|
1163
|
+
visitNode(referenceId(child));
|
|
1164
|
+
};
|
|
1165
|
+
for (const slide of ((_b = show === null || show === void 0 ? void 0 : show.slideTree) === null || _b === void 0 ? void 0 : _b.slides) || [])
|
|
1166
|
+
visitNode(referenceId(slide));
|
|
1167
|
+
visitNode(referenceId((_c = show === null || show === void 0 ? void 0 : show.slideTree) === null || _c === void 0 ? void 0 : _c.rootSlideNode));
|
|
1168
|
+
if (!nodeIds.length)
|
|
1169
|
+
return undefined;
|
|
1170
|
+
const width = Number(((_d = show === null || show === void 0 ? void 0 : show.size) === null || _d === void 0 ? void 0 : _d.width) || 1280);
|
|
1171
|
+
const height = Number(((_e = show === null || show === void 0 ? void 0 : show.size) === null || _e === void 0 ? void 0 : _e.height) || 720);
|
|
1172
|
+
const dataEntries = Object.keys(zip.files).filter(name => /(?:^|\/)data\//i.test(name));
|
|
1173
|
+
const scenes = [];
|
|
1174
|
+
for (const [index, nodeId] of nodeIds.entries()) {
|
|
1175
|
+
const node = (_g = (_f = objects.get(nodeId)) === null || _f === void 0 ? void 0 : _f.messages.find(message => message.info.type === 4)) === null || _g === void 0 ? void 0 : _g.data;
|
|
1176
|
+
const slideId = referenceId(node === null || node === void 0 ? void 0 : node.slide);
|
|
1177
|
+
const slide = slideId && ((_j = (_h = objects.get(slideId)) === null || _h === void 0 ? void 0 : _h.messages.find(message => message.info.type === 5)) === null || _j === void 0 ? void 0 : _j.data);
|
|
1178
|
+
if (!slideId || !slide)
|
|
1179
|
+
continue;
|
|
1180
|
+
const blocks = [];
|
|
1181
|
+
const visualObjects = [];
|
|
1182
|
+
const tables = [];
|
|
1183
|
+
for (const [drawableOrder, drawableRef] of (slide.drawablesZOrder || slide.ownedDrawables || []).entries()) {
|
|
1184
|
+
const drawableId = referenceId(drawableRef);
|
|
1185
|
+
const drawable = drawableId && ((_k = objects.get(drawableId)) === null || _k === void 0 ? void 0 : _k.messages[0]);
|
|
1186
|
+
if (!drawableId || !drawable)
|
|
1187
|
+
continue;
|
|
1188
|
+
const geometry = geometryOf(drawable.data);
|
|
1189
|
+
const storageId = storageIdOf(drawable.data);
|
|
1190
|
+
const value = storageText(objects, storageId);
|
|
1191
|
+
if (value && geometry) {
|
|
1192
|
+
const textStyle = resolveStorageTextStyle(objects, storageId);
|
|
1193
|
+
const shapeTextStyle = resolveShapeTextStyle(objects, shapeStyleIdOf(drawable.data));
|
|
1194
|
+
blocks.push({
|
|
1195
|
+
id: `text-${drawableId}`,
|
|
1196
|
+
text: value,
|
|
1197
|
+
zIndex: drawableOrder,
|
|
1198
|
+
x: Number(((_l = geometry.position) === null || _l === void 0 ? void 0 : _l.x) || 0), y: Number(((_m = geometry.position) === null || _m === void 0 ? void 0 : _m.y) || 0),
|
|
1199
|
+
width: Number(((_o = geometry.size) === null || _o === void 0 ? void 0 : _o.width) || width), height: Number(((_p = geometry.size) === null || _p === void 0 ? void 0 : _p.height) || 48),
|
|
1200
|
+
fontSize: ((_q = drawable.data) === null || _q === void 0 ? void 0 : _q.kind) === 2 ? 48 : ((_r = drawable.data) === null || _r === void 0 ? void 0 : _r.kind) === 3 ? 28 : 22,
|
|
1201
|
+
bold: ((_s = drawable.data) === null || _s === void 0 ? void 0 : _s.kind) === 2,
|
|
1202
|
+
...textStyle,
|
|
1203
|
+
...shapeTextStyle,
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
if (drawable.info.type === 3005) {
|
|
1207
|
+
const object = visualGeometry(drawableId, 'image', geometry);
|
|
1208
|
+
const dataId = referenceId((_t = drawable.data) === null || _t === void 0 ? void 0 : _t.data);
|
|
1209
|
+
const assetName = findAssetName(dataEntries, dataId);
|
|
1210
|
+
if (object && assetName) {
|
|
1211
|
+
const asset = zip.file(assetName);
|
|
1212
|
+
if (asset) {
|
|
1213
|
+
object.bytes = await readBoundedImage(asset, assetName, limits);
|
|
1214
|
+
object.mimeType = imageMimeType(assetName);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
if (object)
|
|
1218
|
+
object.zIndex = drawableOrder;
|
|
1219
|
+
if (object)
|
|
1220
|
+
visualObjects.push(object);
|
|
1221
|
+
}
|
|
1222
|
+
else if (drawable.info.type === 5021) {
|
|
1223
|
+
const object = chartGeometry(drawableId, drawable.data, geometry);
|
|
1224
|
+
if (object)
|
|
1225
|
+
object.zIndex = drawableOrder;
|
|
1226
|
+
if (object)
|
|
1227
|
+
visualObjects.push(object);
|
|
1228
|
+
}
|
|
1229
|
+
else if (drawable.info.type === 6000) {
|
|
1230
|
+
const modelId = referenceId((_u = drawable.data) === null || _u === void 0 ? void 0 : _u.tableModel);
|
|
1231
|
+
const model = messageData(modelId ? (_v = objects.get(modelId)) === null || _v === void 0 ? void 0 : _v.messages.find(message => message.info.type === 6001) : undefined);
|
|
1232
|
+
const rows = Math.max(0, Number((model === null || model === void 0 ? void 0 : model.numberOfRows) || 0));
|
|
1233
|
+
const columns = Math.max(0, Number((model === null || model === void 0 ? void 0 : model.numberOfColumns) || 0));
|
|
1234
|
+
if (geometry && rows && columns) {
|
|
1235
|
+
const tableWidth = Number(((_w = geometry.size) === null || _w === void 0 ? void 0 : _w.width) || 0);
|
|
1236
|
+
const tableHeight = Number(((_x = geometry.size) === null || _x === void 0 ? void 0 : _x.height) || 0);
|
|
1237
|
+
tables.push({
|
|
1238
|
+
id: `table-${drawableId}`,
|
|
1239
|
+
zIndex: drawableOrder,
|
|
1240
|
+
x: Number(((_y = geometry.position) === null || _y === void 0 ? void 0 : _y.x) || 0),
|
|
1241
|
+
y: Number(((_z = geometry.position) === null || _z === void 0 ? void 0 : _z.y) || 0),
|
|
1242
|
+
width: tableWidth || undefined,
|
|
1243
|
+
height: tableHeight || undefined,
|
|
1244
|
+
rows: modernTableRows(objects, model),
|
|
1245
|
+
columnWidths: Array.from({ length: columns }, () => tableWidth ? tableWidth / columns : Number(model.defaultColumnWidth || 98)),
|
|
1246
|
+
rowHeights: Array.from({ length: rows }, () => tableHeight ? tableHeight / rows : Number(model.defaultRowHeight || 22)),
|
|
1247
|
+
headerRows: Number(model.numberOfHeaderRows || 0),
|
|
1248
|
+
headerColumns: Number(model.numberOfHeaderColumns || 0),
|
|
1249
|
+
fontSize: 13.333,
|
|
1250
|
+
fontFamily: 'Helvetica Neue',
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
const object = visualGeometry(drawableId, 'table', geometry);
|
|
1254
|
+
if (object)
|
|
1255
|
+
object.zIndex = drawableOrder;
|
|
1256
|
+
if (object)
|
|
1257
|
+
visualObjects.push(object);
|
|
1258
|
+
}
|
|
1259
|
+
else if ((drawable.info.type === 2011 || drawable.info.type === 3004) && !((_0 = drawable.data) === null || _0 === void 0 ? void 0 : _0.isTextBox)) {
|
|
1260
|
+
const object = visualGeometry(drawableId, 'shape', geometry, value);
|
|
1261
|
+
if (object)
|
|
1262
|
+
object.zIndex = drawableOrder;
|
|
1263
|
+
if (object)
|
|
1264
|
+
visualObjects.push(object);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
const note = referenceId(slide.note);
|
|
1268
|
+
const noteData = messageData(note ? (_1 = objects.get(note)) === null || _1 === void 0 ? void 0 : _1.messages.find(message => message.info.type === 15) : undefined);
|
|
1269
|
+
const noteText = storageText(objects, referenceId(noteData === null || noteData === void 0 ? void 0 : noteData.containedStorage));
|
|
1270
|
+
const name = ((_2 = [...blocks].sort((left, right) => (right.fontSize || 0) - (left.fontSize || 0))[0]) === null || _2 === void 0 ? void 0 : _2.text)
|
|
1271
|
+
|| `Slide ${index + 1}`;
|
|
1272
|
+
scenes.push({ id: `slide-${index + 1}`, name, width, height, blocks, tables, objects: visualObjects, notes: noteText ? [noteText] : [] });
|
|
1273
|
+
}
|
|
1274
|
+
return scenes.length ? { scenes, decodedMessages: decoded.decodedMessages, skippedFrames: decoded.skippedFrames } : undefined;
|
|
1275
|
+
};
|
|
1276
|
+
const parseModern = async (zip, buffer, kind, limits) => {
|
|
1277
|
+
var _a;
|
|
1278
|
+
const entries = await loadModernIwaEntries(zip, limits);
|
|
1279
|
+
const diagnostics = [`Parsed ${entries.length} Snappy-framed IWA object streams.`];
|
|
1280
|
+
let scenes = [];
|
|
1281
|
+
let objectCount = 0;
|
|
1282
|
+
let limitedPreview = false;
|
|
1283
|
+
if (kind === 'numbers') {
|
|
1284
|
+
try {
|
|
1285
|
+
const parsed = await parseModernNumbers(buffer, entries, limits);
|
|
1286
|
+
scenes = parsed.scenes;
|
|
1287
|
+
objectCount = parsed.decodedMessages;
|
|
1288
|
+
diagnostics.push('Decoded Numbers sheets, saved cell results, table layout and typed chart geometry through the IWA workbook model.');
|
|
1289
|
+
if (parsed.skippedFrames)
|
|
1290
|
+
diagnostics.push(`Skipped ${parsed.skippedFrames} unrecognized or malformed Numbers archive frame(s).`);
|
|
1291
|
+
}
|
|
1292
|
+
catch (error) {
|
|
1293
|
+
throwIfSafetyBoundaryError(error);
|
|
1294
|
+
diagnostics.push(`Numbers workbook decoding failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
if (kind === 'pages') {
|
|
1298
|
+
try {
|
|
1299
|
+
const parsed = await parseModernPages(zip, entries, limits);
|
|
1300
|
+
if (parsed) {
|
|
1301
|
+
scenes = parsed.scenes;
|
|
1302
|
+
objectCount = parsed.decodedMessages;
|
|
1303
|
+
diagnostics.push(`Decoded ${scenes.length} Pages page scene(s) and recovered document-level geometry from the typed IWA graph.`);
|
|
1304
|
+
diagnostics.push('Recovered text flow, pagination, tables, saved cell values, charts, shapes and images.');
|
|
1305
|
+
if (parsed.skippedFrames)
|
|
1306
|
+
diagnostics.push(`Skipped ${parsed.skippedFrames} unrecognized or malformed Pages archive frame(s).`);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
catch (error) {
|
|
1310
|
+
throwIfSafetyBoundaryError(error);
|
|
1311
|
+
diagnostics.push(`Typed Pages decoding failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
if (kind === 'keynote') {
|
|
1315
|
+
try {
|
|
1316
|
+
const parsed = await parseModernKeynote(zip, entries, limits);
|
|
1317
|
+
if (parsed) {
|
|
1318
|
+
scenes = parsed.scenes;
|
|
1319
|
+
objectCount = parsed.decodedMessages;
|
|
1320
|
+
diagnostics.push(`Decoded ${scenes.length} document slides from the typed Keynote object graph, excluding theme templates.`);
|
|
1321
|
+
diagnostics.push('Recovered slide geometry, inherited text styles, tables, charts, shapes, images and presenter notes.');
|
|
1322
|
+
if (parsed.skippedFrames)
|
|
1323
|
+
diagnostics.push(`Skipped ${parsed.skippedFrames} unrecognized or malformed Keynote archive frame(s).`);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
catch (error) {
|
|
1327
|
+
throwIfSafetyBoundaryError(error);
|
|
1328
|
+
diagnostics.push(`Typed Keynote decoding failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
if (!scenes.length) {
|
|
1332
|
+
const strings = [];
|
|
1333
|
+
const focusedEntries = kind === 'pages'
|
|
1334
|
+
? entries.filter(entry => /(?:^|\/)document\.iwa$/i.test(entry.name))
|
|
1335
|
+
: kind === 'keynote'
|
|
1336
|
+
? entries.filter(entry => /(?:^|\/)slide(?:-[^/]+)?\.iwa$/i.test(entry.name))
|
|
1337
|
+
: entries;
|
|
1338
|
+
for (const entry of focusedEntries.length ? focusedEntries : entries) {
|
|
1339
|
+
const decompressed = decompressIwaFile(entry.bytes, Math.min(limits.maxUncompressedBytes, 64 * 1024 * 1024));
|
|
1340
|
+
const extracted = extractProtobufStrings(decompressed, limits);
|
|
1341
|
+
objectCount += extracted.objects;
|
|
1342
|
+
strings.push(...extracted.strings);
|
|
1343
|
+
if (objectCount > limits.maxObjects)
|
|
1344
|
+
throw new Error('IWA object count exceeds the configured safety limit.');
|
|
1345
|
+
}
|
|
1346
|
+
const unique = [...new Set(strings)].filter(value => !/^T[SAK]\w{2,}$/.test(value)).slice(0, 5000);
|
|
1347
|
+
const perScene = kind === 'keynote' ? 12 : kind === 'pages' ? Math.max(1, unique.length) : 40;
|
|
1348
|
+
const chunks = Array.from({ length: Math.max(1, Math.ceil(unique.length / perScene)) }, (_, index) => unique.slice(index * perScene, (index + 1) * perScene));
|
|
1349
|
+
scenes = chunks.map((values, index) => createScene(kind, `scene-${index + 1}`, values[0] || `${kind === 'keynote' ? 'Slide' : 'Page'} ${index + 1}`, values));
|
|
1350
|
+
limitedPreview = true;
|
|
1351
|
+
diagnostics.push('The generic IWA object graph produced a searchable static scene; exact object geometry is still experimental.');
|
|
1352
|
+
}
|
|
1353
|
+
return {
|
|
1354
|
+
kind,
|
|
1355
|
+
generation: 'iwork-2013-plus',
|
|
1356
|
+
title: ((_a = scenes[0]) === null || _a === void 0 ? void 0 : _a.name) || `Apple ${kind}`,
|
|
1357
|
+
scenes,
|
|
1358
|
+
preview: await findPreview(zip, limits),
|
|
1359
|
+
diagnostics,
|
|
1360
|
+
limits: kind === 'keynote' ? ['Animations, transitions and video playback are not executed.'] : kind === 'numbers' ? ['Formula values are read from the saved document; formulas are not recalculated.'] : [],
|
|
1361
|
+
objectCount,
|
|
1362
|
+
limitedPreview,
|
|
1363
|
+
};
|
|
1364
|
+
};
|
|
1365
|
+
export const inspectIworkContainer = async (buffer, limits = {}) => {
|
|
1366
|
+
const resolved = { ...DEFAULT_IWORK_PARSE_LIMITS, ...limits };
|
|
1367
|
+
const zip = await JSZip.loadAsync(buffer);
|
|
1368
|
+
validateZipDirectory(zip, resolved);
|
|
1369
|
+
const names = Object.keys(zip.files);
|
|
1370
|
+
if (names.some(name => name === '[Content_Types].xml' || /^xl\//i.test(name))) {
|
|
1371
|
+
throw new IworkContainerMismatchError('spreadsheet-openxml', 'The iWork extension contains an OOXML workbook; route it as XLSX without counting it as iWork evidence.');
|
|
1372
|
+
}
|
|
1373
|
+
if (names.some(name => /\.iwpv2$/i.test(name) || /(?:^|\/)encryptedpackage$/i.test(name))) {
|
|
1374
|
+
throw new Error('Encrypted iWork iwpv2 documents are detected but cannot be decrypted.');
|
|
1375
|
+
}
|
|
1376
|
+
const generation = names.some(name => /(?:^|\/)(index\.xml(?:\.gz)?|index\.apxl)$/i.test(name))
|
|
1377
|
+
? 'iwork-09'
|
|
1378
|
+
: 'iwork-2013-plus';
|
|
1379
|
+
return { zip, generation, limits: resolved };
|
|
1380
|
+
};
|
|
1381
|
+
export const parseIworkDocument = async (buffer, type, limits = {}, createParser = createXmlParser) => {
|
|
1382
|
+
const kind = kindFromType(type);
|
|
1383
|
+
const inspected = await inspectIworkContainer(buffer, limits);
|
|
1384
|
+
return inspected.generation === 'iwork-09'
|
|
1385
|
+
? parseLegacy(inspected.zip, kind, inspected.limits, createParser)
|
|
1386
|
+
: parseModern(inspected.zip, buffer, kind, inspected.limits);
|
|
1387
|
+
};
|