HOW TO GENERATE PDF IN REACT-NATIVE (CROSS-PLATFORM)
Tags: javascript, learning, mobile, reactnative
Published: 2023-09-18 10:09:40

## 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](https://reactnative.dev/docs/environment-setup?guide=native)

```
npm start

npm run android

npm run ios
```

![android app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/az0ebig03kbc2vwfyors.png) 
![ios app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hpuelc1euhugm9od6duo.png)
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](https://www.npmjs.com/package/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.



![generated pdf](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/27pb6cr3nmrujzhv1n0e.png)

**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: 

![pdf with comma](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wp71idktbfmvhckqkvpn.png)

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:
   - [expo-assets](https://docs.expo.dev/versions/latest/sdk/asset/)
   - [expo-image-manipulator](https://docs.expo.dev/versions/latest/sdk/imagemanipulator/)

**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](https://docs.expo.dev/bare/installing-expo-modules/)

```
npx install-expo-modules@latest

#IOS pod installation
cd ios && pod install && cd ..
```
[expo-assets](https://docs.expo.dev/versions/latest/sdk/asset/) will allow us get the file path of the asset from memory  while [expo-image-manipulator](https://docs.expo.dev/versions/latest/sdk/imagemanipulator/) 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);
    }
  };
```


![pdf with image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mkmk2u8p80rwc3t73960.png)

## 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. 

###Useful links
- [tutorial repo](https://github.com/luwa-star/pdf-tutorial.git)