Introduction
PDF generation can be a headache depending on the platform you are building for. PDF generation needs to be seamless and not memory intensive. In this post, I will explain how you can generate a PDF within your project for both android and IOS
Prerequisites:
- react-native project
- Android Studio
- Emulator device with Play Store installed
- Xcode
- Simulator device
Project Setup
Create a new project using the following command
npx react-native@latest init PDFProject
Edit the App.jsx screen to have a simple UI. I added a button to trigger the pdf generation
export default function App() {
const isDarkMode = useColorScheme() === 'dark';
const backgroundStyle = {
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
};
return (
<SafeAreaView style={backgroundStyle}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={backgroundStyle.backgroundColor}
/>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
<View
style={styles.container}>
<TouchableOpacity style={styles.button}>
<Text>Create PDF</Text>
</TouchableOpacity>
</View>
</ScrollView>
</SafeAreaView>
);
}
Run the code: check out documentation
npm start
npm run android
npm run ios
PDF generation is mainly from HTML and there are a number of libraries that can help with this but I will be using react-native-html-to-pdf
Add Dependency Library
npm i react-native-html-to-pdf
IOS only: pod Installation
If you are using react-native version <0.60.0, you need to link the library manually.
cd ios && pod install && cd ..
Android Permission
Edit AndroidManifest.xml to include WRITE_EXTERNAL_STORAGE
and READ_EXTERNAL_STORAGE
permission. These permissions are necessary for the app to create, modify, or delete files (WRITE) access and retrieve data (READ) from external storage locations, such as the SD card or other shared storage areas. In our case, we want to create, access and retrieve data from the device.
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
Usage
- Update your App.jsx screen as per your need
import {
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Text,
useColorScheme,
View,
TouchableOpacity,
Dimensions,
Platform,
alert,
} from 'react-native';
import RNHTMLtoPDF from 'react-native-html-to-pdf';
import {Colors} from 'react-native/Libraries/NewAppScreen';
export default function App() {
const isDarkMode = useColorScheme() === 'dark';
const backgroundStyle = {
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
};
const createPDF = async () => {
try {
let PDFOptions = {
html: '<h1>Generate PDF!</h1>',
fileName: 'file',
directory: Platform.OS === 'android' ? 'Downloads' : 'Documents',
};
let file = await RNHTMLtoPDF.convert(PDFOptions);
if (!file.filePath) return;
alert(file.filePath);
} catch (error) {
console.log('Failed to generate pdf', error.message);
}
};
return (
<SafeAreaView style={backgroundStyle}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={backgroundStyle.backgroundColor}
/>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={backgroundStyle}>
<View
style={[
{
backgroundColor: isDarkMode ? Colors.black : Colors.white,
},
styles.container,
]}>
<TouchableOpacity style={styles.button} onPress={createPDF}>
<Text>Create PDF</Text>
</TouchableOpacity>
</View>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
width: '100%',
height: Dimensions.get('screen').height,
justifyContent: 'center',
},
button: {
padding: 16,
backgroundColor: '#E9EBED',
borderColor: '#f4f5f6',
borderWidth: 1,
},
});
It is important to note that 'Documents' is the accepted 'directory' for IOS .
- Click the button to generate pdf
- To view the generated pdf file on the emulator. On IOS, the file is saved on your pc. If you want to view the file, you need to the code on an actual device or add the generated pdf to your simulator.
Common Challenges:
- HTML Content: A lot of times the UI we want to convert to pdf is not a single line. Depending on your app functionality, you might need to convert a screen you are displaying, an image or even generate a new screen entirely depending on the kind of information you want to pass to the user. Bearing in mind that our HTML content needs to be a single string, this can be very difficult. On the bright side, since the HTML will need will still be in the jsx file, we can write js directly in the HTML without encountering any error. This is because pdf libraries provide full browser environment to render and convert HTML to PDF.
TIP 1: Although you can write js functions in the HTML code, you must note the return values of the functions you are calling because in the grand scheme of things your HTML content is a single string. For example. if you have a list of data you want to convert to pdf, instead of repeating <div>
or <tr>
tag a number of times, you can decide to map the array. arr.map()
returns an array. This function will be in the HTML string, by the time you convert to PDF and view, you will notice that there are commas (,) in the pdf:
const invoice = [
{
id: 1,
key: 'Recipient',
value: 'Kolawole Emmauel',
},
{
id: 2,
key: 'Earrings',
value: '$40.00',
},
{
id: 3,
value: 'necklace',
key: '$100.00',
},
{
id: 4,
key: 'Total',
value: '$140.00',
},
];
const htmlContent = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pdf Content</title>
<style>
body {
font-size: 16px;
color: rgb(255, 196, 0);
}
h1 {
text-align: center;
}
.list {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
justify-content: space-between;
}
.key {
font-family: "Inter", sans-serif;
font-weight: 600;
color: #c9cdd2;
font-size: 12px;
line-height: 1.2;
width: 40%;
}
.value {
font-family: "Inter", sans-serif;
font-weight: 600;
color: #5e6978;
font-size: 12px;
line-height: 1.2;
text-transform: capitalize;
width:60%;
flex-wrap: wrap;
}
</style>
</head>
<body>
<h1>Treasury Jewels</h1>
<div class="confirmationBox_content">
${invoice.map(
el =>
`<div
class="list"
key=${el.id}
>
<p class="key">${el.key}</p>
<p class="key">${el.value}</p>
</div>`,
)}
</div>
</body>
</html>
`;
const createPDF = async () => {
try {
let PDFOptions = {
html: htmlContent,
fileName: 'file',
directory: Platform.OS === 'android' ? 'Downloads' : 'Documents',
};
let file = await RNHTMLtoPDF.convert(PDFOptions);
console.log('pdf', file.filePath);
if (!file.filePath) return;
alert(file.filePath);
} catch (error) {
console.log('Failed to generate pdf', error.message);
}
};
Result:
This happens because the content of the array is being stringified and that includes the comma. To fix this we will need to rewrite our function to be arr.map().join("")
to remove comma from the array.
const htmlContent = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pdf Content</title>
<style>
body {
font-size: 16px;
color: rgb(255, 196, 0);
}
h1 {
text-align: center;
}
.list {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
justify-content: space-between;
}
.key {
font-family: "Inter", sans-serif;
font-weight: 600;
color: #000;
font-size: 12px;
line-height: 1.2;
width: 40%;
}
.value {
font-family: "Inter", sans-serif;
font-weight: 600;
color: #000;
font-size: 12px;
line-height: 1.2;
text-transform: capitalize;
width:60%;
flex-wrap: wrap;
}
</style>
</head>
<body>
<h1>Treasury Jewels</h1>
<div class="confirmationBox_content">
${invoice
.map(
el =>
`<div
class="list"
key=${el.id}
>
<p class="key">${el.key}</p>
<p class="key">${el.value}</p>
</div>`,
)
.join('')}
</div>
</body>
</html>
`;
TIP 2: Writing dynamic content: If you are converting some part of your code to HTML and there are parts you used npm packages, you need to find pure js alternatives to use. This is because pdf libraries cannot convert other npm packages.
- Adding Images and other assets to HTML: Images displayed in your application are already bundled with your app while images to be used in the pdf will be viewed outside of the app so they will not be accessible. You need to convert the image or asset to base64 string. We will need 2 libraries to help with that:
Add Dependency Library
npm i expo-assets expo-image-manipulator
Since we are using a bare react-native app, we need to install expo-modules
npx install-expo-modules@latest
#IOS pod installation
cd ios && pod install && cd ..
expo-assets will allow us get the file path of the asset from memory while expo-image-manipulator will help convert the asset to base64 string. Expo-image-manipulator can also be used to optimize large images.
import {Asset} from 'expo-asset';
import {manipulateAsync} from 'expo-image-manipulator';
#TO GET ASSET FROM DEVICE MEMORY
const copyFromAssets = async asset => {
try {
const [{localUri}] = await Asset.loadAsync(asset);
return localUri;
} catch (error) {
console.log(error);
}
};
#CONVERT LocalUri to base64
const processLocalImage = async imageUri => {
try {
const uriParts = imageUri.split('.');
const formatPart = uriParts[uriParts.length - 1];
let format;
if (formatPart.includes('png')) {
format = 'png';
} else if (formatPart.includes('jpg') || formatPart.includes('jpeg')) {
format = 'jpeg';
}
const {base64} = await manipulateAsync(imageUri, [], {
format: format || 'png',
base64: true,
});
return `data:image/${format};base64,${base64}`;
} catch (error) {
console.log(error);
throw error;
}
};
const htmlContent = async () => {
try {
const asset = require('./src/assets/logo.png');
let src = await copyFromAssets(asset);
src = await processLocalImage(src);
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pdf Content</title>
<style>
body {
font-size: 16px;
color: rgb(255, 196, 0);
}
h1 {
text-align: center;
}
.imgContainer {
display: flex;
flex-direction: row;
align-items: center;
}
.userImage {
width: 50px;
height: 50px;
border-radius: 100px;
}
.list {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
justify-content: space-between;
}
.key {
font-family: "Inter", sans-serif;
font-weight: 600;
color: #000;
font-size: 12px;
line-height: 1.2;
width: 40%;
}
.value {
font-family: "Inter", sans-serif;
font-weight: 600;
color: #000;
font-size: 12px;
line-height: 1.2;
text-transform: capitalize;
width:60%;
flex-wrap: wrap;
}
</style>
</head>
<body>
<div class="imgContainer">
<img
src=${src}
alt="logo"
class="userImage"
/>
<h1>Treasury Jewels</h1>
</div>
<div class="confirmationBox_content">
${invoice
.map(
el =>
`<div
class="list"
key=${el.id}
>
<p class="key">${el.key}</p>
<p class="key">${el.value}</p>
</div>`,
)
.join('')}
</div>
</body>
</html>
`;
} catch (error) {
console.log('pdf generation error', error);
}
};
Conclusion
PDF generation will be very easy if you follow this guide. You can also play around with other packages used for pdf generation if the package used in this article doesn't create the desired user experience you want for your user.
Top comments (1)
I am testing on physical IOS device, upon execution the createPDF functions consoles an address but I couldn't find my PDF there, infact I am unable to locate Documents directory in my iPhone
PS: the createPDF fn works just fine on android