Fix: pdf-lib Not Working — PDF Not Generating, Fonts Not Embedding, or Pages Blank
Quick Answer
How to fix pdf-lib issues — creating PDFs from scratch, modifying existing PDFs, embedding fonts and images, form filling, merging documents, and browser and Node.js usage.
The Problem
A generated PDF is empty:
import { PDFDocument } from 'pdf-lib';
const pdf = await PDFDocument.create();
const page = pdf.addPage();
const bytes = await pdf.save();
// Opens but shows a blank pageOr text renders as squares or missing characters:
page.drawText('Hello こんにちは', { x: 50, y: 500, size: 20 });
// "Hello" renders fine, Japanese characters are squaresOr modifying an existing PDF loses its content:
const existingPdf = await PDFDocument.load(pdfBytes);
// Loaded PDF shows blank pagesWhy This Happens
pdf-lib is a JavaScript library for creating and modifying PDFs. Unlike server-side tools (wkhtmltopdf, Puppeteer), it generates PDF structures directly:
- Pages are blank by default —
addPage()creates an empty page. You must explicitly draw text, images, or shapes. Nothing appears automatically. - Only standard 14 fonts are built-in — Times Roman, Helvetica, and Courier (with bold/italic variants). For non-Latin characters (CJK, Arabic, emoji), you must embed a custom font. Unembedded characters render as squares or tofu.
- Content position uses bottom-left origin — PDF coordinates start at the bottom-left corner (not top-left like HTML).
y: 0is the bottom of the page. This is the opposite of what most developers expect. - pdf-lib works in both browser and Node.js — it’s pure JavaScript with no native dependencies. But loading files differs between environments (fetch vs fs.readFile).
Fix 1: Create a PDF from Scratch
npm install pdf-libimport { PDFDocument, StandardFonts, rgb, degrees } from 'pdf-lib';
async function createPDF() {
const pdf = await PDFDocument.create();
// Embed a standard font
const font = await pdf.embedFont(StandardFonts.Helvetica);
const boldFont = await pdf.embedFont(StandardFonts.HelveticaBold);
// Add a page (A4 size by default, or specify)
const page = pdf.addPage([595.28, 841.89]); // A4 in points
const { width, height } = page.getSize();
// Draw text — y coordinate is from BOTTOM
page.drawText('Invoice', {
x: 50,
y: height - 80, // 80 points from top
size: 28,
font: boldFont,
color: rgb(0.1, 0.1, 0.2),
});
page.drawText('Invoice #INV-001', {
x: 50,
y: height - 120,
size: 12,
font: font,
color: rgb(0.4, 0.4, 0.4),
});
page.drawText('Date: March 29, 2026', {
x: 50,
y: height - 140,
size: 12,
font: font,
color: rgb(0.4, 0.4, 0.4),
});
// Draw a line
page.drawLine({
start: { x: 50, y: height - 160 },
end: { x: width - 50, y: height - 160 },
thickness: 1,
color: rgb(0.8, 0.8, 0.8),
});
// Table-like layout
const items = [
{ description: 'Web Development', qty: 40, rate: 150 },
{ description: 'Design Services', qty: 20, rate: 120 },
{ description: 'Consulting', qty: 10, rate: 200 },
];
let yPosition = height - 200;
// Table header
page.drawText('Description', { x: 50, y: yPosition, size: 10, font: boldFont });
page.drawText('Qty', { x: 300, y: yPosition, size: 10, font: boldFont });
page.drawText('Rate', { x: 380, y: yPosition, size: 10, font: boldFont });
page.drawText('Total', { x: 460, y: yPosition, size: 10, font: boldFont });
yPosition -= 20;
for (const item of items) {
page.drawText(item.description, { x: 50, y: yPosition, size: 10, font });
page.drawText(String(item.qty), { x: 300, y: yPosition, size: 10, font });
page.drawText(`$${item.rate}`, { x: 380, y: yPosition, size: 10, font });
page.drawText(`$${item.qty * item.rate}`, { x: 460, y: yPosition, size: 10, font });
yPosition -= 18;
}
// Total
yPosition -= 10;
page.drawLine({
start: { x: 380, y: yPosition + 5 },
end: { x: width - 50, y: yPosition + 5 },
thickness: 1,
color: rgb(0, 0, 0),
});
const total = items.reduce((sum, i) => sum + i.qty * i.rate, 0);
page.drawText(`Total: $${total}`, {
x: 380,
y: yPosition - 15,
size: 14,
font: boldFont,
});
// Draw a rectangle (colored box)
page.drawRectangle({
x: 50,
y: 50,
width: width - 100,
height: 40,
color: rgb(0.95, 0.95, 0.95),
borderColor: rgb(0.8, 0.8, 0.8),
borderWidth: 1,
});
page.drawText('Thank you for your business!', {
x: 50 + 10,
y: 65,
size: 10,
font,
color: rgb(0.5, 0.5, 0.5),
});
// Save
const pdfBytes = await pdf.save();
return pdfBytes;
}
// Node.js — save to file
import fs from 'fs';
const bytes = await createPDF();
fs.writeFileSync('invoice.pdf', bytes);
// Browser — download
const bytes = await createPDF();
const blob = new Blob([bytes], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'invoice.pdf';
a.click();
URL.revokeObjectURL(url);
// API route — return as response
export async function GET() {
const bytes = await createPDF();
return new Response(bytes, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="invoice.pdf"',
},
});
}Fix 2: Embed Custom Fonts (Non-Latin Characters)
import { PDFDocument } from 'pdf-lib';
import fontkit from '@pdf-lib/fontkit';
import fs from 'fs';
async function createWithCustomFont() {
const pdf = await PDFDocument.create();
// Register fontkit for custom font support
pdf.registerFontkit(fontkit);
// Embed a custom font (TTF or OTF)
const fontBytes = fs.readFileSync('fonts/NotoSansJP-Regular.ttf');
const customFont = await pdf.embedFont(fontBytes);
const page = pdf.addPage();
const { height } = page.getSize();
// Now CJK characters render correctly
page.drawText('Hello こんにちは 你好 안녕하세요', {
x: 50,
y: height - 80,
size: 20,
font: customFont,
});
return pdf.save();
}Fix 3: Embed Images
import { PDFDocument } from 'pdf-lib';
async function addImageToPDF() {
const pdf = await PDFDocument.create();
const page = pdf.addPage();
const { width, height } = page.getSize();
// Embed PNG
const pngBytes = await fetch('/logo.png').then(r => r.arrayBuffer());
const pngImage = await pdf.embedPng(pngBytes);
// Embed JPEG
const jpgBytes = await fetch('/photo.jpg').then(r => r.arrayBuffer());
const jpgImage = await pdf.embedJpg(jpgBytes);
// Draw image — scaled to fit
const pngDims = pngImage.scale(0.5); // Scale to 50%
page.drawImage(pngImage, {
x: 50,
y: height - pngDims.height - 50,
width: pngDims.width,
height: pngDims.height,
});
// Draw image with custom size
page.drawImage(jpgImage, {
x: 50,
y: height - 400,
width: 200,
height: 150,
});
return pdf.save();
}Fix 4: Modify Existing PDFs
import { PDFDocument, rgb } from 'pdf-lib';
// Add watermark to existing PDF
async function addWatermark(pdfBytes: Uint8Array, text: string) {
const pdf = await PDFDocument.load(pdfBytes);
const font = await pdf.embedFont('Helvetica');
const pages = pdf.getPages();
for (const page of pages) {
const { width, height } = page.getSize();
page.drawText(text, {
x: width / 4,
y: height / 2,
size: 50,
font,
color: rgb(0.8, 0.8, 0.8),
opacity: 0.3,
rotate: degrees(45),
});
}
return pdf.save();
}
// Merge multiple PDFs
async function mergePDFs(pdfBytesArray: Uint8Array[]) {
const merged = await PDFDocument.create();
for (const pdfBytes of pdfBytesArray) {
const pdf = await PDFDocument.load(pdfBytes);
const pages = await merged.copyPages(pdf, pdf.getPageIndices());
pages.forEach(page => merged.addPage(page));
}
return merged.save();
}
// Extract specific pages
async function extractPages(pdfBytes: Uint8Array, pageNumbers: number[]) {
const source = await PDFDocument.load(pdfBytes);
const extracted = await PDFDocument.create();
const pages = await extracted.copyPages(source, pageNumbers.map(n => n - 1));
pages.forEach(page => extracted.addPage(page));
return extracted.save();
}Fix 5: Fill PDF Forms
import { PDFDocument } from 'pdf-lib';
async function fillForm(templateBytes: Uint8Array, data: Record<string, string>) {
const pdf = await PDFDocument.load(templateBytes);
const form = pdf.getForm();
// Fill text fields
for (const [fieldName, value] of Object.entries(data)) {
try {
const field = form.getTextField(fieldName);
field.setText(value);
} catch {
console.warn(`Field "${fieldName}" not found in PDF`);
}
}
// Fill checkboxes
const agreeField = form.getCheckBox('agree_terms');
agreeField.check();
// Fill dropdowns
const countryField = form.getDropdown('country');
countryField.select('United States');
// Flatten form (make fields non-editable)
form.flatten();
return pdf.save();
}
// List all form fields
async function listFormFields(pdfBytes: Uint8Array) {
const pdf = await PDFDocument.load(pdfBytes);
const form = pdf.getForm();
const fields = form.getFields();
return fields.map(field => ({
name: field.getName(),
type: field.constructor.name,
}));
}Fix 6: Add Metadata and Links
import { PDFDocument } from 'pdf-lib';
async function addMetadata(pdfBytes: Uint8Array) {
const pdf = await PDFDocument.load(pdfBytes);
// Set metadata
pdf.setTitle('My Document');
pdf.setAuthor('John Doe');
pdf.setSubject('Invoice');
pdf.setKeywords(['invoice', 'billing', '2026']);
pdf.setCreator('My App');
pdf.setProducer('pdf-lib');
pdf.setCreationDate(new Date());
pdf.setModificationDate(new Date());
return pdf.save();
}
// Password protection (basic)
// Note: pdf-lib doesn't support encryption natively
// Use a library like node-qpdf for encryption after generationStill Not Working?
PDF is blank — you added a page but didn’t draw anything on it. addPage() creates an empty page. You must call page.drawText(), page.drawImage(), or other draw methods to add visible content.
Text appears at the bottom instead of the top — PDF coordinates use bottom-left as origin. y: 0 is the bottom. For top-left positioning, use y: height - offsetFromTop where height is from page.getSize().
Non-Latin characters show as squares — embed a custom font that supports the characters. Standard fonts (Helvetica, Times, Courier) only support basic Latin. Install @pdf-lib/fontkit and embed a TTF/OTF font with the required character set.
Loading an existing PDF fails — the PDF might be encrypted or use features pdf-lib doesn’t support. Try PDFDocument.load(bytes, { ignoreEncryption: true }). For heavily protected PDFs, you may need to decrypt them first with another tool.
For related PDF and document issues, see Fix: React PDF Not Working and Fix: MDX Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Pusher Not Working — Events Not Received, Channel Auth Failing, or Connection Dropping
How to fix Pusher real-time issues — client and server setup, channel types, presence channels, authentication endpoints, event binding, connection management, and React integration.
Fix: date-fns Not Working — Wrong Timezone Output, Invalid Date, or Locale Not Applied
How to fix date-fns issues — timezone handling with date-fns-tz, parseISO vs new Date, locale import and configuration, DST edge cases, v3 ESM migration, and common format pattern mistakes.
Fix: Zod Validation Not Working — safeParse Returns Wrong Error, transform Breaks Type, or discriminatedUnion Fails
How to fix Zod schema validation issues — parse vs safeParse, transform and preprocess, refine for cross-field validation, discriminatedUnion, error formatting, and common schema mistakes.
Fix: TypeScript Function Overload Error — No Overload Matches This Call
How to fix TypeScript function overload errors — overload signature compatibility, implementation signature, conditional types as alternatives, method overloads in classes, and common pitfalls.