PDF Reader implementation with pdf-lib and react-lib in Next.js 14

Publish your content safely and make your PDF documents in Next.js readable on desktop and mobile devices.

Tuedo#018

 by  tuedodev

02
Sep 2024
 0

Offer your own content as a PDF on a React or Next.js website, perhaps the first few pages publicly as an appetizer and the rest behind a paywall? This can be a challenge, especially as rendering the PDF on a canvas is no trivial matter. It's a good thing that the pdf-lib and react-lib libraries are proven libraries that can be used for implementation in React.

This code is intended to implement such a PDF reader as a React component in the Next.js framework and to work for both on desktop and mobile devices. The component library react-aria will be used for the UI design and Tailwind will be used for CSS.

The tech stack is as follows:

Two modes are available: in standard mode, the size of the rendered PDF document is adjusted vertically to the available space so that the full viewport is displayed (or in CSS terms height: 100vh). In full text mode, the page is enlarged to full width and the page can be scrolled up and down.

Secure publishing of a PDF with SSR

In this implementation, the program is based on the assumption that the orientation of the device is always portrait (i.e. upright, the landscape orientation is ignored). Landscape pages within the PDF document are always restricted in width to portrait in order to prevent jumping from different orientations within the PDF document.

The PDF library pdf-lib is used to read the PDF document from the file system (in a real situation, the document would probably be retrieved from a content provider) and convert it into a bytestream.

In a variable defined individually for each document, the conversion can be configured so that only part of the entire document is converted (e.g. the first few pages as an appetizer) and only certain users behind a paywall can see the entire document. This takes full advantage of SSR rendering in Next.js introduced as default with version 14, especially as the protected parts of the document are not sent to the client in the first place. In the demo version, this constant is MAXIMAL_NUMBER_OF_PUBLIC_PAGES with the value 9999 and would in reality be specified as variable for each document depending on the rights that are granted to the individual user.

Figure 1 picture illustration pdf-reader
Picture 1: With full size mode the user can scroll up and down.

The aspect ratio of the first page of the document is also determined in the server action convertPDF(). If it is in landscape format, the landscape format is restricted to the width of the portrait format. More sophisticated calculations of documents with different orientations can be implemented here.

'use server';

import { PDFDocument } from 'pdf-lib';

type ConvertPDFObject = {
	base64string: string | null;
	aspectRatio: string;
};
export async function convertPDF(formData: FormData): Promise {
	const file = formData.get('pdffile') as unknown as File;
	const publicpages = formData.get('publicpages') as unknown as number;
	return new Promise(async (resolve, _) => {
		try {
			const arrbuf = await file.arrayBuffer();
			const buffer = Buffer.from(arrbuf);
			const pdfDoc = await PDFDocument.load(buffer);
			const pageCount = pdfDoc.getPageCount();
			const pages = pdfDoc.getPages();
			let aspectRatioFirstPage = '0.70711 / 1';
			let { width, height } = pages[0].getSize();
			if (width && height) {
				/* If first page is landscape, it will be displayed in portrait anyway: */
				const ratio = height >= width ? width / height : height / width;
				aspectRatioFirstPage = `${ratio.toString().slice(0, 7)} / 1`;
			} else {
				throw new Error('Invalid values for width or height.');
			}
			let base64string: string | null = null;
			for (let i = 0; i < pageCount; i++) {
				if (i >= publicpages) {
					pdfDoc.removePage(pageCount - i);
					/* Remove always the last page, because the index will be updated! */
				}
			}
			base64string = await pdfDoc.saveAsBase64({ dataUri: true });
			resolve({ base64string, aspectRatio: aspectRatioFirstPage });
		} catch (error) {
			console.log(error);
			resolve(null);
		}
	});
}

Two resizeObserver hooks ensure that the width is automatically adjusted when the area allocated to the PDF changes (relevant in standard mode). The second hook ensures that the available width of the viewport is determined in full screen mode. Context providers allow access to the current values inside the component tree.

/* determines the acceptable width of the PDF document */
useResizeObserver(standardPageRef, (entry) => {
	const { width } = entry?.contentRect;
	setWidth(width);
	if (typeof document.documentElement !== 'undefined') {
		document.documentElement.style.setProperty('--pdf-width-container', `${width}px`);
	}
});

/* determines the width of viewport for full page */
useResizeObserver(fullPageRef, (entry) => {
	const { width } = entry?.contentRect;
	if (isFullPage) {
		setFullPageWidth(width);
	}
});

Reliable navigation with react-aria

The DisplayPDF component (based on the other PDF library react-pdf) is mainly responsible for the display of PDF pages and the movement of the mouse or pointer. The react-aria component library is very useful for this process of an accessible navigation. In order to be able to navigate and switch horizontally between the pages, the two previous pages and the two following pages are displayed starting from the current page (look at the code inside the return value).

The useful property clip-part in CSS ensures that only the current page is displayed and the transition to the next page is seamless.

A demo version is deployed on a Stackblitz WebContainer. Click on the upload button, load a PDF file from your file system and you are good to go.

Github Repository of the Code

StackBlitz

Check it inside a Stackblitz WebContainer

This post has not been commented on yet.

Add a comment