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/pdf.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pdf.js — PPTX → PDF exporter. Zero dependencies.
|
|
3
|
+
*
|
|
4
|
+
* Renders each slide onto a canvas at target resolution, encodes as JPEG,
|
|
5
|
+
* and packages into a valid PDF/1.4 binary that any PDF viewer can open.
|
|
6
|
+
*
|
|
7
|
+
* Output is pixel-perfect — same rendering as the canvas renderer, just
|
|
8
|
+
* wrapped in a PDF container.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* import { exportToPdf } from 'pptx-canvas-renderer';
|
|
12
|
+
*
|
|
13
|
+
* // Render all slides → PDF bytes
|
|
14
|
+
* const pdfBytes = await exportToPdf(renderer);
|
|
15
|
+
*
|
|
16
|
+
* // Download as file
|
|
17
|
+
* await downloadAsPdf(renderer, 'presentation.pdf');
|
|
18
|
+
*
|
|
19
|
+
* // Or via renderer instance:
|
|
20
|
+
* const bytes = await renderer.toPdf({ quality: 0.92, width: 1920 });
|
|
21
|
+
* await renderer.downloadPdf('presentation.pdf');
|
|
22
|
+
*
|
|
23
|
+
* Options:
|
|
24
|
+
* width {number} render width per slide in pixels (default 1920)
|
|
25
|
+
* quality {number} JPEG quality 0..1 (default 0.92)
|
|
26
|
+
* slides {number[]} only export specified slides
|
|
27
|
+
* onProgress {function} (done, total) => void
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// ── PDF binary helpers ────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
const CR = 0x0D, LF = 0x0A;
|
|
33
|
+
const enc = new TextEncoder();
|
|
34
|
+
|
|
35
|
+
// ── DPI → pixel width conversion ─────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Convert a DPI value to a pixel width for the given renderer's slide size.
|
|
39
|
+
* slideSize.cx is in EMU (914400 EMU = 1 inch).
|
|
40
|
+
*/
|
|
41
|
+
function dpiToWidth(renderer, dpi) {
|
|
42
|
+
const inches = renderer.slideSize.cx / 914400;
|
|
43
|
+
return Math.round(inches * dpi);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class PdfBuf {
|
|
47
|
+
constructor() { this._parts = []; this._size = 0; }
|
|
48
|
+
write(s) {
|
|
49
|
+
const b = typeof s === 'string' ? enc.encode(s) : s;
|
|
50
|
+
this._parts.push(b);
|
|
51
|
+
this._size += b.length;
|
|
52
|
+
return this._size; // returns position AFTER this write
|
|
53
|
+
}
|
|
54
|
+
get size() { return this._size; }
|
|
55
|
+
concat() {
|
|
56
|
+
const out = new Uint8Array(this._size);
|
|
57
|
+
let off = 0;
|
|
58
|
+
for (const p of this._parts) { out.set(p, off); off += p.length; }
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Extract raw JPEG bytes from a canvas element. Returns Uint8Array. */
|
|
64
|
+
async function canvasToJpeg(canvas, quality = 0.92) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
canvas.toBlob(blob => {
|
|
67
|
+
if (!blob) { reject(new Error('canvas.toBlob failed')); return; }
|
|
68
|
+
blob.arrayBuffer().then(buf => resolve(new Uint8Array(buf))).catch(reject);
|
|
69
|
+
}, 'image/jpeg', quality);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Render a slide to an HTMLCanvasElement at given pixel width. */
|
|
74
|
+
async function renderSlide(renderer, slideIndex, widthPx) {
|
|
75
|
+
const { cx, cy } = renderer.slideSize;
|
|
76
|
+
const h = Math.round(widthPx * cy / cx);
|
|
77
|
+
const canvas = typeof OffscreenCanvas !== 'undefined'
|
|
78
|
+
? new OffscreenCanvas(widthPx, h)
|
|
79
|
+
: Object.assign(document.createElement('canvas'), { width: widthPx, height: h });
|
|
80
|
+
await renderer.renderSlide(slideIndex, canvas, widthPx);
|
|
81
|
+
return canvas;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── PDF structure constants ───────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
// All measurements in PDF points (1pt = 1/72 inch)
|
|
87
|
+
// Standard slide = 10 inches wide × 7.5 inches tall = 720pt × 540pt
|
|
88
|
+
// Widescreen (16:9) = 13.33 × 7.5 inches
|
|
89
|
+
|
|
90
|
+
function slidePageSize(cx, cy) {
|
|
91
|
+
// Convert EMU to points: 1 inch = 914400 EMU = 72 pt
|
|
92
|
+
const w = (cx / 914400) * 72;
|
|
93
|
+
const h = (cy / 914400) * 72;
|
|
94
|
+
return { w, h };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── PDF object writer ─────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
class PdfWriter {
|
|
100
|
+
constructor() {
|
|
101
|
+
this._buf = new PdfBuf();
|
|
102
|
+
this._xref = []; // byte offsets of each object
|
|
103
|
+
this._objNum = 0; // current object counter (0 = free entry)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_nextObj() { return ++this._objNum; }
|
|
107
|
+
|
|
108
|
+
/** Write a PDF object. Returns its object number. */
|
|
109
|
+
_writeObj(num, dictStr, streamData = null) {
|
|
110
|
+
const off = this._buf.size;
|
|
111
|
+
this._xref[num] = off;
|
|
112
|
+
|
|
113
|
+
this._buf.write(`${num} 0 obj\n`);
|
|
114
|
+
if (streamData) {
|
|
115
|
+
const len = streamData.length;
|
|
116
|
+
this._buf.write(`${dictStr.replace('__LEN__', len)}\n`);
|
|
117
|
+
this._buf.write('stream\r\n');
|
|
118
|
+
this._buf.write(streamData);
|
|
119
|
+
this._buf.write('\r\nendstream\n');
|
|
120
|
+
} else {
|
|
121
|
+
this._buf.write(`${dictStr}\n`);
|
|
122
|
+
}
|
|
123
|
+
this._buf.write('endobj\n\n');
|
|
124
|
+
return num;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Reserve the next N object numbers. Returns first number. */
|
|
128
|
+
_reserveObjs(n) {
|
|
129
|
+
const first = this._objNum + 1;
|
|
130
|
+
this._objNum += n;
|
|
131
|
+
return first;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Build PDF ───────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
async build(renderer, opts = {}) {
|
|
137
|
+
const {
|
|
138
|
+
widthPx = 1920,
|
|
139
|
+
quality = 0.92,
|
|
140
|
+
slideList = null,
|
|
141
|
+
onProgress = null,
|
|
142
|
+
} = opts;
|
|
143
|
+
|
|
144
|
+
const indices = slideList ?? Array.from({ length: renderer.slideCount }, (_, i) => i);
|
|
145
|
+
const n = indices.length;
|
|
146
|
+
|
|
147
|
+
const { cx, cy } = renderer.slideSize;
|
|
148
|
+
const { w: pgW, h: pgH } = slidePageSize(cx, cy);
|
|
149
|
+
|
|
150
|
+
// ── PDF header ────────────────────────────────────────────────────────
|
|
151
|
+
this._buf.write('%PDF-1.4\n');
|
|
152
|
+
// Comment with 4 high bytes signals binary content to transfer tools
|
|
153
|
+
this._buf.write('%\xFF\xFE\xFD\xFC\n\n');
|
|
154
|
+
|
|
155
|
+
// ── Pre-allocate object numbers ───────────────────────────────────────
|
|
156
|
+
// Object layout:
|
|
157
|
+
// 1 = Catalog
|
|
158
|
+
// 2 = Pages
|
|
159
|
+
// 3..3+n-1 = Page objects
|
|
160
|
+
// 3+n..3+2n-1 = Image XObjects (one per slide)
|
|
161
|
+
// 3+2n..3+3n-1 = Content streams (one per slide)
|
|
162
|
+
|
|
163
|
+
const catalogNum = this._nextObj(); // 1
|
|
164
|
+
const pagesNum = this._nextObj(); // 2
|
|
165
|
+
const pageNums = Array.from({ length: n }, () => this._nextObj());
|
|
166
|
+
const imageNums = Array.from({ length: n }, () => this._nextObj());
|
|
167
|
+
const contentNums = Array.from({ length: n }, () => this._nextObj());
|
|
168
|
+
|
|
169
|
+
// ── Catalog ───────────────────────────────────────────────────────────
|
|
170
|
+
this._writeObj(catalogNum, `<< /Type /Catalog /Pages ${pagesNum} 0 R >>`);
|
|
171
|
+
|
|
172
|
+
// ── Pages (written later after we know all page refs) ─────────────────
|
|
173
|
+
// We'll write pages dict here with placeholders and rewrite isn't easy
|
|
174
|
+
// in streaming mode — instead we write it now knowing all page nums
|
|
175
|
+
const kidsStr = pageNums.map(n => `${n} 0 R`).join(' ');
|
|
176
|
+
this._writeObj(pagesNum, `<< /Type /Pages /Kids [${kidsStr}] /Count ${n} >>`);
|
|
177
|
+
|
|
178
|
+
// ── Render each slide and write objects ───────────────────────────────
|
|
179
|
+
for (let i = 0; i < n; i++) {
|
|
180
|
+
const slideIdx = indices[i];
|
|
181
|
+
onProgress?.(i, n);
|
|
182
|
+
|
|
183
|
+
// Render
|
|
184
|
+
const canvas = await renderSlide(renderer, slideIdx, widthPx);
|
|
185
|
+
const jpegData = await canvasToJpeg(canvas, quality);
|
|
186
|
+
const imgW = canvas.width;
|
|
187
|
+
const imgH = canvas.height;
|
|
188
|
+
|
|
189
|
+
// Page object
|
|
190
|
+
this._writeObj(pageNums[i],
|
|
191
|
+
`<< /Type /Page /Parent ${pagesNum} 0 R ` +
|
|
192
|
+
`/MediaBox [0 0 ${pgW.toFixed(3)} ${pgH.toFixed(3)}] ` +
|
|
193
|
+
`/Resources << /XObject << /Im${i} ${imageNums[i]} 0 R >> >> ` +
|
|
194
|
+
`/Contents ${contentNums[i]} 0 R >>`
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Image XObject
|
|
198
|
+
this._writeObj(imageNums[i],
|
|
199
|
+
`<< /Type /XObject /Subtype /Image ` +
|
|
200
|
+
`/Width ${imgW} /Height ${imgH} ` +
|
|
201
|
+
`/ColorSpace /DeviceRGB /BitsPerComponent 8 ` +
|
|
202
|
+
`/Filter /DCTDecode /Length __LEN__ >>`,
|
|
203
|
+
jpegData
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// Content stream: place image to fill page
|
|
207
|
+
const contentStr =
|
|
208
|
+
`q ${pgW.toFixed(3)} 0 0 ${pgH.toFixed(3)} 0 0 cm /Im${i} Do Q`;
|
|
209
|
+
this._writeObj(contentNums[i],
|
|
210
|
+
`<< /Length __LEN__ >>`,
|
|
211
|
+
enc.encode(contentStr)
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
onProgress?.(n, n);
|
|
216
|
+
|
|
217
|
+
// ── Cross-reference table ─────────────────────────────────────────────
|
|
218
|
+
const xrefOffset = this._buf.size;
|
|
219
|
+
const totalObjs = this._objNum + 1; // +1 for the free entry at 0
|
|
220
|
+
|
|
221
|
+
this._buf.write(`xref\n0 ${totalObjs}\n`);
|
|
222
|
+
// Entry 0: free list head
|
|
223
|
+
this._buf.write('0000000000 65535 f \r\n');
|
|
224
|
+
for (let i = 1; i < totalObjs; i++) {
|
|
225
|
+
const off = this._xref[i] ?? 0;
|
|
226
|
+
this._buf.write(String(off).padStart(10, '0') + ' 00000 n \r\n');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── Trailer ───────────────────────────────────────────────────────────
|
|
230
|
+
this._buf.write(`trailer\n<< /Size ${totalObjs} /Root ${catalogNum} 0 R >>\n`);
|
|
231
|
+
this._buf.write(`startxref\n${xrefOffset}\n%%EOF\n`);
|
|
232
|
+
|
|
233
|
+
return this._buf.concat();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Export all (or selected) slides to a PDF binary.
|
|
241
|
+
*
|
|
242
|
+
* @param {object} renderer — loaded PptxRenderer instance
|
|
243
|
+
* @param {object} [opts]
|
|
244
|
+
* @param {number} [opts.width] render width in pixels (overrides dpi)
|
|
245
|
+
* @param {number} [opts.dpi=150] dots per inch (standard screen=96, print=300)
|
|
246
|
+
* @param {number} [opts.quality=0.92] JPEG quality 0..1
|
|
247
|
+
* @param {number[]} [opts.slides] slide indices to include (default: all)
|
|
248
|
+
* @param {function} [opts.onProgress] (done, total) => void
|
|
249
|
+
* @returns {Promise<Uint8Array>} raw PDF bytes
|
|
250
|
+
*/
|
|
251
|
+
export async function exportToPdf(renderer, opts = {}) {
|
|
252
|
+
const {
|
|
253
|
+
width = null,
|
|
254
|
+
dpi = 150,
|
|
255
|
+
quality = 0.92,
|
|
256
|
+
slides = null,
|
|
257
|
+
onProgress = null,
|
|
258
|
+
} = opts;
|
|
259
|
+
|
|
260
|
+
const resolvedWidth = width ?? dpiToWidth(renderer, dpi);
|
|
261
|
+
const writer = new PdfWriter();
|
|
262
|
+
return writer.build(renderer, {
|
|
263
|
+
widthPx: resolvedWidth,
|
|
264
|
+
quality,
|
|
265
|
+
slideList: slides,
|
|
266
|
+
onProgress,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Export to PDF and trigger a browser download.
|
|
272
|
+
*
|
|
273
|
+
* @param {object} renderer
|
|
274
|
+
* @param {string} [filename='presentation.pdf']
|
|
275
|
+
* @param {object} [opts] — same as exportToPdf options (dpi, quality, slides…)
|
|
276
|
+
* @returns {Promise<void>}
|
|
277
|
+
*/
|
|
278
|
+
export async function downloadAsPdf(renderer, filename = 'presentation.pdf', opts = {}) {
|
|
279
|
+
const bytes = await exportToPdf(renderer, opts);
|
|
280
|
+
const blob = new Blob([bytes], { type: 'application/pdf' });
|
|
281
|
+
const url = URL.createObjectURL(blob);
|
|
282
|
+
const a = document.createElement('a');
|
|
283
|
+
a.href = url;
|
|
284
|
+
a.download = filename;
|
|
285
|
+
a.click();
|
|
286
|
+
setTimeout(() => URL.revokeObjectURL(url), 10000);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Export a single slide to PDF bytes.
|
|
291
|
+
* @param {number} slideIndex
|
|
292
|
+
* @param {object} renderer
|
|
293
|
+
* @param {object} [opts]
|
|
294
|
+
* @returns {Promise<Uint8Array>}
|
|
295
|
+
*/
|
|
296
|
+
export async function exportSlideToPdf(slideIndex, renderer, opts = {}) {
|
|
297
|
+
return exportToPdf(renderer, { ...opts, slides: [slideIndex] });
|
|
298
|
+
}
|