gopdf

Create, read, edit, fill, sign and redact PDF files in pure Go — with nothing but the standard library.

No cgo Zero dependencies MIT licensed Go 1.22+

The document writer, the file parser, the TrueType subsetter, the stream filters and the encryption are all implemented in this repository. Nothing wraps a native PDF library.

Install

go get github.com/SalvioniDigitalSolutions/gopdf

Quick start

package main

import (
	"log"

	"github.com/SalvioniDigitalSolutions/gopdf"
)

func main() {
	doc := gopdf.New()
	page := doc.AddPage()
	page.SetFont(gopdf.Helvetica, 14)
	page.Text(72, 72, "Hello, PDF!")
	if err := doc.Save("hello.pdf"); err != nil {
		log.Fatal(err)
	}
}

Write

Text, vector graphics, images, links, bookmarks, encryption.

Read

Parse any PDF: xref streams, object streams, every common filter.

Edit

Rewrite text in place without disturbing the layout.

Forms

Read, fill, flatten and author interactive AcroForms.

Coordinates & units

All coordinates are in points (1/72 inch) with the origin at the top-left of the page — y grows downward, matching how you read a page. This differs from PDF's own bottom-left convention; the library converts internally so you never have to.

// 25 mm from the left, 30 mm from the top
page.Text(25*gopdf.Mm, 30*gopdf.Mm, "Positioned in millimetres")

Available factors: Pt, Mm, Cm, Inch. Page sizes A3, A4, A5, Letter and Legal each have .Portrait() and .Landscape().

Text & fonts

The standard 14 PDF fonts are always available and need no embedding. They carry real Adobe metrics, so measurement and alignment are exact.

page.SetFont(gopdf.TimesBold, 16)
page.Text(x, y, "Section title")

w := page.TextWidth("Section title")
page.TextAligned(x, y, colWidth, gopdf.AlignCenter, s)
next := page.TextWrapped(x, y, width, lineHeight, longParagraph)

Embedding a TrueType font

For text beyond the WinAnsi character set, load a .ttf, .ttc or .otf. The font is subset to the glyphs you actually use, pair kerning from the font's kern table is applied automatically, and a ToUnicode map keeps the text searchable and copyable.

noto, err := gopdf.LoadFont("NotoSans-Regular.ttf")
if err != nil {
	log.Fatal(err)
}
page.SetFont(noto, 12)
page.Text(72, 72, "Καλημέρα κόσμε — Привет, мир — čeština")

Standard fonts are limited to WinAnsi (CP-1252). Characters outside it render as ?. Embed a font for anything else.

CFF-based OpenType (.otf) fonts work too, embedded as CIDFontType0 and subset like TrueType: unused outlines become empty glyphs, unreachable subroutines a bare return, and dropped glyphs give up their names — all while every glyph ID stays where it was. Embedding one line of STIX General takes the font from 369 KB to 25 KB. CID-keyed CFF fonts are embedded whole.

Graphics

page.SetStrokeColor(gopdf.RGB(30, 60, 120))
page.SetFillColor(gopdf.Gray(240))
page.SetLineWidth(1.5)
page.SetLineCap(gopdf.CapRound)
page.SetDash(4, 2)

page.Line(x1, y1, x2, y2)
page.Rect(x, y, w, h, gopdf.FillStroke)
page.RoundedRect(x, y, w, h, radius, gopdf.Stroke)
page.Circle(cx, cy, r, gopdf.Fill)
page.Ellipse(cx, cy, rx, ry, gopdf.Stroke)
page.Polygon(gopdf.Fill, x1, y1, x2, y2, x3, y3)

// Arbitrary paths
page.MoveTo(x, y)
page.CurveTo(c1x, c1y, c2x, c2y, x2, y2)
page.LineTo(x3, y3)
page.ClosePath()
page.DrawPath(gopdf.FillStroke)

Scoped state: transforms, opacity, clipping

Every Push must be paired with a Pop. Colours, line settings and transforms all restore correctly, and the library skips operators that would not change anything, keeping content streams small.

page.Push()
page.RotateAt(45, cx, cy)
page.SetAlpha(0.5, 1)      // fill, stroke opacity
page.ClipRect(x, y, w, h)
page.Text(cx, cy, "watermark")
page.Pop()

Gradients

Axial (linear) and radial gradients with any number of colour stops. Stop pairs a position from 0 to 1 with a colour; stops are sorted and padded to span the whole range for you.

page.FillGradientRect(30, 60, 200, 80, gopdf.GradientVertical,
	gopdf.Stop(0, gopdf.RGB(40, 90, 200)),
	gopdf.Stop(1, gopdf.RGB(230, 240, 255)))

page.FillGradientCircle(300, 100, 45,
	gopdf.Stop(0, gopdf.White), gopdf.Stop(1, gopdf.RGB(180, 30, 90)))

Directions are GradientVertical, GradientHorizontal and GradientDiagonal. For any other shape, clip first and paint the gradient into the clip:

page.Push()
page.MoveTo(215, 175)
page.LineTo(265, 260)
page.LineTo(165, 260)
page.ClosePath()
page.Clip(false)
page.PaintLinearGradient(165, 175, 265, 260, stops...)
page.Pop()

Any shape method also accepts ClipPath as its draw mode, which turns the shape into a clipping region instead of painting it:

page.Push()
page.Circle(cx, cy, r, gopdf.ClipPath)
page.PaintRadialGradient(cx, cy, 0, r, stops...)
page.Pop()

Images

img, err := doc.AddImageFile("photo.jpg")   // JPEG, PNG or GIF
page.DrawImage(img, x, y, w, h)

// Or any image.Image
img2, err := doc.AddImage(myImage)

JPEG data is embedded byte-for-byte with no re-encoding. PNG and GIF are decoded and stored as raw samples, with any alpha channel preserved as a PDF soft mask. Grayscale images use DeviceGray, and Adobe CMYK JPEGs get the inversion they require.

page.LinkURL(x, y, w, h, "https://example.com")
page.LinkPage(x, y, w, h, otherPage, 0)

chapter := doc.AddOutline(nil, "Chapter 1", page, 0)
doc.AddOutline(chapter, "Section 1.1", otherPage, 200)

Reading an existing PDF

Word breaks are measured, not guessed. Producers set a word in pieces — troff moves the pen a fraction of a point between letters, TeX kerns inside words — and treating every forward move as a space turns BASH into BA SH. Extraction advances the pen by each string's real width and compares the gap against the font's own space, along the baseline rather than the page axes so rotated text reads the same. Measured against pdftotext over 918 documents, agreement rose from 0.773 to 0.849.

r, err := gopdf.Open("report.pdf")
if err != nil {
	log.Fatal(err)
}
fmt.Println(r.NumPages(), r.Info().Title)

size, _ := r.PageSize(0)      // accounts for page rotation
text, _ := r.PageText(0)      // content order, line breaks inferred

The parser handles classic cross-reference tables, PDF 1.5+ cross-reference streams, object streams and hybrid files, with Flate (including PNG and TIFF predictors), LZW, ASCII85, ASCIIHex and RunLength filters. Text extraction uses ToUnicode CMaps where present and falls back to the font's declared encoding — WinAnsi, MacRoman, or a /Differences table — and descends into nested form XObjects.

Merge & split

gopdf.Merge("combined.pdf", "a.pdf", "b.pdf", "c.pdf")
gopdf.ExtractPages("first-two.pdf", "input.pdf", 0, 1)

// Or compose manually
doc := gopdf.New()
doc.AppendPDF(r)
page, _ := doc.ImportPage(r, 3)
page.SetRotate(90)

Stamp & watermark

An imported page is a normal *Page: the entire drawing API works on top of the original content. Source pages are wrapped as form XObjects, so imported resources can never collide with your overlay.

doc := gopdf.New()
for i := 0; i < src.NumPages(); i++ {
	page, _ := doc.ImportPage(src, i)
	page.Push()
	page.SetAlpha(0.3, 0.3)
	page.RotateAt(45, page.Width()/2, page.Height()/2)
	page.SetFont(gopdf.HelveticaBold, 72)
	page.SetFillColor(gopdf.RGB(200, 30, 30))
	page.TextAligned(0, page.Height()/2, page.Width(), gopdf.AlignCenter, "DRAFT")
	page.Pop()
}
doc.Save("watermarked.pdf")

Editing text in place

EditPage keeps a page's original operators editable, preserving its resources, media box and rotation exactly. ReplaceText rewrites text using the page's own font, so it renders identically.

src, _ := gopdf.Open("invoice.pdf")
doc := gopdf.New()

page, err := doc.EditPage(src, 0)
if err != nil {
	log.Fatal(err)
}
n, err := page.ReplaceText("DRAFT", "FINAL")

// An edited page is still a Page — draw on top as usual.
page.SetFont(gopdf.HelveticaBold, 10)
page.Text(40, 40, "revised")

doc.Save("final.pdf")

Inspecting before you change anything

for _, run := range page.Runs() {
	fmt.Printf("%.1f,%.1f  %.1fpt  %-8s %q\n",
		run.X, run.Y, run.FontSize, run.FontName, run.Text)
}

ReplaceFunc rewrites runs matching arbitrary logic, and TextRun.SetText edits one specific run.

How the layout survives

A replacement almost always has a different width. SetFitMode decides what happens:

ModeBehaviour
FitAdvance (default)Compensates for the width difference so everything after the edit stays exactly where it was.
FitScaleAdditionally scales the replacement horizontally to occupy precisely the original width. Use it when the text would otherwise overlap a neighbour.
FitNoneWrites at natural width and lets the rest of the line shift, as a word processor would.

When an edit is refused. PDFs usually embed only a subset of each font — just the glyphs the document draws. If your replacement needs a character that subset lacks, it would render as a blank box, so gopdf refuses the edit, names the missing character, and leaves the document unchanged:

font /Tc1 cannot represent '5': the source file embeds only a
subset of this font, which does not include that character

Add such text with the drawing API and a font of your own instead.

Paragraph reflow

A run is one line. When a change makes a sentence appreciably longer or shorter, reflow re-wraps the whole paragraph instead of stretching a single line. Blocks groups lines into paragraphs — same font, same left edge, constant leading — and measures each one's column width.

for _, b := range page.Blocks() {
	fmt.Printf("%d lines, %.0fpt wide: %q\n", len(b.Lines()), b.Width, b.Text)
}

page.ReplaceTextReflow("internal use only", "any lawful purpose")
page.Blocks()[2].SetText("Entirely new wording for this paragraph.")

Reflow wraps using the paragraph's own font metrics and writes the result back onto the lines it already occupies. If the new text needs more lines, it is refused by default — reflow can re-wrap a paragraph but cannot push the rest of the page down. SetMaxExtraLines(n) allows growth when you know there is room. Shorter text simply leaves trailing lines blank.

Flowing longer text

The reflow above re-wraps a paragraph within the lines it already occupies, and needs every line to be one operation in one font. That rules out the ordinary case of a bold word inside a sentence, and refuses any replacement that needs more room. A flow drops both restrictions.

r, _ := gopdf.Open("contract.pdf")
u := gopdf.Update(r)
page, _ := u.Page(0)

// Rewrites every paragraph containing the phrase, and moves the
// ones below it to make room.
page.ReplaceTextFlow("twelve months",
	"thirty-six calendar months from the effective date")

// Or a paragraph at a time.
for _, f := range page.Flows() {
	f.Replace("EUR 1,200", "EUR 27,450.99")
	fmt.Println(f.LineCount(), f.LineDelta())
}
u.Save("revised.pdf")

Styling survives the edit

A paragraph is modelled as a run of styled spans rather than a run of lines. A replacement inherits the styling of the text it replaces, and everything around it keeps its own — change a figure inside a bold phrase and it stays bold, while the sentence around it does not.

for _, s := range f.Spans() {
	fmt.Printf("%-10s %4.1fpt %q\n", s.FontName, s.FontSize, s.Text)
}
// F2  11.0 "Payment of "
// F1  11.0 "EUR 1,200"        <- bold
// F2  11.0 " is due on Friday."

SetSpans takes the same list back for full control; SetText replaces the paragraph outright in its first span's style.

Length is free

The paragraph is re-wrapped to its own column width, measuring every piece in its own font, and takes however many lines it needs. LineDelta reports how many it gained or lost. Everything below it on the page then moves down, or up, by that much — so a clause that grows does not print over the one after it.

Two details that decide whether this works on real files. A word the content stream broke in two — or drew one glyph at a time, as justified documents often do — is matched all the same, because matching runs over the paragraph rather than one operation at a time. And the baseline step is taken from the leading on the page, not from the difference between two text matrices: a document may position each line under its own transform, which leaves those matrices identical while the lines are plainly apart.

f.SetMaxExtraLines(2)   // refuse to grow by more than two lines
f.SetShrinkToFit(true, 6)   // or set an oversized token smaller, to a floor of 6pt
f.SetFitWidth(true)      // or size it to the width it replaced, and do not re-wrap at all

A flow moves text, not everything. Images, rules and other artwork below a paragraph stay where they are, so leave room or check LineDelta before writing. Re-wrapping also needs the font to be able to set a space: a document that positions every word separately and never draws one is declined rather than run together.

Images in an existing file

Reader.PageImages lists what a page draws, including images reached through a form XObject, with the place each occupies on the page. Decode returns the pixels.

imgs, _ := r.PageImages(0)
for _, im := range imgs {
	fmt.Printf("%dx%d %s at (%.0f,%.0f) %.0fx%.0f\n",
		im.Width, im.Height, im.ColorSpace, im.X, im.Y, im.W, im.H)
	pixels, err := im.Decode()   // an image.Image
}

u.ReplaceImage(imgs[0], newLogo)   // scaled into the same box

Decoding covers JPEG through the standard library, and raw samples in grey, RGB, CMYK and indexed colour at 1, 2, 4, 8 and 16 bits per component, with image masks and soft masks applied as alpha. Fax, JBIG2 and JPEG 2000 report a clear error rather than guessing.

Replacing an image rewrites the shared image object, so every page that draws it shows the replacement. The placement is untouched: an image of a different pixel size is scaled into exactly the same area.

Restyling text

Editing a run changes the characters it draws; restyling changes how they are drawn.

blue := gopdf.RGB(20, 70, 190)
run.Restyle(gopdf.TextStyle{
	Font:  gopdf.HelveticaBold,
	Size:  15,
	Color: &blue,
})

A zero field leaves that aspect alone. The style operators are emitted immediately before the run's own show-text operation and undone immediately after, so the change applies to that run and nothing else — the previous colour operation is restored verbatim, whatever colour space it used.

Size is in the points a reader sees. Many files set a nominal size with Tf and scale it through the text matrix, so the requested size is converted back through that scale rather than written straight into the operator.

A new typeface is registered with the page and the text re-encoded for it; if the font cannot represent one of the characters, the restyle is refused and nothing changes.

Annotations

Annotations are comments and marks that live alongside the page rather than in it, so a reader can select, edit or delete them. They can be read from any document, and added either to a new page or to an existing one through an update.

// Reading
for _, a := range annots {
	fmt.Printf("%-10s %v %q %s\n", a.Type, a.Rect, a.Contents, a.URL)
}
// Link       [29 220 219 230] ""                    https://www.energystar.gov
// Highlight  [29 186 559 198] "confirm this figure"
// Text       [566 180 586 200] "Is 10 minutes still current?"

// Adding — on a new Page, or on an updated one
page.AddHighlight(58, 132, 200, 16, "check this total",
	gopdf.NoteOptions{Author: "Reviewer"})
page.AddNote(420, 70, "please confirm", gopdf.NoteOptions{Author: "Reviewer"})
page.AddUnderline(x, y, w, h, "", gopdf.NoteOptions{})
page.AddStrikeOut(x, y, w, h, "", gopdf.NoteOptions{})
page.AddSquareAnnotation(x, y, w, h, "boxed", gopdf.NoteOptions{})
page.AddCircleAnnotation(x, y, w, h, "", gopdf.NoteOptions{})
page.LinkURL(x, y, w, h, "https://example.com")

// Removing — strip every sticky note, keep the rest
u.RemoveAnnotations(0, func(a gopdf.Annotation) bool {
	return a.Type == gopdf.AnnotText
})

Text-markup annotations are written with the quad points and an appearance stream of their own, so every viewer draws them the same way — a highlight uses a multiply blend, which keeps the text underneath legible. Sticky notes carry no appearance, letting the viewer draw its own icon as users expect.

Page operations

Pages can be removed and reordered in place, without rebuilding the document.

u := gopdf.Update(r)
u.RemovePage(1)                    // drop the second page
u.MovePage(3, 0)                   // bring the fourth to the front
u.SetPageOrder([]int{4, 3, 1, 0})   // reverse, dropping one
u.Save("reordered.pdf")

The page tree is rewritten as one flat node. Attributes a page used to inherit from an intermediate node — its resources, boxes and rotation — are written onto the page first, so flattening cannot change how anything renders. Removed pages stay in the file as unreferenced objects, which is what makes the operation cheap and reversible by another update.

Incremental update

Everything above rebuilds a document: pages are imported into a new Document and written from scratch, which keeps only what this library models. An incremental update instead writes the original file out unchanged and appends the objects that differ, chained to the previous cross-reference section.

r, _ := gopdf.Open("contract.pdf")
u := gopdf.Update(r)

page, _ := u.Page(0)
page.ReplaceText("2024", "2026")
page.ReplaceTextReflow("internal use", "any lawful use")

u.SetFormValues(map[string]string{"signatory": "A. Lovelace"})
u.SetPageRotation(1, 90)
u.SetInfo(gopdf.Info{Title: "Contract (revised)"})

u.Save("contract.pdf")   // safe to overwrite the source

This is the highest-fidelity way to change a PDF. Structure trees, embedded files, optional content groups, scripts, annotations — anything gopdf has no concept of — survives byte for byte, because those bytes are never rewritten. The test suite proves it by planting an object of an invented type in a file and checking it is still readable afterwards.

The appended section matches the original's style: a classic table for a classic file, a cross-reference stream for a PDF 1.5+ one. Encrypted documents stay encrypted, with appended objects protected using the original file key. An update with no changes reproduces the source exactly.

Drawing during an update

An updated page carries the whole drawing API, so a stamp, watermark or signature can be added without rewriting a single original object. What you draw becomes an additional content stream, and the resources it needs are merged into the page under a prefix that cannot collide with the source's own names.

page, _ := u.Page(0)
page.ReplaceText("DRAFT", "FINAL")   // edit what is there…

page.Push()                             // …and draw on top
page.SetAlpha(0.25, 0.25)
page.RotateAt(38, page.Width()/2, page.Height()/2)
page.SetFont(gopdf.HelveticaBold, 58)
page.TextAligned(0, page.Height()/2, page.Width(), gopdf.AlignCenter, "REVISED")
page.Pop()

img, _ := u.AddImageFile("signature.png")
page.DrawImage(img, 400, 640, 120, 40)

Fonts, images, transparency and gradients all work; each is written as a new object in the appended section. If the page's text is left alone, its original content stream is never rewritten at all.

The trade-off is size: an update only ever grows a file, because the objects it supersedes remain in place. Use Document.EditPage when you would rather have a compact, fully rewritten file.

Redaction

Covering something with a black rectangle hides it from a reader and leaves it in the file, where any parser will still hand it over. This removes it: the glyphs come out of the content stream, the pixels out of the image, the annotation off the page.

r, _ := gopdf.Open("case-file.pdf")
rd := gopdf.Redact(r)

rd.Text("Ada Lovelace")                             // every occurrence
rd.Pattern(regexp.MustCompile(`\d{3}-\d{2}-\d{4}`))   // every match
rd.Area(2, 60, 200, 180, 40)                    // a rectangle on page 2
rd.Match(func(run *gopdf.TextRun) bool {        // anything else
	return run.FontName == "F3"
})

marks, _ := rd.Marks()        // review before committing to it
for _, m := range marks {
	fmt.Printf("%s p%d %q\n", m.Kind, m.Page, m.Text)
}
rd.Save("redacted.pdf")

What is removed, and how

ContentWhat happens
TextThe glyphs are cut out of the content stream, and the characters kept are written back as the very codes that drew them. A gap of the same width is left in their place, so nothing else on the line moves.
ImagesThe pixels inside the area are overwritten and the image re-encoded, so the original samples do not survive. One whose pixels cannot be decoded — fax, JBIG2, JPEG 2000 — is dropped whole rather than left in.
Vector artworkA path lying entirely inside the area is deleted. One that straddles the edge is reported by PartialArtwork rather than silently kept. A path that establishes a clip is never removed.
AnnotationsRemoved along with whatever text they hold, unless KeepAnnotations(true).
MetadataThe information dictionary and the XMP stream are discarded by default: they carry author names and earlier titles the visible content no longer does.

Two things it is built around

A word the content stream broke up is still matched. Kerning, a colour change or a producer's whim can split Administration into two operations, or draw it one glyph at a time as justified documents often do. Matching happens over a whole line rather than one operation at a time, so the word is found either way. Runs are only joined when they continue each other — same baseline, starting about where the last one ended — so a match never runs across a line break or into the next column.

The output is a complete rewrite. An incremental update appends, which is right for editing a signed document and precisely wrong here: everything it replaced stays readable in the bytes underneath. A redacted file is written afresh from the objects the document still reaches, so what is not written is gone.

The result is checked before you get it. The written document is read back and the text that global rules removed is searched for again. If any of it is still readable — because the document draws it in a way redaction could not reach — WriteTo reports that and writes nothing, rather than handing back a file that looks redacted and is not. SetVerify(false) turns the check off.

Text inside a scan

A scanned letterhead is pixels, and every rule above walks straight past it. Plug in an engine and the text rules reach words inside images too: the pixels are overwritten, the image re-encoded, and a bar drawn with a token set into it. This library ships no engine — a poor one would be worse than none — but an adapter for tesseract is in the repository and shells out to the binary, so nothing joins your Go dependencies.

engine, _ := tesseract.New(tesseract.Options{Languages: []string{"eng"}})
rd.SetOCR(engine)
rd.Text("Ada Lovelace")
rd.Substitute("4815162342", "[[ACCOUNT_1]]")

Nothing extracts text from an image, so the ordinary read-back cannot check it. The engine is run again instead, over every image the finished document still reaches — which is what catches a thumbnail or an alternate — and the output is withheld if a word can still be read.

Recognition is not exhaustive. An engine misses words, especially on a poor scan, and a word it misses stays in the document. Review Marks(), and prefer an area where you know the region. A literal of several words is matched one word at a time, so redacting Ada Lovelace also removes a lone Ada — more than asked, which is the right way round.

Second copies of the page

Scrubbing what a page draws is necessary and not sufficient. A file may carry the same picture somewhere else: /Thumb is a rendering of the page made before the redaction, /Alternates offers another version of an image, and /PieceInfo holds whatever the producing application cached. None is drawn, all travel with the file, and any will hand back what the redaction was for. They are dropped, and reported as RedactCopy marks.

Redaction removes content, not structure. A string can also live somewhere that is not content — a font's /BaseFont name, an embedded file's name. Those are left alone. Check Marks() before writing and, where it matters, search the output.

Two fonts, one name

A form XObject carries its own /Font dictionary, and producers reuse the same short names inside it: /TT0 on the page and /TT0 in a form are routinely two different faces. Fonts are cached by the object they are rather than by the name a content stream calls them, because drawn with each other's metrics — a widths array that does not cover the codes being drawn, an advance of zero for each — a whole heading is painted one glyph on top of another as a single blot of ink.

Pseudonymization

Redaction takes text out and leaves a gap. Pseudonymization puts something in its place — a stable token, so the same person can be followed through a document, or a plain [REDACTED] marker where that reads better than a blank.

r, _ := gopdf.Open("case.pdf")
out, _ := os.Create("anonymous.pdf")

res, err := gopdf.Pseudonymize(r, out, []gopdf.Pseudonym{
	{From: "Ada Lovelace",    To: "[[PII_NAME_1]]"},
	{From: "Charles Babbage", To: "[[PII_NAME_2]]"},
	{From: "12 Dorset Street", To: "[REDACTED]"},
})
fmt.Println(res.Total(), "paragraphs across", res.Pages, "pages")

PseudonymizeFile does the same between two paths, writing to disk only once the result has proved itself.

The token need not be the same length

The paragraph is re-wrapped around it by the flow engine, measuring each piece in its own font and taking however many lines it needs. Every part keeps the styling it had, so replacing a name inside a bold phrase leaves the phrase bold and the sentence around it alone.

Or the paragraph need not move at all

Re-wrapping keeps the token whole and lets the lines fall where they may. FitWidth makes the other trade: keep the token whole and keep the lines, by setting the token at the size that makes it exactly as wide as the text it replaced.

gopdf.Pseudonymize(r, out, []gopdf.Pseudonym{
	{From: "G. Verdi", To: "[REDACTED]", FitWidth: true},
})

The baseline does not move, and only the token changes: the words around it and the space in front of it stay as the document set them. A token wider than what it replaced is set smaller; a narrower one keeps its size and is padded with a kern. Either way it claims exactly the width that was there, so nothing after it moves.

Where every token on a page fits, the page is edited in place. The strings holding the tokens are rewritten and nothing else is — not the other strings of the same operation, and not the kerns between them, which on a justified line are most of what puts the words where they are. So a highlight, a rule, an underline or the dots of a dash leader, painted against those bytes, still sit over the same text afterwards.

The size is solved for rather than scaled to. Character spacing is an absolute number of points per glyph and does not shrink when the font does, so the advance is affine in the size rather than proportional to it; a token with more characters than the name carries more of that fixed part, and a plain wFrom/wTo ratio would leave it too wide. With no extra spacing set, which is the usual case, the solved answer is that ratio.

MinScale moves the floor for one substitution: {From: "Locarno", To: "[[PII_LOCATION_001]]", FitWidth: true, MinScale: 0.18}. A key-reversible marker is long by construction, and over a short word it needs a fifth of the size rather than a half. The default suits a token meant to be read; a marker is meant to be matched, and stays searchable and extractable however small it is set. Below 5% the request is refused, and above the run's own size nothing is enlarged.

There is a floor at 45% of the run's size. Where even that leaves the token wider than what it replaced, it is set at the floor and the paragraph re-wraps as it otherwise would — a token nobody can read has failed at the only thing it was for. Over 127 documents of a real corpus, FitWidth returned the page exactly as it was, the tokens aside, on 108 of them, against 22 without it, and refused on none. A name split across two show-text operations is still one name: the first takes the token, at the width the whole name covered, and the rest give up their share and keep their own width.

Reverse drops the flag: putting the original text back has no width to fit. And each occurrence is fitted on its own, since the run behind one may be in a different size from the run behind the next. For a caller working through the flow engine rather than through Pseudonymize, the same switch is Flow.SetFitWidth.

Where it looks

Page text is the visible half. The same name sits in places nothing draws and everything reads, and all of them are rewritten: the information dictionary, the XMP packet, annotation notes and their authors, bookmark titles, form field values and tooltips, attachment names and descriptions.

A token the document's font cannot set

A subset font carries the glyphs its document draws and no others, so asking it for [[PII_NAME_1]] very likely fails on the bracket. Inserted text falls back to one of the standard fourteen, added to the page under a collision-free name, with the face matched from the original — bold text gets Helvetica-Bold, italic gets Oblique.

Only inserted text may change font. Text the document already drew keeps its own whatever happens: restyling that would change a page you did not ask to change. A token holding a character even Helvetica cannot set — anything outside cp1252 — is still refused rather than drawn as a blank box.

What counts as a match

One definition, shared by the matcher, the redactor and the read-back that proves nothing survived. A check stricter than the matcher reports a correct pass as a failure; a looser one lets a survivor through.

RuleEffect
Word boundariesRossi matches in Sig. Rossi, and not inside Rossini. Digits count, so 123 is not found in 5123. Punctuation does not block a match, leaving reference numbers, emails and IBANs alone.
Hyphen at a line breakA word justified across two lines — Bian- then chi — reads and is replaced as one, taking the dangling hyphen with it. A hyphen mid-line is real text: CHE-290 stays whole.
Fragmented linesA document setting a line one piece at a time, with the gaps carried by positioning rather than spaces, still reads as words.
Spelling variantsNon-breaking spaces and soft hyphens are matched from a mapping typed with ordinary ones.

MatchSubstrings(true) on a redactor turns the boundary rule off.

Spelling variants

Swiss and German legal documents set every gap as a non-breaking space and every compound hyphen as a soft hyphen. Extraction reports the characters the file holds, so a mapping typed with an ordinary space would match nothing. Each mapping is expanded into the spellings a document might have used — U+00A0, U+00AD, U+2011 and the combinations — all replaced by the same token, and all covered by the check.

Fitting

A token can be wider than the space it has — [[PII_REG_NUMBER_001]] in a one-line table cell has nowhere to wrap to. SetShrinkToFit(true, 6) sets it smaller instead, down to a floor below which it is left alone rather than made unreadable. And a paragraph that grows pushes the ones below it down, which at the foot of a page pushes them off it: OverflowsPage answers that before you write, rather than clipping silently.

Two more things a producer does that used to hide text from a search. Justified lines are set by drawing the space between two words and then moving the pen the rest of the way to the margin, and reading that move as a second word break put two spaces between every pair of words — so a name typed with one space matched nothing, silently. And a face that sets fi as one glyph may never draw a lone f, so a subset of it has no code for one: the text reads back correctly, but writing the same word again was refused for want of a letter that exists only joined to the next. The move that follows a space is now read as justification rather than a break, and a ligature is inverted as a run, so writing fi draws the fi the document already carries.

Some producers never draw a space at all: they set each word on its own and make the gaps by moving the pen, so the subset font they embed has no space glyph in it. Re-wrapping one of those paragraphs would once be refused, because setting the words flush against each other is worse than declining. The gap is now written the way the document already wrote it — as a positioning move, the width the font declares for a space, or a quarter of the size where it declares none. It needs nothing from the caller.

SetFitWidth answers a different question from SetShrinkToFit: not "does the token fit the column" but "does it take the width the old text took". The two compose — a fitted token is shrunk further if the column still demands it — and both apply only to inserted text. See width-fitted substitutions.

The output parses strictly

Every writer here splices replacements into a content stream somebody else wrote, and a splice landing immediately after an operator whose trailing space it consumed leaves Tc and 1 as the single token Tc1: content that is correct and a file that is not. Readers tolerate it, which is why it goes unnoticed until something strict refuses the page. Splices are separated on both sides so it cannot happen, and StrictLexPages is exported so a caller can assert it rather than trust it.

Not recoverable

The output is a complete file with one revision, so there is no earlier state to roll back to — which is the whole reason this is not an edit. Substituting text through an ordinary edit appends, and truncating the file at its first %%EOF hands the original straight back.

Before you get the file it is read back and searched: the page text, every string in every object still reachable, and any metadata packet. If an original is still findable, Pseudonymize reports it and writes nothing.

// gopdf: "Ada Lovelace" survives in a string (object 42);
// the output has been withheld

Two rules worth knowing. Mappings are applied longest first, so a rule for Ada Lovelace is not pre-empted by one for Ada; and a token may not contain its own original, which would leave the name in place. Text baked into a scanned image is pixels — use an area redaction for that.

The full guide, including how to choose between the two, is in docs/REDACTION.md.

Rewriting & repair

Rewrite emits a document as a fresh file holding only the objects it still reaches. Superseded objects left behind by earlier incremental updates are dropped, which shrinks the file and removes content that was replaced but never deleted.

r, _ := gopdf.Open("grown-by-updates.pdf")
out, _ := os.Create("clean.pdf")
gopdf.Rewrite(r, out)

An encrypted source is written out unencrypted: the objects are decrypted in order to be read, and re-encrypting them is a decision to take deliberately rather than a side effect.

Damaged files

Real files get their offsets wrong — a producer miscounts a header, a transfer rewrites line endings, bytes arrive in front of %PDF. Every offset the cross-reference table gives is checked against the bytes it points at, and the ones that are wrong are corrected from a scan of the file. Entries are corrected individually, so whatever the table got right is kept; when it cannot be parsed at all, the whole table is rebuilt from the scan and object streams are expanded to recover what they hold.

r, _ := gopdf.Open("damaged.pdf")
if r.Repaired() {
	// The file's table was unusable and the objects were found by
	// scanning. It reads, but anything the scan could not reach is gone.
}

Reading form fields

r, _ := gopdf.Open("application.pdf")
if r.HasForm() {
	for _, f := range r.FormFields() {
		fmt.Printf("%-14s %-9s page %d  %q %v\n",
			f.Name, f.Type, f.Page, f.Value, f.Options)
	}
}
applicant   text      page 0  ""       []
country     choice    page 0  "Italy"  [Italy France Spain]
subscribe   checkbox  page 0  ""       []
plan        radio     page 0  ""       []

Field types are FieldText, FieldCheckbox, FieldRadio, FieldChoice, FieldButton and FieldSignature. Each field also reports ReadOnly, Required, MaxLen and its widget Rect.

Filling forms

Flattened — final and unchangeable

doc := gopdf.New()
n, err := doc.FillForm(r, map[string]string{
	"applicant": "Ada Lovelace",
	"country":   "France",
	"subscribe": "Yes",
})
doc.Save("application-filled.pdf")

Values are drawn into the page content and the interactive fields are dropped, so the result displays and prints identically everywhere and the recipient cannot alter it. Fields you do not set keep the appearance they already had. Text uses each field's own /DA — font, size, colour and alignment — auto-sized to its box and clipped to its rectangle.

Interactive — still editable

doc.FillFormInteractive(r, map[string]string{"applicant": "Grace Hopper"})

This carries the field tree, the widget annotations and the form's default resources across, and generates a fresh appearance stream for every text and choice field so the values are visible immediately rather than only after a viewer regenerates them. Checkboxes and radio buttons switch to their existing "on" appearance.

Both paths validate before changing anything. An unknown field name, a value past MaxLen, a choice value that is not an option, or a write to a read-only field returns an error and leaves the document untouched.

Authoring forms

doc := gopdf.New()
page := doc.AddPage()

page.AddTextField("name", 160, 100, 240, 20, gopdf.FieldOptions{
	MaxLen:  60,
	Tooltip: "Your full legal name",
})
page.AddChoiceField("country", 160, 130, 160, 20,
	[]string{"Italy", "France", "Spain"}, gopdf.FieldOptions{Value: "Italy"})
page.AddCheckbox("newsletter", 160, 160, 16, gopdf.FieldOptions{Selected: true})
page.AddRadioButton("plan", "basic", 160, 190, 14, gopdf.FieldOptions{})
page.AddRadioButton("plan", "pro", 240, 190, 14, gopdf.FieldOptions{Selected: true})
page.AddTextField("notes", 160, 220, 240, 60, gopdf.FieldOptions{
	Multiline:  true,
	Background: &gopdf.Color{245, 245, 245},
})

doc.Save("application.pdf")

FieldOptions covers the value, font, size, colour, alignment, MaxLen, multiline, read-only and required flags, border and background colours, tooltip, and the initial selection of a checkbox or radio button. Every widget gets a complete appearance stream, so the form looks right before a viewer touches it. Duplicate field names, repeated radio values and out-of-range initial values are all rejected.

Encryption

// Protect a new document
doc.Encrypt("userpw", "ownerpw", gopdf.AllowPrint|gopdf.AllowCopy, gopdf.AES256)

// Read a protected file — either password works
r, err := gopdf.OpenPassword("protected.pdf", "userpw")
if errors.Is(err, gopdf.ErrPasswordRequired) {
	// prompt and retry
}

Reading supports RC4 (40/128-bit), AES-128 and AES-256, including the revision 6 hardened password hash. Writing supports AES128 (revision 4, readable everywhere) and AES256 (revision 6, PDF 2.0). An empty user password means anyone can open the file while the permissions still apply, and Open/NewReader try it automatically.

Permission constants: AllowPrint, AllowModify, AllowCopy, AllowAnnotate, AllowFillForms, AllowAccessible, AllowAssemble, AllowHighResPrint, plus AllowAll and AllowNone.

Permissions are advisory. The PDF specification relies on viewers to honour them; they do not protect content from anyone who can open the document.

Digital signatures

A signature is applied through an incremental update, which is what makes signing safe: the original bytes are never touched, so a signature already on the document keeps covering exactly what it signed.

r, _ := gopdf.Open("contract.pdf")
u := gopdf.Update(r)

if err := u.Sign(gopdf.SignOptions{
	Certificate: cert,                 // *x509.Certificate
	Key:         key,                  // any crypto.Signer
	Name:        "Ada Lovelace",
	Reason:      "Approval",
	Location:    "London",
}); err != nil {
	return err
}
u.Save("signed.pdf")

Key is a crypto.Signer, not a concrete key type, so the private key can live in an HSM, a smartcard or a KMS and never enter the process. Only its Sign method is called, once, over a SHA-256 digest.

What actually gets signed

The signature blob is detached PKCS#7 (adbe.pkcs7.detached) over a SHA-256 digest of the whole file except the blob itself. That exclusion is the awkward part of signing a PDF: the file has to be laid out first, the signature computed over the finished bytes, and the result patched back in without moving anything. gopdf writes fixed-width placeholders for /ByteRange and /Contents, measures the file, then overwrites the placeholders in place — the length never changes, so every offset the cross-reference table records stays correct.

ReservedBytes sizes the placeholder and defaults to 8 KiB, which fits an ordinary certificate chain. If the blob does not fit, signing fails with a message telling you the size it needed rather than producing a truncated signature.

Reading signatures

Reading works on any file, not just ones gopdf wrote.

for _, s := range r.Signatures() {
	fmt.Printf("%s signed %s at %s\n", s.Signer, s.Reason, s.When)
	if !s.CoversWholeFile {
		fmt.Println("  ⚠ the file was changed after this signature")
	}
	if s.Certified {
		fmt.Printf("  certifying, DocMDP level %d\n", s.Permissions)
	}
}

CoversWholeFile is the field to check. A signature whose ByteRange stops short of the end of the file was signed before something else was appended, and only vouches for the part it covers. Certified marks a /DocMDP signature that also restricts later changes; gopdf refuses to modify a document certified at level 1, which forbids any change at all.

Validity is not trust. Signatures reports what a document claims and whether the bytes still match it. Deciding whether the signing certificate is one you trust — chain building, revocation, policy — is your application's job, with crypto/x509. gopdf hands you the certificate; it does not vouch for it.

Signing adds no timestamp from a time-stamping authority: the recorded time is what the signer claimed. A signature is adbe.pkcs7.detached with SHA-256, which every current viewer understands.

Design notes

Rendering a page

Some of what a page holds has no other form. Text can be extracted and a photograph pulled out and re-anchored, but a logo drawn as two hundred Bézier curves, a rule, a checkbox, a watermark across the diagonal — those are instructions, and the only way to carry them anywhere else is to draw them.

Each layer is a separate switch, so the artwork alone is one call and the whole page is another.

r, _ := gopdf.Open("contract.pdf")

// The artwork alone: a layer that can sit behind live, editable,
// redactable text rather than replacing it.
art, err := r.RenderPage(0, gopdf.RenderOpts{
	DPI:           150,
	IncludeVector: true,   // paths, fills, strokes, shadings
	Transparent:   true,   // clear where nothing was painted
})

// Or the page as a reader sees it.
img, report, err := r.RenderPageDetail(0, gopdf.RenderOpts{
	DPI:                 150,
	IncludeVector:       true,
	IncludeText:         true,
	IncludeRasterImages: true,
	SubstituteFont:      gopdf.SystemFonts(),
})
if report.Missing > 0 {
	log.Printf("%d glyphs had no font to draw them with", report.Missing)
}

Glyphs are drawn from the outlines the document's own fonts carry: TrueType contours out of glyf, composites assembled from their parts, and CFF glyphs by running their Type 2 charstrings. A font the document only names is not something any parser can supply, so SubstituteFont lets you hand one over — SystemFonts builds that from the machine's own fonts. A substitute gives shapes only: every advance still comes from the widths in the document, so the text lands where the document says.

Only the large text

MinTextSize draws only glyphs at or above a size in points, measured after the text matrix and the transform have scaled them — the size PageTextFragments reports for the same glyph, so a threshold taken from extracted text means the same thing here.

backdrop, _ := r.RenderPage(0, gopdf.RenderOpts{
	DPI: 150, IncludeText: true, MinTextSize: 72, Transparent: true,
})

A watermark is set many times larger than the body it sits over, so a threshold between the two draws the watermark alone — from the document's own matrices, and so in exactly the place the document puts it. The body is not drawn and painted over: it is never drawn, which is the difference between a backdrop that can be handed on and one that has the text in it. A glyph below the threshold neither paints nor clips, and counts neither drawn nor missing, since it was not attempted; it still advances the pen, so the glyphs that do draw land where they would have.

What it draws

Pathsm l c v y re h, flattened and filled under either winding rule.
StrokesWidth, butt/round/square caps, miter/round/bevel joins with the miter limit honoured, and dash patterns with a phase.
ClippingW and W*, kept as a coverage mask so a clipped edge is as smooth as a painted one.
Stateq/Q, cm, and the constant alpha an ExtGState sets.
Blend modesAll fifteen, so a Multiply highlighter darkens the words under it instead of hiding them, and a Luminosity overlay keeps the colour beneath.
AnnotationsOpt-in with IncludeAnnotations: appearance streams fitted to their rectangles, states chosen by /AS, hidden ones left alone.
Optional contentLayers the document switches off are not painted, whether marked in the content stream or carried on an XObject.
ColourGray, RGB and CMYK; ICC by component count; indexed palettes; separations through their tint function.
ShadingAxial and radial gradients, evaluated through sampled, exponential and stitching functions.
MeshesTriangle meshes and Coons and tensor patches: the patch is evaluated on a grid, cut into triangles, and each filled with its corners spread across it.
PatternsTiling patterns, run cell by cell across the area they fill; shading patterns painted through the path.
Soft masksA luminosity or alpha group rendered on its own and folded into the clip. A mask in force when a transparency group is drawn applies to the whole group and cannot be cleared from inside it, which is what Illustrator's output depends on.
TextAll eight rendering modes: filled, stroked, both, invisible, and the four that add the glyphs to the clip. A text clip is followed even with text switched off, because what it removes is part of the artwork.
FormsXObjects recursed into, with their matrix composed and their bounding box clipping what they draw.
ImagesOff by default; IncludeRasterImages draws them, sampled through the inverse of their placement so a rotated one lands square.

What it does not

A glyph can only be drawn from outlines that exist. A font the document names without embedding has none, and a bare PostScript font is addressed by glyph name through the built-in encodings this package does not carry. Both are answered by supplying a substitute; without one their text is left undrawn, and RenderPageDetail reports how many glyphs that came to rather than handing back a page that looks complete.

A transparency group is composited onto the page rather than onto a surface of its own, so an isolated or knockout group is drawn as an ordinary one. It shows only where a group's own overlaps would have been resolved among themselves before reaching the page.

How close it is

Measured against pdftoppm across 1,500 documents of the corpus, 99.91% of the glyphs a page asks for are drawn, and the median document draws every one. On the one-sided check that matters — ink where the reference has none — the median page scores zero and 99% are under two per cent, the remainder being pages where the two renderers chose different substitutes for a font neither was given.

What a document says about itself

A page carries the marks; the document carries what they mean. Page numbering, layers, metadata, structure and the files travelling inside it are all somewhere other than the content stream, and none of them can be worked out by looking at the page.

Page labels

The page a reader calls iv is the fourth in the file, and the one they call 1 is often the ninth. Nothing in the page says so.

doc.SetPageLabels([]gopdf.PageLabelRange{
	{From: 0, Style: gopdf.LabelRomanLower},              // i, ii, iii…
	{From: 4, Style: gopdf.LabelDecimal, Start: 1},     // 1, 2, 3…
	{From: 40, Style: gopdf.LabelDecimal, Prefix: "A-"}, // A-1, A-2…
})

r.PageLabel(3)   // "iv"
r.PageLabels()   // the ranges, in page order

Styles are LabelDecimal, LabelRomanUpper, LabelRomanLower, LabelLettersUpper, LabelLettersLower and LabelNone. Pages ahead of the first range are numbered plainly, as a viewer numbers a document with no labels at all. Updater.SetPageLabels sets them on a file that already exists.

Layers

Optional content: a draft stamp kept for later, a set of labels for one audience. Both halves are here — putting content on a layer, and saying which layers a document starts with.

draft, _ := doc.AddLayer("Draft stamp", false) // declared, starts hidden
p.BeginLayer(draft)
p.Text(100, 400, "DRAFT")
p.EndLayer()

// And on a document that already has some:
u.SetLayerVisible("Draft stamp", true)
r.Layers()  // name and visibility, in the order a viewer shows them

The renderer honours them: a switched-off layer is not painted, whether it is marked in the content stream or carried on an XObject, and a membership dictionary's policy is read as written.

Metadata

A document records who wrote it twice — in the information dictionary and in an XMP packet — and the two routinely disagree because a tool updates one and leaves the other. Writing generates the packet from the dictionary so they cannot.

doc.SetInfo(gopdf.Info{Title: "Report", Author: "Ada Lovelace"})
doc.SetXMP(true)

x := r.XMP()      // Title, Author, Created…, and Raw for the rest
u.SetXMP(info)   // on an existing document, dictionary and packet together

Reading takes the element form and the attribute form, since producers use both — sometimes in one packet.

Tagged PDF

A tagged document says what its content is: this run is a heading, that image means “revenue, rising”. The page says none of it — the words come off it in the order the operators drew them, which for two columns is both columns interleaved.

if r.Tagged() {
	for _, h := range r.StructOutline() {
		fmt.Printf("%*s%s (page %d)\n", h.Level*2, "", h.Text, h.Page+1)
	}
	text := r.StructText()   // in the structure's order, not the operators'
	tree := r.Structure()    // []*StructNode: Role, Alt, ActualText, Page…
}

Element names go through the document's own role map to the standard ones, so a producer calling a heading Head1 still reports as H1. Alt is the alternate text a screen reader depends on; ActualText is what an element reads as where the glyphs do not spell it.

Embedded files

A PDF can carry other files: the spreadsheet a table came from, the original of a scan. Nothing on the page shows they are there.

doc.Attach("figures.csv", data)
u.AttachWithDescription("notes.txt", "working notes", data)

for _, a := range r.Attachments() {
	b, _ := a.Data()   // decoded, and decrypted if the file is
	_ = b
}
u.RemoveAttachments(func(a gopdf.Attachment) bool {
	return strings.HasSuffix(a.Name, ".csv")
})

Both places a file can live are read: the catalog's collection, through its name tree, and the paperclip annotation on a page. Redaction removes them by default — see Redaction.

The archival profile

PDF/A is mostly a set of refusals: a conforming file may not rely on anything outside itself, and must say what its colours mean and what it claims to be.

doc.SetPDFA(gopdf.PDFA2b)   // or PDFA2u, which also wants searchable text
// Save reports an error rather than writing a file that claims a
// conformance it does not meet — an unembedded font, or encryption.

for _, issue := range r.CheckPDFA(gopdf.PDFA2b) {
	fmt.Println(issue)   // "page 2: every font must be embedded (…)"
}

The check catches what goes wrong in practice — a font the file does not carry, encryption, a script, a missing intent. It is not a certificate: a full validator also examines colour management and the insides of embedded font programs. Across the corpus, 1,703 documents that claim conformance are all agreed with.

API map

What to reach for, by task. Every signature is on pkg.go.dev; this is the shape of it.

Making a document

New, AddPage, AddPageSize, SetPageSizeA document and its pages.
Save, WriteTo, SetInfoWrite it out, with metadata.
Document.Compress, CompressObjects, CreationDateFields, not methods: stream compression, object streams, the timestamp.
LoadFont, ParseFontA TrueType or OpenType face from a path or from memory.
AddImage, AddImageFile, AddImageReaderAn image from an image.Image, a path or a reader.
Push, Pop, Translate, Scale, RotateAtScoped transforms.
SetLineCap, SetLineJoin, SetDash, SetAlphaStroke and transparency state.
AddOutlineA bookmark tree.
SetPageLabelsPageLabelRange, PageLabelStyleThe numbering a reader sees: roman front matter, prefixed appendices.
AddLayerLayer, BeginLayer, EndLayerDeclare optional content and put content on it.
SetXMPA metadata packet generated from the information dictionary, so the two agree.
Attach, AttachWithDescriptionCarry another file inside the document.
SetPDFAPDFAConformanceWrite to the archival profile, or fail rather than claim it falsely.

Reading one

Open, OpenPassword, NewReader, NewReaderPasswordFrom a path or from memory, with or without a password.
NumPages, PageSize, PageText, InfoThe basics.
PageTextFragmentsTextFragmentText one show-text operation at a time, with its baseline, advance, /BaseFont, effective size and render mode. Content-stream order, forms composed. Invisible() marks the OCR layer under a scan.
IsEncrypted, Repaired, HasForm, HasSignaturesWhat kind of file this is.
RenderPage, RenderPageDetailRenderOpts, RenderReportDraw a page to an image, layer by layer. The detailed form reports the glyphs it could not draw.
SystemFontsFontRequestStand-in outlines for fonts the document names but does not embed.
Resolve, Object, Catalog, Trailer, PageDict, PageRef, InheritedPageValue, Objects, WalkStreamThe object graph itself, for whatever the typed API has no word for.
PageLabel, PageLabelsWhat a reader calls a page, and the ranges behind it.
LayersLayerThe optional content a document defines, and whether each starts visible.
XMPXMPThe metadata packet, parsed and raw.
Tagged, MarkedTagged, Structure, StructText, StructOutlineStructNode, StructHeadingWhat a tagged document says its content is: roles through the role map, alternate text, reading order, headings.
AttachmentsAttachmentThe files inside the document, from the collection and from the page.
CheckPDFAPDFAIssueWhat stops a document meeting the archival profile.
Annotations, PageImages, FormFields, SignaturesWhat it holds. An ImageRef carries its draw Matrix, with Rotation() and Upright(), so a turned image reports its angle rather than a box larger than the picture.

Changing one

UpdateUpdaterEdit in place, appended. Keeps everything the library does not model.
EditPageEditablePageRebuild into a new document, keeping the original operators editable.
ImportPageBring a page in as a form XObject, to stamp or watermark.
Runs, ExtractTextThe text on a page, operation by operation.
ReplaceText, Blocks + ReplaceTextReflow, Flows + ReplaceTextFlowIn-line, re-wrapped within a paragraph, and re-wrapped at any length.
RestyleChange a run's typeface, size or colour.
Redactor.Attachments, KeepAttachmentsSee the files a document carries, and decide whether a redaction keeps them.
ReplaceImage, RemoveAnnotations, SetFormValuesImages, annotations, fields.
SetPageOrder, RemovePage, MovePage, SetPageRotationPage operations.
AddObject, SetObject, SetCatalogEntry, SetPageEntry, NewStream, Dict.CloneWrite objects directly, for anything the typed API does not model.
SetPageLabels, SetLayerVisible, SetXMPChange the numbering, a layer's visibility, or the metadata, appended.
Attach, AttachWithDescription, RemoveAttachmentsAdd or take out the files a document carries.
SetFitMode, SetMaxExtraLines, SetCompressHow an edit is allowed to behave.

Whole files

Merge, ExtractPagesJoin documents, or pull pages out.
RewriteEmit only what the document still reaches, dropping superseded objects.
EncryptAES-128 or AES-256 with per-field permissions.
Updater.Sign, SignOptions, SignatureSign, and read the signatures already there.

Removing and replacing

RedactRedactorText, Pattern, Area, Match, Image, Substitute.
Marks, RedactionMark, RedactionKind, PartialArtworkReview before writing.
SetFill, SetLabel, SetLabelColor, SetOverlay, SetVerify, StripMetadata, KeepAnnotationsHow it looks and how hard it checks.
Pseudonymize, PseudonymizeFile, Pseudonym, PseudonymizeResultSubstitute rather than blank.
Pseudonym.FitWidth, Flow.SetFitWidthKeep the whole token and shrink it to the width it replaced, so the line breaks do not move.
Reverse, KeyUndo a substitution, where the pixels were not destroyed.
SetOCR, SetOCRConfidence, OCREngine, OCRWordReach text inside a scan; the engine is yours to supply.

The object model

For anything this package does not model, the parsed objects are exposed directly: Dict, Array, Name, Ref, String. A Reader resolves references and an Updater writes replacements back, so a caller can reach entries the API has no opinion about.

Limitations

Roadmap

CID-keyed CFF subsetting · PAdES timestamps · certificate security handlers · mesh shadings and tiling patterns · PDF/A conformance · linearization · reflow that cascades across pages.