Adding page numbers to a PDF takes under 30 seconds with the right tool. Use the PDF4.dev number PDF tool for an instant browser-based solution — no upload, no install. For programmatic use in Node.js or Python, this article covers working code examples.
The quick way: free online tool
The PDF4.dev number PDF tool adds page numbers client-side in your browser. Your file never leaves your device, which matters when working with confidential documents like contracts or financial reports.
Steps:
- Go to pdf4.dev/tools/number-pdf
- Drop your PDF onto the upload area
- Choose position (bottom center, bottom right, top right, etc.)
- Set font size (10px is standard for most documents)
- Choose starting number (default: 1)
- Click "Add page numbers" and download
The tool works on any modern browser — Chrome, Firefox, Safari, Edge — with no size limit for most documents.
Page number position guide
The position of page numbers affects how professional a document looks and how it behaves when printed.
| Use case | Recommended position | Format |
|---|---|---|
| Standard report or article | Bottom center | 1, 2, 3 |
| Double-sided print (book) | Bottom outer (alt. left/right) | Page X of Y |
| Legal documents | Bottom center or bottom right | 1, 2, 3 |
| Academic thesis | Bottom center | i, ii, iii (front matter) then 1, 2, 3 |
| Bates-numbered exhibits | Bottom right | PREFIX-000001 |
| Slide deck printed as handout | Top right | Page X of Y |
For most general-purpose documents, bottom center at 10-11pt is the safe default.
Add page numbers in Node.js with pdf-lib
pdf-lib is a pure JavaScript library that reads and writes PDF files in Node.js without any native dependencies or Chromium binary.
import { PDFDocument, StandardFonts, rgb } from "pdf-lib";
import { readFileSync, writeFileSync } from "fs";
async function addPageNumbers(inputPath: string, outputPath: string) {
const existingPdfBytes = readFileSync(inputPath);
const pdfDoc = await PDFDocument.load(existingPdfBytes);
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
const pages = pdfDoc.getPages();
const totalPages = pages.length;
pages.forEach((page, index) => {
const { width, height } = page.getSize();
const pageNumber = index + 1;
const text = `${pageNumber} / ${totalPages}`;
const fontSize = 10;
const textWidth = font.widthOfTextAtSize(text, fontSize);
page.drawText(text, {
x: (width - textWidth) / 2, // centered horizontally
y: 20, // 20pt from bottom
size: fontSize,
font,
color: rgb(0.4, 0.4, 0.4), // gray
});
});
const pdfBytes = await pdfDoc.save();
writeFileSync(outputPath, pdfBytes);
console.log(`Added page numbers to ${totalPages} pages → ${outputPath}`);
}
addPageNumbers("input.pdf", "output-numbered.pdf");Install pdf-lib with:
npm install pdf-libThe library is pure JavaScript — no native addons, no Chromium. It works in Node.js, browsers, Deno, and any JavaScript runtime.
Add page numbers in Python
Python has two common approaches: pypdf for reading existing PDFs and overlaying text, or reportlab for generating PDFs with page numbers from scratch.
import io
from pypdf import PdfReader, PdfWriter
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
def add_page_numbers(input_path: str, output_path: str) -> None:
reader = PdfReader(input_path)
writer = PdfWriter()
total_pages = len(reader.pages)
for i, page in enumerate(reader.pages):
# Get page dimensions
width = float(page.mediabox.width)
height = float(page.mediabox.height)
# Create an overlay PDF with just the page number
packet = io.BytesIO()
c = canvas.Canvas(packet, pagesize=(width, height))
c.setFont("Helvetica", 10)
c.setFillColorRGB(0.4, 0.4, 0.4)
text = f"{i + 1} / {total_pages}"
text_width = c.stringWidth(text, "Helvetica", 10)
c.drawString((width - text_width) / 2, 20, text)
c.save()
# Merge the overlay onto the existing page
packet.seek(0)
overlay_pdf = PdfReader(packet)
page.merge_page(overlay_pdf.pages[0])
writer.add_page(page)
with open(output_path, "wb") as f:
writer.write(f)
print(f"Added page numbers to {total_pages} pages → {output_path}")
add_page_numbers("input.pdf", "output-numbered.pdf")Install dependencies:
# pypdf + reportlab approach
pip install pypdf reportlab
# PyMuPDF approach (faster, simpler API)
pip install pymupdfPyMuPDF (also known as fitz) is typically faster than the pypdf+reportlab combination because it reads and writes the PDF content stream directly without creating intermediate overlay documents.
Skip numbering for the first N pages
A common requirement: skip the cover page and table of contents, then start numbering at page 1 for the body content.
// TypeScript — skip first 2 pages, start body at page "1"
pages.forEach((page, index) => {
const skipPages = 2; // cover + table of contents
if (index < skipPages) return; // no number on these pages
const { width } = page.getSize();
const displayNumber = index - skipPages + 1; // page 3 of doc = "1"
const text = `${displayNumber}`;
const textWidth = font.widthOfTextAtSize(text, fontSize);
page.drawText(text, {
x: (width - textWidth) / 2,
y: 20,
size: fontSize,
font,
color: rgb(0.4, 0.4, 0.4),
});
});This is sometimes called an "offset" or "starting number" feature. Many online tools support it via a "Start numbering from page N" option.
Number format options
Different documents call for different number formats. Here are the most common:
| Format | Example | When to use |
|---|---|---|
| Plain number | 3 | Minimal, slides, short reports |
| Page X of Y | Page 3 of 47 | Long documents, formal reports |
| Centered dash | - 3 - | Book chapters, academic papers |
| Roman numerals | iii | Front matter (intro, TOC) |
| Bates number | EXHIBIT-000003 | Legal, litigation, compliance |
| Section prefix | 2-3 | Multi-section technical docs |
For Roman numeral conversion in JavaScript:
function toRoman(num: number): string {
const values = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
const symbols = ["M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"];
let result = "";
for (let i = 0; i < values.length; i++) {
while (num >= values[i]) {
result += symbols[i];
num -= values[i];
}
}
return result.toLowerCase(); // "iii" instead of "III"
}Add page numbers when generating PDFs from HTML
If you generate PDFs from HTML using the PDF4.dev API or a similar HTML-to-PDF service, add page numbers directly in the HTML/CSS using CSS paged media. This is the cleanest approach because the numbering is part of the document design, not a post-processing step.
<!DOCTYPE html>
<html>
<head>
<style>
@page {
margin: 20mm 15mm 25mm 15mm;
@bottom-center {
content: counter(page) " / " counter(pages);
font-family: Helvetica, sans-serif;
font-size: 10pt;
color: #666;
}
}
body {
font-family: Helvetica, sans-serif;
}
</style>
</head>
<body>
<h1>My Report</h1>
<p>Page numbers appear automatically in the footer.</p>
</body>
</html>The counter(page) and counter(pages) CSS values are standard in the CSS Paged Media spec. Chromium-based PDF renderers (including the one PDF4.dev uses) support them natively.
For more CSS print techniques, see the CSS print styles guide.
Adding page numbers via the PDF4.dev REST API
If you need to generate numbered PDFs at scale, use the PDF4.dev API. Include the CSS @page counter technique above in your HTML template, then call the API with your dynamic data.
const response = await fetch("https://api.pdf4.dev/v1/render", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PDF4_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html: myHtmlTemplate, // includes @page { @bottom-center { content: counter(page) } }
data: { title: "Q1 Report", sections: reportData },
}),
});
const pdfBuffer = await response.arrayBuffer();This approach generates the numbered PDF in a single API call. No post-processing step, no second library to install. See the Node.js PDF generation guide for a complete working example.
When post-processing is the right approach
Use a post-processing library (pdf-lib, PyMuPDF) when:
- You receive pre-existing PDFs from a third-party (scanned documents, uploaded files)
- The numbering requirement is an afterthought and you cannot change the original generation pipeline
- You need to add Bates numbers to a batch of unrelated documents for legal purposes
Use CSS @page counters or in-template numbering when:
- You control the HTML that generates the PDF
- The page number style needs to match the document design closely
- You want a single-step pipeline with no additional dependencies
Batch processing: number multiple PDFs
To number hundreds of PDFs in a folder, loop over the files in Node.js:
import { readdirSync } from "fs";
import path from "path";
const inputDir = "./pdfs";
const outputDir = "./pdfs-numbered";
const files = readdirSync(inputDir).filter((f) => f.endsWith(".pdf"));
for (const file of files) {
await addPageNumbers(
path.join(inputDir, file),
path.join(outputDir, file)
);
console.log(`Numbered: ${file}`);
}For parallel processing across a large batch, use Promise.all with a concurrency limit:
import pLimit from "p-limit"; // npm install p-limit
const limit = pLimit(4); // 4 concurrent operations
await Promise.all(
files.map((file) =>
limit(() =>
addPageNumbers(
path.join(inputDir, file),
path.join(outputDir, file)
)
)
)
);Common issues
Page numbers appear on the wrong side when printed double-sided. Use alternating positions: bottom-right on odd pages, bottom-left on even pages. In pdf-lib, check index % 2 === 0 to determine left or right alignment.
Page numbers overlap existing content. Increase the bottom page margin before generating the PDF, or lower the Y position of the number stamp (e.g., from y: 20 to y: 10). If working with a pre-existing PDF you cannot regenerate, you may need to set a crop box or add a white rectangle behind the number.
Roman numerals needed for front matter only. Split the PDF by section, apply Roman numerals to the first N pages and Arabic numerals to the rest, then merge the sections back together.
Numbers appear blurry or pixelated. This happens when the PDF is treated as a raster image internally. Ensure you are using vector text operations (pdf-lib drawText, PyMuPDF insert_text) rather than rasterizing the page first.
Tools at a glance
| Approach | Effort | Best for |
|---|---|---|
| PDF4.dev number-pdf tool | Zero | Quick one-off, any document |
CSS @page counter | Low | New PDFs generated from HTML |
| pdf-lib (Node.js) | Medium | Batch automation, existing PDFs |
| PyMuPDF (Python) | Medium | Python workflows, large batches |
| pdf-lib + reportlab | Medium-high | Python, complex overlay needs |
For most developers generating PDFs from scratch, CSS @page counters are the cleanest solution. For modifying existing PDFs, pdf-lib (Node.js) or PyMuPDF (Python) are the fastest options to add to an existing pipeline.
Once you have page numbers sorted, the next common task is reordering pages or splitting the document into sections.
Free tools mentioned:
Start generating PDFs
Build PDF templates with a visual editor. Render them via API from any language in ~300ms.