Why Your PDF.js Image Extraction Silently Returns Nothing (and How to Fix It)
TLDR
In PDF.js v4, callingpage.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 Strategy | Synchronous page.objs.get() Outcome | Image Resolution | Content Bleed |
|---|---|---|---|
| Direct Pre-Render Lookup | Throws (Object not resolved yet) | N/A (Failed) | N/A |
| Canvas Crop Screenshot | Works | Low (Viewport pixel scale) | High (Overlapping text/borders) |
Render-Populate + page.objs | Passes (Synchronous lookup safe) | Native 1:1 Image Pixels | Zero (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: Executepage.render()to populatepage.objsbefore reading native XObject pixel data in PDF.js Web Worker pipelines.