React Native PDF Digital Signature, with hooks - 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
I’ve been exploring different ways to handle digital signatures in React Native, and your method is really informative! I wanted to share a tool that has made my life significantly easier when working with PDFs https://pdfguru.com/ . It’s been incredibly reliable for creating and managing documents. The user interface is intuitive, and it integrates seamlessly into my workflow, saving me a ton of time.
Hello, thank you for the excellent content! I’ve applied the coordinates calculation section to a personal project, but I’m curious if there’s a method to adapt it to a variable scale of PDF view? Thank you in advance for your article.
Hi @lanarey23
As this was a long time ago, I don’t have a new method. But I’m sure this can be calculated.
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