DEV Community

Mercy Ademiju for Cudium

Posted on

HOW TO GENERATE PDF IN REACT-NATIVE (CROSS-PLATFORM)

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
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Run the code: check out documentation

npm start

npm run android

npm run ios
Enter fullscreen mode Exit fullscreen mode

android app
ios app
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
Enter fullscreen mode Exit fullscreen mode

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 ..
Enter fullscreen mode Exit fullscreen mode

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"/>
Enter fullscreen mode Exit fullscreen mode

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,
  },
});
Enter fullscreen mode Exit fullscreen mode

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

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);
    }
  };
Enter fullscreen mode Exit fullscreen mode

Result:

pdf with comma

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>
`;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 ..
Enter fullscreen mode Exit fullscreen mode

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);
    }
  };
Enter fullscreen mode Exit fullscreen mode

pdf with image

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

Top comments (1)

Collapse
 
alimurad1 profile image
alimurad-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