pptx-browser 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -0
- package/README.md +209 -0
- package/package.json +53 -0
- package/src/animation.js +817 -0
- package/src/charts.js +989 -0
- package/src/clipboard.js +416 -0
- package/src/colors.js +297 -0
- package/src/effects3d.js +312 -0
- package/src/extract.js +535 -0
- package/src/fntdata.js +265 -0
- package/src/fonts.js +676 -0
- package/src/index.js +751 -0
- package/src/pdf.js +298 -0
- package/src/render.js +1964 -0
- package/src/shapes.js +666 -0
- package/src/slideshow.js +492 -0
- package/src/smartart.js +696 -0
- package/src/svg.js +732 -0
- package/src/theme.js +88 -0
- package/src/utils.js +50 -0
- package/src/writer.js +1015 -0
- package/src/zip-writer.js +214 -0
- package/src/zip.js +194 -0
package/src/clipboard.js
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* clipboard.js — Clipboard copy and progressive lazy rendering.
|
|
3
|
+
*
|
|
4
|
+
* CLIPBOARD API
|
|
5
|
+
* ─────────────
|
|
6
|
+
* copySlideToClipboard(slideIndex, renderer)
|
|
7
|
+
* Renders the slide and writes it as a PNG image to the system clipboard.
|
|
8
|
+
* Requires Clipboard API permission (shown to user by browser).
|
|
9
|
+
*
|
|
10
|
+
* copyAllSlidesToClipboard(renderer)
|
|
11
|
+
* Copies all slides as a ZIP of PNGs to clipboard text (data URL list).
|
|
12
|
+
* (Clipboard doesn't support multi-image, so this creates a JSON manifest.)
|
|
13
|
+
*
|
|
14
|
+
* PROGRESSIVE / LAZY RENDERING
|
|
15
|
+
* ─────────────────────────────
|
|
16
|
+
* createLazyDeck(renderer, container, opts)
|
|
17
|
+
* Builds a scrollable deck view where slides are rendered on-demand as
|
|
18
|
+
* they scroll into the viewport, using IntersectionObserver.
|
|
19
|
+
* Shows a low-res placeholder until the full render is ready.
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* // Copy slide 3 to clipboard
|
|
23
|
+
* await copySlideToClipboard(2, renderer);
|
|
24
|
+
*
|
|
25
|
+
* // Progressive deck view (great for large PPTX files)
|
|
26
|
+
* const deck = createLazyDeck(renderer, document.getElementById('deck'));
|
|
27
|
+
* // Each slide renders as you scroll to it
|
|
28
|
+
* deck.destroy(); // clean up
|
|
29
|
+
*
|
|
30
|
+
* // Or via renderer instance:
|
|
31
|
+
* await renderer.copySlide(2);
|
|
32
|
+
* const deck = renderer.createDeck(container);
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
// ── DPI helper ──────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
function dpiToWidth(renderer, dpi) {
|
|
38
|
+
const inches = renderer.slideSize.cx / 914400;
|
|
39
|
+
return Math.round(inches * dpi);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Clipboard ─────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Copy a single slide to the clipboard as a PNG image.
|
|
46
|
+
*
|
|
47
|
+
* Requires Clipboard API and clipboard-write permission.
|
|
48
|
+
* Falls back to opening the image in a new tab if clipboard is unavailable.
|
|
49
|
+
*
|
|
50
|
+
* @param {number} slideIndex
|
|
51
|
+
* @param {object} renderer
|
|
52
|
+
* @param {object|number} [opts] options or legacy pixel width
|
|
53
|
+
* @param {number} [opts.width] pixel width (overrides dpi)
|
|
54
|
+
* @param {number} [opts.dpi=150] dots per inch
|
|
55
|
+
* @returns {Promise<{success: boolean, method: string}>}
|
|
56
|
+
*/
|
|
57
|
+
export async function copySlideToClipboard(slideIndex, renderer, opts = {}) {
|
|
58
|
+
// Legacy: allow passing a raw number as width
|
|
59
|
+
if (typeof opts === 'number') opts = { width: opts };
|
|
60
|
+
const { width = null, dpi = 150 } = opts;
|
|
61
|
+
const resolvedWidth = width ?? dpiToWidth(renderer, dpi);
|
|
62
|
+
// Render to an offscreen canvas
|
|
63
|
+
const canvas = await _renderToCanvas(slideIndex, renderer, resolvedWidth);
|
|
64
|
+
const dataUrl = canvas.toDataURL('image/png');
|
|
65
|
+
|
|
66
|
+
// Method 1: Clipboard API (modern browsers, requires permission)
|
|
67
|
+
if (navigator.clipboard && window.ClipboardItem) {
|
|
68
|
+
try {
|
|
69
|
+
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
|
|
70
|
+
await navigator.clipboard.write([
|
|
71
|
+
new ClipboardItem({ 'image/png': blob }),
|
|
72
|
+
]);
|
|
73
|
+
return { success: true, method: 'clipboard-api', dataUrl };
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (err.name !== 'NotAllowedError') throw err;
|
|
76
|
+
// Permission denied — fall through to fallback
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Method 2: execCommand fallback (deprecated but still works in some contexts)
|
|
81
|
+
if (document.execCommand) {
|
|
82
|
+
try {
|
|
83
|
+
const img = new Image();
|
|
84
|
+
img.src = dataUrl;
|
|
85
|
+
await new Promise(r => { img.onload = r; });
|
|
86
|
+
// Create a contenteditable div, paste the image, copy it
|
|
87
|
+
const div = document.createElement('div');
|
|
88
|
+
div.contentEditable = 'true';
|
|
89
|
+
div.style.cssText = 'position:fixed;left:-9999px;top:-9999px;opacity:0;';
|
|
90
|
+
div.appendChild(img);
|
|
91
|
+
document.body.appendChild(div);
|
|
92
|
+
const range = document.createRange();
|
|
93
|
+
range.selectNodeContents(div);
|
|
94
|
+
const sel = window.getSelection();
|
|
95
|
+
sel.removeAllRanges();
|
|
96
|
+
sel.addRange(range);
|
|
97
|
+
const ok = document.execCommand('copy');
|
|
98
|
+
document.body.removeChild(div);
|
|
99
|
+
sel.removeAllRanges();
|
|
100
|
+
if (ok) return { success: true, method: 'execCommand', dataUrl };
|
|
101
|
+
} catch (_) {}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Method 3: Open in new tab (always works)
|
|
105
|
+
const win = window.open();
|
|
106
|
+
if (win) {
|
|
107
|
+
win.document.write(`<img src="${dataUrl}" style="max-width:100%">`);
|
|
108
|
+
win.document.title = `Slide ${slideIndex + 1}`;
|
|
109
|
+
}
|
|
110
|
+
return { success: false, method: 'opened-tab', dataUrl };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Download a slide as a PNG file.
|
|
115
|
+
*
|
|
116
|
+
* @param {number} slideIndex
|
|
117
|
+
* @param {object} renderer
|
|
118
|
+
* @param {object|number} [opts] options or legacy pixel width
|
|
119
|
+
* @param {number} [opts.width] pixel width (overrides dpi)
|
|
120
|
+
* @param {number} [opts.dpi=300] dots per inch (default 300 for download)
|
|
121
|
+
* @param {string} [opts.filename]
|
|
122
|
+
*/
|
|
123
|
+
export async function downloadSlide(slideIndex, renderer, opts = {}, filename) {
|
|
124
|
+
if (typeof opts === 'number') opts = { width: opts };
|
|
125
|
+
const { width = null, dpi = 300, filename: fn } = opts;
|
|
126
|
+
const resolvedWidth = width ?? dpiToWidth(renderer, dpi);
|
|
127
|
+
const canvas = await _renderToCanvas(slideIndex, renderer, resolvedWidth);
|
|
128
|
+
const dataUrl = canvas.toDataURL('image/png');
|
|
129
|
+
const name = filename || fn || `slide-${slideIndex + 1}.png`;
|
|
130
|
+
const a = document.createElement('a');
|
|
131
|
+
a.href = dataUrl;
|
|
132
|
+
a.download = name;
|
|
133
|
+
a.click();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Download all slides as individual PNG files, or as a ZIP if JSZip is available.
|
|
138
|
+
*
|
|
139
|
+
* @param {object} renderer
|
|
140
|
+
* @param {object|number} [opts] options or legacy pixel width
|
|
141
|
+
* @param {number} [opts.width] pixel width (overrides dpi)
|
|
142
|
+
* @param {number} [opts.dpi=300] dots per inch
|
|
143
|
+
* @param {function} [opts.onProgress] (completed, total) => void
|
|
144
|
+
*/
|
|
145
|
+
export async function downloadAllSlides(renderer, opts = {}, onProgress) {
|
|
146
|
+
if (typeof opts === 'number') opts = { width: opts };
|
|
147
|
+
const { onProgress: progFn, ...slideOpts } = opts;
|
|
148
|
+
const progress = onProgress || progFn;
|
|
149
|
+
const n = renderer.slideCount;
|
|
150
|
+
for (let i = 0; i < n; i++) {
|
|
151
|
+
await downloadSlide(i, renderer, slideOpts);
|
|
152
|
+
progress?.(i + 1, n);
|
|
153
|
+
// Small delay to allow browser to process download
|
|
154
|
+
await new Promise(r => setTimeout(r, 100));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Render a slide to an HTMLCanvasElement.
|
|
160
|
+
*/
|
|
161
|
+
async function _renderToCanvas(slideIndex, renderer, width) {
|
|
162
|
+
const { cx, cy } = renderer.slideSize;
|
|
163
|
+
const aspect = cx / cy;
|
|
164
|
+
const h = Math.round(width / aspect);
|
|
165
|
+
|
|
166
|
+
const canvas = document.createElement('canvas');
|
|
167
|
+
canvas.width = width;
|
|
168
|
+
canvas.height = h;
|
|
169
|
+
await renderer.renderSlide(slideIndex, canvas, width);
|
|
170
|
+
return canvas;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Progressive lazy rendering ────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @typedef {object} LazyDeckOpts
|
|
177
|
+
* @property {number} [thumbWidth=320] px width of initial low-res render
|
|
178
|
+
* @property {number} [fullWidth=1280] px width of full-res render
|
|
179
|
+
* @property {string} [gap='24px'] CSS gap between slides
|
|
180
|
+
* @property {string} [background='#1a1a1a'] container background
|
|
181
|
+
* @property {string} [slideBackground='#fff'] per-slide background
|
|
182
|
+
* @property {boolean} [shadow=true] drop shadow on slides
|
|
183
|
+
* @property {boolean} [clickToShow=false] clicking a slide opens fullscreen
|
|
184
|
+
* @property {string} [maxWidth='900px'] max width of slide display
|
|
185
|
+
* @property {number} [rootMargin=200] px pre-load margin (IntersectionObserver)
|
|
186
|
+
* @property {function} [onSlideVisible] (index) => void
|
|
187
|
+
* @property {function} [onSlideRendered] (index) => void
|
|
188
|
+
*/
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Create a progressive lazy-rendered deck view.
|
|
192
|
+
*
|
|
193
|
+
* Slides are shown as grey placeholders and rendered on-demand as they
|
|
194
|
+
* scroll into the viewport (plus a small lookahead margin).
|
|
195
|
+
*
|
|
196
|
+
* Returns a controller object with:
|
|
197
|
+
* .destroy() — remove everything and clean up
|
|
198
|
+
* .scrollTo(index) — smooth scroll to a slide
|
|
199
|
+
* .renderAll() — force render all slides immediately
|
|
200
|
+
* .getCanvas(index) — get the canvas element for a slide
|
|
201
|
+
*
|
|
202
|
+
* @param {object} renderer
|
|
203
|
+
* @param {HTMLElement} container
|
|
204
|
+
* @param {LazyDeckOpts} [opts]
|
|
205
|
+
* @returns {LazyDeckController}
|
|
206
|
+
*/
|
|
207
|
+
export function createLazyDeck(renderer, container, opts = {}) {
|
|
208
|
+
const {
|
|
209
|
+
thumbWidth = 320,
|
|
210
|
+
fullWidth = 1280,
|
|
211
|
+
gap = '24px',
|
|
212
|
+
background = '#1a1a1a',
|
|
213
|
+
slideBackground = '#fff',
|
|
214
|
+
shadow = true,
|
|
215
|
+
clickToShow = false,
|
|
216
|
+
maxWidth = '900px',
|
|
217
|
+
rootMargin = 200,
|
|
218
|
+
onSlideVisible,
|
|
219
|
+
onSlideRendered,
|
|
220
|
+
} = opts;
|
|
221
|
+
|
|
222
|
+
const { cx, cy } = renderer.slideSize;
|
|
223
|
+
const aspect = cx / cy;
|
|
224
|
+
const n = renderer.slideCount;
|
|
225
|
+
|
|
226
|
+
// ── Build container ────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
container.style.cssText = `
|
|
229
|
+
background:${background};
|
|
230
|
+
padding:${gap};
|
|
231
|
+
display:flex;
|
|
232
|
+
flex-direction:column;
|
|
233
|
+
align-items:center;
|
|
234
|
+
gap:${gap};
|
|
235
|
+
overflow-y:auto;
|
|
236
|
+
position:relative;
|
|
237
|
+
`;
|
|
238
|
+
|
|
239
|
+
// ── Slide elements ────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
const slideWrappers = [];
|
|
242
|
+
const canvases = [];
|
|
243
|
+
const renderStates = []; // 'pending' | 'thumb' | 'full'
|
|
244
|
+
|
|
245
|
+
for (let i = 0; i < n; i++) {
|
|
246
|
+
const wrap = document.createElement('div');
|
|
247
|
+
wrap.style.cssText = `
|
|
248
|
+
width:100%;
|
|
249
|
+
max-width:${maxWidth};
|
|
250
|
+
position:relative;
|
|
251
|
+
background:${slideBackground};
|
|
252
|
+
border-radius:4px;
|
|
253
|
+
${shadow ? 'box-shadow:0 4px 24px rgba(0,0,0,0.5);' : ''}
|
|
254
|
+
overflow:hidden;
|
|
255
|
+
aspect-ratio:${cx}/${cy};
|
|
256
|
+
flex-shrink:0;
|
|
257
|
+
`;
|
|
258
|
+
wrap.setAttribute('data-slide', i);
|
|
259
|
+
|
|
260
|
+
// Slide number badge
|
|
261
|
+
const badge = document.createElement('div');
|
|
262
|
+
badge.style.cssText = `
|
|
263
|
+
position:absolute;bottom:8px;right:10px;
|
|
264
|
+
background:rgba(0,0,0,0.45);color:#fff;
|
|
265
|
+
font:11px/1.4 system-ui,sans-serif;
|
|
266
|
+
padding:2px 7px;border-radius:10px;
|
|
267
|
+
pointer-events:none;z-index:1;
|
|
268
|
+
`;
|
|
269
|
+
badge.textContent = i + 1;
|
|
270
|
+
wrap.appendChild(badge);
|
|
271
|
+
|
|
272
|
+
// Canvas (sized to the wrapper)
|
|
273
|
+
const canvas = document.createElement('canvas');
|
|
274
|
+
canvas.style.cssText = 'display:block;width:100%;height:100%;';
|
|
275
|
+
canvas.width = thumbWidth;
|
|
276
|
+
canvas.height = Math.round(thumbWidth / aspect);
|
|
277
|
+
wrap.appendChild(canvas);
|
|
278
|
+
|
|
279
|
+
// Placeholder while not yet rendered
|
|
280
|
+
const placeholder = _buildPlaceholder(i, cx, cy, slideBackground);
|
|
281
|
+
wrap.appendChild(placeholder);
|
|
282
|
+
|
|
283
|
+
// Click handler
|
|
284
|
+
if (clickToShow) {
|
|
285
|
+
wrap.style.cursor = 'pointer';
|
|
286
|
+
wrap.addEventListener('click', () => {
|
|
287
|
+
const show = new (require('./slideshow.js').SlideShow)(renderer, document.body);
|
|
288
|
+
show.start(i);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
container.appendChild(wrap);
|
|
293
|
+
slideWrappers.push(wrap);
|
|
294
|
+
canvases.push(canvas);
|
|
295
|
+
renderStates.push('pending');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ── IntersectionObserver ──────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
const renderQueue = [];
|
|
301
|
+
let renderBusy = false;
|
|
302
|
+
|
|
303
|
+
async function processQueue() {
|
|
304
|
+
if (renderBusy || renderQueue.length === 0) return;
|
|
305
|
+
renderBusy = true;
|
|
306
|
+
while (renderQueue.length > 0) {
|
|
307
|
+
const idx = renderQueue.shift();
|
|
308
|
+
if (renderStates[idx] === 'full') continue;
|
|
309
|
+
|
|
310
|
+
const canvas = canvases[idx];
|
|
311
|
+
const wrap = slideWrappers[idx];
|
|
312
|
+
const placeholder = wrap.querySelector('[data-placeholder]');
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
// Render at full resolution
|
|
316
|
+
canvas.width = fullWidth;
|
|
317
|
+
canvas.height = Math.round(fullWidth / aspect);
|
|
318
|
+
await renderer.renderSlide(idx, canvas, fullWidth);
|
|
319
|
+
renderStates[idx] = 'full';
|
|
320
|
+
|
|
321
|
+
// Fade in
|
|
322
|
+
canvas.style.transition = 'opacity 0.3s';
|
|
323
|
+
canvas.style.opacity = '1';
|
|
324
|
+
if (placeholder) placeholder.style.display = 'none';
|
|
325
|
+
|
|
326
|
+
onSlideRendered?.(idx);
|
|
327
|
+
} catch (err) {
|
|
328
|
+
console.warn(`LazyDeck: failed to render slide ${idx}`, err);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
renderBusy = false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const observer = new IntersectionObserver((entries) => {
|
|
335
|
+
for (const entry of entries) {
|
|
336
|
+
if (!entry.isIntersecting) continue;
|
|
337
|
+
const idx = parseInt(entry.target.getAttribute('data-slide'), 10);
|
|
338
|
+
if (isNaN(idx) || renderStates[idx] === 'full') continue;
|
|
339
|
+
|
|
340
|
+
onSlideVisible?.(idx);
|
|
341
|
+
|
|
342
|
+
// Enqueue: current slide first, then neighbours
|
|
343
|
+
for (const neighbor of [idx, idx - 1, idx + 1, idx + 2].filter(j => j >= 0 && j < n)) {
|
|
344
|
+
if (renderStates[neighbor] !== 'full' && !renderQueue.includes(neighbor)) {
|
|
345
|
+
renderQueue.push(neighbor);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
processQueue();
|
|
349
|
+
}
|
|
350
|
+
}, {
|
|
351
|
+
root: container,
|
|
352
|
+
rootMargin: `${rootMargin}px`,
|
|
353
|
+
threshold: 0,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
slideWrappers.forEach(w => observer.observe(w));
|
|
357
|
+
|
|
358
|
+
// ── Controller ─────────────────────────────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
destroy() {
|
|
362
|
+
observer.disconnect();
|
|
363
|
+
container.innerHTML = '';
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
scrollTo(index, behavior = 'smooth') {
|
|
367
|
+
const wrap = slideWrappers[index];
|
|
368
|
+
if (wrap) wrap.scrollIntoView({ behavior, block: 'start' });
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
async renderAll(onProgress) {
|
|
372
|
+
for (let i = 0; i < n; i++) {
|
|
373
|
+
if (renderStates[i] !== 'full') {
|
|
374
|
+
renderQueue.push(i);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
await processQueue();
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
getCanvas(index) {
|
|
381
|
+
return canvases[index] || null;
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
get slideCount() { return n; },
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function _buildPlaceholder(index, cx, cy, bg) {
|
|
389
|
+
const el = document.createElement('div');
|
|
390
|
+
el.setAttribute('data-placeholder', '1');
|
|
391
|
+
el.style.cssText = `
|
|
392
|
+
position:absolute;inset:0;
|
|
393
|
+
display:flex;align-items:center;justify-content:center;
|
|
394
|
+
background:${bg};
|
|
395
|
+
flex-direction:column;gap:12px;
|
|
396
|
+
`;
|
|
397
|
+
|
|
398
|
+
// Animated skeleton lines
|
|
399
|
+
const linesHtml = `
|
|
400
|
+
<div style="width:55%;height:18px;background:#e0e0e0;border-radius:3px;animation:pulse 1.4s ease-in-out infinite;"></div>
|
|
401
|
+
<div style="width:72%;height:10px;background:#ebebeb;border-radius:3px;animation:pulse 1.4s ease-in-out infinite 0.1s;"></div>
|
|
402
|
+
<div style="width:62%;height:10px;background:#ebebeb;border-radius:3px;animation:pulse 1.4s ease-in-out infinite 0.2s;"></div>
|
|
403
|
+
<div style="width:45%;height:10px;background:#ebebeb;border-radius:3px;animation:pulse 1.4s ease-in-out infinite 0.3s;"></div>
|
|
404
|
+
`;
|
|
405
|
+
|
|
406
|
+
// Inject keyframes once
|
|
407
|
+
if (!document.getElementById('_pptx_lazy_css')) {
|
|
408
|
+
const style = document.createElement('style');
|
|
409
|
+
style.id = '_pptx_lazy_css';
|
|
410
|
+
style.textContent = `@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}`;
|
|
411
|
+
document.head.appendChild(style);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
el.innerHTML = linesHtml;
|
|
415
|
+
return el;
|
|
416
|
+
}
|