slides-grab 1.1.2 → 1.1.4
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/README.md +25 -8
- package/bin/ppt-agent.js +8 -12
- package/convert.cjs +8 -206
- package/package.json +2 -4
- package/scripts/html2pdf.js +62 -5
- package/skills/slides-grab/SKILL.md +8 -5
- package/skills/slides-grab/references/presentation-workflow-reference.md +52 -0
- package/skills/slides-grab-design/SKILL.md +5 -2
- package/skills/slides-grab-design/references/design-rules.md +49 -0
- package/skills/slides-grab-design/references/design-system-full.md +572 -0
- package/skills/slides-grab-design/references/detailed-design-rules.md +35 -0
- package/skills/slides-grab-export/SKILL.md +10 -5
- package/skills/slides-grab-export/references/export-rules.md +22 -0
- package/skills/slides-grab-export/references/html2pptx.md +625 -0
- package/skills/slides-grab-export/references/ooxml.md +427 -0
- package/skills/slides-grab-export/references/pptx-skill-reference.md +223 -0
- package/skills/slides-grab-plan/SKILL.md +5 -2
- 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/editor/codex-edit.js +2 -2
- package/src/export-resolution.cjs +58 -0
- package/src/pptx-raster-export.cjs +238 -0
- package/scripts/install-codex-skills.js +0 -119
package/README.md
CHANGED
|
@@ -31,16 +31,16 @@ Paste one of these into your coding agent:
|
|
|
31
31
|
**Claude Code:**
|
|
32
32
|
|
|
33
33
|
```
|
|
34
|
-
Read https://raw.githubusercontent.com/vkehfdl1/slides-grab/main/docs/
|
|
34
|
+
Read https://raw.githubusercontent.com/vkehfdl1/slides-grab/main/docs/installation/claude.md and follow every step.
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
**Codex:**
|
|
38
38
|
|
|
39
39
|
```
|
|
40
|
-
Read https://raw.githubusercontent.com/vkehfdl1/slides-grab/main/docs/
|
|
40
|
+
Read https://raw.githubusercontent.com/vkehfdl1/slides-grab/main/docs/installation/codex.md and follow every step.
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
Or
|
|
43
|
+
Or use the repo directly if you want to develop on slides-grab itself:
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
46
|
git clone https://github.com/vkehfdl1/slides-grab.git && cd slides-grab
|
|
@@ -49,6 +49,14 @@ npm ci && npx playwright install chromium
|
|
|
49
49
|
|
|
50
50
|
> Requires **Node.js >= 18**.
|
|
51
51
|
|
|
52
|
+
### No-clone install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install slides-grab
|
|
56
|
+
npx playwright install chromium
|
|
57
|
+
npx skills add ./node_modules/slides-grab -g -a codex -a claude-code --yes --copy
|
|
58
|
+
```
|
|
59
|
+
|
|
52
60
|
## Why This Project?
|
|
53
61
|
|
|
54
62
|
There are many AI tools that generate slide HTML. Almost none let you **visually point at what you want changed** and iterate in-place. slides-grab fills that gap:
|
|
@@ -69,8 +77,10 @@ slides-grab edit # Launch visual slide editor
|
|
|
69
77
|
slides-grab build-viewer # Build single-file viewer.html
|
|
70
78
|
slides-grab validate # Validate slide HTML (Playwright-based)
|
|
71
79
|
slides-grab convert # Export to experimental / unstable PPTX
|
|
80
|
+
slides-grab convert --resolution 2160p # Higher-resolution raster PPTX export
|
|
72
81
|
slides-grab figma # Export an experimental / unstable Figma Slides importable PPTX
|
|
73
82
|
slides-grab pdf # Export PDF in capture mode (default)
|
|
83
|
+
slides-grab pdf --resolution 2160p # Higher-resolution image-backed PDF export
|
|
74
84
|
slides-grab pdf --mode print # Export searchable/selectable text PDF
|
|
75
85
|
slides-grab list-templates # Show available slide templates
|
|
76
86
|
slides-grab list-themes # Show available color themes
|
|
@@ -89,6 +99,8 @@ Run `slides-grab validate --slides-dir <path>` before export to catch missing lo
|
|
|
89
99
|
|
|
90
100
|
`slides-grab pdf` now defaults to `--mode capture`, which rasterizes each rendered slide into the PDF for better visual fidelity. Use `--mode print` when searchable/selectable browser text matters more than pixel-perfect parity.
|
|
91
101
|
|
|
102
|
+
`slides-grab pdf` and `slides-grab convert` now default to `2160p` / `4k` raster output for sharper exports. You can still override with `--resolution <preset>` using `720p`, `1080p`, `1440p`, `2160p`, or `4k` when you want smaller or faster artifacts.
|
|
103
|
+
|
|
92
104
|
### Multi-Deck Workflow
|
|
93
105
|
|
|
94
106
|
Prerequisite: create or generate a deck in `decks/my-deck/` first.
|
|
@@ -114,19 +126,25 @@ This command reuses the HTML to PPTX pipeline and emits a `.pptx` deck intended
|
|
|
114
126
|
|
|
115
127
|
## Installation Guides
|
|
116
128
|
|
|
117
|
-
- [Claude Code setup](docs/prompts/setup-claude.md)
|
|
118
|
-
- [Codex setup](docs/prompts/setup-codex.md)
|
|
119
129
|
- [Claude detailed guide](docs/installation/claude.md)
|
|
120
130
|
- [Codex detailed guide](docs/installation/codex.md)
|
|
121
131
|
|
|
122
132
|
## npm Package
|
|
123
133
|
|
|
124
|
-
Also available as an npm package for standalone CLI usage:
|
|
134
|
+
Also available as an npm package for standalone CLI + skill usage:
|
|
125
135
|
|
|
126
136
|
```bash
|
|
127
137
|
npm install slides-grab
|
|
128
138
|
```
|
|
129
139
|
|
|
140
|
+
Install shared agent skills with Vercel Agent Skills:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npx skills add ./node_modules/slides-grab -g -a codex -a claude-code --yes --copy
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
This npm-install path is enough for normal usage. Clone the repo only when you want to modify or contribute to `slides-grab` itself.
|
|
147
|
+
|
|
130
148
|
## Project Structure
|
|
131
149
|
|
|
132
150
|
```
|
|
@@ -135,8 +153,7 @@ src/editor/ Visual editor (HTML + JS client modules)
|
|
|
135
153
|
scripts/ Build, validate, convert, editor server
|
|
136
154
|
templates/ Slide HTML templates (cover, content, chart, ...)
|
|
137
155
|
themes/ Color themes (modern-dark, executive, sage, ...)
|
|
138
|
-
|
|
139
|
-
skills/ Codex skill definitions
|
|
156
|
+
skills/ Shared Vercel-installable agent skills + references
|
|
140
157
|
docs/ Installation & usage guides
|
|
141
158
|
```
|
|
142
159
|
|
package/bin/ppt-agent.js
CHANGED
|
@@ -106,11 +106,15 @@ program
|
|
|
106
106
|
.description('Convert slide HTML files to experimental / unstable PPTX')
|
|
107
107
|
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
108
108
|
.option('--output <path>', 'Output PPTX file')
|
|
109
|
+
.option('--resolution <preset>', 'Raster size preset: 720p, 1080p, 1440p, 2160p, or 4k (default: 2160p)')
|
|
109
110
|
.action(async (options = {}) => {
|
|
110
111
|
const args = ['--slides-dir', options.slidesDir];
|
|
111
112
|
if (options.output) {
|
|
112
113
|
args.push('--output', String(options.output));
|
|
113
114
|
}
|
|
115
|
+
if (options.resolution) {
|
|
116
|
+
args.push('--resolution', String(options.resolution));
|
|
117
|
+
}
|
|
114
118
|
await runCommand('convert.cjs', args);
|
|
115
119
|
});
|
|
116
120
|
|
|
@@ -120,6 +124,7 @@ program
|
|
|
120
124
|
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
121
125
|
.option('--output <path>', 'Output PDF file')
|
|
122
126
|
.option('--mode <mode>', 'PDF export mode: capture for visual fidelity, print for searchable text', 'capture')
|
|
127
|
+
.option('--resolution <preset>', 'Capture raster size preset: 720p, 1080p, 1440p, 2160p, or 4k (default: 2160p in capture mode)')
|
|
123
128
|
.action(async (options = {}) => {
|
|
124
129
|
const args = ['--slides-dir', options.slidesDir];
|
|
125
130
|
if (options.output) {
|
|
@@ -128,6 +133,9 @@ program
|
|
|
128
133
|
if (options.mode) {
|
|
129
134
|
args.push('--mode', String(options.mode));
|
|
130
135
|
}
|
|
136
|
+
if (options.resolution) {
|
|
137
|
+
args.push('--resolution', String(options.resolution));
|
|
138
|
+
}
|
|
131
139
|
await runCommand('scripts/html2pdf.js', args);
|
|
132
140
|
});
|
|
133
141
|
|
|
@@ -159,18 +167,6 @@ program
|
|
|
159
167
|
await runCommand('scripts/editor-server.js', args);
|
|
160
168
|
});
|
|
161
169
|
|
|
162
|
-
program
|
|
163
|
-
.command('install-codex-skills')
|
|
164
|
-
.description('Install project Codex skills into $CODEX_HOME/skills (default: ~/.codex/skills)')
|
|
165
|
-
.option('--force', 'Overwrite existing skill directories')
|
|
166
|
-
.option('--dry-run', 'Preview what would be installed')
|
|
167
|
-
.action(async (options = {}) => {
|
|
168
|
-
const args = [];
|
|
169
|
-
if (options.force) args.push('--force');
|
|
170
|
-
if (options.dryRun) args.push('--dry-run');
|
|
171
|
-
await runCommand('scripts/install-codex-skills.js', args);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
170
|
// --- Template/theme discovery commands ---
|
|
175
171
|
|
|
176
172
|
program
|
package/convert.cjs
CHANGED
|
@@ -1,211 +1,12 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// Inline a simplified version that uses Playwright Chromium (not Chrome)
|
|
8
|
-
const DEFAULT_SLIDES_DIR = 'slides';
|
|
9
|
-
const DEFAULT_OUTPUT = 'output.pptx';
|
|
10
|
-
const DEFAULT_CAPTURE_VIEWPORT = { width: 960, height: 540 };
|
|
11
|
-
const DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR = 2;
|
|
12
|
-
const TARGET_RASTER_DPI = 150;
|
|
13
|
-
const TARGET_SLIDE_SIZE_IN = { width: 13.33, height: 7.5 };
|
|
14
|
-
|
|
15
|
-
function normalizeDimension(value, fallback) {
|
|
16
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
17
|
-
return fallback;
|
|
18
|
-
}
|
|
19
|
-
return Math.max(1, Math.round(value));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function buildPageOptions() {
|
|
23
|
-
return {
|
|
24
|
-
viewport: {
|
|
25
|
-
width: DEFAULT_CAPTURE_VIEWPORT.width,
|
|
26
|
-
height: DEFAULT_CAPTURE_VIEWPORT.height,
|
|
27
|
-
},
|
|
28
|
-
deviceScaleFactor: DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function getTargetRasterSize() {
|
|
33
|
-
return {
|
|
34
|
-
width: Math.round(TARGET_SLIDE_SIZE_IN.width * TARGET_RASTER_DPI),
|
|
35
|
-
height: Math.round(TARGET_SLIDE_SIZE_IN.height * TARGET_RASTER_DPI),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function printUsage() {
|
|
40
|
-
process.stdout.write(
|
|
41
|
-
[
|
|
42
|
-
'Usage: node convert.cjs [options]',
|
|
43
|
-
'',
|
|
44
|
-
'Options:',
|
|
45
|
-
` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
|
|
46
|
-
` --output <path> Output pptx path (default: ${DEFAULT_OUTPUT})`,
|
|
47
|
-
' -h, --help Show this help message',
|
|
48
|
-
].join('\n'),
|
|
49
|
-
);
|
|
50
|
-
process.stdout.write('\n');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function readOptionValue(args, index, optionName) {
|
|
54
|
-
const next = args[index + 1];
|
|
55
|
-
if (!next || next.startsWith('-')) {
|
|
56
|
-
throw new Error(`Missing value for ${optionName}.`);
|
|
57
|
-
}
|
|
58
|
-
return next;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function parseArgs(args) {
|
|
62
|
-
const options = {
|
|
63
|
-
slidesDir: DEFAULT_SLIDES_DIR,
|
|
64
|
-
output: DEFAULT_OUTPUT,
|
|
65
|
-
help: false,
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
69
|
-
const arg = args[i];
|
|
70
|
-
if (arg === '-h' || arg === '--help') {
|
|
71
|
-
options.help = true;
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (arg === '--slides-dir') {
|
|
76
|
-
options.slidesDir = readOptionValue(args, i, '--slides-dir');
|
|
77
|
-
i += 1;
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (arg.startsWith('--slides-dir=')) {
|
|
82
|
-
options.slidesDir = arg.slice('--slides-dir='.length);
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (arg === '--output') {
|
|
87
|
-
options.output = readOptionValue(args, i, '--output');
|
|
88
|
-
i += 1;
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (arg.startsWith('--output=')) {
|
|
93
|
-
options.output = arg.slice('--output='.length);
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
throw new Error(`Unknown option: ${arg}`);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (typeof options.slidesDir !== 'string' || options.slidesDir.trim() === '') {
|
|
101
|
-
throw new Error('--slides-dir must be a non-empty string.');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (typeof options.output !== 'string' || options.output.trim() === '') {
|
|
105
|
-
throw new Error('--output must be a non-empty string.');
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
options.slidesDir = options.slidesDir.trim();
|
|
109
|
-
options.output = options.output.trim();
|
|
110
|
-
return options;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function convertSlide(htmlFile, pres, browser) {
|
|
114
|
-
const filePath = path.isAbsolute(htmlFile) ? htmlFile : path.join(process.cwd(), htmlFile);
|
|
115
|
-
|
|
116
|
-
const page = await browser.newPage(buildPageOptions());
|
|
117
|
-
await page.goto(`file://${filePath}`);
|
|
118
|
-
|
|
119
|
-
const bodyDimensions = await page.evaluate(() => {
|
|
120
|
-
const body = document.body;
|
|
121
|
-
const style = window.getComputedStyle(body);
|
|
122
|
-
return {
|
|
123
|
-
width: parseFloat(style.width),
|
|
124
|
-
height: parseFloat(style.height),
|
|
125
|
-
};
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
await page.setViewportSize({
|
|
129
|
-
width: normalizeDimension(bodyDimensions.width, DEFAULT_CAPTURE_VIEWPORT.width),
|
|
130
|
-
height: normalizeDimension(bodyDimensions.height, DEFAULT_CAPTURE_VIEWPORT.height)
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// Take screenshot and add as full-slide image
|
|
134
|
-
const screenshot = await page.screenshot({ type: 'png' });
|
|
135
|
-
await page.close();
|
|
136
|
-
|
|
137
|
-
// Resize to exact slide dimensions (13.33" x 7.5" at 150 DPI)
|
|
138
|
-
const targetSize = getTargetRasterSize();
|
|
139
|
-
|
|
140
|
-
const resized = await sharp(screenshot)
|
|
141
|
-
.resize(targetSize.width, targetSize.height, { fit: 'fill' })
|
|
142
|
-
.png()
|
|
143
|
-
.toBuffer();
|
|
144
|
-
|
|
145
|
-
const tmpPath = path.join(process.env.TMPDIR || '/tmp', `slide-${Date.now()}-${Math.random().toString(36).slice(2)}.png`);
|
|
146
|
-
fs.writeFileSync(tmpPath, resized);
|
|
147
|
-
|
|
148
|
-
const slide = pres.addSlide();
|
|
149
|
-
slide.addImage({
|
|
150
|
-
path: tmpPath,
|
|
151
|
-
x: 0,
|
|
152
|
-
y: 0,
|
|
153
|
-
w: '100%',
|
|
154
|
-
h: '100%'
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
return tmpPath;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async function main() {
|
|
161
|
-
const options = parseArgs(process.argv.slice(2));
|
|
162
|
-
if (options.help) {
|
|
163
|
-
printUsage();
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const pres = new PptxGenJS();
|
|
168
|
-
pres.layout = 'LAYOUT_WIDE'; // 16:9
|
|
169
|
-
|
|
170
|
-
const slidesDir = path.resolve(process.cwd(), options.slidesDir);
|
|
171
|
-
const { ensureSlidesPassValidation } = await import('./scripts/validate-slides.js');
|
|
172
|
-
await ensureSlidesPassValidation(slidesDir, { exportLabel: 'PPTX export' });
|
|
173
|
-
const files = fs.readdirSync(slidesDir)
|
|
174
|
-
.filter(f => f.endsWith('.html'))
|
|
175
|
-
.sort();
|
|
176
|
-
|
|
177
|
-
console.log(`Converting ${files.length} slides...`);
|
|
178
|
-
|
|
179
|
-
// Launch Chromium (not Chrome)
|
|
180
|
-
const browser = await chromium.launch();
|
|
181
|
-
const tmpFiles = [];
|
|
182
|
-
|
|
183
|
-
for (const file of files) {
|
|
184
|
-
const filePath = path.join(slidesDir, file);
|
|
185
|
-
console.log(` Processing: ${file}`);
|
|
186
|
-
try {
|
|
187
|
-
const tmpPath = await convertSlide(filePath, pres, browser);
|
|
188
|
-
tmpFiles.push(tmpPath);
|
|
189
|
-
console.log(` ✓ ${file} done`);
|
|
190
|
-
} catch (err) {
|
|
191
|
-
console.error(` ✗ ${file} error: ${err.message}`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
await browser.close();
|
|
196
|
-
|
|
197
|
-
const outputFile = path.resolve(process.cwd(), options.output);
|
|
198
|
-
await pres.writeFile({ fileName: outputFile });
|
|
199
|
-
console.log(`\nSaved: ${outputFile}`);
|
|
200
|
-
|
|
201
|
-
// Cleanup tmp files
|
|
202
|
-
for (const f of tmpFiles) {
|
|
203
|
-
try { fs.unlinkSync(f); } catch {}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
1
|
+
const {
|
|
2
|
+
buildPageOptions,
|
|
3
|
+
getTargetRasterSize,
|
|
4
|
+
main,
|
|
5
|
+
parseArgs,
|
|
6
|
+
} = require('./src/pptx-raster-export.cjs');
|
|
206
7
|
|
|
207
8
|
if (require.main === module) {
|
|
208
|
-
main().catch(err => {
|
|
9
|
+
main().catch((err) => {
|
|
209
10
|
console.error('Fatal error:', err);
|
|
210
11
|
process.exit(1);
|
|
211
12
|
});
|
|
@@ -214,5 +15,6 @@ if (require.main === module) {
|
|
|
214
15
|
module.exports = {
|
|
215
16
|
buildPageOptions,
|
|
216
17
|
getTargetRasterSize,
|
|
18
|
+
main,
|
|
217
19
|
parseArgs,
|
|
218
20
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slides-grab",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "Agent-first presentation framework — plan, design, and visually edit HTML slides with Claude Code or Codex, then export to PDF or experimental/unstable PPTX/Figma formats",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "vkehfdl1",
|
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
"scripts/figma-export.js",
|
|
39
39
|
"scripts/html2pdf.js",
|
|
40
40
|
"scripts/html2pptx.js",
|
|
41
|
-
"scripts/install-codex-skills.js",
|
|
42
41
|
"scripts/validate-slides.js",
|
|
43
42
|
"skills/",
|
|
44
43
|
"src/",
|
|
@@ -52,8 +51,7 @@
|
|
|
52
51
|
"build-viewer": "node scripts/build-viewer.js",
|
|
53
52
|
"validate": "node scripts/validate-slides.js",
|
|
54
53
|
"convert": "node convert.cjs",
|
|
55
|
-
"
|
|
56
|
-
"test": "node --test tests/editor/editor-codex-edit.test.js tests/pdf/html2pdf.test.js tests/pdf/html2pdf.e2e.test.js tests/figma/figma-export.test.js tests/image-contract/image-contract.test.js tests/validation/validate-slides.test.js",
|
|
54
|
+
"test": "node --test tests/editor/editor-codex-edit.test.js tests/pdf/html2pdf.test.js tests/pdf/html2pdf.e2e.test.js tests/figma/figma-export.test.js tests/image-contract/image-contract.test.js tests/validation/validate-slides.test.js tests/skills/installable-skills.test.js",
|
|
57
55
|
"test:e2e": "node --test tests/editor/editor-ui.e2e.test.js tests/editor/editor-concurrency.e2e.test.js"
|
|
58
56
|
},
|
|
59
57
|
"dependencies": {
|
package/scripts/html2pdf.js
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { readdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
4
5
|
import { basename, join, resolve } from 'node:path';
|
|
5
6
|
import { pathToFileURL } from 'node:url';
|
|
6
7
|
import { chromium } from 'playwright';
|
|
7
8
|
import { PDFDocument } from 'pdf-lib';
|
|
9
|
+
import sharp from 'sharp';
|
|
8
10
|
|
|
9
11
|
import { ensureSlidesPassValidation } from './validate-slides.js';
|
|
10
12
|
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const {
|
|
15
|
+
getResolutionChoices,
|
|
16
|
+
getResolutionSize,
|
|
17
|
+
normalizeResolutionPreset,
|
|
18
|
+
} = require('../src/export-resolution.cjs');
|
|
19
|
+
|
|
11
20
|
const DEFAULT_OUTPUT = 'slides.pdf';
|
|
12
21
|
const DEFAULT_SLIDES_DIR = 'slides';
|
|
13
22
|
const DEFAULT_MODE = 'capture';
|
|
23
|
+
const DEFAULT_CAPTURE_RESOLUTION = '2160p';
|
|
14
24
|
const PDF_MODES = new Set(['capture', 'print']);
|
|
15
25
|
const SLIDE_FILE_PATTERN = /^slide-.*\.html$/i;
|
|
16
26
|
const FALLBACK_SLIDE_SIZE = { width: 960, height: 540 };
|
|
@@ -29,12 +39,14 @@ function printUsage() {
|
|
|
29
39
|
` --output <path> Output PDF path (default: ${DEFAULT_OUTPUT})`,
|
|
30
40
|
` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
|
|
31
41
|
` --mode <mode> PDF export mode: capture|print (default: ${DEFAULT_MODE})`,
|
|
42
|
+
` --resolution <preset> Capture raster size preset: ${getResolutionChoices().join('|')}|4k (default: ${DEFAULT_CAPTURE_RESOLUTION}; ignored in print mode)`,
|
|
32
43
|
' -h, --help Show this help message',
|
|
33
44
|
'',
|
|
34
45
|
'Examples:',
|
|
35
46
|
' node scripts/html2pdf.js',
|
|
36
47
|
' node scripts/html2pdf.js --output dist/deck.pdf',
|
|
37
48
|
' node scripts/html2pdf.js --mode print --output dist/searchable.pdf',
|
|
49
|
+
' node scripts/html2pdf.js --resolution 2160p --output dist/deck-4k.pdf',
|
|
38
50
|
].join('\n'),
|
|
39
51
|
);
|
|
40
52
|
process.stdout.write('\n');
|
|
@@ -76,6 +88,25 @@ function cssPixelsToPdfPoints(value) {
|
|
|
76
88
|
return Math.round((normalizeDimension(value, 0) * PDF_POINTS_PER_INCH) / CSS_PIXELS_PER_INCH);
|
|
77
89
|
}
|
|
78
90
|
|
|
91
|
+
async function normalizeCaptureRasterSize(pngBytes, resolution = '') {
|
|
92
|
+
const targetSize = getResolutionSize(resolution);
|
|
93
|
+
if (!targetSize) {
|
|
94
|
+
return pngBytes;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const metadata = await sharp(pngBytes).metadata();
|
|
98
|
+
const currentWidth = normalizeDimension(metadata.width, targetSize.width);
|
|
99
|
+
const currentHeight = normalizeDimension(metadata.height, targetSize.height);
|
|
100
|
+
if (currentWidth === targetSize.width && currentHeight === targetSize.height) {
|
|
101
|
+
return pngBytes;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return sharp(pngBytes)
|
|
105
|
+
.resize(targetSize.width, targetSize.height, { fit: 'fill' })
|
|
106
|
+
.png()
|
|
107
|
+
.toBuffer();
|
|
108
|
+
}
|
|
109
|
+
|
|
79
110
|
function formatDiagnosticEntry(entry) {
|
|
80
111
|
const prefix = entry.slideFile ? `${entry.slideFile}: ` : '';
|
|
81
112
|
return `${prefix}${entry.message}`;
|
|
@@ -108,6 +139,7 @@ export function parseCliArgs(args) {
|
|
|
108
139
|
output: DEFAULT_OUTPUT,
|
|
109
140
|
slidesDir: DEFAULT_SLIDES_DIR,
|
|
110
141
|
mode: DEFAULT_MODE,
|
|
142
|
+
resolution: DEFAULT_CAPTURE_RESOLUTION,
|
|
111
143
|
help: false,
|
|
112
144
|
};
|
|
113
145
|
|
|
@@ -152,6 +184,17 @@ export function parseCliArgs(args) {
|
|
|
152
184
|
continue;
|
|
153
185
|
}
|
|
154
186
|
|
|
187
|
+
if (arg === '--resolution') {
|
|
188
|
+
options.resolution = normalizeResolutionPreset(readOptionValue(args, i, '--resolution'));
|
|
189
|
+
i += 1;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (arg.startsWith('--resolution=')) {
|
|
194
|
+
options.resolution = normalizeResolutionPreset(arg.slice('--resolution='.length));
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
155
198
|
throw new Error(`Unknown option: ${arg}`);
|
|
156
199
|
}
|
|
157
200
|
|
|
@@ -165,6 +208,10 @@ export function parseCliArgs(args) {
|
|
|
165
208
|
options.output = options.output.trim();
|
|
166
209
|
options.slidesDir = options.slidesDir.trim();
|
|
167
210
|
options.mode = normalizeMode(options.mode);
|
|
211
|
+
options.resolution = normalizeResolutionPreset(options.resolution);
|
|
212
|
+
if (options.mode === 'print') {
|
|
213
|
+
options.resolution = '';
|
|
214
|
+
}
|
|
168
215
|
|
|
169
216
|
return options;
|
|
170
217
|
}
|
|
@@ -188,13 +235,18 @@ export function buildPdfOptions(widthPx, heightPx) {
|
|
|
188
235
|
};
|
|
189
236
|
}
|
|
190
237
|
|
|
191
|
-
export function buildPageOptions(mode = DEFAULT_MODE) {
|
|
238
|
+
export function buildPageOptions(mode = DEFAULT_MODE, resolution = '') {
|
|
239
|
+
const targetResolution = normalizeMode(mode) === 'capture' ? getResolutionSize(resolution) : null;
|
|
192
240
|
return {
|
|
193
241
|
viewport: {
|
|
194
242
|
width: FALLBACK_SLIDE_SIZE.width,
|
|
195
243
|
height: FALLBACK_SLIDE_SIZE.height,
|
|
196
244
|
},
|
|
197
|
-
deviceScaleFactor: normalizeMode(mode) === 'capture'
|
|
245
|
+
deviceScaleFactor: normalizeMode(mode) === 'capture'
|
|
246
|
+
? targetResolution
|
|
247
|
+
? targetResolution.height / FALLBACK_SLIDE_SIZE.height
|
|
248
|
+
: DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR
|
|
249
|
+
: 1,
|
|
198
250
|
};
|
|
199
251
|
}
|
|
200
252
|
|
|
@@ -470,6 +522,7 @@ export async function renderSlideToPdf(page, slideFile, slidesDir, options = {})
|
|
|
470
522
|
const slidePath = join(slidesDir, slideFile);
|
|
471
523
|
const slideUrl = pathToFileURL(slidePath).href;
|
|
472
524
|
const mode = normalizeMode(options.mode ?? DEFAULT_MODE);
|
|
525
|
+
const captureResolution = mode === 'capture' ? normalizeResolutionPreset(options.resolution ?? '') : '';
|
|
473
526
|
|
|
474
527
|
await page.goto(slideUrl, { waitUntil: 'load' });
|
|
475
528
|
await waitForSlideRenderReady(page, options);
|
|
@@ -495,11 +548,12 @@ export async function renderSlideToPdf(page, slideFile, slidesDir, options = {})
|
|
|
495
548
|
height: viewportSize.height,
|
|
496
549
|
},
|
|
497
550
|
});
|
|
551
|
+
const normalizedPngBytes = await normalizeCaptureRasterSize(pngBytes, captureResolution);
|
|
498
552
|
return {
|
|
499
553
|
mode,
|
|
500
554
|
width: normalizedSlideFrame.width,
|
|
501
555
|
height: normalizedSlideFrame.height,
|
|
502
|
-
pngBytes,
|
|
556
|
+
pngBytes: normalizedPngBytes,
|
|
503
557
|
};
|
|
504
558
|
}
|
|
505
559
|
|
|
@@ -560,7 +614,7 @@ async function main() {
|
|
|
560
614
|
}
|
|
561
615
|
|
|
562
616
|
const browser = await chromium.launch({ headless: true });
|
|
563
|
-
const page = await browser.newPage(buildPageOptions(options.mode));
|
|
617
|
+
const page = await browser.newPage(buildPageOptions(options.mode, options.resolution));
|
|
564
618
|
const diagnostics = createSlideDiagnostics();
|
|
565
619
|
diagnostics.attach(page);
|
|
566
620
|
const renderedSlides = [];
|
|
@@ -569,7 +623,10 @@ async function main() {
|
|
|
569
623
|
for (const slideFile of slideFiles) {
|
|
570
624
|
diagnostics.beginSlide(slideFile);
|
|
571
625
|
try {
|
|
572
|
-
const slideResult = await renderSlideToPdf(page, slideFile, slidesDir, {
|
|
626
|
+
const slideResult = await renderSlideToPdf(page, slideFile, slidesDir, {
|
|
627
|
+
mode: options.mode,
|
|
628
|
+
resolution: options.resolution,
|
|
629
|
+
});
|
|
573
630
|
renderedSlides.push(slideResult);
|
|
574
631
|
} catch (error) {
|
|
575
632
|
throw decorateError(error, slideFile, diagnostics.getSlideDiagnostics(slideFile));
|
|
@@ -15,7 +15,7 @@ Guides you through the complete presentation pipeline from topic to exported fil
|
|
|
15
15
|
|
|
16
16
|
### Stage 1 — Plan
|
|
17
17
|
|
|
18
|
-
Use **slides-grab-plan**
|
|
18
|
+
Use the installed **slides-grab-plan** skill.
|
|
19
19
|
|
|
20
20
|
1. Take user's topic, audience, and tone.
|
|
21
21
|
2. Create `slide-outline.md`.
|
|
@@ -26,13 +26,13 @@ Use **slides-grab-plan** (`skills/slides-grab-plan/SKILL.md`).
|
|
|
26
26
|
|
|
27
27
|
### Stage 2 — Design
|
|
28
28
|
|
|
29
|
-
Use **slides-grab-design**
|
|
29
|
+
Use the installed **slides-grab-design** skill.
|
|
30
30
|
|
|
31
31
|
1. Read approved `slide-outline.md`.
|
|
32
32
|
2. Generate `slide-*.html` files in the slides workspace (default: `slides/`).
|
|
33
33
|
3. Run validation: `slides-grab validate --slides-dir <path>`
|
|
34
34
|
4. If validation fails, automatically fix the slide HTML/CSS until validation passes.
|
|
35
|
-
5. Build the viewer: `
|
|
35
|
+
5. Build the viewer: `slides-grab build-viewer --slides-dir <path>`
|
|
36
36
|
6. Present viewer to user for review.
|
|
37
37
|
7. Revise individual slides based on feedback, then re-run validation and rebuild the viewer.
|
|
38
38
|
8. Optionally launch the visual editor: `slides-grab edit --slides-dir <path>`
|
|
@@ -41,7 +41,7 @@ Use **slides-grab-design** (`skills/slides-grab-design/SKILL.md`).
|
|
|
41
41
|
|
|
42
42
|
### Stage 3 — Export
|
|
43
43
|
|
|
44
|
-
Use **slides-grab-export**
|
|
44
|
+
Use the installed **slides-grab-export** skill.
|
|
45
45
|
|
|
46
46
|
1. Confirm user wants conversion.
|
|
47
47
|
2. Export to PPTX: `slides-grab convert --slides-dir <path> --output <name>.pptx` (**experimental / unstable**)
|
|
@@ -57,4 +57,7 @@ Use **slides-grab-export** (`skills/slides-grab-export/SKILL.md`).
|
|
|
57
57
|
3. **Read each stage's SKILL.md** for detailed rules — this skill only orchestrates.
|
|
58
58
|
4. **Use `decks/<deck-name>/`** as the slides workspace for multi-deck projects.
|
|
59
59
|
5. **Call out export risk clearly**: PPTX and Figma export are experimental / unstable and must be described as best-effort output.
|
|
60
|
-
6.
|
|
60
|
+
6. Use the stage skills as the source of truth for plan, design, and export rules.
|
|
61
|
+
|
|
62
|
+
## Reference
|
|
63
|
+
- `references/presentation-workflow-reference.md` — archived end-to-end workflow guidance from the legacy skill set
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Presentation Skill - Full Workflow Orchestrator
|
|
2
|
+
|
|
3
|
+
Guides you through the complete presentation pipeline from topic to exported file.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Workflow
|
|
8
|
+
|
|
9
|
+
### Stage 1 — Plan
|
|
10
|
+
|
|
11
|
+
Use **plan-skill** (`.claude/skills/plan-skill/SKILL.md`).
|
|
12
|
+
|
|
13
|
+
1. Take user's topic, audience, and tone.
|
|
14
|
+
2. Delegate outline creation to `organizer-agent`.
|
|
15
|
+
3. Present `slide-outline.md` to user.
|
|
16
|
+
4. Revise until user explicitly approves.
|
|
17
|
+
|
|
18
|
+
**Do not proceed to Stage 2 without approval.**
|
|
19
|
+
|
|
20
|
+
### Stage 2 — Design
|
|
21
|
+
|
|
22
|
+
Use **design-skill** (`.claude/skills/design-skill/SKILL.md`).
|
|
23
|
+
|
|
24
|
+
1. Read approved `slide-outline.md`.
|
|
25
|
+
2. Generate `slide-*.html` files in the slides workspace (default: `slides/`).
|
|
26
|
+
3. Run validation: `slides-grab validate --slides-dir <path>`
|
|
27
|
+
4. If validation fails, automatically fix the slide HTML/CSS until validation passes.
|
|
28
|
+
5. Build the viewer: `node scripts/build-viewer.js --slides-dir <path>`
|
|
29
|
+
6. Present viewer to user for review.
|
|
30
|
+
7. Revise individual slides based on feedback, then re-run validation and rebuild the viewer.
|
|
31
|
+
8. Optionally launch the visual editor: `slides-grab edit --slides-dir <path>`
|
|
32
|
+
|
|
33
|
+
**Do not proceed to Stage 3 without approval.**
|
|
34
|
+
|
|
35
|
+
### Stage 3 — Export
|
|
36
|
+
|
|
37
|
+
Use **pptx-skill** (`.claude/skills/pptx-skill/SKILL.md`).
|
|
38
|
+
|
|
39
|
+
1. Confirm user wants conversion.
|
|
40
|
+
2. Export to PPTX: `slides-grab convert --slides-dir <path> --output <name>.pptx` (**experimental / unstable**)
|
|
41
|
+
3. Export to PDF (if requested): `slides-grab pdf --slides-dir <path> --output <name>.pdf`
|
|
42
|
+
4. Report results.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Rules
|
|
47
|
+
|
|
48
|
+
1. **Always follow the stage order**: Plan → Design → Export.
|
|
49
|
+
2. **Get explicit user approval** before advancing to the next stage.
|
|
50
|
+
3. **Read each stage's SKILL.md** for detailed rules — this skill only orchestrates.
|
|
51
|
+
4. **Use `decks/<deck-name>/`** as the slides workspace for multi-deck projects.
|
|
52
|
+
5. **Call out export risk clearly**: PPTX and Figma export are experimental / unstable and should be described as best-effort output.
|
|
@@ -26,7 +26,7 @@ Generate high-quality `slide-XX.html` files in the selected slides workspace (`s
|
|
|
26
26
|
2. Generate slide HTML files with 2-digit numbering in selected `--slides-dir`.
|
|
27
27
|
3. Run `slides-grab validate --slides-dir <path>` after generation or edits.
|
|
28
28
|
4. If validation fails, automatically fix the source slide HTML/CSS and re-run validation until it passes.
|
|
29
|
-
5. Run `
|
|
29
|
+
5. Run `slides-grab build-viewer --slides-dir <path>` only after validation passes.
|
|
30
30
|
6. Iterate on user feedback by editing only requested slide files, then re-run validation and rebuild the viewer.
|
|
31
31
|
7. Keep revising until user approves conversion stage.
|
|
32
32
|
|
|
@@ -39,7 +39,10 @@ Generate high-quality `slide-XX.html` files in the selected slides workspace (`s
|
|
|
39
39
|
- Prefer `<img>` for slide imagery and `data-image-placeholder` when no final asset exists.
|
|
40
40
|
- Do not present slides for review until `slides-grab validate --slides-dir <path>` passes.
|
|
41
41
|
- Do not start conversion before approval.
|
|
42
|
+
- Use the packaged CLI and bundled references only; do not depend on unpublished agent-specific files.
|
|
42
43
|
|
|
43
44
|
## Reference
|
|
44
45
|
For full constraints and style system, follow:
|
|
45
|
-
-
|
|
46
|
+
- `references/design-rules.md`
|
|
47
|
+
- `references/detailed-design-rules.md`
|
|
48
|
+
- `references/design-system-full.md` — archived full design system, templates, and advanced pattern guidance
|