@ohos-ports/slides-grab 1.5.0-beta.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 +21 -0
- package/README-ko.md +292 -0
- package/README.md +301 -0
- package/bin/ohos-postinstall.cjs +118 -0
- package/bin/ppt-agent.js +620 -0
- package/convert.cjs +20 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/glib-2.0/include/glibconfig.h +220 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/index.js +1 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libexpat.so +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libffi.so +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libjpeg.so +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libpcre2-8.so.0 +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libvips-cpp.so.42.20.3 +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libvips.so.42.20.3 +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/package.json +13 -0
- package/native/@img/sharp-openharmony-arm64/lib/sharp-openharmony-arm64.node +0 -0
- package/native/@img/sharp-openharmony-arm64/package.json +16 -0
- package/package.json +95 -0
- package/runtimes/claude-code/agents/design-critic-agent.md +23 -0
- package/runtimes/codex/agents/slides-grab-design-critic.md +22 -0
- package/scripts/build-viewer.js +444 -0
- package/scripts/design-gate.js +258 -0
- package/scripts/download-video.js +213 -0
- package/scripts/editor-server.js +1167 -0
- package/scripts/figma-export.js +169 -0
- package/scripts/generate-image.js +116 -0
- package/scripts/generate-images.js +164 -0
- package/scripts/html2pdf.js +822 -0
- package/scripts/html2png.js +246 -0
- package/scripts/html2pptx.js +162 -0
- package/scripts/import-template.js +86 -0
- package/scripts/install-runtime.js +216 -0
- package/scripts/render-tldraw.js +44 -0
- package/scripts/validate-slides.js +221 -0
- package/skills/slides-grab/SKILL.md +65 -0
- package/skills/slides-grab/references/presentation-workflow-reference.md +61 -0
- package/skills/slides-grab-card-news/SKILL.md +35 -0
- package/skills/slides-grab-design/SKILL.md +88 -0
- package/skills/slides-grab-design/references/beautiful-slide-defaults.md +88 -0
- package/skills/slides-grab-design/references/design-gate.md +349 -0
- package/skills/slides-grab-design/references/design-rules.md +76 -0
- package/skills/slides-grab-design/references/design-system-full.md +568 -0
- package/skills/slides-grab-design/references/detailed-design-rules.md +70 -0
- package/skills/slides-grab-export/SKILL.md +55 -0
- package/skills/slides-grab-export/references/export-rules.md +27 -0
- package/skills/slides-grab-export/references/html2pptx.md +627 -0
- package/skills/slides-grab-export/references/ooxml.md +427 -0
- package/skills/slides-grab-export/references/pptx-skill-reference.md +189 -0
- package/skills/slides-grab-html/SKILL.md +67 -0
- package/skills/slides-grab-image/SKILL.md +82 -0
- package/skills/slides-grab-plan/SKILL.md +68 -0
- package/skills/slides-grab-plan/references/design-md-to-slides-conversion.md +135 -0
- package/skills/slides-grab-plan/references/outline-format.md +47 -0
- package/skills/slides-grab-plan/references/plan-workflow-reference.md +140 -0
- package/src/codex-imagen.js +182 -0
- package/src/design-diversity-data.js +6932 -0
- package/src/design-gate-report.js +244 -0
- package/src/design-gate-state.js +329 -0
- package/src/design-import.js +164 -0
- package/src/design-md-parser.js +415 -0
- package/src/design-styles-data.js +1928 -0
- package/src/design-styles.js +209 -0
- package/src/editor/codex-edit.js +584 -0
- package/src/editor/edit-subprocess.js +170 -0
- package/src/editor/editor-codex-prompt.md +50 -0
- package/src/editor/editor.html +1785 -0
- package/src/editor/js/editor-bbox.js +332 -0
- package/src/editor/js/editor-chat.js +56 -0
- package/src/editor/js/editor-direct-edit.js +238 -0
- package/src/editor/js/editor-dom.js +59 -0
- package/src/editor/js/editor-init.js +405 -0
- package/src/editor/js/editor-navigation.js +54 -0
- package/src/editor/js/editor-select.js +558 -0
- package/src/editor/js/editor-send.js +175 -0
- package/src/editor/js/editor-sse.js +163 -0
- package/src/editor/js/editor-state.js +41 -0
- package/src/editor/js/editor-type.js +71 -0
- package/src/editor/js/editor-utils.js +167 -0
- package/src/editor/js/model-registry.js +37 -0
- package/src/editor/screenshot.js +82 -0
- package/src/export-resolution.cjs +68 -0
- package/src/figma.js +71 -0
- package/src/html2pptx-scale.cjs +120 -0
- package/src/html2pptx.cjs +1262 -0
- package/src/image-contract.js +329 -0
- package/src/image-native.js +468 -0
- package/src/nano-banana.js +841 -0
- package/src/ohos-browser-bridge.cjs +272 -0
- package/src/pptx-raster-export.cjs +299 -0
- package/src/resolve.js +110 -0
- package/src/slide-mode.cjs +72 -0
- package/src/template-fidelity.js +267 -0
- package/src/template-import.js +505 -0
- package/src/template-layout.js +261 -0
- package/src/template-pack.js +255 -0
- package/src/tldraw/render.js +473 -0
- package/src/validation/cli.js +120 -0
- package/src/validation/core.js +1023 -0
- package/templates/chart.html +121 -0
- package/templates/closing.html +54 -0
- package/templates/content.html +50 -0
- package/templates/contents.html +60 -0
- package/templates/cover.html +64 -0
- package/templates/custom/.gitkeep +0 -0
- package/templates/custom/README.md +7 -0
- package/templates/design-styles/README.md +20 -0
- package/templates/design-styles/preview.html +4438 -0
- package/templates/diagram-tldraw.html +56 -0
- package/templates/diagram.html +98 -0
- package/templates/quote.html +31 -0
- package/templates/section-divider.html +43 -0
- package/templates/split-layout.html +43 -0
- package/templates/statistics.html +55 -0
- package/templates/team.html +49 -0
- package/templates/timeline.html +59 -0
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { basename, join, resolve } from 'node:path';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
7
|
+
import { chromium } from 'playwright';
|
|
8
|
+
import { PDFDocument } from 'pdf-lib';
|
|
9
|
+
import sharp from 'sharp';
|
|
10
|
+
|
|
11
|
+
import { ensureSlidesPassValidation } from './validate-slides.js';
|
|
12
|
+
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const {
|
|
15
|
+
getResolutionChoices,
|
|
16
|
+
getResolutionSize,
|
|
17
|
+
normalizeResolutionPreset,
|
|
18
|
+
} = require('../src/export-resolution.cjs');
|
|
19
|
+
const {
|
|
20
|
+
DEFAULT_SLIDE_MODE,
|
|
21
|
+
getSlideModeChoices,
|
|
22
|
+
getSlideModeConfig,
|
|
23
|
+
normalizeSlideMode,
|
|
24
|
+
} = require('../src/slide-mode.cjs');
|
|
25
|
+
|
|
26
|
+
const DEFAULT_OUTPUT = 'slides.pdf';
|
|
27
|
+
const DEFAULT_SLIDES_DIR = 'slides';
|
|
28
|
+
const DEFAULT_MODE = 'capture';
|
|
29
|
+
const DEFAULT_CAPTURE_RESOLUTION = '2160p';
|
|
30
|
+
const PDF_MODES = new Set(['capture', 'print']);
|
|
31
|
+
const SLIDE_FILE_PATTERN = /^slide-.*\.html$/i;
|
|
32
|
+
const DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR = 2;
|
|
33
|
+
const RENDER_SETTLE_MS = 120;
|
|
34
|
+
const CSS_PIXELS_PER_INCH = 96;
|
|
35
|
+
const PDF_POINTS_PER_INCH = 72;
|
|
36
|
+
const VIDEO_EXPORT_PREP_TIMEOUT_MS = 4000;
|
|
37
|
+
|
|
38
|
+
function printUsage() {
|
|
39
|
+
process.stdout.write(
|
|
40
|
+
[
|
|
41
|
+
'Usage: node scripts/html2pdf.js [options]',
|
|
42
|
+
'',
|
|
43
|
+
'Options:',
|
|
44
|
+
` --output <path> Output PDF path (default: ${DEFAULT_OUTPUT})`,
|
|
45
|
+
` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
|
|
46
|
+
` --mode <mode> PDF export mode: capture|print (default: ${DEFAULT_MODE})`,
|
|
47
|
+
` --slide-mode <mode> Slide mode: ${getSlideModeChoices().join('|')} (default: ${DEFAULT_SLIDE_MODE})`,
|
|
48
|
+
` --resolution <preset> Capture raster size preset: ${getResolutionChoices().join('|')}|4k (default: ${DEFAULT_CAPTURE_RESOLUTION}; ignored in print mode)`,
|
|
49
|
+
' -h, --help Show this help message',
|
|
50
|
+
'',
|
|
51
|
+
'Examples:',
|
|
52
|
+
' node scripts/html2pdf.js',
|
|
53
|
+
' node scripts/html2pdf.js --output dist/deck.pdf',
|
|
54
|
+
' node scripts/html2pdf.js --mode print --output dist/searchable.pdf',
|
|
55
|
+
' node scripts/html2pdf.js --resolution 2160p --output dist/deck-4k.pdf',
|
|
56
|
+
].join('\n'),
|
|
57
|
+
);
|
|
58
|
+
process.stdout.write('\n');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readOptionValue(args, index, optionName) {
|
|
62
|
+
const next = args[index + 1];
|
|
63
|
+
if (!next || next.startsWith('-')) {
|
|
64
|
+
throw new Error(`Missing value for ${optionName}.`);
|
|
65
|
+
}
|
|
66
|
+
return next;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function toSlideOrder(fileName) {
|
|
70
|
+
const match = fileName.match(/\d+/);
|
|
71
|
+
return match ? Number.parseInt(match[0], 10) : Number.POSITIVE_INFINITY;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeDimension(value, fallback) {
|
|
75
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
76
|
+
return fallback;
|
|
77
|
+
}
|
|
78
|
+
return Math.max(1, Math.round(value));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeMode(value) {
|
|
82
|
+
if (typeof value !== 'string') {
|
|
83
|
+
throw new Error(`--mode must be one of: ${Array.from(PDF_MODES).join(', ')}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const mode = value.trim().toLowerCase();
|
|
87
|
+
if (!PDF_MODES.has(mode)) {
|
|
88
|
+
throw new Error(`Unknown PDF mode "${value}". Expected one of: ${Array.from(PDF_MODES).join(', ')}`);
|
|
89
|
+
}
|
|
90
|
+
return mode;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function cssPixelsToPdfPoints(value) {
|
|
94
|
+
return Math.round((normalizeDimension(value, 0) * PDF_POINTS_PER_INCH) / CSS_PIXELS_PER_INCH);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function normalizeCaptureRasterSize(pngBytes, resolution = '', slideMode = DEFAULT_SLIDE_MODE) {
|
|
98
|
+
const targetSize = getResolutionSize(resolution, slideMode);
|
|
99
|
+
if (!targetSize) {
|
|
100
|
+
return pngBytes;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const metadata = await sharp(pngBytes).metadata();
|
|
104
|
+
const currentWidth = normalizeDimension(metadata.width, targetSize.width);
|
|
105
|
+
const currentHeight = normalizeDimension(metadata.height, targetSize.height);
|
|
106
|
+
if (currentWidth === targetSize.width && currentHeight === targetSize.height) {
|
|
107
|
+
return pngBytes;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return sharp(pngBytes)
|
|
111
|
+
.resize(targetSize.width, targetSize.height, { fit: 'fill' })
|
|
112
|
+
.png()
|
|
113
|
+
.toBuffer();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function formatDiagnosticEntry(entry) {
|
|
117
|
+
const prefix = entry.slideFile ? `${entry.slideFile}: ` : '';
|
|
118
|
+
return `${prefix}${entry.message}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function formatDiagnostics(slideFile, diagnostics = []) {
|
|
122
|
+
const relevantDiagnostics = diagnostics.filter((entry) => entry.slideFile === slideFile);
|
|
123
|
+
if (relevantDiagnostics.length === 0) {
|
|
124
|
+
return '';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return relevantDiagnostics.map((entry) => ` - ${formatDiagnosticEntry(entry)}`).join('\n');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function decorateError(error, slideFile, diagnostics = []) {
|
|
131
|
+
const baseMessage = error instanceof Error ? error.message : String(error);
|
|
132
|
+
const details = formatDiagnostics(slideFile, diagnostics);
|
|
133
|
+
return new Error(details ? `${slideFile}: ${baseMessage}\nDiagnostics:\n${details}` : `${slideFile}: ${baseMessage}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function sortSlideFiles(a, b) {
|
|
137
|
+
const orderA = toSlideOrder(a);
|
|
138
|
+
const orderB = toSlideOrder(b);
|
|
139
|
+
if (orderA !== orderB) return orderA - orderB;
|
|
140
|
+
return a.localeCompare(b);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function parseCliArgs(args) {
|
|
144
|
+
const options = {
|
|
145
|
+
output: DEFAULT_OUTPUT,
|
|
146
|
+
slidesDir: DEFAULT_SLIDES_DIR,
|
|
147
|
+
mode: DEFAULT_MODE,
|
|
148
|
+
slideMode: DEFAULT_SLIDE_MODE,
|
|
149
|
+
resolution: DEFAULT_CAPTURE_RESOLUTION,
|
|
150
|
+
help: false,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
154
|
+
const arg = args[i];
|
|
155
|
+
|
|
156
|
+
if (arg === '-h' || arg === '--help') {
|
|
157
|
+
options.help = true;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (arg === '--output') {
|
|
162
|
+
options.output = readOptionValue(args, i, '--output');
|
|
163
|
+
i += 1;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (arg.startsWith('--output=')) {
|
|
168
|
+
options.output = arg.slice('--output='.length);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (arg === '--slides-dir') {
|
|
173
|
+
options.slidesDir = readOptionValue(args, i, '--slides-dir');
|
|
174
|
+
i += 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (arg.startsWith('--slides-dir=')) {
|
|
179
|
+
options.slidesDir = arg.slice('--slides-dir='.length);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (arg === '--mode') {
|
|
184
|
+
options.mode = normalizeMode(readOptionValue(args, i, '--mode'));
|
|
185
|
+
i += 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (arg.startsWith('--mode=')) {
|
|
190
|
+
options.mode = normalizeMode(arg.slice('--mode='.length));
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (arg === '--slide-mode') {
|
|
195
|
+
options.slideMode = normalizeSlideMode(readOptionValue(args, i, '--slide-mode'), { optionName: '--slide-mode' });
|
|
196
|
+
i += 1;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (arg.startsWith('--slide-mode=')) {
|
|
201
|
+
options.slideMode = normalizeSlideMode(arg.slice('--slide-mode='.length), { optionName: '--slide-mode' });
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (arg === '--resolution') {
|
|
206
|
+
options.resolution = normalizeResolutionPreset(readOptionValue(args, i, '--resolution'));
|
|
207
|
+
i += 1;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (arg.startsWith('--resolution=')) {
|
|
212
|
+
options.resolution = normalizeResolutionPreset(arg.slice('--resolution='.length));
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (typeof options.output !== 'string' || options.output.trim() === '') {
|
|
220
|
+
throw new Error('--output must be a non-empty string.');
|
|
221
|
+
}
|
|
222
|
+
if (typeof options.slidesDir !== 'string' || options.slidesDir.trim() === '') {
|
|
223
|
+
throw new Error('--slides-dir must be a non-empty string.');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
options.output = options.output.trim();
|
|
227
|
+
options.slidesDir = options.slidesDir.trim();
|
|
228
|
+
options.mode = normalizeMode(options.mode);
|
|
229
|
+
options.slideMode = normalizeSlideMode(options.slideMode, { optionName: '--slide-mode' });
|
|
230
|
+
options.resolution = normalizeResolutionPreset(options.resolution);
|
|
231
|
+
if (options.mode === 'print') {
|
|
232
|
+
options.resolution = '';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return options;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function findSlideFiles(slidesDir = resolve(process.cwd(), DEFAULT_SLIDES_DIR)) {
|
|
239
|
+
const entries = await readdir(slidesDir, { withFileTypes: true });
|
|
240
|
+
return entries
|
|
241
|
+
.filter((entry) => entry.isFile() && SLIDE_FILE_PATTERN.test(entry.name))
|
|
242
|
+
.map((entry) => entry.name)
|
|
243
|
+
.sort(sortSlideFiles);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function buildPdfOptions(widthPx, heightPx) {
|
|
247
|
+
const fallbackSize = getSlideModeConfig(DEFAULT_SLIDE_MODE).framePx;
|
|
248
|
+
return {
|
|
249
|
+
width: `${normalizeDimension(widthPx, fallbackSize.width)}px`,
|
|
250
|
+
height: `${normalizeDimension(heightPx, fallbackSize.height)}px`,
|
|
251
|
+
printBackground: true,
|
|
252
|
+
pageRanges: '1',
|
|
253
|
+
margin: { top: '0px', right: '0px', bottom: '0px', left: '0px' },
|
|
254
|
+
preferCSSPageSize: false,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function buildPageOptions(mode = DEFAULT_MODE, resolution = '', slideMode = DEFAULT_SLIDE_MODE) {
|
|
259
|
+
const { framePx } = getSlideModeConfig(slideMode);
|
|
260
|
+
const targetResolution = normalizeMode(mode) === 'capture' ? getResolutionSize(resolution, slideMode) : null;
|
|
261
|
+
return {
|
|
262
|
+
viewport: {
|
|
263
|
+
width: framePx.width,
|
|
264
|
+
height: framePx.height,
|
|
265
|
+
},
|
|
266
|
+
deviceScaleFactor: normalizeMode(mode) === 'capture'
|
|
267
|
+
? targetResolution
|
|
268
|
+
? targetResolution.height / framePx.height
|
|
269
|
+
: DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR
|
|
270
|
+
: 1,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function chooseSlideFrame(metrics, slideMode = DEFAULT_SLIDE_MODE) {
|
|
275
|
+
const { framePx } = getSlideModeConfig(slideMode);
|
|
276
|
+
const targetAspectRatio = framePx.width / framePx.height;
|
|
277
|
+
const viewportArea = Math.max(1, metrics.viewport.width * metrics.viewport.height);
|
|
278
|
+
const bodyArea = Math.max(1, metrics.body.width * metrics.body.height);
|
|
279
|
+
const bodyScrollArea = Math.max(1, metrics.body.scrollWidth * metrics.body.scrollHeight);
|
|
280
|
+
const documentScrollArea = Math.max(1, metrics.document.scrollWidth * metrics.document.scrollHeight);
|
|
281
|
+
const bodyHasOverflowingContent =
|
|
282
|
+
metrics.body.scrollWidth > metrics.body.width * 1.05 ||
|
|
283
|
+
metrics.body.scrollHeight > metrics.body.height * 1.05 ||
|
|
284
|
+
metrics.document.scrollWidth > metrics.body.width * 1.05 ||
|
|
285
|
+
metrics.document.scrollHeight > metrics.body.height * 1.05;
|
|
286
|
+
const candidates = [
|
|
287
|
+
{ ...metrics.body, source: 'body' },
|
|
288
|
+
...metrics.candidates.map((candidate, index) => ({ ...candidate, candidateIndex: index, source: 'body-child' })),
|
|
289
|
+
]
|
|
290
|
+
.filter((candidate) => candidate.width > 0 && candidate.height > 0)
|
|
291
|
+
.map((candidate) => ({
|
|
292
|
+
...candidate,
|
|
293
|
+
area: candidate.width * candidate.height,
|
|
294
|
+
aspectDelta: Math.abs(candidate.width / candidate.height - targetAspectRatio),
|
|
295
|
+
coverage: (candidate.width * candidate.height) / viewportArea,
|
|
296
|
+
}))
|
|
297
|
+
.sort((left, right) => right.area - left.area);
|
|
298
|
+
|
|
299
|
+
// When the body has explicit slide dimensions (fills the viewport with no
|
|
300
|
+
// default margins), no overflow, and a good 16:9 aspect ratio, it IS the
|
|
301
|
+
// slide frame — skip the body-child heuristic entirely.
|
|
302
|
+
const bodyCandidate = candidates.find((c) => c.source === 'body');
|
|
303
|
+
const bodyMatchesViewport =
|
|
304
|
+
Math.abs(metrics.body.width - metrics.viewport.width) < 2 &&
|
|
305
|
+
Math.abs(metrics.body.height - metrics.viewport.height) < 2;
|
|
306
|
+
if (bodyMatchesViewport && !bodyHasOverflowingContent && bodyCandidate && bodyCandidate.aspectDelta < 0.12) {
|
|
307
|
+
return bodyCandidate;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const preferredCandidate = candidates.find((candidate) => {
|
|
311
|
+
if (candidate.source !== 'body-child') return false;
|
|
312
|
+
if (candidate.coverage < 0.45) return false;
|
|
313
|
+
return candidate.aspectDelta < 0.2;
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
if (preferredCandidate && (bodyHasOverflowingContent || bodyArea > preferredCandidate.area * 1.15 || bodyScrollArea > preferredCandidate.area * 1.15 || documentScrollArea > preferredCandidate.area * 1.15)) {
|
|
317
|
+
return preferredCandidate;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const bestAspectCandidate = candidates.find((candidate) => {
|
|
321
|
+
if (candidate.source === 'body' && bodyHasOverflowingContent) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
return candidate.aspectDelta < 0.12;
|
|
325
|
+
});
|
|
326
|
+
return bestAspectCandidate || candidates[0] || { ...metrics.body, source: 'fallback' };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export async function waitForSlideRenderReady(page, options = {}) {
|
|
330
|
+
const settleMs = normalizeDimension(options.settleMs ?? RENDER_SETTLE_MS, RENDER_SETTLE_MS);
|
|
331
|
+
const shouldRunReadySignal = options.runReadySignal !== false;
|
|
332
|
+
|
|
333
|
+
await page.waitForLoadState('load');
|
|
334
|
+
await page.evaluate(async ({ settleMs: settleDelay, runReadySignal }) => {
|
|
335
|
+
if (document.fonts?.ready) {
|
|
336
|
+
await document.fonts.ready.catch(() => {});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
await Promise.all(
|
|
340
|
+
Array.from(document.images || [], async (image) => {
|
|
341
|
+
if (typeof image.decode === 'function') {
|
|
342
|
+
await image.decode().catch(() => {});
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (image.complete) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
await new Promise((resolve) => {
|
|
351
|
+
const done = () => resolve();
|
|
352
|
+
image.addEventListener('load', done, { once: true });
|
|
353
|
+
image.addEventListener('error', done, { once: true });
|
|
354
|
+
});
|
|
355
|
+
}),
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
if (runReadySignal) {
|
|
359
|
+
const readySignal =
|
|
360
|
+
window.__slidesGrabReady ??
|
|
361
|
+
window.__SLIDES_GRAB_READY ??
|
|
362
|
+
window.slidesGrabReady ??
|
|
363
|
+
document.documentElement?.dataset?.slidesGrabReady ??
|
|
364
|
+
document.body?.dataset?.slidesGrabReady;
|
|
365
|
+
|
|
366
|
+
if (typeof readySignal === 'function') {
|
|
367
|
+
await readySignal();
|
|
368
|
+
} else if (readySignal && typeof readySignal.then === 'function') {
|
|
369
|
+
await readySignal.catch(() => {});
|
|
370
|
+
} else if (readySignal === 'pending') {
|
|
371
|
+
await new Promise((resolve) => {
|
|
372
|
+
const listener = () => resolve();
|
|
373
|
+
window.addEventListener('slides-grab-ready', listener, { once: true });
|
|
374
|
+
setTimeout(resolve, 5000);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
380
|
+
await new Promise((resolve) => setTimeout(resolve, settleDelay));
|
|
381
|
+
}, { settleMs, runReadySignal: shouldRunReadySignal });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export async function detectSlideFrame(page, slideMode = DEFAULT_SLIDE_MODE) {
|
|
385
|
+
const metrics = await page.evaluate(() => {
|
|
386
|
+
function toBox(element) {
|
|
387
|
+
const rect = element.getBoundingClientRect();
|
|
388
|
+
return {
|
|
389
|
+
x: Math.max(0, rect.x),
|
|
390
|
+
y: Math.max(0, rect.y),
|
|
391
|
+
width: rect.width,
|
|
392
|
+
height: rect.height,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const body = document.body;
|
|
397
|
+
const bodyStyle = window.getComputedStyle(body);
|
|
398
|
+
const bodyBox = toBox(body);
|
|
399
|
+
const directChildren = Array.from(body.children)
|
|
400
|
+
.map((element) => ({
|
|
401
|
+
tagName: element.tagName.toLowerCase(),
|
|
402
|
+
...toBox(element),
|
|
403
|
+
}))
|
|
404
|
+
.filter((box) => box.width > 0 && box.height > 0);
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
408
|
+
document: {
|
|
409
|
+
scrollWidth: document.documentElement.scrollWidth || bodyBox.width || 0,
|
|
410
|
+
scrollHeight: document.documentElement.scrollHeight || bodyBox.height || 0,
|
|
411
|
+
},
|
|
412
|
+
body: {
|
|
413
|
+
...bodyBox,
|
|
414
|
+
width: Number.parseFloat(bodyStyle.width) || bodyBox.width || 0,
|
|
415
|
+
height: Number.parseFloat(bodyStyle.height) || bodyBox.height || 0,
|
|
416
|
+
scrollWidth: body.scrollWidth || bodyBox.width || 0,
|
|
417
|
+
scrollHeight: body.scrollHeight || bodyBox.height || 0,
|
|
418
|
+
},
|
|
419
|
+
candidates: directChildren,
|
|
420
|
+
};
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const frame = chooseSlideFrame(metrics, slideMode);
|
|
424
|
+
const fallbackSize = getSlideModeConfig(slideMode).framePx;
|
|
425
|
+
return {
|
|
426
|
+
x: normalizeDimension(frame.x, 0),
|
|
427
|
+
y: normalizeDimension(frame.y, 0),
|
|
428
|
+
width: normalizeDimension(frame.width, fallbackSize.width),
|
|
429
|
+
height: normalizeDimension(frame.height, fallbackSize.height),
|
|
430
|
+
candidateIndex: Number.isInteger(frame.candidateIndex) ? frame.candidateIndex : null,
|
|
431
|
+
source: frame.source,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export async function prepareVideosForExport(page, options = {}) {
|
|
436
|
+
const timeoutMs = normalizeDimension(options.timeoutMs ?? VIDEO_EXPORT_PREP_TIMEOUT_MS, VIDEO_EXPORT_PREP_TIMEOUT_MS);
|
|
437
|
+
|
|
438
|
+
await page.evaluate(async ({ timeoutMs: captureTimeoutMs }) => {
|
|
439
|
+
const videos = Array.from(document.querySelectorAll('video'));
|
|
440
|
+
if (videos.length === 0) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function waitForVideoReady(video) {
|
|
445
|
+
if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
446
|
+
return Promise.resolve();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return new Promise((resolve) => {
|
|
450
|
+
const cleanup = () => {
|
|
451
|
+
video.removeEventListener('loadeddata', onReady);
|
|
452
|
+
video.removeEventListener('canplay', onReady);
|
|
453
|
+
video.removeEventListener('error', onReady);
|
|
454
|
+
};
|
|
455
|
+
const onReady = () => {
|
|
456
|
+
cleanup();
|
|
457
|
+
resolve();
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
video.addEventListener('loadeddata', onReady, { once: true });
|
|
461
|
+
video.addEventListener('canplay', onReady, { once: true });
|
|
462
|
+
video.addEventListener('error', onReady, { once: true });
|
|
463
|
+
setTimeout(onReady, captureTimeoutMs);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function captureVideoFrame(video) {
|
|
468
|
+
await waitForVideoReady(video);
|
|
469
|
+
if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA || video.videoWidth <= 0 || video.videoHeight <= 0) {
|
|
470
|
+
return '';
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const canvas = document.createElement('canvas');
|
|
474
|
+
canvas.width = video.videoWidth;
|
|
475
|
+
canvas.height = video.videoHeight;
|
|
476
|
+
const context = canvas.getContext('2d');
|
|
477
|
+
if (!context) {
|
|
478
|
+
return '';
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
483
|
+
return canvas.toDataURL('image/png');
|
|
484
|
+
} catch {
|
|
485
|
+
return '';
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function copyComputedStyle(sourceElement, targetElement) {
|
|
490
|
+
const computed = window.getComputedStyle(sourceElement);
|
|
491
|
+
for (const propertyName of Array.from(computed)) {
|
|
492
|
+
targetElement.style.setProperty(
|
|
493
|
+
propertyName,
|
|
494
|
+
computed.getPropertyValue(propertyName),
|
|
495
|
+
computed.getPropertyPriority(propertyName),
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
return computed;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const replacementImages = [];
|
|
502
|
+
|
|
503
|
+
for (const video of videos) {
|
|
504
|
+
const rect = video.getBoundingClientRect();
|
|
505
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const thumbnail = (video.getAttribute('poster') || '').trim() || await captureVideoFrame(video);
|
|
510
|
+
if (!thumbnail) {
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
video.pause?.();
|
|
515
|
+
const replacement = document.createElement('img');
|
|
516
|
+
replacement.setAttribute('data-slides-grab-video-replacement', 'true');
|
|
517
|
+
replacement.src = thumbnail;
|
|
518
|
+
replacement.alt = video.getAttribute('aria-label') || video.getAttribute('title') || 'Video thumbnail';
|
|
519
|
+
replacement.className = video.className;
|
|
520
|
+
|
|
521
|
+
const computed = copyComputedStyle(video, replacement);
|
|
522
|
+
replacement.style.objectFit = computed.objectFit || 'contain';
|
|
523
|
+
replacement.style.objectPosition = computed.objectPosition || '50% 50%';
|
|
524
|
+
replacement.style.pointerEvents = 'none';
|
|
525
|
+
replacement.style.visibility = 'visible';
|
|
526
|
+
|
|
527
|
+
video.replaceWith(replacement);
|
|
528
|
+
replacementImages.push(replacement);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
await Promise.all(
|
|
532
|
+
replacementImages.map(async (replacement) => {
|
|
533
|
+
if (typeof replacement.decode === 'function') {
|
|
534
|
+
await replacement.decode().catch(() => {});
|
|
535
|
+
}
|
|
536
|
+
}),
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
540
|
+
}, { timeoutMs });
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export async function normalizeBodyToSlideFrame(page, slideFrame) {
|
|
544
|
+
return page.evaluate(({ width, height }) => {
|
|
545
|
+
const body = document.body;
|
|
546
|
+
const documentElement = document.documentElement;
|
|
547
|
+
|
|
548
|
+
body.style.margin = '0';
|
|
549
|
+
body.style.width = `${width}px`;
|
|
550
|
+
body.style.height = `${height}px`;
|
|
551
|
+
body.style.minWidth = `${width}px`;
|
|
552
|
+
body.style.minHeight = `${height}px`;
|
|
553
|
+
body.style.overflow = 'hidden';
|
|
554
|
+
|
|
555
|
+
documentElement.style.margin = '0';
|
|
556
|
+
documentElement.style.padding = '0';
|
|
557
|
+
documentElement.style.width = `${width}px`;
|
|
558
|
+
documentElement.style.height = `${height}px`;
|
|
559
|
+
documentElement.style.minWidth = `${width}px`;
|
|
560
|
+
documentElement.style.minHeight = `${height}px`;
|
|
561
|
+
documentElement.style.overflow = 'hidden';
|
|
562
|
+
}, slideFrame);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export async function isolateSlideFrame(page, slideFrame) {
|
|
566
|
+
return page.evaluate(({ x, y, width, height, source, candidateIndex }) => {
|
|
567
|
+
const body = document.body;
|
|
568
|
+
if (body.querySelector(':scope > [data-slides-grab-print-frame="true"]')) {
|
|
569
|
+
return { x: 0, y: 0, width, height, source: 'body', candidateIndex: null };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const shouldWrapBodyChildren = source === 'body-child' || x !== 0 || y !== 0;
|
|
573
|
+
if (!shouldWrapBodyChildren) {
|
|
574
|
+
return { x, y, width, height, source, candidateIndex: candidateIndex ?? null };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const clipFrame = document.createElement('div');
|
|
578
|
+
clipFrame.setAttribute('data-slides-grab-print-frame', 'true');
|
|
579
|
+
clipFrame.style.position = 'relative';
|
|
580
|
+
clipFrame.style.width = `${width}px`;
|
|
581
|
+
clipFrame.style.height = `${height}px`;
|
|
582
|
+
clipFrame.style.margin = '0';
|
|
583
|
+
clipFrame.style.padding = '0';
|
|
584
|
+
clipFrame.style.overflow = 'hidden';
|
|
585
|
+
clipFrame.style.boxSizing = 'border-box';
|
|
586
|
+
|
|
587
|
+
const translatedContent = document.createElement('div');
|
|
588
|
+
translatedContent.setAttribute('data-slides-grab-print-content', 'true');
|
|
589
|
+
translatedContent.style.position = 'absolute';
|
|
590
|
+
translatedContent.style.left = `${-x}px`;
|
|
591
|
+
translatedContent.style.top = `${-y}px`;
|
|
592
|
+
translatedContent.style.width = `${Math.max(width + x, body.scrollWidth, document.documentElement.scrollWidth)}px`;
|
|
593
|
+
translatedContent.style.height = `${Math.max(height + y, body.scrollHeight, document.documentElement.scrollHeight)}px`;
|
|
594
|
+
|
|
595
|
+
// Preserve the original node order inside one translated subtree so overlap
|
|
596
|
+
// paint order and live DOM state survive both capture and print exports.
|
|
597
|
+
const childNodes = Array.from(body.childNodes);
|
|
598
|
+
body.replaceChildren(clipFrame);
|
|
599
|
+
clipFrame.append(translatedContent);
|
|
600
|
+
for (const node of childNodes) {
|
|
601
|
+
translatedContent.append(node);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return { x: 0, y: 0, width, height, source: 'body', candidateIndex: null };
|
|
605
|
+
}, slideFrame);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function createSlideDiagnostics() {
|
|
609
|
+
const diagnostics = [];
|
|
610
|
+
let currentSlide = null;
|
|
611
|
+
|
|
612
|
+
function push(type, message) {
|
|
613
|
+
diagnostics.push({
|
|
614
|
+
type,
|
|
615
|
+
slideFile: currentSlide,
|
|
616
|
+
message,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return {
|
|
621
|
+
attach(page) {
|
|
622
|
+
page.on('console', (message) => {
|
|
623
|
+
const type = message.type();
|
|
624
|
+
if (type !== 'error' && type !== 'warning') {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const location = message.location();
|
|
629
|
+
const locationLabel = location.url ? ` (${basename(location.url)}:${location.lineNumber ?? 0})` : '';
|
|
630
|
+
push(`console:${type}`, `${type}${locationLabel}: ${message.text()}`);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
page.on('pageerror', (error) => {
|
|
634
|
+
push('pageerror', error instanceof Error ? error.message : String(error));
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
page.on('requestfailed', (request) => {
|
|
638
|
+
const failure = request.failure();
|
|
639
|
+
push(
|
|
640
|
+
'requestfailed',
|
|
641
|
+
`request failed: ${request.url()}${failure?.errorText ? ` (${failure.errorText})` : ''}`,
|
|
642
|
+
);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
page.on('response', (response) => {
|
|
646
|
+
if (response.status() >= 400) {
|
|
647
|
+
push('response', `HTTP ${response.status()}: ${response.url()}`);
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
},
|
|
651
|
+
beginSlide(slideFile) {
|
|
652
|
+
currentSlide = slideFile;
|
|
653
|
+
},
|
|
654
|
+
endSlide() {
|
|
655
|
+
currentSlide = null;
|
|
656
|
+
},
|
|
657
|
+
getSlideDiagnostics(slideFile) {
|
|
658
|
+
return diagnostics.filter((entry) => entry.slideFile === slideFile);
|
|
659
|
+
},
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
export async function renderSlideToPdf(page, slideFile, slidesDir, options = {}) {
|
|
664
|
+
const slidePath = join(slidesDir, slideFile);
|
|
665
|
+
const slideUrl = pathToFileURL(slidePath).href;
|
|
666
|
+
const mode = normalizeMode(options.mode ?? DEFAULT_MODE);
|
|
667
|
+
const slideMode = normalizeSlideMode(options.slideMode ?? DEFAULT_SLIDE_MODE, { optionName: '--slide-mode' });
|
|
668
|
+
const captureResolution = mode === 'capture' ? normalizeResolutionPreset(options.resolution ?? '') : '';
|
|
669
|
+
|
|
670
|
+
await page.goto(slideUrl, { waitUntil: 'load' });
|
|
671
|
+
await waitForSlideRenderReady(page, options);
|
|
672
|
+
|
|
673
|
+
const slideFrame = await detectSlideFrame(page, slideMode);
|
|
674
|
+
const normalizedSlideFrame = await isolateSlideFrame(page, slideFrame);
|
|
675
|
+
await normalizeBodyToSlideFrame(page, normalizedSlideFrame);
|
|
676
|
+
await waitForSlideRenderReady(page, { ...options, runReadySignal: false });
|
|
677
|
+
|
|
678
|
+
if (mode === 'capture') {
|
|
679
|
+
const fallbackSize = getSlideModeConfig(slideMode).framePx;
|
|
680
|
+
const viewportSize = {
|
|
681
|
+
width: normalizeDimension(normalizedSlideFrame.width, fallbackSize.width),
|
|
682
|
+
height: normalizeDimension(normalizedSlideFrame.height, fallbackSize.height),
|
|
683
|
+
};
|
|
684
|
+
await page.setViewportSize(viewportSize);
|
|
685
|
+
await waitForSlideRenderReady(page, { ...options, runReadySignal: false });
|
|
686
|
+
// Reset autoplay videos to show their poster instead of a mid-play frame,
|
|
687
|
+
// without replacing the <video> element (which would break slide layout).
|
|
688
|
+
await page.evaluate(() => {
|
|
689
|
+
for (const video of document.querySelectorAll('video')) {
|
|
690
|
+
video.pause();
|
|
691
|
+
video.removeAttribute('autoplay');
|
|
692
|
+
if (video.getAttribute('poster')) {
|
|
693
|
+
video.load();
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
await waitForSlideRenderReady(page, { ...options, runReadySignal: false, settleMs: 200 });
|
|
698
|
+
const pngBytes = await page.screenshot({
|
|
699
|
+
type: 'png',
|
|
700
|
+
clip: {
|
|
701
|
+
x: 0,
|
|
702
|
+
y: 0,
|
|
703
|
+
width: viewportSize.width,
|
|
704
|
+
height: viewportSize.height,
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
const normalizedPngBytes = await normalizeCaptureRasterSize(pngBytes, captureResolution, slideMode);
|
|
708
|
+
return {
|
|
709
|
+
mode,
|
|
710
|
+
slideMode,
|
|
711
|
+
width: normalizedSlideFrame.width,
|
|
712
|
+
height: normalizedSlideFrame.height,
|
|
713
|
+
pngBytes: normalizedPngBytes,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
await prepareVideosForExport(page);
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
mode,
|
|
721
|
+
slideMode,
|
|
722
|
+
width: normalizedSlideFrame.width,
|
|
723
|
+
height: normalizedSlideFrame.height,
|
|
724
|
+
pdfBytes: await page.pdf(buildPdfOptions(normalizedSlideFrame.width, normalizedSlideFrame.height)),
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
export async function mergePdfBuffers(pdfBuffers) {
|
|
729
|
+
const outputPdf = await PDFDocument.create();
|
|
730
|
+
|
|
731
|
+
for (const pdfBuffer of pdfBuffers) {
|
|
732
|
+
const sourcePdf = await PDFDocument.load(pdfBuffer);
|
|
733
|
+
const pageIndices = sourcePdf.getPageIndices();
|
|
734
|
+
const pages = await outputPdf.copyPages(sourcePdf, pageIndices);
|
|
735
|
+
for (const page of pages) {
|
|
736
|
+
outputPdf.addPage(page);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return outputPdf.save();
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export async function buildCapturePdf(slides) {
|
|
744
|
+
const outputPdf = await PDFDocument.create();
|
|
745
|
+
|
|
746
|
+
for (const slide of slides) {
|
|
747
|
+
const pageWidth = cssPixelsToPdfPoints(slide.width);
|
|
748
|
+
const pageHeight = cssPixelsToPdfPoints(slide.height);
|
|
749
|
+
const page = outputPdf.addPage([pageWidth, pageHeight]);
|
|
750
|
+
const image = await outputPdf.embedPng(slide.pngBytes);
|
|
751
|
+
page.drawImage(image, {
|
|
752
|
+
x: 0,
|
|
753
|
+
y: 0,
|
|
754
|
+
width: pageWidth,
|
|
755
|
+
height: pageHeight,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return outputPdf.save();
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function main() {
|
|
763
|
+
const options = parseCliArgs(process.argv.slice(2));
|
|
764
|
+
if (options.help) {
|
|
765
|
+
printUsage();
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const slidesDir = resolve(process.cwd(), options.slidesDir);
|
|
770
|
+
await ensureSlidesPassValidation(slidesDir, { exportLabel: 'PDF export', slideMode: options.slideMode });
|
|
771
|
+
const slideFiles = await findSlideFiles(slidesDir);
|
|
772
|
+
if (slideFiles.length === 0) {
|
|
773
|
+
throw new Error(`No slide-*.html files found in: ${slidesDir}`);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const browser = await chromium.launch({ headless: true });
|
|
777
|
+
const page = await browser.newPage(buildPageOptions(options.mode, options.resolution, options.slideMode));
|
|
778
|
+
const diagnostics = createSlideDiagnostics();
|
|
779
|
+
diagnostics.attach(page);
|
|
780
|
+
const renderedSlides = [];
|
|
781
|
+
|
|
782
|
+
try {
|
|
783
|
+
for (const slideFile of slideFiles) {
|
|
784
|
+
diagnostics.beginSlide(slideFile);
|
|
785
|
+
try {
|
|
786
|
+
const slideResult = await renderSlideToPdf(page, slideFile, slidesDir, {
|
|
787
|
+
mode: options.mode,
|
|
788
|
+
slideMode: options.slideMode,
|
|
789
|
+
resolution: options.resolution,
|
|
790
|
+
});
|
|
791
|
+
renderedSlides.push(slideResult);
|
|
792
|
+
} catch (error) {
|
|
793
|
+
throw decorateError(error, slideFile, diagnostics.getSlideDiagnostics(slideFile));
|
|
794
|
+
} finally {
|
|
795
|
+
const slideDiagnostics = diagnostics.getSlideDiagnostics(slideFile);
|
|
796
|
+
if (slideDiagnostics.length > 0) {
|
|
797
|
+
process.stderr.write(`[slides-grab] Diagnostics for ${slideFile}:\n${formatDiagnostics(slideFile, slideDiagnostics)}\n`);
|
|
798
|
+
}
|
|
799
|
+
diagnostics.endSlide();
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
} finally {
|
|
803
|
+
await browser.close();
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const mergedPdf =
|
|
807
|
+
options.mode === 'capture'
|
|
808
|
+
? await buildCapturePdf(renderedSlides)
|
|
809
|
+
: await mergePdfBuffers(renderedSlides.map((slide) => slide.pdfBytes));
|
|
810
|
+
|
|
811
|
+
const outputPath = resolve(process.cwd(), options.output);
|
|
812
|
+
await writeFile(outputPath, mergedPdf);
|
|
813
|
+
|
|
814
|
+
process.stdout.write(`Generated PDF (${options.mode} mode): ${outputPath}\n`);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
818
|
+
main().catch((error) => {
|
|
819
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
820
|
+
process.exit(1);
|
|
821
|
+
});
|
|
822
|
+
}
|