Skip to content

Latest commit

 

History

History
1061 lines (914 loc) · 47.8 KB

usage.md

File metadata and controls

1061 lines (914 loc) · 47.8 KB

Usage

Open a document, render a page into an image the caller owns, read its text:

doc, err := cera.Open(data)
page, err := doc.Page(0)
dst := image.NewRGBA(page.Bounds(150.0 / 72)) // owned by the caller, reusable
var st cera.Stats
err = page.Render(ctx, dst, cera.RenderOptions{
	Scale:      150.0 / 72,
	Background: color.RGBA{255, 255, 255, 255},
	Deadline:   time.Now().Add(time.Second), // partial image + ErrDeadline
	Stats:      &st,                         // ops, fills, unsupported features
	Workers:    0,                           // goroutines per page; 0 = all cores
})
page.Release() // drop the cached display list when the page leaves the view

text, err := page.Text(ctx) // characters, lines and blocks in reading order
fmt.Println(text.String())

A Document is safe for concurrent use: render its pages from as many goroutines as there are cores, each with Workers: 1, or one page with all of them; objects, fonts and images are loaded once and shared.

For a monochrome printer, a fax or text recognition, page.RenderGray(ctx, gray, opt) draws into an *image.Gray with the same options: the pixels of the page drawn in colour and converted as PDF converts DeviceRGB to DeviceGray, without holding the page in colour — it is drawn a band at a time.

For a press or a proof, page.RenderCMYK(ctx, cmyk, opt) draws into an *image.CMYK in the CMYK of the document — the profile of its output intent, OpenOptions.CMYKProfile or SWOP: DeviceCMYK keeps its values (black text stays 0 0 0 100 %), grey goes on black, Separation and DeviceN colorants named Cyan, Magenta, Yellow and Black on those inks, and RGB is converted through the profile's colour-to-ink table where it has one, else as PDF converts DeviceRGB. What is drawn on nothing is paper, no ink. The page is drawn twice, about twice the time of Render; overprint is not simulated in it yet (#155). A page in another RGB than sRGB is not there yet.

A viewer that turns its view asks for the page turned: page.Rotated(90) is the page as it is shown then — a Page with its own bounds, display list and text, the same each time it is asked for. Everything in points of the displayed page is in its coordinates: Bounds, Render, Text, Links, Annotations and EditAnnotation. Where an API takes a page number, the view is said beside it: page.Rotated(90).Destination(&target) for a link's target, index.SetRotation(90) for a TextIndex, and View: 90 in a NewAnnotation or Highlight whose geometry was taken from the turned view. Page.Rotate is for reading.

A page for print has more boxes than the one a viewer shows. page.Boxes() returns them — Media, Crop, Bleed, Trim, Art — in default user space, with the defaults of the specification for those a page does not have; page.DisplayMatrix() maps them to the points of the displayed page, where a viewer of print files outlines them. page.InBox(cera.BoxMedia) is the page showing its whole medium with the marks around it, and page.InBox(cera.BoxTrim) the finished page, as a thumbnail wants it: views like those of Rotated, which they combine with, and said beside a page number in the same way, by index.SetBox(cera.BoxMedia) and Box: cera.BoxMedia in a NewAnnotation or Highlight. The command draws one with -box.

A file need not be in memory: cera.OpenReaderAt(f, size, opts) reads through an io.ReaderAt (an *os.File, a browser File in js/wasm) only what the pages drawn need.

A document keeps what it decodes for the next page that needs it: 256 MB of images and 64 MB of streams (content, forms, Type 3 glyphs) at most, least recently used dropped first. That suits one document in a viewer. The bounds are options, and the caches can be emptied and shared:

// A thumbnailer draws each page once: keep nothing.
doc, err := cera.OpenWith(data, cera.OpenOptions{ImageCacheBytes: -1, StreamCacheBytes: -1})

// js/wasm, where the heap is small.
doc, err = cera.OpenWith(data, cera.OpenOptions{ImageCacheBytes: 32 << 20, StreamCacheBytes: 8 << 20})
doc.Trim() // the tab went to the background: drop what is decoded

// A server with many documents open: one bound for all of them.
cache := cera.NewCache(512<<20, 128<<20)
doc, err = cera.OpenWith(data, cera.OpenOptions{Cache: cache})
doc.Trim() // done with the document; otherwise its entries leave as others come in

// What the process keeps between renders, whatever the document.
cera.SetPoolLimits(cera.PoolLimits{GlyphCaches: 4, LayerBytes: 8 << 20})

Trim leaves the document and its pages usable: what they show is decoded again when it is drawn. It does not free the display list of a page — Page.Release does — nor the fonts and colour spaces the document has loaded, which go with the document. A bound of 0 is the default, a negative one none. The budgets on what one input may cost (budgets.md) are not options.

An encrypted file opens with its password. ErrPassword says that it needs one, or another one, and ErrUnsupportedEncryption that no password helps:

doc, err := cera.Open(data)
for errors.Is(err, cera.ErrPassword) {
	pw, ok := askPassword()
	if !ok {
		return
	}
	doc, err = cera.OpenWith(data, cera.OpenOptions{Password: pw})
}

if doc.Encrypted() && doc.Permissions()&cera.PermCopy == 0 {
	// the author does not want text copied
}

Permissions is what the author allows a reader with the user password, and everything for the owner password and for a file that is not encrypted. The editor refuses what they forbid; rendering and text extraction do not ask, so whether to print or to copy is the application's decision.

Fonts a document names but does not embed can come from a provider; the CJK module links Noto Sans only for the collections it is asked for:

import (
	"github.com/timzifer/cera/fonts/cjk"
	"github.com/timzifer/cera/fonts/cjk/japan1"
)

doc, err := cera.OpenWith(data, cera.OpenOptions{
	Fonts: cjk.Provider{japan1.Collection}, // or a cera.FontProvider of your own
})

The fonts installed on the machine are a provider too, in a module of its own, fonts/system: a document that asks for "Segoe UI" or "MS Mincho" where they are installed gets them. cera.FontProviders asks several in their order:

import "github.com/timzifer/cera/fonts/system"

doc, err := cera.OpenWith(data, cera.OpenOptions{
	Fonts: cera.FontProviders{system.New(), cjk.Provider{japan1.Collection}},
})

system.New() reads the font directories of the platform on first use — Windows' Fonts and the user's, macOS' three Fonts folders, what fontconfig reads elsewhere, nothing on js/wasm — and keeps the names. A font is found by the name the document spells (Arial,Bold, ArialMT, Arial-BoldMT), or by its family with the weight and slope asked for, and never by a font of another family: there the stand-ins below do better. A page drawn with the machine's fonts is not the same on every machine; that is why it is a module to ask for.

Helvetica, Times and Courier that a document does not embed are drawn with TeX Gyre Heros, Termes and Cursor, the shapes PDFium and MuPDF draw too. They add 1.45 MB to the binary; the build tag cera_nogyre leaves them out, and Arimo, Tinos and Cousine (same metrics, Arial's, Times New Roman's and Courier New's shapes) draw that text instead. Line lengths and layout are the same either way:

go build -tags cera_nogyre ./...
GOOS=js GOARCH=wasm go build -C cmd/cera -tags cera_nogyre -o "$PWD/cera.wasm" .

CMYK (DeviceCMYK, and ICCBased spaces of four components whose profile cera cannot read) converts through a bundled SWOP press profile (CGATS TR 005). A caller who prints or proofs against a known condition gives cera that profile instead; documents opened with the same profile share its tables. An ICCBased space whose own CMYK profile cera reads (lut8, lut16 or lutAtoB tables) converts through it, as PDFium and MuPDF do:

icc, err := os.ReadFile("CoatedFOGRA39.icc")
doc, err := cera.OpenWith(data, cera.OpenOptions{
	CMYKProfile: icc,   // an error from OpenWith if cera cannot read it
	NaiveCMYK:   false, // true: device values, R = (1-C)(1-K), over all profiles
})

PDFium and MuPDF convert DeviceCMYK close to Adobe's U.S. Web Coated (SWOP) v2, which may not be redistributed modified; with that profile as CMYKProfile, cera's CMYK colours come within a few levels of MuPDF's (ADR 0003).

A PDF/X, PDF/A or PDF/E file names the printing condition its device colours are meant for, usually with the profile (/OutputIntents). Where the caller gives no profile, cera converts DeviceCMYK through that one, as Acrobat and MuPDF do, so a file made for uncoated paper or newsprint is shown with those colours. OpenOptions.IgnoreOutputIntent keeps the bundled profile, as PDFium does.

Layers (optional content) are switched per render, not on the document, so several views can show different layers; switching draws the cached display list again without interpreting the page:

cfg := doc.Layers() // nil without /OCProperties
for _, l := range cfg.Layers {
	fmt.Println(l.Name, l.Visible)
}
vis := cfg.Visibility().With(cfg.Layers[0], false)
err = page.Render(ctx, dst, cera.RenderOptions{Scale: 150.0 / 72, Layers: &vis})

Annotations are drawn over the page content, as on screen by default; RenderOptions.Annotations selects AnnotsPrint or AnnotsNone, and SkipAnnotation leaves single ones out (a viewer drawing form fields itself). Both only redraw the cached display list:

for _, a := range page.Annotations() {
	if a.Link != nil {
		fmt.Println(a.Rect, a.Link.URI, a.Link.Page)
	}
}

Interactive forms (ADR 0006): Document.Form reads the fields of /AcroForm with their widgets, and a FormState the caller owns holds the values a user enters. Rendering with RenderOptions.Form shows them through generated appearances, recorded once per widget and value; the page itself is not interpreted again. A FormLayer places native widgets of a FormWidgetProvider over the page image and leaves them out of it; form/fyneform is such a provider for fyne, a module of its own so that cera stays free of toolkits and cgo:

form := doc.Form() // nil without /AcroForm
state := form.NewState()
err = state.SetValue(form.Field("address.street"), cera.TextValue("Main St 1"))
// A field with Computed set is formatted, validated and calculated as its
// scripts say: SetValue refuses a number out of range (ErrInvalidValue)
// and recalculates the totals; f.Format(state.Value(f)) is what it shows.

layer := cera.NewFormLayer(page, state, provider)
layer.Update(cera.View{Scale: scale, Viewport: visible})
err = page.Render(ctx, dst, cera.RenderOptions{Scale: scale, Form: state, SkipAnnotation: layer.Skip})

A push button does what its widget's Actions say, in their order. The state carries out those of the form — ResetForm sets the fields named to their defaults, Hide shows or hides widgets, which renders with the state and a FormLayer then obey — and the rest is the viewer's:

for _, a := range widget.Actions {
	switch {
	case state.Do(a): // ResetForm, Hide
	case a.Target != nil:
		follow(a.Target) // GoTo, URI, Named: as for a link
	case a.Kind == "SubmitForm":
		body, contentType, err := state.Submission(a) // FDF, XFDF or an HTML form
		// ask the user, then send body to a.URL — or do not: cera sends nothing
	case a.Kind == "JavaScript":
		// a.Script: reported, not run
	}
}

state.SetHidden and state.ResetFields do the same by hand. What a state hides is how the document is shown, not part of it: SetFields does not write it.

The values of a state travel as FDF or XFDF, the exchange formats of form data: state.ExportFDF(w) and state.ExportXFDF(w) write every field that has a value, by its name — but for passwords —, and state.ImportFDF(r) and state.ImportXFDF(r) set the fields the data names, as SetValue does, so that the form checks them and calculated fields follow:

err = state.ImportXFDF(f)
var fail *cera.FormDataError
if errors.As(err, &fail) {
	// the rest is set; fail.Unknown: names the form does not have,
	// fail.Refused: values it does not take (ErrInvalidValue, ErrReadOnly)
}
err = e.SetFields(state) // and written into the document

Rich text values are dropped for the plain ones beside them, and what else the formats carry — annotations, pages, scripts — is not read.

dst may be any sub-rectangle of the page (a tile or viewport): only that region is drawn. RenderOptions.Region narrows it further. A region gets, byte for byte, the pixels the page drawn at once has there: tiles can be cached and compared, and the number of workers does not show. Errors in the content never stop a page; a non-nil error means a partial image (ErrDeadline, a rasterizer budget, or a recovered *PanicError).

Page.Run drives any Device directly, without a display list; a device that also implements TextDevice receives every string shown, in every render mode, and one that implements MarkedContentDevice the marked-content sequences (tag, MCID, /ActualText, /Alt, /Lang). Page.Text is built on both: each TextChar has the Quad of its em box, its baseline direction and advance, its MCID and whether it is an artifact. Its coordinates are points of the displayed page, y down, after /Rotate and /UserUnit; Page.DisplayMatrix maps user space to them.

Page.Text lays the characters out for reading: PageText.Chars are in reading order, Lines and Blocks index into them. It drops the copies fake bold draws on top of a character, splits ligatures (also U+FB00 to U+FB06) into one character each, puts /ActualText on the first glyph it replaces, joins text shown out of order on one baseline, and orders blocks by the structure tree of a tagged PDF, the rest by an XY cut: columns, rows, text in other directions last. TextLine.Hyphenated marks a word broken across lines. String separates lines by a line break and blocks by an empty line. Page.TextWith selects layers, annotations and form values, and can ignore the structure tree.

Arabic and Hebrew come out as they are read and typed: a page shows them in visual order and as presentation forms, and the layout puts the characters of such a line into logical order (the Unicode bidirectional algorithm, numbers and Latin words inside kept as they read) and gives the letters for the forms, so the text can be searched and copied. TextLine.RTL marks a line that reads from the right — its first character is at its right end, and PosAt and Select follow the pointer there; Quads of a range across both directions gives a box for each stretch. The columns of a page that reads from the right, by its lines or by /ViewerPreferences /Direction /R2L, are read from the right.

Selection works on the PageText alone, in points of the displayed page; a viewer divides its pointer positions by its zoom:

a := text.PosAt(x0, y0)                       // where the drag started
b := text.PosAt(x1, y1)                       // where the pointer is
sel := text.Select(a, b, cera.SelectWords)    // SelectChars, SelectWords, SelectLines, SelectBlocks
for _, q := range text.Quads(sel) { /* highlight q, scaled by the zoom */ }
copied := text.TextOf(sel, cera.TextFormat{JoinHyphenated: true})

WordAt and LineAt give the word and line for a double and triple click, CharAt the character under the pointer, and InRect the characters in a rectangle (a column of a table). A selection is a TextRange of indices into Chars: keep as many as there are marks. To store a mark, keep bytes of the text instead, which outlive changes to the layout: Offset maps a position to its byte offset in String, and RangeOfOffsets maps a byte range back.

Search runs on the text as String has it, with runs of white space as one space, soft hyphens left out and hyphenated words joined, so a query finds a word broken across lines. A TextIndex keeps the laid-out text of recently used pages and searches the document from a position on, wrapping, forward or back:

x := cera.NewTextIndex(doc, cera.TextOptions{}, 0) // 0 keeps 64 pages
opt := cera.SearchOptions{WholeWord: true}        // also MatchCase, IgnoreDiacritics, Context
for m, err := range x.Find(ctx, "gamma", opt, cera.DocPos{Page: cur}, false) {
	if err != nil { /* a page that failed, or ctx ended */ continue }
	text, _ := x.Page(ctx, m.Page)
	show(m.Page, text.Quads(m.Range), m.Quote.Prefix, m.Quote.Exact, m.Quote.Suffix)
}

Each match carries a Quote: its text and up to Context characters before and after it. A quote is an anchor that survives a new extraction and works for text layers that are not cera's (such as GROBID's), where byte offsets mean nothing: QuoteOf makes one from a range, and PageText.FindQuote or TextIndex.FindQuote (from a page outward) find the place whose surroundings agree most. TextIndex.TextOf copies a selection across pages, from one DocPos to another.

For the pointer, TextIndex.Hit says what lies at a point of the displayed page, and a click on a link resolves its target to a view:

h, _ := x.Hit(ctx, page, px/zoom, py/zoom)
switch h.Kind {
case cera.HitLink: // hand; on click:
	if t := h.Link.Target; t.URI != "" {
		open(t.URI)
	} else if d, ok := doc.Destination(&t); ok {
		show(d.Page, d.Fit, d.X, d.Y, d.Zoom, d.Rect) // displayed points of page d.Page
	} else if n, ok := t.NamedPage(current, doc.NumPages()); ok {
		showPage(n) // /Named: NextPage, PrevPage, FirstPage, LastPage
	} else if t.External {
		ask(t.File) // /GoToR, /GoToE, /Launch: cera follows none of them
	} else {
		say(t.Action) // "JavaScript", "SubmitForm", ...: nothing cera runs
	}
case cera.HitWidget: // the form layer's; h.Widget
case cera.HitText:   // I-beam; h.Pos starts a selection
}

Page.Links returns the link annotations with their quads (from /QuadPoints, else the rectangle), PageText.URLs the addresses written in the text. Every link with a destination or an action is one: LinkTarget.Action names the action, and of chained actions (/Next) the target is the first that leads somewhere — a script before a GoTo is skipped. A relative URI is resolved against the document's base (/URI /Base).

Page.Annotations lists the annotations, and for the markup among them (notes, highlights, ink, shapes, free text, stamps) Markup says what a comments panel shows: author and dates, colour, the exact geometry in points of the displayed page, the popup, and which annotation one replies to:

for _, a := range page.Annotations() {
	m := a.Markup
	if m == nil || m.Reply != nil {
		continue // no markup, or a reply: shown under its annotation
	}
	addComment(a.Index, a.Subtype, m.Author, m.Modified, a.Contents, m.Bounds)
}

A highlight written with Editor.AddHighlight reads back with the quads, colour, opacity, author, date and note it was made from.

Document.PageLabel(i) is the number printed on page i — "iv", "12", "A-3" — for the page box of a viewer, and Document.PageByLabel finds the page for what a user types into it; both say nothing in a document without /PageLabels.

Document.Outline returns the bookmarks for a side panel, a tree of entries with a title, its style and a target that resolves like a link's:

var show func(items []cera.OutlineItem, depth int)
show = func(items []cera.OutlineItem, depth int) {
	for _, it := range items {
		row := addRow(depth, it.Title, it.Bold, it.Italic, it.Color, it.Open)
		if d, ok := doc.Destination(it.Target); ok {
			row.onClick(func() { goTo(d) })
		}
		show(it.Children, depth+1)
	}
}
show(doc.Outline(), 0)

Document.ViewSettings is how the document asks to be opened: where to start, which panel to show, how to lay out the pages, and what its print dialogue starts with. They are wishes, with the defaults of the specification where the document has none:

vs := doc.ViewSettings()
if d, ok := doc.Destination(vs.Open); ok {
	goTo(d) // the page and zoom of /OpenAction
}
switch vs.PageMode {
case "UseOutlines":
	showOutline()
case "UseThumbs":
	showThumbnails()
case "UseAttachments":
	showAttachments()
}
setLayout(vs.PageLayout, vs.RightToLeft) // "TwoPageRight": spreads with a cover page
if vs.DisplayDocTitle {
	setTitle(doc.Metadata().Title)
}
printDialog(vs.Print.Ranges, vs.Print.Copies, vs.Print.Duplex, vs.Print.Scaling == "None")

Open is nil for an /OpenAction that does not lead to a place in the document — a script, an address, another file.

Structure

A tagged document says what its content is: headings, paragraphs, lists, tables with their cells, figures with a description. Document.Structure returns that tree, nil for a document without one — what a bridge to a screen reader reads by, and what an export to HTML keeps headings and tables with:

tree := doc.Structure()
if tree == nil {
	return // untagged: the text of the pages in the order of their layout
}
var walk func(e *cera.StructElem, depth int)
walk = func(e *cera.StructElem, depth int) {
	fmt.Printf("%s%s %q\n", strings.Repeat("  ", depth), e.Role, e.Alt)
	for _, k := range e.Kids {
		switch {
		case k.Elem != nil:
			walk(k.Elem, depth+1)
		case k.MCID >= 0:
			// the characters of page k.Page whose TextChar.MCID is k.MCID
		case k.Annot >= 0:
			// the annotation of page k.Page with that Index: the link of a Link
		}
	}
}
for _, e := range tree.Elems {
	walk(e, 0)
}

Type is the name the document gives an element and Role the standard type its role map makes of it ("Heading 1" is an "H1"). Kids are in reading order: elements, the marked content of a page by its identifier, and annotations. Title, Alt, ActualText, Expansion and Lang are those of the element, the language inherited from above; Attr() has the attributes of tables and lists (RowSpan, ColSpan, Headers, Scope, Summary, ListNumbering) and the BBox of a figure, and tree.ByID(id) finds the header cell a cell names. tree.Suspects says that the producer guessed the tree from a layout. Other attributes, and content marked inside form XObjects, are not read.

Metadata

m := doc.Metadata()
fmt.Println(m.Title, m.Author, m.Created.Format(time.DateOnly))
if m.XMP != nil {
	// parse with encoding/xml if dc:title and the like are needed
}

Metadata never fails: a missing or damaged /Info leaves fields empty. Dates are read leniently (a missing D:, missing fields, Z00'00'); a date without a time zone is taken as UTC. PDF 2.0 deprecates /Info, but for its dates, in favour of XMP, which is returned raw.

Signatures

Document.Signatures lists the signatures in the order they were made, and Verify checks one:

for _, s := range doc.Signatures() {
	st, err := s.Verify(cera.VerifyOptions{Roots: roots}) // roots: *x509.CertPool, or nil
	switch {
	case err != nil:           // cera.ErrSignatureUnsupported: cannot be checked
	case !st.Intact:           // the signed bytes changed, or the signature is not the certificate's
	case !st.ChangesAllowed:   // something was appended that the signatures do not allow
	case st.Changes.Any():     // signed, then filled in, signed again or annotated: st.Changes
	}
	// s.Signer, s.Time (st.Timestamped: from a timestamp token), s.Reason,
	// s.Certification; st.Trusted and st.TrustError for the chain
}

Intact is the cryptography: the digest of the bytes the signature covers and the signature of the certificate's key (signed data of adbe.pkcs7.detached, ETSI.CAdES.detached and adbe.pkcs7.sha1, bare adbe.x509.rsa_sha1, document timestamps ETSI.RFC3161; RSA and ECDSA). Changes compares the document as signed with the document as it is. Trust is against the roots the caller gives: cera has no trust list, and does not check revocation.

Editor.Sign signs: it writes the document with an update that holds the changes of the editor and a signature over all of it, so that the signatures the document has stay valid.

e := doc.Edit()
err := e.Sign(out, cera.SignOptions{
	Key:         key,  // a crypto.Signer: a key in memory, on a token, with a service
	Certificate: cert, // its certificate, and above it
	Chain:       []*x509.Certificate{intermediate},
	Reason:      "Approved",
	Location:    "Berlin",

	Field: "approval",                 // a signature field of the form, or a new one
	Page:  0,                          // where a new field shows, in points of the
	Rect:  cera.Rect{360, 700, 560, 760}, // displayed page; the zero Rect: invisible
	Appearance: func(c *cera.Canvas) { // what it shows, as Editor.Draw draws
		w, h := c.Size()
		c.Stroke(rect(0, 0, w, h), cera.StrokeStyle{Width: 1}, color.Black)
		c.DrawText("Signed by Ada Lovelace", 6, 20, font, 10, color.Black)
	},
})

The signature is a PAdES baseline signature (B-B, ETSI.CAdES.detached) with SHA-256, by an RSA or an ECDSA key, which is asked once. It has no timestamp and no validation data: its time is the signer's word. Certify: 2 makes the author's certification instead, which says what may change afterwards — 1 nothing, 2 filling in forms and signing, 3 also annotating — and Lock names the fields that may not change once it is made. Form values, annotations and metadata of the same editor are written with the signature; changes of the pages are not: update the document first, open the result and sign that. Likewise, to go on after a signature, open what Sign wrote. The file is held in memory while it is signed.

Attachments

Document.Attachments lists the files a document embeds — the XML of an e-invoice, the data behind a report — with name, description, media type, dates and Relationship (/AFRelationship), and Open reads one:

for _, a := range doc.Attachments() {
	if a.Relationship == "Alternative" && a.MIME == "text/xml" { // Factur-X, ZUGFeRD
		r, err := a.Open()
		// ...
	}
}

The file of a FileAttachment annotation is Annotation.File on its page. A document may encrypt its attachments alone: it opens without the password, and Open returns ErrPassword until it is given.

Editor.SetMetadata writes one back, into /Info and into the XMP packet so that both agree — the properties that mirror /Info are changed in the packet the document has, the rest of it stays, and a document without a packet gets one:

m := doc.Metadata()
m.Title, m.Modified = "Annual report", time.Now()
err = e.SetMetadata(m)

Save and Update set no date or producer on their own: the same edit gives the same file.

Editing

An Editor (ADR 0013) records changes to a document, or puts a new one together, and writes the result as a new file:

e := doc.Edit()          // all pages of doc, and everything around them
e := cera.NewEditor()    // or an empty document

err = e.ImportPages(at, other, cera.EditPage{Index: 2, Rotate: 90})
err = e.DeletePages(3, 4)
err = e.MovePage(from, to)
err = e.RotatePage(i, 90)
err = e.SetFields(state) // values of doc.Form().NewState() the user changed
err = e.Flatten()        // draw the form into the pages and drop it
err = e.FlattenAnnotations() // and the other annotations
err = e.AddHighlight(i, cera.Highlight{Quads: text.Quads(sel), Note: "check"})
k, err := e.AddAnnotation(i, cera.NewAnnotation{Subtype: "Text", Contents: "A note",
	Markup: cera.Markup{Bounds: cera.Rect{X0: 40, Y0: 40, X1: 60, Y1: 60}, Author: "Ada"}})
err = e.Save(w)          // a new file
err = e.Update(w)        // the file as it was, and an incremental update

Everything at the level of the document (outline, names, structure tree, form, layers, page labels, metadata) belongs to the edited document. Pages imported from another document bring only what is on the page: content, resources, annotations and links to other pages imported with them; their widgets stay as plain annotations — unless the page is imported with EditPage{Index: i, Fields: true}: then the fields reaching its widgets come too, with their values, a root renamed name_2 where the result has the name (signature fields stay behind). Layers of imported pages are added, so hidden layers stay hidden. Deleting a page removes its annotations and the form fields whose widgets were all on deleted pages; outline entries and links to it lose their target.

Pages are copied as they are: every object once per source document, byte for byte, streams with their filters. An encrypted document may be reassembled when its permissions allow it, else Save and ImportPages return ErrNoAssembly. Save may be called again; documents an editor uses may be rendered meanwhile.

Save keeps the protection of an encrypted document: the saved file opens with the same passwords and grants the same permissions. SaveWith changes that — it protects a file, or writes a protected one plain — and for an encrypted document either needs the owner password (ErrNotPermitted otherwise):

err = e.SaveWith(w, cera.SaveOptions{Encrypt: &cera.Encryption{
	UserPassword:  "to open",
	OwnerPassword: "to change",
	Permissions:   cera.PermPrint | cera.PermPrintFaithful,
}})
err = e.SaveWith(w, cera.SaveOptions{Decrypt: true})

New protection is AES-256 (PDF 2.0, revision 6) with a random file key. Pages imported from other documents are encrypted, or not, as the result is.

ImportPages brings what is on the pages. ImportPagesWith brings what their document says about them too, so that a merged file is no worse than its parts:

err = e.ImportPagesWith(e.NumPages(), other, cera.ImportOptions{
	Outline:      true,       // the bookmarks that lead to the imported pages
	OutlineTitle: "Appendix", // under one entry; "" puts them at the top level
	Destinations: true,       // named destinations, renamed name_2 where taken
	Labels:       true,       // the page numbers printed on them
}, pages...)

Bookmarks and names whose page is not in the result are left out. The structure tree does not come: imported pages are untagged.

AddPage makes a blank page, and Draw draws on any page of the result — over its content or under it — with a Canvas in points of the displayed page, the origin at the top left, as everywhere in the editor: a watermark, page numbers, a header, a cover sheet, pages of scanned images, several pages on a sheet.

font, err := cera.StandardFont("Helvetica") // or cera.NewCanvasFont(ttf)
logo := cera.NewCanvasImage(img)             // or cera.NewCanvasJPEG(data)

err = e.AddPage(0, 595, 842) // a cover sheet, A4
for i := range e.NumPages() {
	err = e.Draw(i, true, func(c *cera.Canvas) {
		w, h := c.Size()
		label := fmt.Sprintf("%d / %d", i+1, e.NumPages())
		c.DrawText(label, (w-font.Width(label, 9))/2, h-24, font, 9, color.Gray{90})
		c.DrawImage(logo, cera.Rect{X0: w - 96, Y0: 24, X1: w - 24, Y1: 48})

		c.Save()
		c.SetOpacity(0.15)
		c.Transform(cera.Matrix{0.7, -0.7, 0.7, 0.7, w / 2, h / 2}) // turned about the middle
		c.DrawText("DRAFT", -120, 30, font, 80, color.RGBA{200, 0, 0, 255})
		c.Restore()
	})
}

A canvas fills, strokes and clips paths (cera.Path), with an opacity and a blend mode; draws images, a JPEG as it is and anything else compressed without loss, with its alpha; draws text in the standard 14 fonts or in a TrueType or OpenType font, of which the file gets the glyphs drawn; and draws another page (DrawPage), for sheets of several pages and for a stamp that is a PDF. Text is one line, character by character: there is no shaping and no line breaking. A font or an image made once is one object in the file however many pages show it.

The content of a page drawn on is kept as it is: a drawing is a form of its own before or after it. It belongs to its page — it moves and turns with it. In an encrypted document, drawing needs the permission to modify; a certification forbids it.

Redact takes content out of a page for good. A box drawn over text leaves the text in the file; a redaction rewrites the page, so that what was in its areas is not there to be found by a viewer, a text extraction or a look at the bytes:

text, _ := page.Text(ctx)
for _, m := range text.Find("Jane Doe", cera.SearchOptions{}) {
	err = e.Redact(page.Index(), text.Quads(m.Range), color.Black) // areas in points of the displayed page
}
n, err := e.ApplyRedactions() // the /Redact annotations a reviewer left
err = e.Save(out)             // never Update: that would keep the bytes

In the areas, the glyphs leave their strings (the rest of each line stays where it was), paths that lie in them are left out and paths that reach into them are clipped, images are decoded, emptied there and encoded again, inline images are left out; form XObjects are rewritten as the page is, each use a copy, and hidden layers like shown ones. Annotations, links and fields that reach into an area go, as do /ActualText, /Alt and /E of the marked content and the structure elements the content belonged to, and the thumbnail of the page. It fails closed: what cannot be rewritten is left out whole, and Save returns ErrRedaction rather than write a page it could not rewrite.

What it leaves for the caller to look at: the outline, whose titles may quote a heading; metadata and attachments; other pages, also where they show the same form or image. A path that reaches into an area is clipped, not cut — its numbers stay in the file —, and the cells of tiling patterns and soft masks are not rewritten, only clipped away with what they fill. An emptied image is written in RGB.

AddField makes a form, or adds to one: a text field, a check box, the buttons of a radio group, a combo or list box, a push button, or a signature field for Sign to sign later. RemoveField takes one out, with its widgets.

boxed := cera.Appearance{Border: &black, Background: &white}
err = e.AddField(0, cera.Rect{60, 100, 260, 120}, cera.NewField{
	Name: "address.street", // below a node "address", made if there is none
	Type: cera.FieldText, Value: cera.TextValue("Main St 1"), Look: boxed,
})
// A radio group: a widget each, under one name.
err = e.AddField(0, cera.Rect{60, 130, 74, 144}, cera.NewField{Name: "plan", Type: cera.FieldRadio, OnState: "basic", Value: cera.StateValue("basic"), Look: boxed})
err = e.AddField(0, cera.Rect{90, 130, 104, 144}, cera.NewField{Name: "plan", OnState: "pro", Look: boxed})
err = e.AddField(0, cera.Rect{60, 160, 260, 220}, cera.NewField{Name: "approval", Type: cera.FieldSignature})
err = e.RemoveField("obsolete")

The rectangle is in points of the displayed page; a name the form has adds a widget to that field. A new field is written with a default appearance in Helvetica — the form's /Helv, added to its resources if it has none — and an appearance stream for its value, as cera draws fields that have none. Pages are those of the edited document; a certified document takes no fields and gives none up (ErrCertified), a field a signature locks stays (ErrLocked), and Sign wants the fields written first.

SetFields records the values of a FormState of the edited document that differ from what the file holds: a text as a text string (its rich text value removed), a button state as a name with every widget's /AS, a selection as export values with /I for list boxes. Every widget of a changed text or choice field gets an appearance stream of its new value, made as cera draws it (the form's /DA font, or Helvetica when that cannot show the text), so every viewer shows the values without regenerating them; check boxes and radio buttons switch between their appearances.

What Helvetica lacks is drawn with a fallback font — Arimo for Greek and Cyrillic, and for Chinese, Japanese and Korean the fonts of the document's FontProvider (fonts/cjk) — on screen and in the file, which gets a subset of the font with the glyphs used and a /ToUnicode map. A character no font has is drawn as ?: e.MissingGlyphs() returns the fields with such a value, and the written form gets /NeedAppearances, which asks a viewer that has a font to draw the fields itself.

Flatten draws every widget the document shows into the content of its page, as cera renders it (with values from SetFields), and removes the form: the widgets leave the pages, /AcroForm the catalogue. Hidden widgets are dropped, widgets in a layer stay in it. Page content lies under the annotations a page keeps, so an annotation that lay under a widget and overlaps it is flattened with the widget, and so on down the stack: the page shows what it showed. Annotations that touch no flattened one stay as they are.

FlattenAnnotations does the same for the other annotations, for print or the archive: every annotation shown on screen is drawn into its page — with its appearance stream or the one cera generates, its opacity, and in its layer — and leaves the page with its popup. Links stay, as do annotations that show nothing, and the widgets of the form unless Flatten is called too; an annotation that lies on a widget then stays with it. Annotations added by the same editor are not flattened.

Flattening changes the content of pages: Update refuses it with ErrNotPermitted for an encrypted document without the permission to modify, and FlattenAnnotations with ErrCertified for a certified one.

AddHighlight marks text on a page of the edited document: a /Highlight (or Underline, StrikeOut, Squiggly) annotation from quads in points of the displayed page, as PageText.Quads returns them for a selection or a search match. It is written with /QuadPoints in user space and an appearance stream (a highlight multiplies with the page below it), so every viewer shows it alike; a Note goes into /Contents with a closed popup. The highlight stays with its page when the page moves, and goes when it is deleted.

AddAnnotation does the same for the other annotations a user comments with: notes (Text), FreeText, Ink, Square, Circle, Line, Polygon, PolyLine, Caret, the standard stamps and links. A NewAnnotation is a type, a text and a Markup, the struct Annotation.Markup reads: geometry in points of the displayed page, colours, border, author and dates, the popup, what it replies to. The appearance stream is what cera draws for the annotation without one, so every viewer shows it alike; on a turned page text, stamps and icons stay upright. It returns the index of the annotation, for a reply to it or to change it again.

SetAnnotation changes an annotation of the document and RemoveAnnotation removes one with its popup and its replies:

a, _ := page.EditAnnotation(index) // as it is
a.Contents, a.Color = "Done", []float64{0, 0.6, 0}
err = e.SetAnnotation(i, index, a)
err = e.RemoveAnnotation(i, other)

What SetAnnotation does not describe stays in the annotation; its appearance is made anew. Changes to annotations need the permission to annotate or to modify, and a certification with DocMDP permissions 1 or 2 refuses them (ErrCertified); a signature's lock covers fields, not annotations. A flatten in the same editor draws the document's annotations as they were and leaves added ones alone: write the changes first.

Signatures are not checked, but what they declare is kept: a signed document is not saved as a new file (ErrSigned, use Update), a field a signature locks (/Lock) does not change (ErrLocked), and a certification's DocMDP permissions allow no change with P 1, no change of the pages with P 2 and 3 and no added annotations with P 2 (ErrCertified). Writing values removes an XFA form, which would show the old ones. Calculated fields are not computed again: cera runs no JavaScript.

Save writes a document that was packed — its objects in object streams, with a cross-reference stream, the compact form of PDF 1.5 — in that form again, and others with every object on its own and a cross-reference table. SaveWith(w, cera.SaveOptions{ObjectStreams: cera.ObjectStreamsAlways}) packs any file, which makes a form of thousands of small objects a tenth smaller, and ObjectStreamsNever writes the classic form for a reader that knows no other.

The outline and the page labels of a result are the edited document's, kept with their pages, unless they are set:

// The outline as it stands, with the pages of the result as targets.
items := e.Outline()
items[0].Title = "Introduction"
items = append(items, cera.OutlineItem{Title: "Appendix", Target: &cera.LinkTarget{Page: 12}})
err := e.SetOutline(items)

// After DeletePages: no entries that lead to pages which are gone.
e.PruneOutline()

// i, ii, then 1, 2, 3 from the third page on.
err = e.SetPageLabels([]cera.LabelRange{
	{Start: 0, Style: cera.LabelRomanLower},
	{Start: 2, Style: cera.LabelDecimal},
})

SetOutline takes the entries of reading. A target names a page of the result and stays with that page when pages are moved afterwards; besides pages an entry may lead to an address, a named destination or another file. Entries that come from Editor.Outline or from the document's outline and still lead where they led are written with what the document has for them — a JavaScript action, a structure element —, so renaming, reordering and removing entries loses nothing of the others. An outline that is set is the whole outline: what ImportPagesWith would bring is not added. PruneOutline works on the outline as it is written, set or not. SetPageLabels labels the pages by their place when the result is written; Document.PageLabels reads the ranges of a document.

The size of pages changes in three ways, none of which rewrites their content:

// Show less of a page, or state the boxes of a page for print.
err := e.SetPageBox(0, cera.BoxCrop, cera.Rect{X0: 36, Y0: 36, X1: 576, Y1: 756})
// A Letter page as an A4 page: scaled to fit, in the middle.
err = e.ResizePage(0, 595.28, 841.89, cera.FitInside)
// Four pages on a sheet, after the others.
err = e.AddSheets(e.NumPages(), doc, cera.Sheet{Width: 841.89, Height: 595.28, Columns: 2, Rows: 2, Margin: 20, Gap: 10})

SetPageBox takes the box in default user space, as Page.Boxes gives it. ResizePage puts what the page shows — its CropBox — on a page of the size given, as FitInside (the default), FitFill, FitStretch or FitNone says: the content becomes a form XObject drawn through one matrix, so that patterns move with what they fill; the annotations move to where the content went, with the points of markup, lines and ink; the BleedBox, TrimBox and ArtBox follow. Not moved are destinations that name a place on the page by coordinates, and the text of a form field keeps its size. Both change the page as it is written: Draw, the annotations and a redaction in the same editor go on speaking of the page as its document shows it, and move with it.

AddSheets draws pages of a document on new sheets, row by row, each scaled into its place: n-up, and with an order of one's own and -1 for a place left empty the imposition of a booklet. It is Canvas.DrawPage on pages made with AddPage; a page is drawn without its annotations.

SaveOptions{Linearize: true} writes the file linearised ("fast web view"; PDF 2.0, Annex F): the catalogue and everything the first page needs come first, then the other pages one after another, then what several of them share, and a table of hints says where each page is and which shared objects it needs. A viewer that is sent the file from its start shows the first page before the rest has arrived, and fetches any other page with a few requests. It goes with object streams and with encryption. The file is laid out in memory before it is written, and is a little larger: half a percent with a cross-reference table, two with object streams, which are then made by part of the file. Viewers that read by ranges anyway (pdf.js, cera's own OpenReaderAt) gain little from it; servers that deliver to browser plug-ins and to Acrobat ask for it. Update on a linearised file gives an ordinary one, as in every writer.

Save copies what the pages reach as it is. SaveOptions has more that makes the file smaller and changes nothing of what it shows, each on request:

  • PruneResources leaves out the resources no content names. A resource dictionary that all pages share reaches every font and image of the document from every page: after DeletePages, or in a file made of some pages of another, what only the other pages drew is still written. With the option the content of the pages, and of the form XObjects, tiling patterns, Type 3 fonts and appearance streams they draw, is read for the names it uses, and a dictionary is written without the entries that none of those that have it names. A dictionary several pages share stays one dictionary. Content that does not read to its end keeps what it has, and so do the resources of the form (/DR), with which a viewer writes what is typed into a field.
  • JoinDuplicates writes streams that are the same once — an image or a font program embedded once for every page it is on — and the fonts of one program. It joins what the pages of different documents have in common too, which a merged file gains from. Layers, annotations and fields that read the same stay as many as they are.
  • Compress stores streams that are plain, or under LZW, run length, ASCII85 or ASCIIHex, with the Flate filter where that is shorter. An image under ASCII85 and DCT stays the JPEG it is, without the ASCII85. Metadata streams stay plain.
  • Drop leaves out the thumbnails of the pages (DropThumbnails), the private data of the programs that made the document (DropPieceInfo: what an illustration program keeps to edit its file again) and the metadata of pages, images and fonts (DropObjectMetadata; the document's own stays).
err := e.SaveWith(w, cera.SaveOptions{
	ObjectStreams:  cera.ObjectStreamsAlways,
	PruneResources: true, JoinDuplicates: true, Compress: true,
	Drop:           cera.DropThumbnails | cera.DropPieceInfo | cera.DropObjectMetadata,
})

All of it together writes the test files of pdf.js at 84 % of what Save writes. No image is sampled anew and no font subset: what the pages show is what they showed, sample for sample.

Update writes the edited file unchanged and appends only what changed (PDF 2.0, 7.5.6), so signatures over the original bytes stay valid: the document's pages keep their objects, deleted pages and what hung on them alone are freed, imported pages are copied in. An unchanged document is written as it is. It returns ErrNoUpdate for an editor made with NewEditor and for a file that needed a repair. An encrypted file stays encrypted: what the update appends is encrypted with its key (RC4, AES-128 or AES-256), and ErrNotPermitted refuses what its permissions do not allow (page changes need the right to modify or assemble, form values the right to fill in forms, annotate or modify, highlights the right to annotate or modify).

Command line

go build -C cmd/cera -o "$PWD/cera" .
./cera -dpi 150 -v -o 'page-%d.png' input.pdf
./cera -cmyk-profile USWebCoatedSWOP.icc -page 1 print.pdf   # or -naive-cmyk, -ignore-output-intent

cmd/cera is a module of its own, so that cera itself depends on nothing it does not draw with: it writes the PNGs with calamus, which encodes them in bands on all cores (encoding was most of the command's time with image/png). The ending of -o says what is written: .png, .jpg (-quality), .tif (LZW) or .webp (without loss); -gray draws in grey. A TIFF is one page: several pages are several files.

Besides drawing, the command does what the library does, as subcommands (cera <command> -h lists the flags of each):

./cera text input.pdf                       # the text; -json: every line with its box
./cera search input.pdf 'a phrase'          # page: context [match] context
./cera info input.pdf                       # pages, metadata, layers, fields, signatures,
                                            # and what of each page is not drawn exactly
./cera merge -o all.pdf a.pdf b.pdf         # with outlines, labels and fields
./cera split -o 'page-%d.pdf' input.pdf     # -every 4: four pages a file
./cera rotate -by 90 -pages 2-3 -o out.pdf input.pdf
./cera delete -pages 1,7- -o out.pdf input.pdf
./cera fill -field name=Ada -o out.pdf form.pdf   # -flatten; -update keeps signatures

Pages are given as 1-3,5,8-. The commands that write a PDF take -small (unused resources left out, duplicates joined, streams compressed, object streams) and -linearize. cera info is the first thing to send with a bug report: it says what cera makes of a file.