How to Embed Text and Hand-Drawn Signatures into PDFs with Vue 3 and pdf-lib

javascript dev.to

A client sent me a contract last week. I needed to sign it and send it back without installing anything or uploading the file to a server. So I built a signature feature into my Vue 3 PDF toolkit.

It supports two input methods: typing your name with a signature font, or drawing your actual signature on an HTML5 canvas. Both are embedded into the PDF using pdf-lib.


The Signature Input Components

1. Text Signature

The user types their name, selects a font, and we render it to a canvas.

<template>
  <canvas ref="textCanvas" width="400" height="120" />
</template>

<script setup lang="ts">
import { ref, watch } from 'vue'

const props = defineProps<{ text: string; font: string; color: string }>()
const textCanvas = ref<HTMLCanvasElement>()

watch(() => [props.text, props.font, props.color], () => {
  const canvas = textCanvas.value
  if (!canvas) return
  const ctx = canvas.getContext('2d')!
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  ctx.font = `bold 48px "${props.font}"`
  ctx.fillStyle = props.color
  ctx.textBaseline = 'middle'
  const metrics = ctx.measureText(props.text)
  const x = (canvas.width - metrics.width) / 2
  const y = canvas.height / 2
  ctx.fillText(props.text, x, y)
}, { immediate: true })
</script>
Enter fullscreen mode Exit fullscreen mode

I load Google Fonts like "Great Vibes", "Dancing Script", and "Parisienne" to give users a few signature-style options.

2. Hand-Drawn Signature

This is a touch-aware canvas that handles both mouse and touch events.

<template>
  <canvas
    ref="drawCanvas"
    @mousedown="start"
    @mousemove="draw"
    @mouseup="stop"
    @touchstart.prevent="start"
    @touchmove.prevent="draw"
    @touchend="stop"
  />
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'

const drawCanvas = ref<HTMLCanvasElement>()
const ctx = ref<CanvasRenderingContext2D | null>(null)
const drawing = ref(false)

onMounted(() => {
  const c = drawCanvas.value
  if (!c) return
  const rect = c.getBoundingClientRect()
  const dpr = window.devicePixelRatio || 1
  c.width = rect.width * dpr
  c.height = rect.height * dpr
  ctx.value = c.getContext('2d')
  if (ctx.value) {
    ctx.value.scale(dpr, dpr)
    ctx.value.lineCap = 'round'
    ctx.value.lineJoin = 'round'
    ctx.value.lineWidth = 2.5
    ctx.value.strokeStyle = '#000000'
  }
})

function getPoint(e: MouseEvent | TouchEvent) {
  const rect = drawCanvas.value!.getBoundingClientRect()
  const ev = 'touches' in e ? e.touches[0] : e
  return { x: ev.clientX - rect.left, y: ev.clientY - rect.top }
}

function start(e: MouseEvent | TouchEvent) {
  drawing.value = true
  const p = getPoint(e)
  ctx.value?.beginPath()
  ctx.value?.moveTo(p.x, p.y)
}

function draw(e: MouseEvent | TouchEvent) {
  if (!drawing.value) return
  const p = getPoint(e)
  ctx.value?.lineTo(p.x, p.y)
  ctx.value?.stroke()
}

function stop() {
  drawing.value = false
}
</script>
Enter fullscreen mode Exit fullscreen mode

Embedding the Signature into the PDF

Both input methods produce a PNG data URL. The embedding step is the same.

import { PDFDocument, degrees } from 'pdf-lib'

async function embedSignature(
  pdfBytes: ArrayBuffer,
  signatureDataUrl: string,
  box: { x: number; y: number; width: number; height: number; page: number; rotation?: number }
) {
  const pdfDoc = await PDFDocument.load(pdfBytes)
  const page = pdfDoc.getPages()[box.page]
  const { width, height } = page.getSize()

  const base64 = signatureDataUrl.split(',')[1]
  const pngBytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0))
  const signatureImage = await pdfDoc.embedPng(pngBytes)

  // Browser preview uses top-left origin; PDF uses bottom-left
  const pdfY = height - box.y - box.height

  page.drawImage(signatureImage, {
    x: box.x,
    y: pdfY,
    width: box.width,
    height: box.height,
    rotate: box.rotation ? degrees(box.rotation) : degrees(0),
  })

  return await pdfDoc.save()
}
Enter fullscreen mode Exit fullscreen mode

Coordinate Mapping

Users interact with a preview canvas that uses top-left coordinates. PDF pages use bottom-left coordinates. We also have to account for preview scaling.

function mapPreviewToPdf(
  previewX: number,
  previewY: number,
  previewWidth: number,
  previewHeight: number,
  pdfWidth: number,
  pdfHeight: number
) {
  const scale = pdfWidth / previewWidth
  return {
    x: previewX * scale,
    y: (previewHeight - previewY) * scale,
  }
}
Enter fullscreen mode Exit fullscreen mode

This keeps the signature exactly where the user placed it, regardless of screen size.


Why Keep It in the Browser?

Server-side signing is easier. Upload the file, overlay the signature, download. But the document and signature travel to a third-party server.

Client-side signing keeps the PDF on the user's device. The trade-off is handling canvas rendering, coordinate math, and binary PDF embedding yourself. For personal and legal documents, that trade-off is usually worth it.

You can see the final tool here: en.sotool.top/sign-pdf


Need certificate-based digital signatures, OCR, or advanced PDF protection? Wondershare PDFelement is a desktop alternative worth considering. This post contains affiliate links.

Source: dev.to

arrow_back Back to Tutorials