nexu-io/open-design · error · Error
no slides to export
Error message
no slides to export
What it means
Thrown by buildScreenshotPptx in apps/daemon/src/deck-export.ts:144 when its `images` argument is an empty array. The screenshot-based .pptx builder needs at least one full-bleed slide image to lay out one slide per page, so zero images is treated as an unrecoverable export request rather than producing an empty deck file. It is a plain `Error` (no HTTP status), so the route layer in import-export-routes.ts is responsible for translating it into an HTTP response. The `images` come from `SlideImage[]` (each `{ buffer, jpeg }`) produced by `decodeSlideDataUrls` or `readSlideFiles` from the desktop renderer output.
Source
Thrown at apps/daemon/src/deck-export.ts:144
// Standard PowerPoint 16:9 slide is 13.333" x 7.5". We keep 13.333" as the slide
// width and derive the height from the deck's actual aspect ratio, so a 4:3,
// square, or portrait deck gets a correctly-proportioned slide instead of being
// letterboxed into a 16:9 frame.
const PPTX_SLIDE_WIDTH_IN = 13.333;
/**
* Assembles per-slide images into a screenshot-based .pptx — one full-bleed
* image per slide. The slide aspect ratio follows the deck's authored size
* (`opts.aspect` = width/height); falls back to 16:9. The slides are
* pixel-perfect images (not editable text), the "exactly what you see" export
* mode. Returns the .pptx bytes.
*/
export async function buildScreenshotPptx(
images: SlideImage[],
opts: { title?: string; aspect?: number } = {},
): Promise<Buffer> {
if (images.length === 0) throw new Error('no slides to export');
const pptx = new PptxGenJS();
const aspect = opts.aspect && Number.isFinite(opts.aspect) && opts.aspect > 0 ? opts.aspect : 16 / 9;
if (Math.abs(aspect - 16 / 9) < 0.01) {
pptx.layout = 'LAYOUT_16x9';
} else {
const height = Number((PPTX_SLIDE_WIDTH_IN / aspect).toFixed(3));
pptx.defineLayout({ name: 'OD_DECK', width: PPTX_SLIDE_WIDTH_IN, height });
pptx.layout = 'OD_DECK';
}
pptx.author = 'Open Design';
if (opts.title) pptx.title = opts.title;
pptx.subject = 'Screenshot-based PPTX';
for (const img of images) {
const slide = pptx.addSlide();
slide.addImage({
data: `data:image/${img.jpeg ? 'jpeg' : 'png'};base64,${img.buffer.toString('base64')}`,
x: 0,
y: 0,View on GitHub (pinned to 5be4028344)
Solutions
- Confirm the artifact being exported is a deck/page with at least one renderable slide before issuing the export request.
- If calling buildScreenshotPptx directly, guard with `if (images.length === 0) throw new Error('deck has no slides')` (or return early) before the call.
- Inspect the renderer response: check that `rendered.slides` is a non-empty array of data URLs, or that `safeFiles` resolves to existing image files on disk.
- In the route handler, catch this Error and translate it into a 400/422 with a user-facing message instead of letting it surface as a 500.
Example fix
// before
buffer = await buildScreenshotPptx(images, { title: resolvedTitle, ...(aspect ? { aspect } : {}) });
// after
if (images.length === 0) {
return sendApiError(res, 400, 'Deck has no slides to export.');
}
buffer = await buildScreenshotPptx(images, { title: resolvedTitle, ...(aspect ? { aspect } : {}) }); Defensive patterns
Strategy: validation
Validate before calling
function hasSlides(images: unknown): images is SlideImage[] {
return Array.isArray(images) && images.length > 0;
}
// before calling buildScreenshotPptx:
if (!hasSlides(images)) {
return sendApiError(res, 400, 'Deck has no slides to export.');
} Type guard
function isSlideImage(v: unknown): v is SlideImage {
return typeof v === 'object' && v !== null
&& Buffer.isBuffer((v as { buffer?: unknown }).buffer)
&& typeof (v as { jpeg?: unknown }).jpeg === 'boolean';
}
function hasRenderedSlides(v: unknown): v is SlideImage[] {
return Array.isArray(v) && v.length > 0 && v.every(isSlideImage);
} Try / catch
try {
buffer = await buildScreenshotPptx(images, opts);
} catch (err) {
if (err instanceof Error && err.message === 'no slides to export') {
return sendApiError(res, 400, 'Deck has no slides to export.');
}
throw err;
} Prevention
- Validate that the renderer returned at least one slide image before entering the export branch.
- Treat an empty rendered.slides array as a renderer failure and surface it earlier, not as an export-time crash.
- Add a route-level guard before buildScreenshotPptx/buildScreenshotPdf so the generic Error never reaches the client as a 500.
When it happens
Trigger: Calling `buildScreenshotPptx(images, opts)` where `images.length === 0`. In the live route (import-export-routes.ts:997) this happens when `decodeSlideDataUrls(rendered.slides)` or `readSlideFiles(safeFiles)` yields zero entries — i.e. the desktop renderer returned no `slides`, or the rendered slide file list was empty.
Common situations: Exporting a deck artifact that has zero slides; the renderer failed or timed out and returned an empty slides array; a page-mode export was misrouted into the deck export path; the outputDir contained no image files and readSlideFiles mapped over an empty list.
Related errors
- slide ${index + 1} has no file path
- slide ${index + 1} is not a base64 PNG/JPEG data URL
- --page/--no-deck is not valid with --format pptx
- invalid JSON in ${filePath}: ${message}
- ${filePath} must contain a JSON object
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/6f9ee81bc968a7df.
Report an issue: GitHub.