Digital Signature Android Demo.

Anik Dey
4 min readMay 12, 2021

--

Here I will share a feature that I needed to implement in one of my project. The requirement was to add a signature from the app to a pdf file that is generated from the server side. There are few limitations in the way the feature was implemented. But still I thought of sharing with you guys.

Please have a look for an overview of the functionality.

Let us start with the code. You can find the source code here.

Add these dependencies in the gradle file

implementation 'com.github.barteksc:android-pdf-viewer:2.8.2'
implementation 'com.github.gcacace:signature-pad:1.2.1'
implementation 'com.itextpdf:itextg:5.5.10'

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<com.github.barteksc.pdfviewer.PDFView
android:id="@+id/pdfView"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/signDocumentButton"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>

<androidx.appcompat.widget.AppCompatButton
android:id="@+id/signDocumentButton"
style="@style/DigitalSignPanelButton"
android:layout_width="wrap_content"
android:text="Sign Document"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pdfView"/>

<com.github.gcacace.signaturepad.views.SignaturePad
android:visibility="gone"
android:id="@+id/signaturePad"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/buttonContainer" />


<LinearLayout
android:visibility="gone"
android:id="@+id/buttonContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent">

<androidx.appcompat.widget.AppCompatButton
android:id="@+id/fromGallery"
style="@style/DigitalSignPanelButton"
android:layout_width="0dp"
android:layout_weight="1"
android:text="Saved Signature" />

<androidx.appcompat.widget.AppCompatButton
android:id="@+id/clearPad"
style="@style/DigitalSignPanelButton"
android:layout_width="0dp"
android:layout_weight="1"
android:text="Clear" />

<androidx.appcompat.widget.AppCompatButton
android:id="@+id/saveSignature"
style="@style/DigitalSignPanelButton"
android:layout_width="0dp"
android:layout_weight="1"
android:text="Generate PDF" />

</LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

saved_signature_alert.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:layout_gravity="center"
android:gravity="center">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/savedSignatureImageView"
android:layout_width="200dp"
android:layout_height="200dp"
tools:src="@mipmap/ic_launcher"/>

</LinearLayout>

MainActivity

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setUpListener()
FileUtil.copyAsset(getContext(), assets)

val tmpLocalFile = File(getContext().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString(), Constants.ORIGINAL_PDF_NAME)
binding.pdfView.fromFile(tmpLocalFile).load()
}

private fun setUpListener() {

binding.signDocumentButton.setOnClickListener {

binding.pdfView.visibility = View.GONE
binding.signDocumentButton.visibility = View.GONE

binding.signaturePad.visibility = View.VISIBLE
binding.buttonContainer.visibility = View.VISIBLE
}

binding.clearPad.setOnClickListener{ binding.signaturePad.clear() }

binding.signaturePad.setOnSignedListener(object : SignaturePad.OnSignedListener {

override fun onStartSigning() {}

override fun onSigned() {
binding.saveSignature.isEnabled = true
binding.clearPad.isEnabled = true
}

override fun onClear() {
binding.saveSignature.isEnabled = false
binding.clearPad.isEnabled = false
}

})

binding.saveSignature.setOnClickListener {
try {
FileUtil.saveSignature(binding.signaturePad.signatureBitmap, getContext())?.let { signaturePath->
FileUtil.generateSignedPdf(signaturePath, getContext())?.let { generatedPdfPath->
loadPreviewActivity(generatedPdfPath)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}

binding.fromGallery.setOnClickListener {
val path = getContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES).toString() + "/"+Constants.SAVED_SIGNATURE_IMAGE_NAME
if(isSignatureExists(path)) {
showAlertDialog(path)
} else {
Toast.makeText(getContext(), "There is no previously saved signature.", Toast.LENGTH_SHORT).show()
}
}
}

private fun showAlertDialog(path: String) {
val builder = AlertDialog.Builder(this)
var view = layoutInflater.inflate(R.layout.saved_signature_alert, null)
var imageView = view.findViewById<AppCompatImageView>(R.id.savedSignatureImageView)
val bitmap = BitmapFactory.decodeFile(path)
imageView.setImageBitmap(bitmap)
builder.setView(view)
builder.apply {
setPositiveButton("Ok") { _, _ ->
FileUtil.generateSignedPdf(path, getContext())?.let {
loadPreviewActivity(it)
}
}
setNegativeButton("Cancel") { _, _ ->

}
}
builder.show()
}

private fun isSignatureExists(path: String): Boolean {
return File(path).exists()
}

private fun getContext(): Context {
return this
}

private fun loadPreviewActivity(generatedPdfPath: String) {
startActivity(PreviewActivity.newIntent(getContext(), generatedPdfPath))
}
}

activity_preview.xml

<?xml version="1.0" encoding="utf-8"?>
<layout>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".PreviewActivity">

<com.github.barteksc.pdfviewer.PDFView
android:id="@+id/pdfView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

PreviewActivity

class PreviewActivity : AppCompatActivity() {

private lateinit var binding: ActivityPreviewBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityPreviewBinding.inflate(layoutInflater)
setContentView(binding.root)

var path = intent.getStringExtra(EXTRA_GENERATED_PDF_PATH)
var tmpLocalFile = File(path)
binding.pdfView.fromFile(tmpLocalFile).load()
}

companion object {

private const val EXTRA_GENERATED_PDF_PATH = "generated_pdf_path"

@JvmStatic
fun newIntent(context: Context, generatedPdfPath: String) : Intent {
var intent = Intent(context, PreviewActivity::class.java)
intent.putExtra(EXTRA_GENERATED_PDF_PATH, generatedPdfPath)
return intent
}

}

}

FileUtil

class FileUtil {



companion object {

@JvmStatic
fun dateToString(date: Date, format: String): String {
var dateStr = ""
val formatter = SimpleDateFormat(format, Locale.US)
try {
dateStr = formatter.format(date)
} catch (e: Exception) {
e.printStackTrace()
}
return dateStr
}

@JvmStatic
fun copyAsset(context: Context, assetManager: AssetManager) {
val path = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString()
val dir = File(path)
if (!dir.exists()) {
dir.mkdirs()
}
var inputStream: InputStream? = null
var outputStream: OutputStream? = null
try {
inputStream = assetManager.open("sample.pdf")
val outFile = File(path, Constants.ORIGINAL_PDF_NAME)
outputStream = FileOutputStream(outFile)
copyFile(inputStream, outputStream)
} catch (e: java.lang.Exception) {
val message = e.message
}
}

@JvmStatic
private fun copyFile(inputStream: InputStream, outputStream: OutputStream) {
val buffer = ByteArray(1024)
var read: Int
while (inputStream.read(buffer).also { read = it } != -1) {
outputStream.write(buffer, 0, read)
}
}

@JvmStatic
private fun generateDate(width: Float, context: Context): String {

val bitmap = Bitmap.createBitmap(width.toInt(), 25, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.drawColor(Color.WHITE)
val paint = Paint()
paint.color = Color.BLACK
paint.isAntiAlias = true
paint.textSize = 10f

val r = Rect()
canvas.getClipBounds(r)
val cHeight: Int = r.height()
val cWidth: Int = r.width()
paint.textAlign = Paint.Align.LEFT

var formattedDate = dateToString(Date(), Constants.SIGNATURE_DATE_FORMAT)

paint.getTextBounds(formattedDate, 0, formattedDate.length, r)
val x: Float = cWidth / 2f - r.width() / 2f - r.left
val y: Float = cHeight / 2f + r.height() / 2f - r.bottom
canvas.drawText(formattedDate, x, y, paint)
canvas.save()
canvas.restore()

val path = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + Constants.GENERATED_SIGNATURE_IMAGE_NAME
val fos = FileOutputStream(File(path))
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
fos.flush()
fos.close()
return path
}

@JvmStatic
fun saveSignature(signature: Bitmap, context: Context): String? {
try {
val path = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES).toString()
val dir = File(path)
if (!dir.exists()) {
dir.mkdirs()
}
var output = File(path, Constants.SAVED_SIGNATURE_IMAGE_NAME)
saveBitmapToPNG(signature, output, context)
return output.absolutePath
} catch (e: IOException) {
e.printStackTrace()
}
return null
}

@Throws(IOException::class)
private fun saveBitmapToPNG(bitmap: Bitmap, photo: File, context: Context) {
val newBitmap = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(newBitmap)
canvas.drawColor(ContextCompat.getColor(context, R.color.black))
canvas.drawBitmap(bitmap, 0f, 0f, null)
val stream: OutputStream = FileOutputStream(photo)
newBitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
stream.close()
}

@JvmStatic
fun generateSignedPdf(signatureImagePath: String, context: Context): String? {
var srcPdf = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + Constants.ORIGINAL_PDF_NAME
var destPdf = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + Constants.GENERATED_PDF_NAME
try{
val reader = PdfReader(srcPdf)
val stamper = PdfStamper(reader, FileOutputStream(destPdf))
val content = stamper.getOverContent(1)
val image = Image.getInstance(signatureImagePath)
image.scalePercent(15f, 2f)
image.setAbsolutePosition(262f, 115f)
image.alignment = Image.RIGHT
content.addImage(image)
val path = generateDate(image.scaledWidth, context)
val dateImage = Image.getInstance(path)
dateImage.setAbsolutePosition(262f, image.absoluteY - dateImage.height)
content.addImage(dateImage)
stamper.close()
} catch (e: Exception){
var message = e.message
}
return destPdf
}

}

}

Here is the Constants

class Constants {

companion object {

const val ORIGINAL_PDF_NAME = "/copied.pdf"
const val GENERATED_PDF_NAME = "/generated.pdf"
const val GENERATED_SIGNATURE_IMAGE_NAME = "/date_signature.png"
const val SAVED_SIGNATURE_IMAGE_NAME = "/signature.png"
const val SIGNATURE_DATE_FORMAT = "hh:mm a dd-mm-yyyy"

}

}

Here is the git repository link.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

No responses yet

Write a response