@file-viewer/ppt 0.2.0 → 0.3.1
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 +99 -173
- package/NOTICE +19 -19
- package/README.md +333 -113
- package/frame-cache.mjs +731 -0
- package/index.d.ts +116 -37
- package/index.mjs +633 -98
- package/manifest.json +25 -11
- package/package.json +7 -2
- package/ppt-font-cjk.otf +0 -0
- package/ppt-native.wasm +0 -0
- package/worker.mjs +920 -0
package/index.mjs
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
const MANIFEST = Object.freeze({"kind":"flyfish-ppt-public-native-wasm-
|
|
1
|
+
const MANIFEST = Object.freeze({"kind":"flyfish-ppt-public-native-wasm-v2","product":"Flyfish PPT Viewer","format":"PowerPoint 97-2003 (.ppt)","packageName":"@file-viewer/ppt","packageVersion":"0.3.1","engine":"flyfish-classic-ppt-native-engine","engineBuild":"593d0a6cf32f928f","id":"FV-PPT-PUBLIC-WATERMARKED-V2","feature":"ppt","edition":"public-watermarked","holder":"Individuals and Organizations","distribution":"proprietary-public-binary","commercialUse":true,"publicAnyOrigin":true,"watermarkRequired":true,"watermarkText":"Flyfish Viewer","usePolicy":"public-watermarked-general-use","licensePolicy":"flyfish-public-watermarked-binary-license-v2","sourceCodeRights":false,"sourceDeliveryRights":false,"apacheLicenseApplies":false,"redistributionAllowed":true,"redistributionPolicy":"unmodified-integrated-bundling-only","standaloneRedistributionAllowed":false,"watermarkRemovalRequiresCommercialAuthorization":true,"runtimeLicenseRequired":false,"renderer":"native-wasm-worker-offscreen-canvas","integrity":"native-wasm-font-sha256-v2","wasmFile":"ppt-native.wasm","wasmBytes":1557871,"workerFile":"worker.mjs","fontPack":{"kind":"flyfish-cjk-font-pack-v1","file":"ppt-font-cjk.otf","bytes":16437364,"sha256":"2c76254f6fc379fddfce0a7e84fb5385bb135d3e399294f6eeb6680d0365b74b"},"workerDefault":true,"virtualScrollDefault":true,"frameCache":"indexeddb-final-watermarked-png-lru","watermarkEnforcement":"native-final-frame","wasmSha256":"8e95d7de0e322d6c96dcd7ea18c5cd9b6783cb82f3fe1c8714008119f19e7915"});
|
|
2
2
|
const DEFAULT_WASM_URL = new URL('./ppt-native.wasm', import.meta.url);
|
|
3
|
+
const DEFAULT_FONT_URL = new URL(`./${MANIFEST.fontPack.file}`, import.meta.url);
|
|
4
|
+
const DEFAULT_WORKER_URL = new URL(`./${MANIFEST.workerFile}`, import.meta.url);
|
|
3
5
|
const WASM_EXPORTS = Object.freeze([
|
|
4
6
|
'memory',
|
|
5
7
|
'ppt_input_alloc',
|
|
6
8
|
'ppt_input_free',
|
|
9
|
+
'ppt_font_pack_install',
|
|
10
|
+
'ppt_font_pack_is_installed',
|
|
7
11
|
'ppt_document_open',
|
|
8
12
|
'ppt_document_close',
|
|
9
13
|
'ppt_slide_count',
|
|
@@ -22,78 +26,368 @@ const WASM_EXPORTS = Object.freeze([
|
|
|
22
26
|
const LICENSE = Object.freeze({
|
|
23
27
|
id: 'FV-PPT-PUBLIC-WATERMARKED-V2',
|
|
24
28
|
edition: 'public-watermarked',
|
|
25
|
-
holder: '
|
|
26
|
-
policy: 'flyfish-public-watermarked-binary-license-
|
|
27
|
-
commercialUse:
|
|
29
|
+
holder: 'Individuals and Organizations',
|
|
30
|
+
policy: 'flyfish-public-watermarked-binary-license-v2',
|
|
31
|
+
commercialUse: true,
|
|
28
32
|
sourceCodeRights: false,
|
|
29
33
|
sourceDeliveryRights: false,
|
|
30
34
|
apacheLicenseApplies: false,
|
|
31
|
-
redistributionAllowed:
|
|
35
|
+
redistributionAllowed: true,
|
|
36
|
+
redistributionPolicy: 'unmodified-integrated-bundling-only',
|
|
37
|
+
standaloneRedistributionAllowed: false,
|
|
38
|
+
watermarkRemovalRequiresCommercialAuthorization: true
|
|
32
39
|
});
|
|
33
40
|
|
|
34
41
|
const WATERMARK = Object.freeze({ required: true, text: 'Flyfish Viewer' });
|
|
35
|
-
|
|
42
|
+
const directEnginePromises = new Map();
|
|
36
43
|
|
|
37
44
|
export function getPptPackageManifest() {
|
|
38
|
-
return { ...MANIFEST };
|
|
45
|
+
return { ...MANIFEST, fontPack: { ...MANIFEST.fontPack } };
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
export async function loadPptViewer(options = {}) {
|
|
42
|
-
const
|
|
43
|
-
|
|
49
|
+
const mode = selectRuntimeMode(options);
|
|
50
|
+
if (mode === 'worker') {
|
|
51
|
+
try {
|
|
52
|
+
return await createWorkerRuntime(options);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (options.worker === true) throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const engine = await loadDirectEngine(options.wasmUrl, options.fontUrl);
|
|
58
|
+
return createDirectRuntime(engine);
|
|
44
59
|
}
|
|
45
60
|
|
|
46
61
|
export const createPptViewer = loadPptViewer;
|
|
47
62
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
function selectRuntimeMode(options) {
|
|
64
|
+
const requested = options.worker ?? 'auto';
|
|
65
|
+
if (requested !== true && requested !== false && requested !== 'auto') {
|
|
66
|
+
throw new TypeError('worker must be true, false, or "auto".');
|
|
67
|
+
}
|
|
68
|
+
const workerCapable = typeof Worker === 'function'
|
|
69
|
+
&& typeof OffscreenCanvas === 'function'
|
|
70
|
+
&& (typeof HTMLCanvasElement === 'undefined'
|
|
71
|
+
|| typeof HTMLCanvasElement.prototype?.transferControlToOffscreen === 'function');
|
|
72
|
+
const sourcesAreUrls = isUrlSource(options.wasmUrl) && isUrlSource(options.fontUrl) && isUrlSource(options.workerUrl);
|
|
73
|
+
if (requested === true && !workerCapable) {
|
|
74
|
+
throw packageError('worker-unavailable', 'Worker and OffscreenCanvas support are required for worker rendering.');
|
|
75
|
+
}
|
|
76
|
+
if (requested === true && !sourcesAreUrls) {
|
|
77
|
+
throw packageError('worker-source-invalid', 'Worker rendering requires URL-based WASM, font, and worker assets.');
|
|
78
|
+
}
|
|
79
|
+
return requested !== false && workerCapable && sourcesAreUrls ? 'worker' : 'direct';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isUrlSource(source) {
|
|
83
|
+
return source === undefined || source instanceof URL || typeof source === 'string';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function createWorkerRuntime(options) {
|
|
87
|
+
const bridge = new WorkerBridge(resolveUrl(options.workerUrl, DEFAULT_WORKER_URL));
|
|
88
|
+
const transferInputOwnership = options.transferInputOwnership === true;
|
|
89
|
+
try {
|
|
90
|
+
await bridge.request('init', {
|
|
91
|
+
manifest: getPptPackageManifest(),
|
|
92
|
+
wasmUrl: resolveUrl(options.wasmUrl, DEFAULT_WASM_URL).href,
|
|
93
|
+
fontUrl: resolveUrl(options.fontUrl, DEFAULT_FONT_URL).href,
|
|
94
|
+
cacheOptions: normalizeCacheOptions(options.cache)
|
|
54
95
|
});
|
|
96
|
+
} catch (error) {
|
|
97
|
+
bridge.terminate();
|
|
98
|
+
throw error;
|
|
55
99
|
}
|
|
56
|
-
|
|
100
|
+
|
|
101
|
+
let activeDocument = null;
|
|
102
|
+
const runtime = {
|
|
103
|
+
mode: 'worker',
|
|
104
|
+
license: LICENSE,
|
|
105
|
+
watermark: WATERMARK,
|
|
106
|
+
async open(input) {
|
|
107
|
+
if (activeDocument && !activeDocument.closed) {
|
|
108
|
+
throw packageError('document-active', 'Close the current worker document before opening another presentation.');
|
|
109
|
+
}
|
|
110
|
+
const bytes = normalizeInput(input);
|
|
111
|
+
if (bytes.byteLength < 8 || !hasCfbSignature(bytes)) {
|
|
112
|
+
throw packageError('ppt-signature-invalid', 'Input is not a PowerPoint 97-2003 (.ppt) compound document.');
|
|
113
|
+
}
|
|
114
|
+
const transferred = transferableInputBuffer(input, bytes, transferInputOwnership);
|
|
115
|
+
const metadata = await bridge.request('open', { input: transferred }, [transferred]);
|
|
116
|
+
activeDocument = createWorkerDocument(bridge, metadata);
|
|
117
|
+
return activeDocument;
|
|
118
|
+
},
|
|
119
|
+
async mount(target, input, mountOptions = {}) {
|
|
120
|
+
return mountDocument(runtime, target, input, mountOptions);
|
|
121
|
+
},
|
|
122
|
+
async cacheStats() {
|
|
123
|
+
return bridge.request('cacheStats');
|
|
124
|
+
},
|
|
125
|
+
async close() {
|
|
126
|
+
try {
|
|
127
|
+
if (activeDocument && !activeDocument.closed) await activeDocument.close();
|
|
128
|
+
} finally {
|
|
129
|
+
bridge.terminate();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
return Object.freeze(runtime);
|
|
57
134
|
}
|
|
58
135
|
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
const
|
|
136
|
+
function createWorkerDocument(bridge, metadata) {
|
|
137
|
+
const slideCount = positiveInteger(metadata.slideCount, 'slide count');
|
|
138
|
+
const width = positiveNumber(metadata.width, 'slide width');
|
|
139
|
+
const height = positiveNumber(metadata.height, 'slide height');
|
|
140
|
+
const documentId = String(metadata.documentId || '');
|
|
141
|
+
const attached = new Map();
|
|
142
|
+
let closed = false;
|
|
143
|
+
|
|
144
|
+
const document = {
|
|
145
|
+
mode: 'worker',
|
|
146
|
+
documentId,
|
|
147
|
+
get slideCount() { return slideCount; },
|
|
148
|
+
get width() { return width; },
|
|
149
|
+
get height() { return height; },
|
|
150
|
+
get closed() { return closed; },
|
|
151
|
+
async renderSlide(slideIndex, canvas, options = {}) {
|
|
152
|
+
ensureOpen(closed);
|
|
153
|
+
validateSlideIndex(slideIndex, slideCount);
|
|
154
|
+
const renderOptions = normalizeRenderOptions(options);
|
|
155
|
+
let state = attached.get(slideIndex);
|
|
156
|
+
if (!state) {
|
|
157
|
+
const transferable = transferCanvas(canvas, width, height, renderOptions.scale);
|
|
158
|
+
await bridge.request('attach', {
|
|
159
|
+
slideIndex,
|
|
160
|
+
canvas: transferable,
|
|
161
|
+
scale: renderOptions.scale,
|
|
162
|
+
pixelRatio: renderOptions.pixelRatio
|
|
163
|
+
}, [transferable]);
|
|
164
|
+
state = { canvas, scale: renderOptions.scale, pixelRatio: renderOptions.pixelRatio };
|
|
165
|
+
attached.set(slideIndex, state);
|
|
166
|
+
} else if (state.canvas !== canvas) {
|
|
167
|
+
throw packageError('canvas-already-attached', `Slide ${slideIndex + 1} is already attached to another Canvas.`);
|
|
168
|
+
}
|
|
169
|
+
const result = await bridge.request('activate', {
|
|
170
|
+
slideIndex,
|
|
171
|
+
scale: renderOptions.scale,
|
|
172
|
+
pixelRatio: renderOptions.pixelRatio
|
|
173
|
+
});
|
|
174
|
+
if (result?.cancelled) {
|
|
175
|
+
return Object.freeze({
|
|
176
|
+
cancelled: true,
|
|
177
|
+
slideIndex,
|
|
178
|
+
width: Math.round(width * renderOptions.nativeScale),
|
|
179
|
+
height: Math.round(height * renderOptions.nativeScale),
|
|
180
|
+
logicalWidth: width,
|
|
181
|
+
logicalHeight: height,
|
|
182
|
+
scale: renderOptions.scale,
|
|
183
|
+
pixelRatio: renderOptions.pixelRatio
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return Object.freeze({ ...result, logicalWidth: width, logicalHeight: height });
|
|
187
|
+
},
|
|
188
|
+
async releaseSlide(slideIndex) {
|
|
189
|
+
ensureOpen(closed);
|
|
190
|
+
validateSlideIndex(slideIndex, slideCount);
|
|
191
|
+
if (!attached.has(slideIndex)) return false;
|
|
192
|
+
await bridge.request('deactivate', { slideIndex });
|
|
193
|
+
return true;
|
|
194
|
+
},
|
|
195
|
+
async cacheStats() {
|
|
196
|
+
ensureOpen(closed);
|
|
197
|
+
return bridge.request('cacheStats');
|
|
198
|
+
},
|
|
199
|
+
async close() {
|
|
200
|
+
if (closed) return;
|
|
201
|
+
closed = true;
|
|
202
|
+
try {
|
|
203
|
+
await bridge.request('close');
|
|
204
|
+
} finally {
|
|
205
|
+
attached.clear();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
return Object.freeze(document);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function transferCanvas(canvas, logicalWidth, logicalHeight, scale) {
|
|
213
|
+
if (typeof HTMLCanvasElement !== 'undefined' && canvas instanceof HTMLCanvasElement) {
|
|
214
|
+
if (typeof canvas.transferControlToOffscreen !== 'function') {
|
|
215
|
+
throw packageError('offscreen-canvas-unavailable', 'This Canvas cannot be transferred to the rendering worker.');
|
|
216
|
+
}
|
|
217
|
+
prepareCanvasElement(canvas, logicalWidth, logicalHeight, scale);
|
|
218
|
+
canvas.width = 1;
|
|
219
|
+
canvas.height = 1;
|
|
220
|
+
return canvas.transferControlToOffscreen();
|
|
221
|
+
}
|
|
222
|
+
if (typeof OffscreenCanvas !== 'undefined' && canvas instanceof OffscreenCanvas) return canvas;
|
|
223
|
+
throw new TypeError('renderSlide requires an HTMLCanvasElement or OffscreenCanvas.');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
class WorkerBridge {
|
|
227
|
+
constructor(url) {
|
|
228
|
+
this.worker = new Worker(url, { type: 'module', name: 'flyfish-ppt-renderer' });
|
|
229
|
+
this.nextId = 1;
|
|
230
|
+
this.pending = new Map();
|
|
231
|
+
this.terminated = false;
|
|
232
|
+
this.worker.addEventListener('message', (event) => this.receive(event.data));
|
|
233
|
+
this.worker.addEventListener('error', (event) => {
|
|
234
|
+
this.failAll(packageError('worker-failed', event.message || 'The PPT rendering worker failed.'));
|
|
235
|
+
});
|
|
236
|
+
this.worker.addEventListener('messageerror', () => {
|
|
237
|
+
this.failAll(packageError('worker-message-invalid', 'The PPT rendering worker returned an invalid message.'));
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
request(type, payload = {}, transfer = []) {
|
|
242
|
+
if (this.terminated) return Promise.reject(packageError('worker-closed', 'The PPT rendering worker is closed.'));
|
|
243
|
+
const id = this.nextId++;
|
|
244
|
+
return new Promise((resolve, reject) => {
|
|
245
|
+
this.pending.set(id, { resolve, reject });
|
|
246
|
+
try {
|
|
247
|
+
this.worker.postMessage({ id, type, ...payload }, transfer);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
this.pending.delete(id);
|
|
250
|
+
reject(error);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
receive(message) {
|
|
256
|
+
if (!message || !Number.isInteger(message.id)) return;
|
|
257
|
+
const pending = this.pending.get(message.id);
|
|
258
|
+
if (!pending) return;
|
|
259
|
+
this.pending.delete(message.id);
|
|
260
|
+
if (message.ok) pending.resolve(message.result);
|
|
261
|
+
else pending.reject(workerError(message.error));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
failAll(error) {
|
|
265
|
+
for (const pending of this.pending.values()) pending.reject(error);
|
|
266
|
+
this.pending.clear();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
terminate() {
|
|
270
|
+
if (this.terminated) return;
|
|
271
|
+
this.terminated = true;
|
|
272
|
+
this.worker.terminate();
|
|
273
|
+
this.failAll(packageError('worker-closed', 'The PPT rendering worker was closed.'));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function workerError(value) {
|
|
278
|
+
const error = packageError(value?.code || 'worker-operation-failed', value?.message || 'The PPT rendering worker rejected the operation.');
|
|
279
|
+
if (value?.name) error.name = value.name;
|
|
280
|
+
return error;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function loadDirectEngine(wasmSource, fontSource) {
|
|
284
|
+
const key = directEngineKey(wasmSource, fontSource);
|
|
285
|
+
if (!key) return instantiateEngine(wasmSource ?? DEFAULT_WASM_URL, fontSource ?? DEFAULT_FONT_URL);
|
|
286
|
+
if (!directEnginePromises.has(key)) {
|
|
287
|
+
directEnginePromises.set(key, instantiateEngine(wasmSource ?? DEFAULT_WASM_URL, fontSource ?? DEFAULT_FONT_URL).catch((error) => {
|
|
288
|
+
directEnginePromises.delete(key);
|
|
289
|
+
throw error;
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
return directEnginePromises.get(key);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function directEngineKey(wasmSource, fontSource) {
|
|
296
|
+
if (!isUrlSource(wasmSource) || !isUrlSource(fontSource)) return '';
|
|
297
|
+
return `${resolveUrl(wasmSource, DEFAULT_WASM_URL).href}\n${resolveUrl(fontSource, DEFAULT_FONT_URL).href}`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function instantiateEngine(wasmSource, fontSource) {
|
|
301
|
+
const fontPromise = loadAssetBytes(fontSource, MANIFEST.fontPack.bytes, 'CJK font pack');
|
|
302
|
+
void fontPromise.catch(() => {});
|
|
303
|
+
const wasm = await loadAssetBytes(wasmSource, MANIFEST.wasmBytes, 'PPT rendering engine');
|
|
304
|
+
await verifySha256(wasm, MANIFEST.wasmSha256, 'wasm-integrity-invalid', 'The PPT rendering engine failed its SHA-256 integrity check.');
|
|
305
|
+
const module = await WebAssembly.compile(wasm);
|
|
63
306
|
const imports = WebAssembly.Module.imports(module);
|
|
64
307
|
if (imports.length !== 0) {
|
|
65
308
|
throw packageError('wasm-imports-rejected', 'The PPT rendering engine must not import browser or host functions.');
|
|
66
309
|
}
|
|
67
310
|
const instance = await WebAssembly.instantiate(module, {});
|
|
68
311
|
validateEngine(instance.exports);
|
|
312
|
+
const font = await fontPromise;
|
|
313
|
+
await verifySha256(font, MANIFEST.fontPack.sha256, 'font-integrity-invalid', 'The CJK font pack failed its SHA-256 integrity check.');
|
|
314
|
+
installFontPack(instance.exports, font);
|
|
69
315
|
return Object.freeze({ module, exports: instance.exports });
|
|
70
316
|
}
|
|
71
317
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
318
|
+
function installFontPack(api, font) {
|
|
319
|
+
if (Number(api.ppt_font_pack_is_installed()) === 1) return;
|
|
320
|
+
const pointer = Number(api.ppt_input_alloc(font.byteLength));
|
|
321
|
+
if (!pointer) throw engineError(api, 'Unable to allocate native font-pack memory.');
|
|
322
|
+
let installed = false;
|
|
323
|
+
try {
|
|
324
|
+
new Uint8Array(api.memory.buffer, pointer, font.byteLength).set(font);
|
|
325
|
+
installed = Number(api.ppt_font_pack_install(pointer, font.byteLength)) === 1;
|
|
326
|
+
if (!installed) throw engineError(api, 'The native engine rejected the CJK font pack.');
|
|
327
|
+
} finally {
|
|
328
|
+
if (!installed) api.ppt_input_free(pointer, font.byteLength);
|
|
76
329
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (actual !== MANIFEST.wasmSha256) {
|
|
80
|
-
throw packageError('wasm-integrity-invalid', 'The PPT rendering engine failed its SHA-256 integrity check.');
|
|
330
|
+
if (Number(api.ppt_font_pack_is_installed()) !== 1) {
|
|
331
|
+
throw packageError('font-install-invalid', 'The native engine did not retain the verified CJK font pack.');
|
|
81
332
|
}
|
|
82
333
|
}
|
|
83
334
|
|
|
84
|
-
async function
|
|
85
|
-
if (
|
|
86
|
-
throw packageError('
|
|
335
|
+
async function loadAssetBytes(source, expectedBytes, label) {
|
|
336
|
+
if (!Number.isInteger(expectedBytes) || expectedBytes < 1) {
|
|
337
|
+
throw packageError('asset-manifest-invalid', `The ${label} manifest length is invalid.`);
|
|
87
338
|
}
|
|
88
|
-
if (source instanceof Uint8Array) return
|
|
89
|
-
if (source instanceof ArrayBuffer) return new Uint8Array(source.slice(0));
|
|
339
|
+
if (source instanceof Uint8Array) return exactAsset(Uint8Array.from(source), expectedBytes, label);
|
|
340
|
+
if (source instanceof ArrayBuffer) return exactAsset(new Uint8Array(source.slice(0)), expectedBytes, label);
|
|
90
341
|
if (typeof Response !== 'undefined' && source instanceof Response) {
|
|
91
|
-
if (!source.ok) throw packageError('
|
|
92
|
-
return
|
|
342
|
+
if (!source.ok) throw packageError('asset-request-failed', `${label} request failed with HTTP ${source.status}.`);
|
|
343
|
+
return exactAsset(await readResponseBytes(source, expectedBytes), expectedBytes, label);
|
|
93
344
|
}
|
|
94
345
|
const response = await fetch(source instanceof URL || typeof source === 'string' ? source : DEFAULT_WASM_URL);
|
|
95
|
-
if (!response.ok) throw packageError('
|
|
96
|
-
return
|
|
346
|
+
if (!response.ok) throw packageError('asset-request-failed', `${label} request failed with HTTP ${response.status}.`);
|
|
347
|
+
return exactAsset(await readResponseBytes(response, expectedBytes), expectedBytes, label);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function readResponseBytes(response, maximumBytes) {
|
|
351
|
+
if (!response.body || typeof response.body.getReader !== 'function') {
|
|
352
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
353
|
+
if (bytes.byteLength > maximumBytes) throw packageError('asset-too-large', 'A protected viewer asset exceeded its fixed byte limit.');
|
|
354
|
+
return bytes;
|
|
355
|
+
}
|
|
356
|
+
const reader = response.body.getReader();
|
|
357
|
+
const output = new Uint8Array(maximumBytes);
|
|
358
|
+
let total = 0;
|
|
359
|
+
try {
|
|
360
|
+
for (;;) {
|
|
361
|
+
const { done, value } = await reader.read();
|
|
362
|
+
if (done) break;
|
|
363
|
+
total += value.byteLength;
|
|
364
|
+
if (total > maximumBytes) {
|
|
365
|
+
await reader.cancel('asset byte limit exceeded');
|
|
366
|
+
throw packageError('asset-too-large', 'A protected viewer asset exceeded its fixed byte limit.');
|
|
367
|
+
}
|
|
368
|
+
output.set(value, total - value.byteLength);
|
|
369
|
+
}
|
|
370
|
+
} finally {
|
|
371
|
+
reader.releaseLock();
|
|
372
|
+
}
|
|
373
|
+
return total === maximumBytes ? output : output.subarray(0, total);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function exactAsset(bytes, expectedBytes, label) {
|
|
377
|
+
if (bytes.byteLength !== expectedBytes) {
|
|
378
|
+
throw packageError('asset-length-invalid', `${label} byte length is ${bytes.byteLength}; expected ${expectedBytes}.`);
|
|
379
|
+
}
|
|
380
|
+
return bytes;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function verifySha256(bytes, expected, code, message) {
|
|
384
|
+
const subtle = globalThis.crypto?.subtle;
|
|
385
|
+
if (!subtle || typeof subtle.digest !== 'function') {
|
|
386
|
+
throw packageError('asset-integrity-unavailable', 'Web Crypto SHA-256 support is required to verify protected viewer assets.');
|
|
387
|
+
}
|
|
388
|
+
const digest = new Uint8Array(await subtle.digest('SHA-256', bytes));
|
|
389
|
+
const actual = Array.from(digest, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
390
|
+
if (actual !== expected) throw packageError(code, message);
|
|
97
391
|
}
|
|
98
392
|
|
|
99
393
|
function validateEngine(api) {
|
|
@@ -112,27 +406,26 @@ function validateEngine(api) {
|
|
|
112
406
|
}
|
|
113
407
|
}
|
|
114
408
|
|
|
115
|
-
function
|
|
116
|
-
const
|
|
117
|
-
|
|
409
|
+
function createDirectRuntime(engine) {
|
|
410
|
+
const runtime = {
|
|
411
|
+
mode: 'direct',
|
|
118
412
|
license: LICENSE,
|
|
119
413
|
watermark: WATERMARK,
|
|
120
|
-
open(input) {
|
|
121
|
-
return
|
|
414
|
+
async open(input) {
|
|
415
|
+
return openDirectDocument(engine.exports, input);
|
|
122
416
|
},
|
|
123
417
|
async mount(target, input, options = {}) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
});
|
|
418
|
+
return mountDocument(runtime, target, input, options);
|
|
419
|
+
},
|
|
420
|
+
async cacheStats() {
|
|
421
|
+
return disabledCacheStats(engine.exports.memory.buffer.byteLength);
|
|
422
|
+
},
|
|
423
|
+
async close() {}
|
|
424
|
+
};
|
|
425
|
+
return Object.freeze(runtime);
|
|
133
426
|
}
|
|
134
427
|
|
|
135
|
-
function
|
|
428
|
+
function openDirectDocument(api, input) {
|
|
136
429
|
const bytes = normalizeInput(input);
|
|
137
430
|
if (bytes.byteLength < 8 || !hasCfbSignature(bytes)) {
|
|
138
431
|
throw packageError('ppt-signature-invalid', 'Input is not a PowerPoint 97-2003 (.ppt) compound document.');
|
|
@@ -149,28 +442,46 @@ function openDocument(api, input) {
|
|
|
149
442
|
if (!handle) throw engineError(api, 'The native PPT parser rejected the document.');
|
|
150
443
|
|
|
151
444
|
const slideCount = Number(api.ppt_slide_count(handle));
|
|
152
|
-
const
|
|
153
|
-
const
|
|
445
|
+
const width = Number(api.ppt_slide_width(handle, 1000));
|
|
446
|
+
const height = Number(api.ppt_slide_height(handle, 1000));
|
|
154
447
|
if (!Number.isInteger(slideCount) || slideCount < 1 || slideCount > 10000
|
|
155
|
-
|| !Number.isFinite(
|
|
156
|
-
|| !Number.isFinite(slideHeight) || slideHeight <= 0) {
|
|
448
|
+
|| !Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) {
|
|
157
449
|
api.ppt_document_close(handle);
|
|
158
450
|
throw engineError(api, 'The native PPT parser returned invalid presentation dimensions.');
|
|
159
451
|
}
|
|
160
452
|
|
|
453
|
+
const renderedCanvases = new Map();
|
|
161
454
|
let closed = false;
|
|
162
455
|
const document = {
|
|
456
|
+
mode: 'direct',
|
|
457
|
+
documentId: '',
|
|
163
458
|
get slideCount() { return slideCount; },
|
|
164
|
-
get width() { return
|
|
165
|
-
get height() { return
|
|
459
|
+
get width() { return width; },
|
|
460
|
+
get height() { return height; },
|
|
166
461
|
get closed() { return closed; },
|
|
167
|
-
renderSlide(slideIndex, canvas, options = {}) {
|
|
462
|
+
async renderSlide(slideIndex, canvas, options = {}) {
|
|
168
463
|
if (closed) throw packageError('document-closed', 'The PPT document is already closed.');
|
|
169
|
-
|
|
464
|
+
const result = renderDirectSlide(api, handle, slideCount, width, height, slideIndex, canvas, options);
|
|
465
|
+
renderedCanvases.set(slideIndex, canvas);
|
|
466
|
+
return result;
|
|
170
467
|
},
|
|
171
|
-
|
|
468
|
+
async releaseSlide(slideIndex) {
|
|
469
|
+
if (closed) return false;
|
|
470
|
+
validateSlideIndex(slideIndex, slideCount);
|
|
471
|
+
const canvas = renderedCanvases.get(slideIndex);
|
|
472
|
+
if (!canvas) return false;
|
|
473
|
+
canvas.width = 1;
|
|
474
|
+
canvas.height = 1;
|
|
475
|
+
renderedCanvases.delete(slideIndex);
|
|
476
|
+
return true;
|
|
477
|
+
},
|
|
478
|
+
async cacheStats() {
|
|
479
|
+
return disabledCacheStats(api.memory.buffer.byteLength);
|
|
480
|
+
},
|
|
481
|
+
async close() {
|
|
172
482
|
if (!closed) {
|
|
173
483
|
closed = true;
|
|
484
|
+
renderedCanvases.clear();
|
|
174
485
|
if (Number(api.ppt_document_close(handle)) !== 1) throw engineError(api, 'Unable to close the native PPT document.');
|
|
175
486
|
}
|
|
176
487
|
}
|
|
@@ -178,19 +489,12 @@ function openDocument(api, input) {
|
|
|
178
489
|
return Object.freeze(document);
|
|
179
490
|
}
|
|
180
491
|
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
throw new RangeError(`slideIndex must be between 0 and ${slideCount - 1}.`);
|
|
184
|
-
}
|
|
492
|
+
function renderDirectSlide(api, documentHandle, slideCount, logicalWidth, logicalHeight, slideIndex, canvas, options) {
|
|
493
|
+
validateSlideIndex(slideIndex, slideCount);
|
|
185
494
|
if (!canvas || typeof canvas.getContext !== 'function') {
|
|
186
495
|
throw new TypeError('renderSlide requires an HTMLCanvasElement or OffscreenCanvas.');
|
|
187
496
|
}
|
|
188
|
-
const scale =
|
|
189
|
-
const pixelRatio = finitePositive(options.pixelRatio, defaultPixelRatio());
|
|
190
|
-
const nativeScale = scale * pixelRatio;
|
|
191
|
-
if (nativeScale < 0.1 || nativeScale > 8) {
|
|
192
|
-
throw new RangeError('scale * pixelRatio must be between 0.1 and 8.');
|
|
193
|
-
}
|
|
497
|
+
const { scale, pixelRatio, nativeScale } = normalizeRenderOptions(options);
|
|
194
498
|
const scaleMilli = Math.round(nativeScale * 1000);
|
|
195
499
|
const pixelWidth = Number(api.ppt_slide_width(documentHandle, scaleMilli));
|
|
196
500
|
const pixelHeight = Number(api.ppt_slide_height(documentHandle, scaleMilli));
|
|
@@ -205,24 +509,20 @@ function renderSlide(api, documentHandle, slideCount, logicalWidth, logicalHeigh
|
|
|
205
509
|
if (width !== pixelWidth || height !== pixelHeight || stride !== width * 4 || length !== stride * height || !pointer) {
|
|
206
510
|
throw packageError('wasm-frame-invalid', 'The native PPT engine returned an invalid RGBA frame.');
|
|
207
511
|
}
|
|
208
|
-
const pixels = new Uint8ClampedArray(api.memory.buffer, pointer, length).slice();
|
|
209
512
|
canvas.width = width;
|
|
210
513
|
canvas.height = height;
|
|
211
|
-
if ('
|
|
212
|
-
canvas
|
|
213
|
-
canvas.style.maxWidth = '100%';
|
|
214
|
-
canvas.style.height = 'auto';
|
|
215
|
-
canvas.style.aspectRatio = `${width} / ${height}`;
|
|
216
|
-
canvas.style.display = 'block';
|
|
514
|
+
if (typeof HTMLCanvasElement !== 'undefined' && canvas instanceof HTMLCanvasElement) {
|
|
515
|
+
prepareCanvasElement(canvas, logicalWidth, logicalHeight, scale);
|
|
217
516
|
}
|
|
218
517
|
const context = canvas.getContext('2d', { alpha: false, desynchronized: false });
|
|
219
518
|
if (!context || typeof context.putImageData !== 'function') {
|
|
220
519
|
throw packageError('canvas-context-unavailable', 'A 2D Canvas context is required to display PPT slides.');
|
|
221
520
|
}
|
|
521
|
+
const pixels = new Uint8ClampedArray(api.memory.buffer, pointer, length);
|
|
222
522
|
let imageData;
|
|
223
|
-
|
|
523
|
+
try {
|
|
224
524
|
imageData = new ImageData(pixels, width, height);
|
|
225
|
-
}
|
|
525
|
+
} catch {
|
|
226
526
|
imageData = context.createImageData(width, height);
|
|
227
527
|
imageData.data.set(pixels);
|
|
228
528
|
}
|
|
@@ -233,25 +533,152 @@ function renderSlide(api, documentHandle, slideCount, logicalWidth, logicalHeigh
|
|
|
233
533
|
}
|
|
234
534
|
}
|
|
235
535
|
|
|
236
|
-
async function mountDocument(
|
|
536
|
+
async function mountDocument(runtime, target, input, options) {
|
|
237
537
|
const container = resolveElement(target);
|
|
538
|
+
const document = await runtime.open(input);
|
|
238
539
|
const root = documentRoot(container, options);
|
|
239
540
|
const canvases = [];
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
document.
|
|
252
|
-
|
|
541
|
+
const shells = [];
|
|
542
|
+
const states = Array.from({ length: document.slideCount }, () => 'idle');
|
|
543
|
+
const visible = Array.from({ length: document.slideCount }, () => false);
|
|
544
|
+
const operations = new Map();
|
|
545
|
+
const releaseOperations = new Map();
|
|
546
|
+
const releaseTimers = new Map();
|
|
547
|
+
let observer = null;
|
|
548
|
+
let closed = false;
|
|
549
|
+
|
|
550
|
+
try {
|
|
551
|
+
for (let slideIndex = 0; slideIndex < document.slideCount; slideIndex += 1) {
|
|
552
|
+
const { shell, canvas } = documentCanvas(root, slideIndex, document.width, document.height, finitePositive(options.scale, 1));
|
|
553
|
+
shells.push(shell);
|
|
554
|
+
canvases.push(canvas);
|
|
253
555
|
}
|
|
254
|
-
|
|
556
|
+
|
|
557
|
+
const activate = (slideIndex) => {
|
|
558
|
+
if (closed || states[slideIndex] === 'rendered') return operations.get(slideIndex) || Promise.resolve();
|
|
559
|
+
const pending = operations.get(slideIndex);
|
|
560
|
+
if (pending) return pending;
|
|
561
|
+
clearRelease(slideIndex);
|
|
562
|
+
states[slideIndex] = 'rendering';
|
|
563
|
+
canvases[slideIndex].dataset.renderState = 'rendering';
|
|
564
|
+
const operation = document.renderSlide(slideIndex, canvases[slideIndex], options)
|
|
565
|
+
.then((result) => {
|
|
566
|
+
if (result?.cancelled) {
|
|
567
|
+
states[slideIndex] = 'released';
|
|
568
|
+
canvases[slideIndex].dataset.renderState = 'released';
|
|
569
|
+
return result;
|
|
570
|
+
}
|
|
571
|
+
states[slideIndex] = 'rendered';
|
|
572
|
+
canvases[slideIndex].dataset.renderState = 'rendered';
|
|
573
|
+
return result;
|
|
574
|
+
})
|
|
575
|
+
.catch((error) => {
|
|
576
|
+
states[slideIndex] = 'error';
|
|
577
|
+
canvases[slideIndex].dataset.renderState = 'error';
|
|
578
|
+
throw error;
|
|
579
|
+
})
|
|
580
|
+
.finally(() => operations.delete(slideIndex));
|
|
581
|
+
operations.set(slideIndex, operation);
|
|
582
|
+
return operation;
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const release = async (slideIndex) => {
|
|
586
|
+
if (closed || releaseOperations.has(slideIndex)) return releaseOperations.get(slideIndex);
|
|
587
|
+
const operation = (async () => {
|
|
588
|
+
const rendering = operations.get(slideIndex);
|
|
589
|
+
if (rendering) await rendering.catch(() => {});
|
|
590
|
+
if (closed || visible[slideIndex] || states[slideIndex] !== 'rendered') return;
|
|
591
|
+
try {
|
|
592
|
+
await document.releaseSlide(slideIndex);
|
|
593
|
+
states[slideIndex] = 'released';
|
|
594
|
+
canvases[slideIndex].dataset.renderState = 'released';
|
|
595
|
+
if (visible[slideIndex] && !closed) await activate(slideIndex);
|
|
596
|
+
} catch (error) {
|
|
597
|
+
canvases[slideIndex].dataset.renderState = 'error';
|
|
598
|
+
console.warn('Flyfish PPT slide release failed.', error);
|
|
599
|
+
}
|
|
600
|
+
})().finally(() => releaseOperations.delete(slideIndex));
|
|
601
|
+
releaseOperations.set(slideIndex, operation);
|
|
602
|
+
return operation;
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const scheduleRelease = (slideIndex) => {
|
|
606
|
+
clearRelease(slideIndex);
|
|
607
|
+
const delay = boundedInteger(options.releaseDelayMs, 1200, 0, 30000);
|
|
608
|
+
const timer = setTimeout(() => {
|
|
609
|
+
releaseTimers.delete(slideIndex);
|
|
610
|
+
void release(slideIndex);
|
|
611
|
+
}, delay);
|
|
612
|
+
releaseTimers.set(slideIndex, timer);
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
function clearRelease(slideIndex) {
|
|
616
|
+
const timer = releaseTimers.get(slideIndex);
|
|
617
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
618
|
+
releaseTimers.delete(slideIndex);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const virtualize = options.virtualize !== false && typeof IntersectionObserver === 'function';
|
|
622
|
+
if (virtualize) {
|
|
623
|
+
observer = new IntersectionObserver((entries) => {
|
|
624
|
+
for (const entry of entries) {
|
|
625
|
+
const slideIndex = Number(entry.target.dataset.slideIndex);
|
|
626
|
+
if (entry.isIntersecting) {
|
|
627
|
+
visible[slideIndex] = true;
|
|
628
|
+
clearRelease(slideIndex);
|
|
629
|
+
void activate(slideIndex).catch((error) => console.error(error));
|
|
630
|
+
} else {
|
|
631
|
+
visible[slideIndex] = false;
|
|
632
|
+
scheduleRelease(slideIndex);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}, {
|
|
636
|
+
root: options.scrollRoot || null,
|
|
637
|
+
rootMargin: typeof options.rootMargin === 'string' ? options.rootMargin : '150% 0px',
|
|
638
|
+
threshold: 0
|
|
639
|
+
});
|
|
640
|
+
for (const shell of shells) observer.observe(shell);
|
|
641
|
+
await activate(0);
|
|
642
|
+
} else {
|
|
643
|
+
for (let slideIndex = 0; slideIndex < document.slideCount; slideIndex += 1) {
|
|
644
|
+
await activate(slideIndex);
|
|
645
|
+
if (slideIndex + 1 < document.slideCount) await yieldToBrowser();
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return Object.freeze({
|
|
650
|
+
document,
|
|
651
|
+
root,
|
|
652
|
+
canvases: Object.freeze(canvases),
|
|
653
|
+
virtualized: virtualize,
|
|
654
|
+
async renderSlide(slideIndex) {
|
|
655
|
+
validateSlideIndex(slideIndex, document.slideCount);
|
|
656
|
+
return activate(slideIndex);
|
|
657
|
+
},
|
|
658
|
+
async cacheStats() {
|
|
659
|
+
return document.cacheStats();
|
|
660
|
+
},
|
|
661
|
+
async close() {
|
|
662
|
+
if (closed) return;
|
|
663
|
+
closed = true;
|
|
664
|
+
observer?.disconnect();
|
|
665
|
+
for (const timer of releaseTimers.values()) clearTimeout(timer);
|
|
666
|
+
releaseTimers.clear();
|
|
667
|
+
await Promise.allSettled(operations.values());
|
|
668
|
+
await Promise.allSettled(releaseOperations.values());
|
|
669
|
+
await document.close();
|
|
670
|
+
root.remove();
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
} catch (error) {
|
|
674
|
+
closed = true;
|
|
675
|
+
observer?.disconnect();
|
|
676
|
+
for (const timer of releaseTimers.values()) clearTimeout(timer);
|
|
677
|
+
await Promise.allSettled(releaseOperations.values());
|
|
678
|
+
root.remove();
|
|
679
|
+
await document.close().catch(() => {});
|
|
680
|
+
throw error;
|
|
681
|
+
}
|
|
255
682
|
}
|
|
256
683
|
|
|
257
684
|
function resolveElement(target) {
|
|
@@ -267,21 +694,34 @@ function documentRoot(container, options) {
|
|
|
267
694
|
const root = document.createElement('div');
|
|
268
695
|
root.className = options.className || 'flyfish-ppt-viewer';
|
|
269
696
|
root.dataset.flyfishPptEdition = 'public-watermarked';
|
|
697
|
+
root.dataset.flyfishPptVirtualized = String(options.virtualize !== false);
|
|
270
698
|
root.style.cssText = 'display:grid;width:100%;min-width:0;justify-content:center;gap:20px;padding:20px;box-sizing:border-box;background:#eef1f5;';
|
|
271
699
|
container.append(root);
|
|
272
700
|
return root;
|
|
273
701
|
}
|
|
274
702
|
|
|
275
|
-
function documentCanvas(root, slideIndex) {
|
|
703
|
+
function documentCanvas(root, slideIndex, logicalWidth, logicalHeight, scale) {
|
|
276
704
|
const shell = document.createElement('section');
|
|
277
705
|
shell.dataset.slideIndex = String(slideIndex);
|
|
278
|
-
shell.style.cssText =
|
|
706
|
+
shell.style.cssText = `width:${Math.round(logicalWidth * scale)}px;max-width:100%;min-width:0;aspect-ratio:${logicalWidth}/${logicalHeight};overflow:hidden;background:#fff;box-shadow:0 8px 28px rgba(15,23,42,.14);`;
|
|
279
707
|
const canvas = document.createElement('canvas');
|
|
708
|
+
canvas.width = 1;
|
|
709
|
+
canvas.height = 1;
|
|
710
|
+
canvas.dataset.renderState = 'idle';
|
|
280
711
|
canvas.setAttribute('role', 'img');
|
|
281
712
|
canvas.setAttribute('aria-label', `Slide ${slideIndex + 1}`);
|
|
713
|
+
canvas.style.cssText = 'display:block;width:100%;height:100%;';
|
|
282
714
|
shell.append(canvas);
|
|
283
715
|
root.append(shell);
|
|
284
|
-
return canvas;
|
|
716
|
+
return { shell, canvas };
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function prepareCanvasElement(canvas, logicalWidth, logicalHeight, scale) {
|
|
720
|
+
canvas.style.width = `${Math.round(logicalWidth * scale)}px`;
|
|
721
|
+
canvas.style.maxWidth = '100%';
|
|
722
|
+
canvas.style.height = 'auto';
|
|
723
|
+
canvas.style.aspectRatio = `${logicalWidth} / ${logicalHeight}`;
|
|
724
|
+
canvas.style.display = 'block';
|
|
285
725
|
}
|
|
286
726
|
|
|
287
727
|
function normalizeInput(input) {
|
|
@@ -291,11 +731,88 @@ function normalizeInput(input) {
|
|
|
291
731
|
throw new TypeError('PPT input must be an ArrayBuffer or Uint8Array.');
|
|
292
732
|
}
|
|
293
733
|
|
|
734
|
+
function copyToArrayBuffer(bytes) {
|
|
735
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
736
|
+
copy.set(bytes);
|
|
737
|
+
return copy.buffer;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function transferableInputBuffer(input, bytes, transferOwnership) {
|
|
741
|
+
if (transferOwnership
|
|
742
|
+
&& bytes.byteOffset === 0
|
|
743
|
+
&& bytes.buffer instanceof ArrayBuffer
|
|
744
|
+
&& bytes.byteLength === bytes.buffer.byteLength
|
|
745
|
+
&& (input instanceof ArrayBuffer || ArrayBuffer.isView(input))) {
|
|
746
|
+
return bytes.buffer;
|
|
747
|
+
}
|
|
748
|
+
return copyToArrayBuffer(bytes);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function disabledCacheStats(wasmMemoryBytes = 0) {
|
|
752
|
+
return Object.freeze({
|
|
753
|
+
enabled: false,
|
|
754
|
+
available: false,
|
|
755
|
+
disabledReason: 'worker-disabled',
|
|
756
|
+
entries: 0,
|
|
757
|
+
bytes: 0,
|
|
758
|
+
maxBytes: 0,
|
|
759
|
+
maxEntries: 0,
|
|
760
|
+
maxEntryBytes: 0,
|
|
761
|
+
hits: 0,
|
|
762
|
+
misses: 0,
|
|
763
|
+
writes: 0,
|
|
764
|
+
nativeRenders: 0,
|
|
765
|
+
cancellations: 0,
|
|
766
|
+
attachedCanvases: 0,
|
|
767
|
+
activeCanvases: 0,
|
|
768
|
+
wasmMemoryBytes
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
294
772
|
function hasCfbSignature(bytes) {
|
|
295
773
|
const signature = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];
|
|
296
774
|
return signature.every((value, index) => bytes[index] === value);
|
|
297
775
|
}
|
|
298
776
|
|
|
777
|
+
function normalizeRenderOptions(options = {}) {
|
|
778
|
+
const scale = finitePositive(options.scale, 1);
|
|
779
|
+
const pixelRatio = finitePositive(options.pixelRatio, defaultPixelRatio());
|
|
780
|
+
const nativeScale = scale * pixelRatio;
|
|
781
|
+
if (nativeScale < 0.1 || nativeScale > 8) {
|
|
782
|
+
throw new RangeError('scale * pixelRatio must be between 0.1 and 8.');
|
|
783
|
+
}
|
|
784
|
+
return { scale, pixelRatio, nativeScale };
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function normalizeCacheOptions(value) {
|
|
788
|
+
if (value === false) return { enabled: false };
|
|
789
|
+
const options = value && typeof value === 'object' ? value : {};
|
|
790
|
+
return {
|
|
791
|
+
enabled: options.enabled !== false,
|
|
792
|
+
dbName: typeof options.dbName === 'string' && options.dbName.trim() ? options.dbName.trim() : 'flyfish-ppt-frame-cache-v1',
|
|
793
|
+
maxBytes: boundedInteger(options.maxBytes, 256 * 1024 * 1024, 16 * 1024 * 1024, 1024 * 1024 * 1024),
|
|
794
|
+
maxEntries: boundedInteger(options.maxEntries, 200, 8, 2000),
|
|
795
|
+
maxEntryBytes: boundedInteger(options.maxEntryBytes, 32 * 1024 * 1024, 1024 * 1024, 128 * 1024 * 1024)
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function resolveUrl(value, fallback) {
|
|
800
|
+
if (value === undefined) return fallback;
|
|
801
|
+
if (value instanceof URL) return value;
|
|
802
|
+
if (typeof value === 'string') return new URL(value, globalThis.location?.href || import.meta.url);
|
|
803
|
+
throw new TypeError('Worker rendering asset sources must be URLs or URL strings.');
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function validateSlideIndex(slideIndex, slideCount) {
|
|
807
|
+
if (!Number.isInteger(slideIndex) || slideIndex < 0 || slideIndex >= slideCount) {
|
|
808
|
+
throw new RangeError(`slideIndex must be between 0 and ${slideCount - 1}.`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function ensureOpen(closed) {
|
|
813
|
+
if (closed) throw packageError('document-closed', 'The PPT document is already closed.');
|
|
814
|
+
}
|
|
815
|
+
|
|
299
816
|
function finitePositive(value, fallback) {
|
|
300
817
|
if (value === undefined) return fallback;
|
|
301
818
|
const number = Number(value);
|
|
@@ -303,6 +820,24 @@ function finitePositive(value, fallback) {
|
|
|
303
820
|
return number;
|
|
304
821
|
}
|
|
305
822
|
|
|
823
|
+
function boundedInteger(value, fallback, minimum, maximum) {
|
|
824
|
+
const number = value === undefined ? fallback : Number(value);
|
|
825
|
+
if (!Number.isFinite(number)) return fallback;
|
|
826
|
+
return Math.min(maximum, Math.max(minimum, Math.round(number)));
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function positiveInteger(value, label) {
|
|
830
|
+
const number = Number(value);
|
|
831
|
+
if (!Number.isInteger(number) || number < 1) throw packageError('worker-metadata-invalid', `Worker ${label} is invalid.`);
|
|
832
|
+
return number;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function positiveNumber(value, label) {
|
|
836
|
+
const number = Number(value);
|
|
837
|
+
if (!Number.isFinite(number) || number <= 0) throw packageError('worker-metadata-invalid', `Worker ${label} is invalid.`);
|
|
838
|
+
return number;
|
|
839
|
+
}
|
|
840
|
+
|
|
306
841
|
function defaultPixelRatio() {
|
|
307
842
|
return typeof devicePixelRatio === 'number' && Number.isFinite(devicePixelRatio) ? Math.max(1, devicePixelRatio) : 1;
|
|
308
843
|
}
|