Home
Stamp “Page 3 of 10” (or just “3”) on every page of a PDF with a short script you run with sumatrapdf-tool run.
Available in pre-release 3.7
Save the script below as add-page-numbers.js and run it on your PDF. At a glance:
  • Run: sumatrapdf-tool run add-page-numbers.js in.pdf out.pdf
  • Text: FORMAT is Page {n} of {total}, {n}, - {n} -, …
  • Position: POSITION puts the number at the bottom or top, left, center or right; MARGIN sets the distance from the edge.
  • Look: FONT, FONT_SIZE, COLOR.
  • Cover page: SKIP_PAGES leaves the first pages without a number; FIRST_NUMBER sets where counting starts.

Add page numbers #

  1. Save the script as add-page-numbers.js.
  2. Change the settings at the top of the script, if you want.
  3. Run it:
sumatrapdf-tool run add-page-numbers.js in.pdf out.pdf
out.pdf is a copy of in.pdf with a page number on every page. SumatraPDF.exe run with the same arguments works the same.
// Add page numbers to every page of a PDF.
// Usage: sumatrapdf-tool run add-page-numbers.js in.pdf out.pdf

// ---- Settings ----
var FORMAT = "Page {n} of {total}" // "{n}" for just the number
var POSITION = "bottom-center"     // bottom-left, bottom-center, bottom-right,
                                   // top-left, top-center, top-right
var MARGIN = 24                    // distance from the page edge, in points (72 = 1 inch)
var FONT = "Helvetica"             // Helvetica, Times-Roman, Courier, Helvetica-Bold, ...
var FONT_SIZE = 10                 // in points
var COLOR = [0, 0, 0]              // red, green, blue from 0 to 1
var SKIP_PAGES = 0                 // leave the first N pages (e.g. a cover) without a number
var FIRST_NUMBER = 1               // number shown on the first numbered page
// ------------------

if (scriptArgs.length != 2) {
	print("usage: sumatrapdf-tool run add-page-numbers.js in.pdf out.pdf")
	quit(1)
}

var doc = Document.openDocument(scriptArgs[0])
var font = new Font(FONT)
var fontRef = doc.addSimpleFont(font)
var total = FIRST_NUMBER + doc.countPages() - SKIP_PAGES - 1

// open "q" before the page's own content, close it with "Q" after,
// so changes the page makes to the graphics state don't affect our text
var saveState = doc.addStream("q\n")

function textWidth(s) {
	var w = 0
	for (var i = 0; i < s.length; i++)
		w += font.advanceGlyph(font.encodeCharacter(s.charCodeAt(i)), 0)
	return w * FONT_SIZE
}

function num(v) {
	return Math.round(v * 100) / 100
}

function pdfString(s) {
	return "(" + s.replace(/[\\()]/g, "\\$&") + ")"
}

for (var i = SKIP_PAGES; i < doc.countPages(); i++) {
	var page = doc.loadPage(i)
	var pageObj = page.getObject()

	// add the font to the page resources
	var res = pageObj.getInheritable("Resources")
	if (!res) {
		res = doc.newDictionary()
		pageObj.put("Resources", res)
	}
	if (!res.get("Font"))
		res.put("Font", doc.newDictionary())
	res.get("Font").put("PageNumFont", fontRef)

	// page size as you see it (crop box, after rotation)
	var b = page.getBounds()
	var w = b[2] - b[0], h = b[3] - b[1]

	var text = FORMAT.replace("{n}", FIRST_NUMBER + i - SKIP_PAGES).replace("{total}", total)
	var tw = textWidth(text)
	var x = MARGIN, y = MARGIN
	if (POSITION.indexOf("center") >= 0) x = (w - tw) / 2
	if (POSITION.indexOf("right") >= 0) x = w - MARGIN - tw
	if (POSITION.indexOf("top") >= 0) y = h - MARGIN - FONT_SIZE

	// map "bottom-left corner as you see it, y up" to the page's own coordinates;
	// handles /Rotate and crop boxes that don't start at 0,0
	var m = Matrix.concat([1, 0, 0, -1, b[0], b[3]], Matrix.invert(page.getTransform()))

	var stamp = "\nQ q " + m.map(num).join(" ") + " cm " +
		"BT /PageNumFont " + FONT_SIZE + " Tf " + COLOR.join(" ") + " rg " +
		num(x) + " " + num(y) + " Td " + pdfString(text) + " Tj ET Q\n"

	// new page content: q, the old content, Q, page number
	var contents = doc.newArray()
	contents.push(saveState)
	var old = pageObj.get("Contents")
	if (old && old.isArray())
		old.forEach(function (c) { contents.push(c) })
	else if (old)
		contents.push(old)
	contents.push(doc.addStream(stamp))
	pageObj.put("Contents", contents)
}

doc.save(scriptArgs[1], "garbage,compress")
The numbers are part of the page, so they show in every PDF viewer and print. They are real text: you can search for them and select them.
The number is placed on the page as you see it: landscape pages, pages with a rotation (/Rotate) and cropped pages get it at the bottom of the visible page, reading left to right.

Change the text and position #

Edit the settings at the top of the script:
You want Setting
Just the number: 7 FORMAT = "{n}"
- 7 - FORMAT = "- {n} -"
Number in the bottom right corner POSITION = "bottom-right"
Number at the top POSITION = "top-center"
Closer to the edge MARGIN = 12
Bigger, bold, gray FONT_SIZE = 14, FONT = "Helvetica-Bold", COLOR = [0.5, 0.5, 0.5]
No number on the cover, page 2 shows 1 SKIP_PAGES = 1
No number on the cover, page 2 shows 2 SKIP_PAGES = 1, FIRST_NUMBER = 2
Continue from a previous file, start at 41 FIRST_NUMBER = 41
{total} is the last number printed, so Page {n} of {total} stays right when you skip pages or start at another number.
FONT is a standard PDF font: Helvetica, Times-Roman or Courier, or a bold or italic variant such as Helvetica-Bold, Times-Italic, Courier-Oblique, Helvetica-BoldOblique. Standard fonts are not embedded, so the file stays small. Use only Latin characters in FORMAT.

Number many PDFs #

Run the script in a loop. In cmd.exe, to write numbered copies of every PDF in the current folder to a numbered folder:
mkdir numbered
for %f in (*.pdf) do sumatrapdf-tool run add-page-numbers.js "%f" "numbered\%f"
More: Batch process PDFs.

Tips #

  • Write to a new file. Saving over the input file fails with cannot remove file.
  • Keep the original: running the script twice stamps the numbers twice.
  • Open out.pdf in SumatraPDF to check the position before you number many files.
  • Adding numbers changes the pages, so it breaks existing digital signatures. Number the PDF before you sign it.
  • To number only part of a document, split it, number the part, and merge it back.

See also #