slides-grab 1.1.2 → 1.1.3

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 CHANGED
@@ -69,8 +69,10 @@ slides-grab edit # Launch visual slide editor
69
69
  slides-grab build-viewer # Build single-file viewer.html
70
70
  slides-grab validate # Validate slide HTML (Playwright-based)
71
71
  slides-grab convert # Export to experimental / unstable PPTX
72
+ slides-grab convert --resolution 2160p # Higher-resolution raster PPTX export
72
73
  slides-grab figma # Export an experimental / unstable Figma Slides importable PPTX
73
74
  slides-grab pdf # Export PDF in capture mode (default)
75
+ slides-grab pdf --resolution 2160p # Higher-resolution image-backed PDF export
74
76
  slides-grab pdf --mode print # Export searchable/selectable text PDF
75
77
  slides-grab list-templates # Show available slide templates
76
78
  slides-grab list-themes # Show available color themes
@@ -89,6 +91,8 @@ Run `slides-grab validate --slides-dir <path>` before export to catch missing lo
89
91
 
90
92
  `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
93
 
94
+ `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.
95
+
92
96
  ### Multi-Deck Workflow
93
97
 
94
98
  Prerequisite: create or generate a deck in `decks/my-deck/` first.
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
 
package/convert.cjs CHANGED
@@ -3,10 +3,16 @@ const { chromium } = require('playwright');
3
3
  const path = require('path');
4
4
  const fs = require('fs');
5
5
  const sharp = require('sharp');
6
+ const {
7
+ getResolutionChoices,
8
+ getResolutionSize,
9
+ normalizeResolutionPreset,
10
+ } = require('./src/export-resolution.cjs');
6
11
 
7
12
  // Inline a simplified version that uses Playwright Chromium (not Chrome)
8
13
  const DEFAULT_SLIDES_DIR = 'slides';
9
14
  const DEFAULT_OUTPUT = 'output.pptx';
15
+ const DEFAULT_RESOLUTION = '2160p';
10
16
  const DEFAULT_CAPTURE_VIEWPORT = { width: 960, height: 540 };
11
17
  const DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR = 2;
12
18
  const TARGET_RASTER_DPI = 150;
@@ -19,17 +25,25 @@ function normalizeDimension(value, fallback) {
19
25
  return Math.max(1, Math.round(value));
20
26
  }
21
27
 
22
- function buildPageOptions() {
28
+ function buildPageOptions(resolution = '') {
29
+ const targetResolution = getResolutionSize(resolution);
23
30
  return {
24
31
  viewport: {
25
32
  width: DEFAULT_CAPTURE_VIEWPORT.width,
26
33
  height: DEFAULT_CAPTURE_VIEWPORT.height,
27
34
  },
28
- deviceScaleFactor: DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR,
35
+ deviceScaleFactor: targetResolution
36
+ ? targetResolution.height / DEFAULT_CAPTURE_VIEWPORT.height
37
+ : DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR,
29
38
  };
30
39
  }
31
40
 
32
- function getTargetRasterSize() {
41
+ function getTargetRasterSize(resolution = '') {
42
+ const targetResolution = getResolutionSize(resolution);
43
+ if (targetResolution) {
44
+ return targetResolution;
45
+ }
46
+
33
47
  return {
34
48
  width: Math.round(TARGET_SLIDE_SIZE_IN.width * TARGET_RASTER_DPI),
35
49
  height: Math.round(TARGET_SLIDE_SIZE_IN.height * TARGET_RASTER_DPI),
@@ -44,6 +58,7 @@ function printUsage() {
44
58
  'Options:',
45
59
  ` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
46
60
  ` --output <path> Output pptx path (default: ${DEFAULT_OUTPUT})`,
61
+ ` --resolution <preset> Raster size preset: ${getResolutionChoices().join('|')}|4k (default: ${DEFAULT_RESOLUTION})`,
47
62
  ' -h, --help Show this help message',
48
63
  ].join('\n'),
49
64
  );
@@ -62,6 +77,7 @@ function parseArgs(args) {
62
77
  const options = {
63
78
  slidesDir: DEFAULT_SLIDES_DIR,
64
79
  output: DEFAULT_OUTPUT,
80
+ resolution: DEFAULT_RESOLUTION,
65
81
  help: false,
66
82
  };
67
83
 
@@ -94,6 +110,17 @@ function parseArgs(args) {
94
110
  continue;
95
111
  }
96
112
 
113
+ if (arg === '--resolution') {
114
+ options.resolution = normalizeResolutionPreset(readOptionValue(args, i, '--resolution'));
115
+ i += 1;
116
+ continue;
117
+ }
118
+
119
+ if (arg.startsWith('--resolution=')) {
120
+ options.resolution = normalizeResolutionPreset(arg.slice('--resolution='.length));
121
+ continue;
122
+ }
123
+
97
124
  throw new Error(`Unknown option: ${arg}`);
98
125
  }
99
126
 
@@ -107,13 +134,14 @@ function parseArgs(args) {
107
134
 
108
135
  options.slidesDir = options.slidesDir.trim();
109
136
  options.output = options.output.trim();
137
+ options.resolution = normalizeResolutionPreset(options.resolution);
110
138
  return options;
111
139
  }
112
140
 
113
- async function convertSlide(htmlFile, pres, browser) {
141
+ async function convertSlide(htmlFile, pres, browser, options = {}) {
114
142
  const filePath = path.isAbsolute(htmlFile) ? htmlFile : path.join(process.cwd(), htmlFile);
115
143
 
116
- const page = await browser.newPage(buildPageOptions());
144
+ const page = await browser.newPage(buildPageOptions(options.resolution));
117
145
  await page.goto(`file://${filePath}`);
118
146
 
119
147
  const bodyDimensions = await page.evaluate(() => {
@@ -135,7 +163,7 @@ async function convertSlide(htmlFile, pres, browser) {
135
163
  await page.close();
136
164
 
137
165
  // Resize to exact slide dimensions (13.33" x 7.5" at 150 DPI)
138
- const targetSize = getTargetRasterSize();
166
+ const targetSize = getTargetRasterSize(options.resolution);
139
167
 
140
168
  const resized = await sharp(screenshot)
141
169
  .resize(targetSize.width, targetSize.height, { fit: 'fill' })
@@ -184,7 +212,7 @@ async function main() {
184
212
  const filePath = path.join(slidesDir, file);
185
213
  console.log(` Processing: ${file}`);
186
214
  try {
187
- const tmpPath = await convertSlide(filePath, pres, browser);
215
+ const tmpPath = await convertSlide(filePath, pres, browser, { resolution: options.resolution });
188
216
  tmpFiles.push(tmpPath);
189
217
  console.log(` ✓ ${file} done`);
190
218
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slides-grab",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
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",
@@ -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' ? DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR : 1,
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, { mode: options.mode });
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));
@@ -0,0 +1,58 @@
1
+ const RESOLUTION_PRESETS = Object.freeze({
2
+ '720p': { width: 1280, height: 720 },
3
+ '1080p': { width: 1920, height: 1080 },
4
+ '1440p': { width: 2560, height: 1440 },
5
+ '2160p': { width: 3840, height: 2160 },
6
+ });
7
+
8
+ const RESOLUTION_ALIASES = Object.freeze({
9
+ '4k': '2160p',
10
+ uhd: '2160p',
11
+ });
12
+
13
+ function getResolutionChoices() {
14
+ return Object.keys(RESOLUTION_PRESETS);
15
+ }
16
+
17
+ function normalizeResolutionPreset(value, options = {}) {
18
+ const { allowEmpty = true } = options;
19
+
20
+ if (typeof value !== 'string') {
21
+ if (allowEmpty) {
22
+ return '';
23
+ }
24
+ throw new Error(`--resolution must be one of: ${getResolutionChoices().join(', ')}, 4k`);
25
+ }
26
+
27
+ const trimmed = value.trim().toLowerCase();
28
+ if (trimmed === '') {
29
+ if (allowEmpty) {
30
+ return '';
31
+ }
32
+ throw new Error(`--resolution must be one of: ${getResolutionChoices().join(', ')}, 4k`);
33
+ }
34
+
35
+ const normalized = RESOLUTION_ALIASES[trimmed] || trimmed;
36
+ if (!RESOLUTION_PRESETS[normalized]) {
37
+ throw new Error(`Unknown resolution "${value}". Expected one of: ${getResolutionChoices().join(', ')}, 4k`);
38
+ }
39
+
40
+ return normalized;
41
+ }
42
+
43
+ function getResolutionSize(value) {
44
+ const normalized = normalizeResolutionPreset(value);
45
+ if (!normalized) {
46
+ return null;
47
+ }
48
+
49
+ const preset = RESOLUTION_PRESETS[normalized];
50
+ return { width: preset.width, height: preset.height };
51
+ }
52
+
53
+ module.exports = {
54
+ RESOLUTION_PRESETS,
55
+ getResolutionChoices,
56
+ getResolutionSize,
57
+ normalizeResolutionPreset,
58
+ };