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

  1. Confirm the artifact being exported is a deck/page with at least one renderable slide before issuing the export request.
  2. If calling buildScreenshotPptx directly, guard with `if (images.length === 0) throw new Error('deck has no slides')` (or return early) before the call.
  3. 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.
  4. 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

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


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/6f9ee81bc968a7df. Report an issue: GitHub.