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>
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>
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()
}
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,
}
}
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.