Stirling-Tools/Stirling-PDF · error · Error

Could not get canvas context

Error message

Could not get canvas context

What it means

`HTMLCanvasElement.getContext('2d')` returns null instead of throwing when the 2D rendering context cannot be acquired. This guard fires in `generateThumbnailFromPDF` (and the sibling `generatePageThumbnail`), which both create a throwaway canvas to render a pdf.js page into a data-URL thumbnail. A null return means the browser refused to hand out a 2D context for that canvas element.

Source

Thrown at frontend/editor/src/core/hooks/usePDFProcessor.ts:62

  );

  // Internal function to generate thumbnail from already-opened PDF
  const generateThumbnailFromPDF = useCallback(
    async (
      pdf: any,
      pageNumber: number,
      scale: number = 0.5,
    ): Promise<string> => {
      const page = await pdf.getPage(pageNumber);

      const viewport = page.getViewport({ scale });
      const canvas = document.createElement("canvas");
      canvas.width = viewport.width;
      canvas.height = viewport.height;

      const context = canvas.getContext("2d");
      if (!context) {
        throw new Error("Could not get canvas context");
      }

      await page.render({ canvasContext: context, viewport }).promise;
      return canvas.toDataURL();
    },
    [],
  );

  const processPDFFile = useCallback(
    async (file: File): Promise<PDFDocument> => {
      setLoading(true);
      setError(null);

      try {
        const arrayBuffer = await file.arrayBuffer();
        const pdf = await pdfWorkerManager.createDocument(arrayBuffer);
        const totalPages = pdf.numPages;

View on GitHub (pinned to 9ef20dcab8)

Solutions

  1. Recycle a single shared canvas across thumbnail renders instead of calling `document.createElement('canvas')` per page, and reset it via `context.clearRect(0,0,canvas.width,canvas.height)` between renders.
  2. Rate-limit concurrent thumbnail generation (queue with a small concurrency cap, e.g. 3-4) so the live-context budget is never exhausted.
  3. Before rendering, guard `const ctx = canvas.getContext('2d'); if (!ctx) return placeholderThumbnail();` to degrade gracefully rather than aborting the whole file.
  4. For headless test environments, configure the runner with `--use-gl=swiftshader` / a software WebGL backend so canvas contexts are allocatable.

Example fix

// before
const canvas = document.createElement("canvas");
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext("2d");
if (!context) {
  throw new Error("Could not get canvas context");
}

// after (graceful degradation + reusable canvas)
const context = canvas.getContext("2d");
if (!context) {
  console.warn(`Canvas 2D context unavailable for page ${pageNumber}; using placeholder`);
  return PLACEHOLDER_THUMBNAIL_DATA_URL;
}
Defensive patterns

Strategy: fallback

Validate before calling

// Check before requesting the context
function canGet2dContext(): boolean {
  try {
    const probe = document.createElement("canvas");
    return probe.getContext("2d") !== null;
  } catch {
    return false;
  }
}

// Concurrency-cap thumbnail generation so the live-context budget isn't hit
const MAX_CONCURRENT_THUMBS = 4;
async function pool<T>(items: T[], n: number, fn: (t: T) => Promise<T>): Promise<T[]> { /* simple semaphore */ return items; }

Type guard

// A canvas whose 2D context is actually available
function hasUsable2dContext(canvas: HTMLCanvasElement): canvas is HTMLCanvasElement & { getContext(c: "2d"): CanvasRenderingContext2D } {
  return canvas.getContext("2d") !== null;
}

Try / catch

try {
  const context = canvas.getContext("2d");
  if (!context) return PLACEHOLDER_THUMBNAIL_DATA_URL;
  await page.render({ canvasContext: context, viewport }).promise;
  return canvas.toDataURL();
} catch (e) {
  console.warn('Thumbnail render failed, using placeholder', e);
  return PLACEHOLDER_THUMBNAIL_DATA_URL;
}

Prevention

When it happens

Trigger: Calling `generatePageThumbnail`/`generateThumbnailFromPDF` in a tight loop (e.g. generating thumbnails for a 100+ page PDF) exhausts the browser's per-tab canvas-context budget; Safari/WebKit hard-caps live contexts. It also returns null in headless/SSR environments where `document.createElement('canvas')` has no GPU/2D backend, or if `OffscreenCanvas`/a non-2D context was already requested on that element.

Common situations: Thumbnail generation for large PDFs; running the editor in a headless browser (Puppeteer/Playwright) or an environment without a real compositor; Safari with many simultaneous canvases; memory pressure causing the browser to deny context allocation.

Related errors


AI-assisted analysis of Stirling-Tools/Stirling-PDF@9ef20dcab8 (2026-08-13). Data as JSON: /api/errors/095ebbdf9f0b201c. Report an issue: GitHub.