Convert Images to PDF

Ifeoma Imoh

PDF (portable document format) incorporates several technologies, including encryption, data compression, font management, computer graphics, and so on. All of this is designed to describe the contents of read-only documents and make them reliable for sharing among communicating parties while preserving layout and formatting to ensure consistency regardless of the viewing device or underlying software architecture.

The wide adoption and the open-source nature of the PDF standard led to the creation of so many programs (most of which are free) to read or write to PDF files. The most common is the web browser or applications like Adobe Acrobat that fulfill this need. Most PDF writers require us to interact using a GUI to create PDF files. While this is fantastic for many use cases, it can become rather tiresome when we want to repeatedly generate PDF receipts for end-users or carry out some task of that sort. Several tools have been developed to create PDF files programmatically to fulfill this need.

In this article, we will use PDF-LIB in a simple React application to create a PDF file, embed an image in it, and finally download the resulting PDF file.

Here is a link to the demo on CodeSandbox.

Project Setup

Let’s start by creating a new React application using CRA.

1npx create-react-app image-to-pdf

Once that’s done installing, we can install the dependency we’ll need for our application.

1npm install pdf-lib

The pdf-lib module will provide us with the APIs to create PDF files.

Building the UI

Replace the content of your App.js file to match the following:

1import './App.css';
2 import { useState } from 'react';
3 import { PDFDocument } from 'pdf-lib';
4 function App() {
5 const [file, setFile] = useState(null);
6 const fileToArrBuffer = (file) =>
7 new Promise((res, rej) => {
8 const fileReader = new FileReader();
9 fileReader.onload = () => res(fileReader.result);
10 fileReader.onerror = () => rej(fileReader.error);
11 fileReader.readAsArrayBuffer(file);
12 });
13 const downloadFile = async (blob) => {
14 const URL = window.URL.createObjectURL(blob);
15 const el = document.createElement('a');
16 el.download = 'mydummyfile.pdf';
17 el.href = URL;
18 el.click();
19 window.URL.revokeObjectURL(URL);
20 };
21 const embedImageInPdfAndDownload = async () => {};
22 return (
23 <>
24 <div className="App">
25 <label htmlFor="file" className="btn-grey">
26 {' '}
27 select image
28 </label>
29 <input
30 id="file"
31 type="file"
32 onChange={(e) => setFile(e.target.files[0])}
33 multiple={false}
34 accept="image/*"
35 name="file"
36 />
37 {file && (
38 <section>
39 <img src={URL.createObjectURL(file)} alt="some_image" width={300} />
40 <span> {file.name}</span>
41 </section>
42 )}
43 {file && (
44 <>
45 <button className="btn-green" onClick={embedImageInPdfAndDownload}>
46 {'embed image in PDF and download'}
47 </button>
48 </>
49 )}
50 </div>
51 </>
52 );
53 }
54 export default App;

In the body of our App component, we start by creating a state variable to hold and set the contents of our image file. Next, we define three functions, from top to bottom; the first two functions expect a Blob as a parameter. The first function, fileToArrBuffer uses the FileReader() module to read and return an Arraybuffer representing the chunk of bytes in the Blob(file).

The second function, downloadFile as the name implies, creates a pointer to a Blob in memory and binds it to an anchor tag, after which it specifies the download attribute, which would signify the name of the downloaded file. In our case, we just called the file dummyfile.pdf because this function expects the Blob to be a PDF file. The anchor tag is then clicked programmatically to download the file, after which the pointer is deleted by the call to revokeObjectURL to avoid memory leak.

The third function is responsible for creating the contents of the PDF file, including the image that will be embedded in it. The body of this function is intentionally left blank. In the sections that follow, we will incrementally update its contents and explain each step as we go.

Our App component returns some basic JSX, such as an input field and its associated label to allow the user to select a file, an img element to render the selected image on the screen, and a button to trigger our yet to be defined embedImageInPdfAndDownload function.

Add the following to your App.css file:

1* {
2 box-sizing: border-box;
3 font-family: monospace;
4 }
5 .App {
6 text-align: center;
7 display: grid;
8 gap: 2rem;
9 max-width: 470px;
10 padding: 1.5rem;
11 margin: auto;
12 margin-top: 4rem;
13 }
14 label,
15 button {
16 font-size: 1.4rem;
17 color: #fff;
18 padding: 0.5em 0.9em;
19 cursor: pointer;
20 border: none;
21 }
22 [type='file'] {
23 display: none;
24 }
25 [class*='btn'] {
26 border-radius: 5px;
27 text-transform: capitalize;
28 }
29 .btn-grey {
30 background-color: #f2f2f2;
31 color: #333;
32 border: 1px solid #888;
33 }
34 .btn-green {
35 background-color: #63dd47;
36 }
37 .btn-blue {
38 background-color: #0e80c9;
39 }
40 p {
41 display: block;
42 width: 60%;
43 font-family: monospace;
44 font-size: 1.2rem;
45 background-color: #000;
46 color: #f00;
47 margin: auto;
48 text-overflow: ellipsis;
49 }
50 code {
51 display: grid;
52 gap: 4px;
53 padding: 1rem;
54 background-color: #000;
55 }
56 section {
57 text-align: center;
58 display: grid;
59 justify-items: center;
60 gap: 1.4rem;
61 }
62 section span {
63 font-size: 1.8rem;
64 }

Create the Pdf File and Embed the Image

In the current version of our app, we can only select an image, but we are yet to create the PDF file. Over the next steps, we will be updating the contents of the embedImageInPdfAndDownload function to achieve this.

First, let's use the PDFDocument class to create an instance of a pdf file.

1const pdfDoc = await PDFDocument.create();

Since we want to display the selected image in the PDF file, we need to store it as a resource in the PDF.

1const buffer = await fileToArrBuffer(file);
2 console.log({ buffer });
3 let image;
4 if (/jpe?g/i.test(file.type)) image = await pdfDoc.embedJpg(buffer);
5 else if (/png/i.test(file.type)) image = await pdfDoc.embedPng(buffer);
6 else throw Error('please choose a JPEG or PNG file to proceed');

First, we convert our image to an array buffer by calling the fileToArrBuffer function we defined earlier. Next, we define a variable called image which will be used to hold information about the embedded image. The pdf-kit module only allows embedding JPEG or PNG files using the embedJPEG and embedPNG methods. So based on the image file type, we call the appropriate method, each of which returns an object stored in the image variable. The returned object contains information ( width, height, reference id, etc. ) about the image and some helper functions that will be used to manipulate the image.

The embedJPEG and embedPNG functions also accept the image data contents as either base64, an array buffer, or a unit8Array.

Having an image stored as a resource in a PDF file doesn’t mean the image will be displayed when we view the PDF file. We need to create a page, reference our image as a resource for that page, and then paint it to the screen.

1const page = pdfDoc.addPage();
2 page.drawImage(image, {
3 width: image.scale(0.5).width,
4 height: image.scale(0.5).height,
5 x: 100,
6 y: 100,
7 });

We first create a page then draw the image on the page using the drawImage() method, where we specify two parameters: the image we want to draw and an object that defines the image options.

We specified the width and height of the image to 50% of its original size to maintain its aspect ratio. We also specified the coordinates of the image, 100 points on both the x and y-axis. It is also important to note that in the PDF coordinate system, the bottom left part of a PDF page is the Origin, i.e., the 0,0 point, which means that the measurements will be taken from that point.

Finally, we will need to download the resulting PDF file created.

1let b64Chunk = await pdfDoc.saveAsBase64();
2 b64Chunk = 'data:application/pdf;base64,' + b64Chunk;
3 const blob = await (await fetch(b64Chunk)).blob();
4 downloadFile(blob);

The pdf-kit module provides several options for getting the data representing the PDF file. In this case, we chose the base64 option, and we are retrieving the resulting data by calling the saveAsBase64 function. Remember that the downloadFile function we defined earlier expects a blob as its parameter to perform the download. To that end, we convert the base64 encoded string to a Data URI and then use the fetch API to convert this data to a blob, which is then fed to the downloadFile function to save the resulting PDF locally.

To view the running application, run this command in your terminal:

1npm start

We should be able to embed an image in a PDF file and download the resulting PDF file.

You can find the complete project here on GitHub.

Conclusion

PDF has become the world’s leading page description language for documents, and it can be used for different purposes. This guide describes a simple use case showing how to create a PDF file and embed an image in it.

Resources you may find helpful:

Ifeoma Imoh

Software Developer

Ifeoma is a software developer and technical content creator in love with all things JavaScript.