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";23const 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 };1011 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 };1920 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 });2728 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]);3637 useEffect(() => {38 getBase64Strings().catch(console.error);39 }, [getBase64Strings]);40 return [addFile, removeFile, base64Strings, isLoading];41};4243export 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";34const 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 Upload20 </Button>21 </Upload>22 );23};2425export 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";78const App = () => {9 const [addFile, removeFile, base64Strings, isLoading] = useFileSelection();10 const ref = useRef();1112 return (13 <div style={{ margin: "2%" }}>14 <Row justify="center" style={{ marginBottom: "10px" }}>15 <Col span={6}>16 <UploadButton17 addFile={addFile}18 removeFile={removeFile}19 loading={isLoading}20 />21 </Col>22 <Col span={6}>23 {base64Strings.length >= 1 && (24 <Pdf25 targetRef={ref}26 filename="images.pdf"27 options={{ orientation: "landscape" }}28 scale={0.9}29 >30 {({ toPdf }) => (31 <Button danger onClick={toPdf}>32 Generate Pdf33 </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 <img44 src={base64String}45 key={index}46 style={{ height: "200px", width: "250px" }}47 />48 </Col>49 ))}50 </Row>51 </div>52 </div>53 );54};5556export 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:
targetRef
- This is the ref for the target component (in our case, the div element wrapping the second row).filename
- This is the name that should be given to the generated pdf document.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.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: