React Native PDF Digital Signature

In this post we are going to explore, making a component for signing digitally a PDF document.

CASE STUDY By Osledy Bazó FECHA: 07/02/20

In this post we are going to explore, making a component for signing digitally a PDF document.

I will explain one approach, and one way we can do this, take into account that this example is far from perfect, but it can help you into getting a idea of how something like this can be accomplished;

Also we will have to modify Wonday react-native-pdf package A LOT.

Please do not use this code directly as it is not performant nor the best solution for the bests results. This was an experiment to see how far can we get with current libraries for react native, because there are no free alternatives.

So let’s start.

 

Project setup

Start a new react-native-project
react-native init RNPdfSignature

And install our fist dependency for Loading and viewing a PDF document on the screen, we are going to use Wonday react-native-pdf: https://github.com/wonday/react-native-pdf

npm install react-native-pdf rn-fetch-blob

Don’t forget to install the packages via pods
cd ios; pod install; cd ..

We now have our basic React Native application.

Modify the Main Screen to show a PDF as per Wonday example:

import React from ‘react’;
import { StyleSheet, Dimensions, View } from ‘react-native’;

import Pdf from ‘react-native-pdf’;

export default PDFExample = () => {
  const source = {uri:’http://samples.leanpub.com/thereactnativebook-sample.pdf',cache:true};

  return (
    <View style={styles.container}>
      <Pdf
        source={source}
        onLoadComplete={(numberOfPages,filePath, {width, height})=>{
            console.log(`number of pages: ${numberOfPages}`);
            console.log(`width: ${width}`);
            console.log(`height: ${height}`);
        }}
        onPageChanged={(page,numberOfPages)=>{
            console.log(`current page: ${page}`);
        }}
        onError={(error)=>{
            console.log(error);
        }}
        onPressLink={(uri)=>{
            console.log(`Link presse: ${uri}`)
        }}
        style={styles.pdf}/>
    </View>
  )
}
const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: ‘center’,
    marginTop: 25,
    backgroundColor: ‘#f4f4f4’
  },
  pdf: {
    width:Dimensions.get('window').width,
    height: Dimensions.get('window'). height,
  }
});

Making the signature to work will be easier if we show only one page on the screen, so modify the code for the pdf component like this:

<Pdf
  minScale={1.0}
  maxScale={1.0}
  scale={1.0}
  spacing={0}
  fitPolicy={0}
  enablePaging={true}
  …
/>

It will help us also, to show the pdf on a fixed height container

container: {
  flex:1,
  justifyContent: ‘center’,
  alignItems: ‘center’,
  marginTop: 25,
  backgroundColor: ‘#f4f4f4’
},
pdf: {
  width:Dimensions.get(‘window’).width,
  height: 540,
}

Support for onPageSingleTap

Wonday package react-native-pdf has support for onPageSingleTap,
We need this latter to place the signature on the coordinates the user has tapped.
Unfortunately, Wonday removed the support for this feature (maybe by mistake) and the current version of the package does not work (6.0.1).

PLEASE: use original Wonday package one the issue is solved. Reference: onPageSingleTap not working properly · Issue #431 · wonday/react-native-pdf · GitHub

Replace Wonday react-native-pdf with https://github.com/bouncingshield/react-native-pdf.git#restore_onPageSingleTap

// package.json
…
  “dependencies”: {
    …
    “react-native-pdf”: “git+https://github.com/bouncingshield/react-native-pdf.git#restore_onPageSingleTap”,
    …
  },
…

Once we add the function handler to App.js:

// App.js
<Pdf
…
  onPageSingleTap={(page, x, y) => {
    console.log(`tap: ${page}`);
    console.log(`x: ${x}`);
    console.log(`y: ${y}`);
  }}
…
/>

We should see something like this on the console:

Store PDF on device

In order for working with the pdf, we will need to download and store it to have it on our device, so later we can add the signature and edit and also save the pdf. We are going to use RNFS package for this.

npm install react-native-fs —save
react-native link react-native-fs
cd ios; pod install; cd ..

Let’s store the document on the device on the app launch.

import React, { useEffect, useState } from ‘react’;
import { StyleSheet, Dimensions, View, Text } from ‘react-native’;

import Pdf from ‘react-native-pdf’;
const RNFS = require(‘react-native-fs’);

export default PDFExample = () => {
  const sourceUrl = ‘http://samples.leanpub.com/thereactnativebook-sample.pdf’;
  const filePath = `${RNFS.DocumentDirectoryPath}/react-native.pdf`;

  const [fileDownloaded, setFileDownloaded] = useState(false);

  useEffect(() => {
    this.downloadFile()
  }, []);

  downloadFile = () => {
    console.log(“___downloadFile -> Start”);

    RNFS.downloadFile({
       fromUrl: sourceUrl,
       toFile: filePath,
    }).promise.then((res) => {
      console.log(“___downloadFile -> File downloaded”, res);
      setFileDownloaded(true);
    })
  }

  return (
    <View style={styles.container}>
      { fileDownloaded && (
        <Pdf
          minScale={1.0}
          maxScale={1.0}
          scale={1.0}
          spacing={0}
          fitPolicy={0}
          enablePaging={true}
          source={{uri: filePath}}
          usePDFKit={false}
          onLoadComplete={(numberOfPages,filePath, {width, height})=>{
              console.log(`number of pages: ${numberOfPages}`);
              console.log(`width: ${width}`);
              console.log(`height: ${height}`);
          }}
          onPageSingleTap={(page, x, y) => {
            console.log(`tap: ${page}`);
            console.log(`x: ${x}`);
            console.log(`y: ${y}`);
          }}
          onPageChanged={(page,numberOfPages)=>{
              console.log(`current page: ${page}`);
          }}
          onError={(error)=>{
              console.log(error);
          }}
          onPressLink={(uri)=>{
              console.log(`Link presse: ${uri}`)
          }}
          style={styles.pdf}/>
        )}
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    marginTop: 25,
    backgroundColor: '#f4f4f4'
  },
  pdf: {
    width: Dimensions.get('window').width,
    height: 540,
  }
});

Get signature from user

Let’s get the digital signature for placing it on the PDF file.
We will ask the user to sing using the phone screen as a signature pad and store the resulting image. For this I’m going to use: react-native-signature-canvas

npm install —save react-native-signature-canvas

Be careful with this issue: Invariant Violation: requireNativeComponent: “RNCWebView” was not found in the UIManager · Issue #18 · YanYuanFE/react-native-signature-canvas · GitHub

import React, { useEffect, useState } from ‘react’;
import { StyleSheet, Dimensions, View, Text, Button } from ‘react-native’;

import Pdf from ‘react-native-pdf’;
const RNFS = require(‘react-native-fs’);

import Signature from ‘react-native-signature-canvas’;

export default PDFExample = () => {
  const sourceUrl = ‘http://samples.leanpub.com/thereactnativebook-sample.pdf’;
  const filePath = `${RNFS.DocumentDirectoryPath}/react-native.pdf`;

  const [fileDownloaded, setFileDownloaded] = useState(false);
  const [getSignaturePad, setSignaturePad] = useState(false);
  const [signatureBase64, setsignatureBase64] = useState(“”);

  useEffect(() => {
    this.downloadFile()
  }, []);

  downloadFile = () => {
    console.log("___downloadFile -> Start");

    RNFS.downloadFile({
       fromUrl: sourceUrl,
       toFile: filePath,
    }).promise.then((res) => {
      console.log("___downloadFile -> File downloaded", res);
      setFileDownloaded(true);
    })
  }

  getSignature = () => {
    console.log(“___getSignature -> Start”);
    setSignaturePad(true);
  }

  handleSignature = signature => {
    console.log(“___handleSignature -> Start”, signature);
    setsignatureBase64(signature);
    setSignaturePad(false);
  }

  return (
    <View style={styles.container}>
      { getSignaturePad ? (
        <Signature
        onOK={(sig) => this.handleSignature(sig)}
        onEmpty={() => console.log(‘___onEmpty’)}
        descriptionText=“Sign”
        clearText=“Clear”
        confirmText=“Save”
      />
      ) : ((fileDownloaded) && (
        <View>
          <Button
            title=“Sign Document”
            onPress={this.getSignature}
          />
          <Pdf
            minScale={1.0}
            maxScale={1.0}
            scale={1.0}
            spacing={0}
            fitPolicy={0}
            enablePaging={true}
            source={{uri: filePath}}
            usePDFKit={false}
            onLoadComplete={(numberOfPages,filePath, {width, height})=>{
                console.log(`number of pages: ${numberOfPages}`);
                console.log(`width: ${width}`);
                console.log(`height: ${height}`);
            }}
            onPageSingleTap={(page, x, y) => {
              console.log(`tap: ${page}`);
              console.log(`x: ${x}`);
              console.log(`y: ${y}`);
            }}
            onPageChanged={(page,numberOfPages)=>{
                console.log(`current page: ${page}`);
            }}
            onError={(error)=>{
                console.log(error);
            }}
            onPressLink={(uri)=>{
                console.log(`Link presse: ${uri}`)
            }}
            style={styles.pdf}/>
        </View>
      ))}
    
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: ‘center’,
    alignItems: ‘center’,
    marginTop: 25,
    backgroundColor: ‘#f4f4f4’
  },
  pdf: {
    width: Dimensions.get(‘window’).width,
    height: 540,
  }
});

After we have the signature stored as Base64 string. We will aso need to convert all Base64 files into ArrayBuffer. Then, we can start manipulating the pdf file.

Place signature on PDF

We are going to ask the user where they like the signature to be placed. For this the flow is something like this:

  • We need a way to put the pdf view in “edit mode”.
  • Ask the user for the place of the signature.
  • Save the touched coordinates.
  • Add the signature to the pdf on the coordinates.
  • And save the pdf document.

For all this we need now a package for manipulating the pdf file, edit it and add images. The package we are going to use is GitHub – Hopding/pdf-lib: Create and modify PDF documents in any JavaScript environment

npm install pdf-lib base-64 --save

A lot of modifications going on here:

/**
 * Copyright (c) 2020-present, Bouncing Shield (bouncingshield.com)
 * All rights reserved.
 *
 * This source code is licensed under the MIT-style license found in the
 * LICENSE file in the root directory of this source tree.
 */

import React, { useEffect, useState } from 'react';
import { StyleSheet, Dimensions, View, Text, Image, TouchableOpacity } from 'react-native';

import Pdf from 'react-native-pdf';
const RNFS = require('react-native-fs');
import { PDFDocument } from 'pdf-lib';
import Signature from 'react-native-signature-canvas';
import { decode as atob, encode as btoa } from 'base-64'

export default PDFExample = () => {
  const sourceUrl = 'http://samples.leanpub.com/thereactnativebook-sample.pdf';

  const [fileDownloaded, setFileDownloaded] = useState(false);
  const [getSignaturePad, setSignaturePad] = useState(false);
  const [pdfEditMode, setPdfEditMode] = useState(false);
  const [signatureBase64, setSignatureBase64] = useState(null);
  const [signatureArrayBuffer, setSignatureArrayBuffer] = useState(null);
  const [pdfBase64, setPdfBase64] = useState(null);
  const [pdfArrayBuffer, setPdfArrayBuffer] = useState(null);
  const [newPdfSaved, setNewPdfSaved] = useState(false);
  const [newPdfPath, setNewPdfPath] = useState(null);
  const [pageWidth, setPageWidth] = useState(0);
  const [pageHeight, setPageHeight] = useState(0);
  const [filePath, setFilePath] = useState(`${RNFS.DocumentDirectoryPath}/react-native.pdf`);

  useEffect(() => {
    this.downloadFile();
    if (signatureBase64){
      setSignatureArrayBuffer(this._base64ToArrayBuffer(signatureBase64));
    }
    if (newPdfSaved){
      setFilePath(newPdfPath);
      setPdfArrayBuffer(this._base64ToArrayBuffer(pdfBase64));
    }
  }, [signatureBase64, filePath, newPdfSaved]);

  _base64ToArrayBuffer = (base64) => {
    const binary_string = atob(base64);
    const len = binary_string.length;
    const bytes = new Uint8Array(len);
    for (let i = 0; i < len; i++) {
      bytes[i] = binary_string.charCodeAt(i);
    }
    return bytes.buffer;
  }

  _uint8ToBase64 = (u8Arr) => {
    const CHUNK_SIZE = 0x8000; //arbitrary number
    let index = 0;
    const length = u8Arr.length;
    let result = '';
    let slice;
    while (index < length) {
      slice = u8Arr.subarray(index, Math.min(index + CHUNK_SIZE, length));
      result += String.fromCharCode.apply(null, slice);
      index += CHUNK_SIZE;
    }
    return btoa(result);
  }

  downloadFile = () => {
    if (!fileDownloaded){
      RNFS.downloadFile({
        fromUrl: sourceUrl,
        toFile: filePath,
     }).promise.then((res) => {
       setFileDownloaded(true);
       this.readFile();
     });
    }
  }

  readFile = () => {
    RNFS.readFile(`${RNFS.DocumentDirectoryPath}/react-native.pdf`, 'base64').then((contents) => {
      setPdfBase64(contents);
      setPdfArrayBuffer(this._base64ToArrayBuffer(contents));
    })
  }

  getSignature = () => {
    setSignaturePad(true);
  }

  handleSignature = signature => {
    setSignatureBase64(signature.replace('data:image/png;base64,', ''));
    setSignaturePad(false);
    setPdfEditMode(true);
  }

  handleSingleTap = async (page, x, y) => {
    if (pdfEditMode){
      setNewPdfSaved(false);
      setFilePath(null);
      setPdfEditMode(false);
      const pdfDoc = await PDFDocument.load(pdfArrayBuffer);
      const pages = pdfDoc.getPages();
      const firstPage = pages[page - 1]

      // The meat
      const signatureImage = await pdfDoc.embedPng(signatureArrayBuffer)
      firstPage.drawImage(signatureImage, {
        x: ((pageWidth * (x - 12)) / Dimensions.get('window').width),
        y: pageHeight - ((pageHeight * (y + 12)) / 540),
        width: 50,
        height: 50,
      });
      // Play with these values as every project has diferent requirements

      const pdfBytes = await pdfDoc.save();
      const pdfBase64 = this._uint8ToBase64(pdfBytes);
      const path = `${RNFS.DocumentDirectoryPath}/react-native_signed_${Date.now()}.pdf`;
  
      RNFS.writeFile(path, pdfBase64, 'base64').then((success) => {
        setNewPdfPath(path);
        setNewPdfSaved(true);
        setPdfBase64(pdfBase64);
      })
      .catch((err) => {
        console.log(err.message);
      });
    }
  }

  return (
    <View style={styles.container}>
      { getSignaturePad ? (
        <Signature
          onOK={(sig) => this.handleSignature(sig)}
          onEmpty={() => console.log('___onEmpty')}
          descriptionText="Sign"
          clearText="Clear"
          confirmText="Save"
        />
      ) : ((fileDownloaded) && (
        <View>
          { filePath ? (
            <View>
              <Text style={styles.headerText}>React Native Digital PDF Signature</Text>
              <Pdf
                minScale={1.0}
                maxScale={1.0}
                scale={1.0}
                spacing={0}
                fitPolicy={0}
                enablePaging={true}
                source={{uri: filePath}}
                usePDFKit={false}
                onLoadComplete={(numberOfPages,filePath, {width, height})=>{
                  setPageWidth(width);
                  setPageHeight(height);
                }}
                onPageSingleTap={(page, x, y) => {
                  this.handleSingleTap(page, x, y);
                }}
                style={styles.pdf}/>
            </View>
           ) : (
             <View style={styles.button}>
               <Text style={styles.buttonText}>Saving PDF File...</Text>
             </View>
           )}
          { pdfEditMode ? (
            <View style={styles.message}>
              <Text>* EDIT MODE *</Text>
              <Text>Touch where you want to place the signature</Text>
            </View>
          ) : (filePath &&  (
            <View>
              <TouchableOpacity
                onPress={this.getSignature}
                style={styles.button}
              >
                <Text style={styles.buttonText}>Sign Document</Text>
              </TouchableOpacity>
              <View>
                <Image 
                  source={{uri: 'http://www.bouncingshield.com/icons/icon-512x512.png'}}
                  style={{width: 40, height: 40, alignSelf: 'center'}}
                />
                <Text style={styles.headerText}>bouncingshield.com</Text>
              </View>
            </View>
          ))}
        </View>
      ))}
    
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#f4f4f4'
  },
  headerText: {
    color: '#508DBC',
    fontSize: 20,
    marginBottom: 20,
    alignSelf: 'center'
  },
  pdf: {
    width: Dimensions.get('window').width,
    height: 540,
  },
  button: {
    alignItems: 'center',
    backgroundColor: '#508DBC',
    padding: 10,
    marginVertical: 10
  },
  buttonText: {
    color: "#DAFFFF",
  },
  message: {
    alignItems: 'center',
    padding: 15,
    backgroundColor: '#FFF88C'
  }
});

Now we have something like this:

Applications

From here on you can think of many implementations as:

  • Load the pdf from the user device documents.
  • Save signature on device and use it as many times as required.
  • Save many signatures and choose which one to use.
  • Change signature size and color.
  • Share the signed pdf.
  • Add other kind of images as stamps or geometrical forms.
  • Etc.

Please do not use this code directly as it is not performant nor the best solution for the bests results. This was an experiment to see how far can we get with current libraries for react native, because there are no free alternatives.

 

If you have any question, you can contact us at info@bouncingshield.com.
And https://bouncingshield.com

© 2019 BOUNCING SHIELD ALL RIGHTS RESERVED #REMOTEWORK #NOMADLIFE

Siguenos