Engineering Journal
Pdf Processor
Pdf Processor

Why Your PDF.js Image Extraction Silently Returns Nothing (and How to Fix It)

2026-05-30

TLDR

In PDF.js v4, calling page.objs.get(id) synchronously before executing page.render() throws a synchronous error (Requesting object that isn't resolved yet) because PDF.js defers loading XObject pixel data until rendering runs. Executing a throwaway page.render() pass onto an OffscreenCanvas populates page.objs synchronously, allowing clean native pixel extraction (RGB, RGBA, and Grayscale) without canvas crop bleed or resolution loss.
Image Extraction StrategySynchronous page.objs.get() OutcomeImage ResolutionContent Bleed
Direct Pre-Render LookupThrows (Object not resolved yet)N/A (Failed)N/A
Canvas Crop ScreenshotWorksLow (Viewport pixel scale)High (Overlapping text/borders)
Render-Populate + page.objsPasses (Synchronous lookup safe)Native 1:1 Image PixelsZero (Isolated pixel data)

Technical problem statement: deferred pixel loading

PDF.js defers loading raw raster binary streams during page.getOperatorList() calls. While operator list parsing identifies image XObject names (e.g. img_p0_1), page.objs remains unpopulated until page.render() is invoked.

Calling page.objs.get(id) before page.render() throws a synchronous error caught silently by extraction try/catch blocks:

Requesting object that isn't resolved yet: img_p0_1

Technical solution: throwaway render-and-extract pattern

Trigger page.render() to populate page.objs, then read pixel byte arrays directly from memory:

export async function extractNativePdfImages(page, imageMetadataList, viewport) {
  const extractedImagesMap = {};

if (imageMetadataList.length === 0 || typeof OffscreenCanvas === 'undefined') { return extractedImagesMap; }

// 1. Render to a throwaway OffscreenCanvas to force page.objs population const pageCanvas = new OffscreenCanvas( Math.round(viewport.width), Math.round(viewport.height) ); await page.render({ canvasContext: pageCanvas.getContext('2d'), viewport }).promise;

// 2. Safely query page.objs synchronously for native pixel data for (const meta of imageMetadataList) { try { const obj = page.objs.get(meta.id); if (!obj || !obj.data || !obj.width || !obj.height) continue;

const { width: w, height: h, data } = obj; const rgba = new Uint8ClampedArray(w h 4);

// Handle 4-channel RGBA, 3-channel RGB, and 1-channel Grayscale if (data.length === w h 4) { rgba.set(data); } else if (data.length === w h 3) { for (let i = 0, j = 0; i < data.length; i += 3, j += 4) { rgba[j] = data[i]; rgba[j+1] = data[i+1]; rgba[j+2] = data[i+2]; rgba[j+3] = 255; } } else if (data.length === w * h) { for (let i = 0, j = 0; i < data.length; i++, j += 4) { rgba[j] = rgba[j+1] = rgba[j+2] = data[i]; rgba[j+3] = 255; } } else continue;

const imgCanvas = new OffscreenCanvas(w, h); imgCanvas.getContext('2d').putImageData(new ImageData(rgba, w, h), 0, 0); extractedImagesMap[meta.id] = await imgCanvas.convertToBlob({ type: 'image/png' }); } catch (_) { // Handle isolated image object read errors gracefully } }

return extractedImagesMap; }

Rule of thumb: Execute page.render() to populate page.objs before reading native XObject pixel data in PDF.js Web Worker pipelines.
Read this post in the full Engineering Journal →