Rendering PDF Pages to Canvas
Description
One of the most common operations when working with PDFs is rendering pages to display them visually. PDFium provides powerful page rendering capabilities through its WebAssembly API. This guide explains how to render PDF pages to an HTML canvas element with complete control over scaling, rotation, and other rendering parameters.
Interactive Example
This interactive example demonstrates how to render PDF pages to a canvas element. You can navigate between pages, zoom in/out, and rotate the page:
The Basic Workflow
Rendering a PDF page to a canvas involves several steps:
- Initialize PDFium
- Load the PDF document
- Load the specific page to render
- Create a bitmap for rendering
- Render the page to the bitmap
- Transfer the bitmap data to the canvas
- Clean up resources
Prerequisites
The examples in this guide use the initializePdfium
helper function from our Getting Started guide. This function properly initializes the PDFium library, including calling the required PDFiumExt_Init()
function. Make sure to include this initialization code in your application before trying the examples below.
Example Implementation
Here’s a complete implementation for loading a PDF document and rendering its pages to a canvas:
import { initializePdfium } from './initialize-pdfium';
// Note: The initializePdfium function is a helper that initializes the PDFium library.
// For the full implementation, see: /docs/pdfium/getting-started
/**
* Loads a PDF document and returns an object with methods to access and render it.
* This avoids loading the document multiple times for different operations.
*/
export async function loadPdfDocument(pdfData: Uint8Array) {
// Initialize PDFium
const pdfium = await initializePdfium();
// Allocate memory for the PDF data
const filePtr = pdfium.pdfium.wasmExports.malloc(pdfData.length);
pdfium.pdfium.HEAPU8.set(pdfData, filePtr);
// Load the document
const docPtr = pdfium.FPDF_LoadMemDocument(filePtr, pdfData.length, 0);
if (!docPtr) {
const error = pdfium.FPDF_GetLastError();
pdfium.pdfium.wasmExports.free(filePtr);
// Handle password-protected documents
if (error === 4) {
return {
hasPassword: true,
pageCount: 0,
close: () => {}, // No-op since no document was loaded
getPageCount: () => 0,
renderPage: async () => { throw new Error('Document is password protected'); }
};
}
throw new Error(`Failed to load PDF: ${error}`);
}
// Get page count
const pageCount = pdfium.FPDF_GetPageCount(docPtr);
// Return an object with document info and rendering capabilities
return {
hasPassword: false,
pageCount,
// Close the document and free resources
close: () => {
pdfium.FPDF_CloseDocument(docPtr);
pdfium.pdfium.wasmExports.free(filePtr);
},
// Get the current page count (useful if pages are added/removed)
getPageCount: () => pdfium.FPDF_GetPageCount(docPtr),
// Render a specific page to a canvas
renderPage: async (
pageIndex: number,
scale: number = 1.0,
rotation: number = 0,
canvas: HTMLCanvasElement,
dpr: number = window.devicePixelRatio || 1.0
): Promise<{
width: number;
height: number;
}> => {
// Check if the page index is valid
if (pageIndex < 0 || pageIndex >= pageCount) {
throw new Error(`Invalid page index: ${pageIndex}. Document has ${pageCount} pages.`);
}
// Load the page
const pagePtr = pdfium.FPDF_LoadPage(docPtr, pageIndex);
if (!pagePtr) {
throw new Error(`Failed to load page ${pageIndex}`);
}
try {
// Get the page dimensions
const width = pdfium.FPDF_GetPageWidthF(pagePtr);
const height = pdfium.FPDF_GetPageHeightF(pagePtr);
// Calculate the scaled dimensions with device pixel ratio
const effectiveScale = scale * dpr;
let scaledWidth = Math.floor(width * effectiveScale);
let scaledHeight = Math.floor(height * effectiveScale);
// Apply rotation if requested
let rotateFlag = 0;
switch (rotation) {
case 90: rotateFlag = 1; break;
case 180: rotateFlag = 2; break;
case 270: rotateFlag = 3; break;
}
// Swap dimensions for 90 and 270 degree rotations
if (rotation === 90 || rotation === 270) {
[scaledWidth, scaledHeight] = [scaledHeight, scaledWidth];
}
// Create a bitmap for rendering
const bitmapPtr = pdfium.FPDFBitmap_Create(scaledWidth, scaledHeight, 0);
if (!bitmapPtr) {
throw new Error('Failed to create bitmap');
}
try {
// Set canvas CSS dimensions for proper display
canvas.style.width = `${scaledWidth / dpr}px`;
canvas.style.height = `${scaledHeight / dpr}px`;
// Set actual canvas buffer size
canvas.width = scaledWidth;
canvas.height = scaledHeight;
// Fill the bitmap with white background
pdfium.FPDFBitmap_FillRect(bitmapPtr, 0, 0, scaledWidth, scaledHeight, 0xFFFFFFFF);
// Render the page to the bitmap
pdfium.FPDF_RenderPageBitmap(
bitmapPtr,
pagePtr,
0,
0,
scaledWidth,
scaledHeight,
rotateFlag,
16 // Use FPDF_REVERSE_BYTE_ORDER flag for correct color representation
);
// Get the bitmap buffer
const bufferPtr = pdfium.FPDFBitmap_GetBuffer(bitmapPtr);
if (!bufferPtr) {
throw new Error('Failed to get bitmap buffer');
}
const bufferSize = scaledWidth * scaledHeight * 4; // RGBA
// Create a COPY of the buffer data to prevent memory issues
// This is crucial - we must slice() to copy the data instead of using a view
const buffer = new Uint8Array(
pdfium.pdfium.HEAPU8.buffer,
pdfium.pdfium.HEAPU8.byteOffset + bufferPtr,
bufferSize
).slice();
// Create ImageData from the buffer copy
const imageData = new ImageData(
new Uint8ClampedArray(buffer.buffer),
scaledWidth,
scaledHeight
);
// Draw the image data to the canvas
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Failed to get 2D context from canvas');
}
ctx.putImageData(imageData, 0, 0);
// Return the dimensions adjusted for DPR
return {
width: scaledWidth / dpr,
height: scaledHeight / dpr
};
} finally {
// Clean up bitmap
pdfium.FPDFBitmap_Destroy(bitmapPtr);
}
} finally {
// Clean up page
pdfium.FPDF_ClosePage(pagePtr);
}
}
};
}
Usage Example
Here’s a simple example of how to use the rendering functions with a canvas element:
// Get a reference to the canvas element
const canvas = document.getElementById('pdf-canvas');
// Load a PDF file
async function loadAndRenderPdf() {
try {
// Fetch the PDF file
const response = await fetch('sample.pdf');
const arrayBuffer = await response.arrayBuffer();
const pdfData = new Uint8Array(arrayBuffer);
// Load the document
const pdfDocument = await loadPdfDocument(pdfData);
// Set up page navigation
let currentPage = 0;
const pageCount = pdfDocument.pageCount;
// Render the first page
await pdfDocument.renderPage(currentPage, 1.0, 0, canvas);
// Set up navigation
document.getElementById('prev-button').addEventListener('click', async () => {
if (currentPage > 0) {
currentPage--;
await pdfDocument.renderPage(currentPage, 1.0, 0, canvas);
}
});
document.getElementById('next-button').addEventListener('click', async () => {
if (currentPage < pageCount - 1) {
currentPage++;
await pdfDocument.renderPage(currentPage, 1.0, 0, canvas);
}
});
// Clean up when done
window.addEventListener('beforeunload', () => {
pdfDocument.close();
});
} catch (error) {
console.error('Error rendering PDF:', error);
}
}
loadAndRenderPdf();
Memory Management Considerations
When rendering PDF pages, proper memory management is critical to prevent memory leaks:
-
Bitmap cleanup: Always destroy bitmaps using
FPDFBitmap_Destroy
when you’re done with them. -
Page cleanup: Always close pages using
FPDF_ClosePage
when you’re done with them. -
Buffer copying: When accessing the bitmap buffer, create a copy of the data instead of using a direct view. This prevents issues with memory being freed while you’re still using the data:
// GOOD - Create a copy of the buffer data
const buffer = new Uint8Array(
pdfium.pdfium.HEAPU8.buffer,
pdfium.pdfium.HEAPU8.byteOffset + bufferPtr,
bufferSize
).slice();
// BAD - Direct view can lead to problems if memory is freed
const buffer = new Uint8Array(
pdfium.pdfium.HEAPU8.buffer,
pdfium.pdfium.HEAPU8.byteOffset + bufferPtr,
bufferSize
);
Performance Tips
-
Cache rendered pages: If you’re building a PDF viewer, consider caching rendered pages to avoid re-rendering the same page repeatedly.
-
Progressive rendering: For large documents, implement progressive loading and rendering to improve the perceived performance.
-
Render at appropriate resolution: Adjust the scale based on the display device. For high-DPI displays, you may want to render at a higher scale.
-
Use Web Workers: Consider using Web Workers for rendering to avoid blocking the main thread.
Handling High-DPI Displays
The example implementation includes support for high-DPI displays (like Retina displays) through the optional dpr
parameter, which defaults to window.devicePixelRatio
. This ensures optimal rendering quality on all devices.
When working with high-DPI displays:
- The canvas is rendered at the physical resolution (scaled by DPR) for sharpness
- CSS dimensions are set to maintain the correct visual size
- The returned dimensions are adjusted to match the logical size rather than the buffer size
This approach prevents PDFs from appearing blurry on high-resolution displays while maintaining the expected size in the user interface.
// Example of explicitly setting DPR when rendering
await pdfDocument.renderPage(
0, // First page
1.0, // 100% scale
0, // No rotation
canvas, // Canvas element
window.devicePixelRatio // Current device's pixel ratio
);
Related Functions
- FPDF_LoadMemDocument - Load a PDF document from memory
- FPDF_GetPageCount - Get the number of pages in a document
- FPDF_LoadPage - Load a specific page from a document
- FPDF_GetPageWidthF - Get the width of a page in points
- FPDF_GetPageHeightF - Get the height of a page in points
- FPDFBitmap_Create - Create a bitmap for rendering
- FPDFBitmap_FillRect - Fill a rectangle in a bitmap
- FPDF_RenderPageBitmap - Render a page to a bitmap
- FPDFBitmap_GetBuffer - Get the buffer containing bitmap data
- FPDFBitmap_Destroy - Destroy a bitmap and release resources
- FPDF_ClosePage - Close a page and release resources
- FPDF_CloseDocument - Close a document and release resources
- FPDF_GetLastError - Get the error code for the last failed operation