From conveying deep messages to preserving memories and everything in between, images have become integral to the human experience. In this article, we’ll 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’ll use antd, while react-to-pdf will be used to create a PDF document containing the images rendered by our React component.
Here’s a link to the demo CodeSandbox.
Create a React app using the following command:
npx create-react-app react-pdf_demo
Next, add the project dependencies using the following command:
npm install antd @ant-design/icons react-to-pdf
Code language: CSS (css)
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:
@import "~antd/dist/antd.css";
Code language: CSS (css)
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.
import { useCallback, useEffect, useState } from "react";
const useFileSelection = () => {
const [selectedFiles, setSelectedFiles] = useState([]);
const [base64Strings, setBase64Strings] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const addFile = (file) => {
setSelectedFiles((currentSelection) => [...currentSelection, file]);
};
const removeFile = (file) => {
setSelectedFiles((currentSelection) => {
const fileIndex = currentSelection.indexOf(file);
const newSelection = currentSelection.slice();
newSelection.splice(fileIndex, 1);
return newSelection;
});
};
const getBase64Representation = (file) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
});
const getBase64Strings = useCallback(async () => {
setIsLoading(true);
const base64Strings = await Promise.all(
selectedFiles.map((file) => getBase64Representation(file))
);
setBase64Strings(base64Strings);
setIsLoading(false);
}, [selectedFiles]);
useEffect(() => {
getBase64Strings().catch(console.error);
}, [getBase64Strings]);
return [addFile, removeFile, base64Strings, isLoading];
};
export default useFileSelection;
Code language: JavaScript (javascript)
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.
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:
import { UploadOutlined } from "@ant-design/icons";
import { Button, Upload } from "antd";
const UploadButton = ({ addFile, removeFile, loading }) => {
const beforeUpload = (file) => {
addFile(file);
return false;
};
const props = {
onRemove: removeFile,
multiple: true,
showUploadList: false,
beforeUpload,
accept: "image/*",
};
return (
<Upload {...props}>
<Button loading={loading} icon={<UploadOutlined />} type="primary" ghost>
Click to Upload
</Button>
</Upload>
);
};
export default UploadButton;
Code language: JavaScript (javascript)
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
.
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:
import "./App.css";
import { useRef } from "react";
import useFileSelection from "./hooks/useFileSelection";
import UploadButton from "./components/UploadButton";
import { Button, Row, Col } from "antd";
import Pdf from "react-to-pdf";
const App = () => {
const [addFile, removeFile, base64Strings, isLoading] = useFileSelection();
const ref = useRef();
return (
<div style={{ margin: "2%" }}>
<Row justify="center" style={{ marginBottom: "10px" }}>
<Col span={6}>
<UploadButton
addFile={addFile}
removeFile={removeFile}
loading={isLoading}
/>
</Col>
<Col span={6}>
{base64Strings.length >= 1 && (
<Pdf
targetRef={ref}
filename="images.pdf"
options={{ orientation: "landscape" }}
scale={0.9}
>
{({ toPdf }) => (
<Button danger onClick={toPdf}>
Generate Pdf
</Button>
)}
</Pdf>
)}
</Col>
</Row>
<div ref={ref}>
<Row gutter={[0, 16]} justify="center">
{base64Strings.map((base64String, index) => (
<Col span={5}>
<img
src={base64String}
key={index}
style={{ height: "200px", width: "250px" }}
/>
</Col>
))}
</Row>
</div>
</div>
);
};
export default App;
Code language: JavaScript (javascript)
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:
npm 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.
While the above method is great for more advanced applications, especially ones requiring multiple image uploads, we’ll also explore a simpler approach that allows users to upload a single image and then generate a PDF file from that image. This method involves direct saving to the local file system, and the use of a basic HTML input for image upload. It also provides a simple button to generate the PDF. This can be beneficial for applications that don’t require complex setups or multiple image uploads.
Here’s an example of how to convert a single image to PDF using React:
import React, { useState } from "react";
import PDF from "react-to-pdf";
const App = () => {
const [imageData, setImageData] = useState(null);
const handleImageChange = (e) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (e) => {
setImageData(e.target.result);
};
reader.readAsDataURL(file);
};
const handleGeneratePDF = () => {
const pdf = new PDF();
pdf.addPage(imageData);
pdf.save("image.pdf");
};
return (
<div>
<input type="file" onChange={handleImageChange} />
<button onClick={handleGeneratePDF}>Generate PDF</button>
</div>
);
};
export default App;
This more streamlined method offers a way to quickly integrate image-to-PDF functionality without relying heavily on external UI components. Do remember to always use code with caution and adapt wherever you see fit.
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: