Merge PDF Docs with Next.js

Banner for a MediaJam post

Eugene Musebe

Introduction

This article is to demonstrate how separate pdf documents can be merged into a single document.

Codesandbox

The final version of this project can be viewed on Codesandbox.

You can find the full source code on my Github repo.

Prerequisites

Basic/entry-level knowledge and understanding of javascript and React/Nextjs.

Setting Up the Sample Project

Create your project root directory: npx create-next-app videocall

Enter the directory: cd videocall

We will use Cloudinary online storage feature to store the processed pdf files.

Start by including Cloudinary in your project dependencies: npm install cloudinary

Cloudinary Credentials Setup

Use the following link to create or log into your Cloudinary account. You will be provided with a dashboard containing the necessary environment variables for integration.

In your project root directory, create a new file named .env.local and use the following guide to fill your variables.

1"pages/api/upload.js"
2
3
4CLOUDINARY_CLOUD_NAME =
5
6CLOUDINARY_API_KEY =
7
8CLOUDINARY_API_SECRET=

Restart your project: npm run dev.

Create a directory pages/api/upload.js.

Configure the environment keys and libraries.

1"pages/api/upload.js"
2
3
4var cloudinary = require("cloudinary").v2;
5
6cloudinary.config({
7 cloud_name: process.env.CLOUDINARY_NAME,
8 api_key: process.env.CLOUDINARY_API_KEY,
9 api_secret: process.env.CLOUDINARY_API_SECRET,
10});

Finally, add a handler function to execute Nextjs post request:

1"pages/api/upload.js"
2
3
4export default async function handler(req, res) {
5 if (req.method === "POST") {
6 let url = ""
7 try {
8 let fileStr = req.body.data;
9 const uploadedResponse = await cloudinary.uploader.upload(
10 fileStr,
11 {
12 resource_type: "video",
13 chunk_size: 6000000,
14 }
15 );
16 } catch (error) {
17 res.status(500).json({ error: "Something wrong" });
18 }
19
20 res.status(200).json("backend complete");
21 }
22}

The above function will upload media files to Cloudinary and return the file's Cloudinary link as a response

We can now work on our front end.

We will use PDF-LIB javascript library to achieve our merging. The library is also efficient in many other pdf functionalities as you can check out on the website.

Include PDF-LIB in your dependencies: npm install --save pdf-lib.

In the pages/index directory, add the dependency to your imports:

1"pages/index"
2
3import { PDFDocument } from 'pdf-lib';

Next, include the following code in your return statement. The css files can be found inside the Github repository.

1return (
2 <div className="container">
3 <h2>Merge PDFs with Nextjs</h2>
4 <div className="row">
5 <div className="column">
6 <object name="bane" width="400px" height="400px" data="https://pdf-lib.js.org/assets/american_flag.pdf"></object><br />
7 PDF_1
8 </div>
9 <div className="column">
10 <object width="400px" height="400px" data="https://pdf-lib.js.org/assets/with_update_sections.pdf"></object><br />
11 PDF_2
12 </div>
13 <div>
14 </div>
15 </div>
16 <button onClick={mergePDF} className="btn btn-">Merge PDF</button>
17 </div>
18)

The above code should result in a UI like below

complete UI

Let us now include the mergePdf function inside the home component.

1const mergePDF = async() => {
2
3}

We will use two pdf documents. Since we are using pdf urls, we begin by fetching their arrayButffers and loading them to the cover and content variables respectively.

1const mergePDF = async() => {
2 const coverBytes = await fetch('https://pdf-lib.js.org/assets/american_flag.pdf').then(res => res.arrayBuffer())
3 console.log(typeof coverBytes)
4 const cover = await PDFDocument.load(coverBytes);
5
6 const contentBytes = await fetch('https://pdf-lib.js.org/assets/with_update_sections.pdf').then(res => res.arrayBuffer())
7 const content = await PDFDocument.load(contentBytes);
8
9}

Create a new PDF document:

1const mergePDF = async() => {
2 const coverBytes = await fetch('https://pdf-lib.js.org/assets/american_flag.pdf').then(res => res.arrayBuffer())
3 console.log(typeof coverBytes)
4 const cover = await PDFDocument.load(coverBytes);
5
6 const contentBytes = await fetch('https://pdf-lib.js.org/assets/with_update_sections.pdf').then(res => res.arrayBuffer())
7 const content = await PDFDocument.load(contentBytes);
8
9// Create a new Document
10 const doc = await PDFDocument.create();
11}

We then get the doc object and copy the cover pages and get the page indices using the getPagesIndices method in form of arrays. We use a for-loop to loop through the pages and add the pages to the initially empty doc object.

1const mergePDF = async() => {
2 const coverBytes = await fetch('https://pdf-lib.js.org/assets/american_flag.pdf').then(res => res.arrayBuffer())
3 console.log(typeof coverBytes)
4 const cover = await PDFDocument.load(coverBytes);
5
6 const contentBytes = await fetch('https://pdf-lib.js.org/assets/with_update_sections.pdf').then(res => res.arrayBuffer())
7 const content = await PDFDocument.load(contentBytes);
8
9// Create a new Document
10 const doc = await PDFDocument.create();
11}
12
13// Add cover to the new doc.
14 const contentPages1 = await doc.copyPages(cover, cover.getPageIndices());
15 for (const page of contentPages1) {
16 doc.addPage(page);
17 }

Use the concept above to add another for loop that concatenates the second pdf to the docs and finally, passes the base64 format of the result to the uploadHandler function.

1const mergePDF = async() => {
2 const coverBytes = await fetch('https://pdf-lib.js.org/assets/american_flag.pdf').then(res => res.arrayBuffer())
3 console.log(typeof coverBytes)
4 const cover = await PDFDocument.load(coverBytes);
5
6 const contentBytes = await fetch('https://pdf-lib.js.org/assets/with_update_sections.pdf').then(res => res.arrayBuffer())
7 const content = await PDFDocument.load(contentBytes);
8
9// Create a new Document
10 const doc = await PDFDocument.create();
11}
12
13// Add cover to the new doc.
14 const contentPages1 = await doc.copyPages(cover, cover.getPageIndices());
15 for (const page of contentPages1) {
16 doc.addPage(page);
17 }
18
19// add individual content pages to the new doc
20
21 const contentPages2 = await doc.copyPages(content, content.getPageIndices());
22 for(const page of contentPages2) {
23 doc.addPage(page);
24 }
25
26//Upload base64 format
27 const pdfBytes = await doc.saveAsBase64({ dataUri: true });
28 uploadHandler(pdfBytes)
29 }

The upload function will upload the result to Cloudinary and use a state hook to capture the response and display the link to the user.

Thats it! Ensure to go through the article to enjoy the experience.

Eugene Musebe

Software Developer

I’m a full-stack software developer, content creator, and tech community builder based in Nairobi, Kenya. I am addicted to learning new technologies and loves working with like-minded people.