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