Skip to content

Fix: pdf-lib Not Working — PDF Not Generating, Fonts Not Embedding, or Pages Blank

FixDevs ·

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 page

Or text renders as squares or missing characters:

page.drawText('Hello こんにちは', { x: 50, y: 500, size: 20 });
// "Hello" renders fine, Japanese characters are squares

Or modifying an existing PDF loses its content:

const existingPdf = await PDFDocument.load(pdfBytes);
// Loaded PDF shows blank pages

Why 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 defaultaddPage() 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: 0 is 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-lib
import { 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,
  }));
}
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 generation

Still 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles