Codementor Events

React Native PDF Digital Signature, with hooks - 2020

Published Feb 07, 2020Last updated Aug 30, 2020

In this post we are going to explore, making a component for signing digitally a PDF document this is part of Alameda Dev Explorations

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;

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.

This is an example of how it looks.

Find the complete code here https://github.com/uokesita/RNPdfSignature

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.

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
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 press: ${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 react-native-signature-canvas --save

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

The final code will look like this now:

/**
 * Copyright (c) 2020-present, Alameda Dev (alamedadev.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, Platform } 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));
    }
    console.log('filePath', filePath)
  }, [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)
      if (Platform.OS == 'ios') {
        firstPage.drawImage(signatureImage, {
          x: ((pageWidth * (x - 12)) / Dimensions.get("window").width),
          y: pageHeight - ((pageHeight * (y + 12)) / 540),
          width: 50,
          height: 50,
        })
      } else {
        firstPage.drawImage(signatureImage, {
          x: (firstPage.getWidth() * x ) / pageWidth,
          y: (firstPage.getHeight() - ((firstPage.getHeight() * y ) / pageHeight)) - 25,
          width: 50,
          height: 50,
        })
      }
      // Play with these values as every project has different requirements

      const pdfBytes = await pdfDoc.save();
      const pdfBase64 = this._uint8ToBase64(pdfBytes);
      const path = `${RNFS.DocumentDirectoryPath}/react-native_signed_${Date.now()}.pdf`;
      console.log('path', path)

      
      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.alamedadev.com/icons/icon-512x512.png"}}
                  style={{width: 40, height: 40, alignSelf: "center"}}
                />
                <Text style={styles.headerText}>alamedadev.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 hola@alamedadev.com.
And https://alamedadev.com

Discover and read more posts from Osledy Bazó
get started
post commentsBe the first to share your opinion
Joan Young
6 months ago

Exploring react native PDF digital signature with hooks is an exciting venture that aligns with the broader landscape of business process automation (BPA). BPA https://gowombat.team/business-process-automation-service is a transformative approach that leverages technology to streamline and enhance workflows, and the integration of digital signatures is a significant step in this direction

wilian-wagner
a year ago

Hi, thanks for the great content! I have used the coordinates calculate part to a personal job, but i need to know if there is a way to make it work with variable scale of pdf view? In advance thanks for your article

Vishesh Gupta
3 years ago

How can i convert bufferdata to base64 or bolb string url

Show more replies