gopdf
Create, read, edit, fill, sign and redact PDF files in pure Go — with nothing but the standard library.
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.
Links & bookmarks
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:
| Mode | Behaviour |
|---|---|
FitAdvance (default) | Compensates for the width difference so everything after the edit stays exactly where it was. |
FitScale | Additionally scales the replacement horizontally to occupy precisely the original width. Use it when the text would otherwise overlap a neighbour. |
FitNone | Writes 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
| Content | What happens |
|---|---|
| Text | The 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. |
| Images | The 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 artwork | A 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. |
| Annotations | Removed along with whatever text they hold, unless KeepAnnotations(true). |
| Metadata | The 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.
| Rule | Effect |
|---|---|
| Word boundaries | Rossi 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 break | A 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 lines | A document setting a line one piece at a time, with the gaps carried by positioning rather than spaces, still reads as words. |
| Spelling variants | Non-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
- The writer emits objects in a single pass with pre-assigned object numbers, then writes the cross-reference table from recorded offsets. Unencrypted output is byte-for-byte deterministic.
- Setting
doc.CompressObjectspacks the document's dictionaries into an object stream and writes a cross-reference stream instead of a table — 71% smaller on a twelve-page document with many small objects. Streams stay in the file body, where the format requires them. It is opt-in, and skipped for encrypted documents, whose strings are protected per object rather than by an enclosing stream. - Graphics-state setters skip operators that would not change anything,
with
Push/Pop-aware tracking, so repetitive drawing code still produces compact content streams. - Embedded fonts are subset by clearing the outlines of unused glyphs; glyph IDs are preserved and the result is a valid font in its own right — the test suite re-parses it to prove that. CFF subsetting rewrites the top dictionary's offsets and prunes the subroutines a Type 2 charstring interpreter proves unreachable, keeping the indexes the same length so the numbers charstrings call by stay valid. Anything it cannot follow means every subroutine is kept, and anything about the font beyond the subsetter means the whole program is embedded.
- Text editing splices new show-text operators over the originals; every other operator is left untouched. An edited page keeps its own resource dictionary, and this library's resources are merged in under a prefix that cannot collide with the source's names.
- Imported pages become form XObjects with their original streams copied verbatim and shared objects deduplicated per source document.
- Encryption applies to every string and stream, after compression, with
per-object keys. The
/Encryptdictionary and file identifier are written in the clear, as the specification requires. - Stream decoding is bounded against decompression bombs, and the reader and font parser are continuously fuzzed with a checked-in corpus.
- A 4,000-document redaction sweep removes a word from each and has
pdftotextconfirm it is gone: 3,986 succeed, none silently, none damaged.
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
| Paths | m l c v y re h, flattened and filled under either winding rule. |
| Strokes | Width, butt/round/square caps, miter/round/bevel joins with the miter limit honoured, and dash patterns with a phase. |
| Clipping | W and W*, kept as a coverage mask so a clipped edge is as smooth as a painted one. |
| State | q/Q, cm, and the constant alpha an ExtGState sets. |
| Blend modes | All fifteen, so a Multiply highlighter darkens the words under it instead of hiding them, and a Luminosity overlay keeps the colour beneath. |
| Annotations | Opt-in with IncludeAnnotations: appearance streams fitted to their rectangles, states chosen by /AS, hidden ones left alone. |
| Optional content | Layers the document switches off are not painted, whether marked in the content stream or carried on an XObject. |
| Colour | Gray, RGB and CMYK; ICC by component count; indexed palettes; separations through their tint function. |
| Shading | Axial and radial gradients, evaluated through sampled, exponential and stitching functions. |
| Meshes | Triangle 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. |
| Patterns | Tiling patterns, run cell by cell across the area they fill; shading patterns painted through the path. |
| Soft masks | A 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. |
| Text | All 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. |
| Forms | XObjects recursed into, with their matrix composed and their bounding box clipping what they draw. |
| Images | Off 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, SetPageSize | A document and its pages. |
Save, WriteTo, SetInfo | Write it out, with metadata. |
Document.Compress, CompressObjects, CreationDate | Fields, not methods: stream compression, object streams, the timestamp. |
LoadFont, ParseFont | A TrueType or OpenType face from a path or from memory. |
AddImage, AddImageFile, AddImageReader | An image from an image.Image, a path or a reader. |
Push, Pop, Translate, Scale, RotateAt | Scoped transforms. |
SetLineCap, SetLineJoin, SetDash, SetAlpha | Stroke and transparency state. |
AddOutline | A bookmark tree. |
SetPageLabels → PageLabelRange, PageLabelStyle | The numbering a reader sees: roman front matter, prefixed appendices. |
AddLayer → Layer, BeginLayer, EndLayer | Declare optional content and put content on it. |
SetXMP | A metadata packet generated from the information dictionary, so the two agree. |
Attach, AttachWithDescription | Carry another file inside the document. |
SetPDFA → PDFAConformance | Write to the archival profile, or fail rather than claim it falsely. |
Reading one
Open, OpenPassword, NewReader, NewReaderPassword | From a path or from memory, with or without a password. |
NumPages, PageSize, PageText, Info | The basics. |
PageTextFragments → TextFragment | Text 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, HasSignatures | What kind of file this is. |
RenderPage, RenderPageDetail → RenderOpts, RenderReport | Draw a page to an image, layer by layer. The detailed form reports the glyphs it could not draw. |
SystemFonts → FontRequest | Stand-in outlines for fonts the document names but does not embed. |
Resolve, Object, Catalog, Trailer, PageDict, PageRef, InheritedPageValue, Objects, Walk → Stream | The object graph itself, for whatever the typed API has no word for. |
PageLabel, PageLabels | What a reader calls a page, and the ranges behind it. |
Layers → Layer | The optional content a document defines, and whether each starts visible. |
XMP → XMP | The metadata packet, parsed and raw. |
Tagged, MarkedTagged, Structure, StructText, StructOutline → StructNode, StructHeading | What a tagged document says its content is: roles through the role map, alternate text, reading order, headings. |
Attachments → Attachment | The files inside the document, from the collection and from the page. |
CheckPDFA → PDFAIssue | What stops a document meeting the archival profile. |
Annotations, PageImages, FormFields, Signatures | What 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
Update → Updater | Edit in place, appended. Keeps everything the library does not model. |
EditPage → EditablePage | Rebuild into a new document, keeping the original operators editable. |
ImportPage | Bring a page in as a form XObject, to stamp or watermark. |
Runs, ExtractText | The text on a page, operation by operation. |
ReplaceText, Blocks + ReplaceTextReflow, Flows + ReplaceTextFlow | In-line, re-wrapped within a paragraph, and re-wrapped at any length. |
Restyle | Change a run's typeface, size or colour. |
Redactor.Attachments, KeepAttachments | See the files a document carries, and decide whether a redaction keeps them. |
ReplaceImage, RemoveAnnotations, SetFormValues | Images, annotations, fields. |
SetPageOrder, RemovePage, MovePage, SetPageRotation | Page operations. |
AddObject, SetObject, SetCatalogEntry, SetPageEntry, NewStream, Dict.Clone | Write objects directly, for anything the typed API does not model. |
SetPageLabels, SetLayerVisible, SetXMP | Change the numbering, a layer's visibility, or the metadata, appended. |
Attach, AttachWithDescription, RemoveAttachments | Add or take out the files a document carries. |
SetFitMode, SetMaxExtraLines, SetCompress | How an edit is allowed to behave. |
Whole files
Merge, ExtractPages | Join documents, or pull pages out. |
Rewrite | Emit only what the document still reaches, dropping superseded objects. |
Encrypt | AES-128 or AES-256 with per-field permissions. |
Updater.Sign, SignOptions, Signature | Sign, and read the signatures already there. |
Removing and replacing
Redact → Redactor | Text, Pattern, Area, Match, Image, Substitute. |
Marks, RedactionMark, RedactionKind, PartialArtwork | Review before writing. |
SetFill, SetLabel, SetLabelColor, SetOverlay, SetVerify, StripMetadata, KeepAnnotations | How it looks and how hard it checks. |
Pseudonymize, PseudonymizeFile, Pseudonym, PseudonymizeResult | Substitute rather than blank. |
Pseudonym.FitWidth, Flow.SetFitWidth | Keep the whole token and shrink it to the width it replaced, so the line breaks do not move. |
Reverse, Key | Undo a substitution, where the pixels were not destroyed. |
SetOCR, SetOCRConfidence, OCREngine, OCRWord | Reach 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
- Editing can only use glyphs a document's fonts actually contain.
- Reflow re-wraps within a paragraph's existing lines. Use a flow when the length changes: it adds or removes lines and moves the text below, though not images or rules.
- An incremental update only grows a file; superseded objects stay in it.
FillFormflattens; useFillFormInteractiveto keep fields editable.- OpenType subsetting keeps glyph names and subroutines, so
.otfembeds are larger than TrueType ones. - Encrypted files are read, but public-key (certificate) security handlers are not supported.
- Type 3 fonts are read — their widths are scaled through the font matrix and their text extracts — but authoring one is not supported.
- Redaction removes content, not structural names such as a font's
/BaseFont. Vector artwork straddling the edge of an area is covered but not deleted;PartialArtworkreports it. - Signing produces the blob and the byte range; trust decisions and timestamps from a TSA are left to the caller.
Roadmap
CID-keyed CFF subsetting · PAdES timestamps · certificate security handlers · mesh shadings and tiling patterns · PDF/A conformance · linearization · reflow that cascades across pages.