Convert Images to PDF Files with React-To-PDF

Banner for a MediaJam post

Ifeoma Imoh

From conveying deep messages to preserving memories and everything in-between, images have become integral to human living. In this article, we will look at how to save uploaded images into a PDF file, making it easier to share multiple images without spamming the recipient while taking up less memory. This feature can be used in an application that generates photo albums or brochures for users on the fly.

For UI components in our application, we will use antd, while react-to-pdf will be used to create a PDF document containing the images rendered by our React component.

Here is a link to the demo CodeSandbox.

Project Setup

Create a React app using the following command:

1npx create-react-app react-pdf_demo

Next, add the project dependencies using the following command:

1npm install antd @ant-design/icons react-to-pdf

Next, we need to import the antd CSS. To do this, open your src/App.css file and edit its content to match the following:

1@import "~antd/dist/antd.css";

Add Hook

We’ll create a hook that will contain the code to provide the necessary functionality associated with file selection. This makes for better abstraction of code as well as leaner components.

In the src folder, create a new folder named hooks. In the src/hooks folder, create a new file called useFileSelection.js and add the following to it.

1import { useCallback, useEffect, useState } from "react";
2
3const useFileSelection = () => {
4 const [selectedFiles, setSelectedFiles] = useState([]);
5 const [base64Strings, setBase64Strings] = useState([]);
6 const [isLoading, setIsLoading] = useState(false);
7 const addFile = (file) => {
8 setSelectedFiles((currentSelection) => [...currentSelection, file]);
9 };
10
11 const removeFile = (file) => {
12 setSelectedFiles((currentSelection) => {
13 const fileIndex = currentSelection.indexOf(file);
14 const newSelection = currentSelection.slice();
15 newSelection.splice(fileIndex, 1);
16 return newSelection;
17 });
18 };
19
20 const getBase64Representation = (file) =>
21 new Promise((resolve, reject) => {
22 const reader = new FileReader();
23 reader.readAsDataURL(file);
24 reader.onload = () => resolve(reader.result);
25 reader.onerror = (error) => reject(error);
26 });
27
28 const getBase64Strings = useCallback(async () => {
29 setIsLoading(true);
30 const base64Strings = await Promise.all(
31 selectedFiles.map((file) => getBase64Representation(file))
32 );
33 setBase64Strings(base64Strings);
34 setIsLoading(false);
35 }, [selectedFiles]);
36
37 useEffect(() => {
38 getBase64Strings().catch(console.error);
39 }, [getBase64Strings]);
40 return [addFile, removeFile, base64Strings, isLoading];
41};
42
43export default useFileSelection;

This hook contains three state variables. selectedFiles keeps track of the files the user has selected, base64Strings holds the base64 encoded string for each selected file, and the isLoading state variable is used to indicate whether or not the encoding process is ongoing. Next, we declare two functions, addFile and removeFile, which are used to update the file selection.

The getBase64Representation function takes a file and generates the base64 encoding of the provided file. This process is asynchronous, and as a result, the function returns a promise. This function is used in the getBase64Strings to update the base64 strings whenever the selectedFiles state variable changes. Finally, with the useEffect hook, we call the getBase64Strings whenever the list of selected files changes.

The last thing we do is export an array containing the addFile, removeFile functions, and the base64Strings and isLoading variables.

Add UploadButton Component

Next, we need a component to handle the selection of images. In the src directory, create a new folder named components. In the src/components directory, create a new file named UploadButton.js and add the following to it:

1import { UploadOutlined } from "@ant-design/icons";
2import { Button, Upload } from "antd";
3
4const UploadButton = ({ addFile, removeFile, loading }) => {
5 const beforeUpload = (file) => {
6 addFile(file);
7 return false;
8 };
9 const props = {
10 onRemove: removeFile,
11 multiple: true,
12 showUploadList: false,
13 beforeUpload,
14 accept: "image/*",
15 };
16 return (
17 <Upload {...props}>
18 <Button loading={loading} icon={<UploadOutlined />} type="primary" ghost>
19 Click to Upload
20 </Button>
21 </Upload>
22 );
23};
24
25export default UploadButton;

This component renders an antd Upload component that provides the addFile, removeFile functions, and isLoading boolean received as props. Additionally, we declared a beforeUpload function which overrides antd’s default behavior of trying to upload the image to a server. In our case, we want the new file to be added to our state variable and nothing else; hence the beforeUpload function returns false.

Putting Everything Together

The last thing we need to do is update our src/App.js file to render our upload button and display a grid of images (which are targeted and used in the pdf generation). To do this, open your src/App.js file and update it to match the following:

1import "./App.css";
2import { useRef } from "react";
3import useFileSelection from "./hooks/useFileSelection";
4import UploadButton from "./components/UploadButton";
5import { Button, Row, Col } from "antd";
6import Pdf from "react-to-pdf";
7
8const App = () => {
9 const [addFile, removeFile, base64Strings, isLoading] = useFileSelection();
10 const ref = useRef();
11
12 return (
13 <div style={{ margin: "2%" }}>
14 <Row justify="center" style={{ marginBottom: "10px" }}>
15 <Col span={6}>
16 <UploadButton
17 addFile={addFile}
18 removeFile={removeFile}
19 loading={isLoading}
20 />
21 </Col>
22 <Col span={6}>
23 {base64Strings.length >= 1 && (
24 <Pdf
25 targetRef={ref}
26 filename="images.pdf"
27 options={{ orientation: "landscape" }}
28 scale={0.9}
29 >
30 {({ toPdf }) => (
31 <Button danger onClick={toPdf}>
32 Generate Pdf
33 </Button>
34 )}
35 </Pdf>
36 )}
37 </Col>
38 </Row>
39 <div ref={ref}>
40 <Row gutter={[0, 16]} justify="center">
41 {base64Strings.map((base64String, index) => (
42 <Col span={5}>
43 <img
44 src={base64String}
45 key={index}
46 style={{ height: "200px", width: "250px" }}
47 />
48 </Col>
49 ))}
50 </Row>
51 </div>
52 </div>
53 );
54};
55
56export default App;

First, we retrieve the functions and variables to help with the file selection process from our previously declared useFileSelection hook. Next, we create a mutable ref object using the useRef hook. The rendered component consists of two rows. The first row holds the button to upload images and another to generate the pdf file from the uploaded images - the latter which is wrapped in a PDF component. The PDF component has four props, namely:

  1. targetRef - This is the ref for the target component (in our case, the div element wrapping the second row).
  2. filename - This is the name that should be given to the generated pdf document.
  3. options - You can view the complete list of available options here. For our use case, we change the page orientation to landscape. This provides more real estate for our images and allows us to fit more into the page.
  4. scale - We used this to scale down the image to 0.9 of the original size.

With everything in place, run your application using the following command:

1npm start

By default, the application should be available at http://localhost:3000/. The final result will look like the gif shown below.

Find the complete project here on GitHub.

Conclusion

In this article, we looked at how we can convert images into a PDF document on the fly using the react-to-pdf library. We rendered a grid of images for our demonstration, which we eventually converted to a PDF document. This is just one area of application as we could also use this functionality to render, edit and save brochures or posters, or any other graphic content for printing or further dissemination.

Resources you may find helpful:

Ifeoma Imoh

Software Developer

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