ops

package module
v0.7.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 27, 2026 License: BSD-3-Clause Imports: 9 Imported by: 0

README

ops

CI Go Reference Go Report Card Coverage

The verbs of go-pdfkit: what people actually do to a PDF they already have. Merge two files, pull out pages three to seven, turn a page the right way up, drop the metadata, split a report into chapters.

Built on reader, which takes a file apart and puts one back together; nothing outside the Go standard library is used, so this builds for GOOS=js/wasm and every architecture the fleet targets.

The model

A document here is an ordered list of pages, each borrowed from a source file, plus the document-level pieces. Every operation rearranges that list or annotates its entries; nothing is applied until the document is written out. An operation therefore costs nothing until it has to, several files can be mixed freely, and — the reason the model is shaped this way — the list is a plain value that two people can edit at the same time.

What it does

go install github.com/go-pdfkit/ops/cmd/pdfops@latest

pdfops merge whole.pdf part1.pdf part2.pdf
pdfops select -pages 3-7,last report.pdf extract.pdf
pdfops delete -pages even scan.pdf fronts.pdf
pdfops rotate -pages all -by 90 sideways.pdf upright.pdf
pdfops crop -box 20,20,575,820 wide.pdf trimmed.pdf
pdfops split -every 10 book.pdf chapters/
pdfops reverse back-to-front.pdf right-way-round.pdf
pdfops nup -n 4 slides.pdf handout.pdf
pdfops booklet chapter.pdf to-fold.pdf
pdfops overlay -with letterhead.pdf plain.pdf headed.pdf
pdfops blank -before 3 report.pdf report-with-a-gap.pdf
pdfops watermark -text CONFIDENTIAL contract.pdf marked.pdf
pdfops number -format "{page} of {pages}" report.pdf numbered.pdf
pdfops bates -prefix ACME- -start 1 exhibits.pdf stamped.pdf
pdfops stamp -text "seen 25 August" -at top-right -bold notes.pdf seen.pdf
pdfops sanitize downloaded.pdf safe.pdf
pdfops flatten filled-form.pdf final.pdf
pdfops strip -annotations -bookmarks private.pdf clean.pdf
pdfops compress fat.pdf lean.pdf
pdfops encrypt -user letmein -allow print,copy plain.pdf locked.pdf
pdfops -password letmein decrypt locked.pdf plain.pdf
pdfops -password letmein permissions locked.pdf
pdfops text paper.pdf
pdfops text -layout -pages 1 paper.pdf
pdfops images paper.pdf pictures/
pdfops info file.pdf

A page range is written 1-3,7,10- and may say all, even, odd or last. It keeps its own order and its own repeats, so select -pages 3-1 reverses three pages and select -pages 1,1 gives you two copies.

-password opens an encrypted file.

text reads the page back as words, and -layout says where every piece of it sits — a page, a place, a size, and what it says. A piece the document gave no way to read comes back marked rather than guessed at, so you can tell a page that says nothing from one that could not be read. images writes out the pictures a page places, a JPEG as a JPEG.

Links and bookmarks are carried over and pointed at the pages they became here, so extracting three pages of a book leaves the links between those three working and drops the ones that led out of them. A link or a bookmark that already led nowhere in the source keeps its place without a destination, because a file with broken bookmarks should keep its shape rather than lose it.

The catalogue is always written from nothing, so a document-level script, an action that runs on opening, or a tree of embedded files never survives any operation at all. sanitize deals with what travels attached to a page: its own actions, an annotation's actions, JavaScript, launching, form submission, the annotation types that exist to play or embed something, and files associated with a page. A link to the web is not executable and stays.

What a screen reader follows

A tagged document says which run of marks on which page is a heading, a paragraph, a table cell, the label of a form field. That is the reading order a screen reader follows, and for a government form it is often what the law requires — RGAA in France, Section 508 in the United States, EN 301 549 in the EU. It cannot be copied across a rebuild: every part of it points into the document, and the number tree that indexes it is keyed by a number each page carries. So it is rebuilt, element by element, around the pages that survived, and those numbers are handed out afresh. A page that kept the number it had in a file it is no longer part of is a page a reader would look up and be told, with every confidence, about somebody else's.

An element whose every page has gone is removed and its parent pruned; one with some pages gone keeps the children that are left. An empty table cell is kept, because the shape of a table is part of what it says. A page written twice carries the structure once, on the first copy, since an element names one page. Merging two files carries no tree at all: two files have two role maps in which the same name may mean two different things, and there is no honest way to choose between them.

Of 1 633 real government forms from eighteen issuers, 1 021 carry a structure tree. After a rotate 1 014 still do — the seven that do not are the seven whose tree was empty in the file we were given — and 991 of the 1 021 are identical down to the last element, mark and annotation reference. The thirty that differ are accounted for one by one in the commit that added this. What cannot be carried is named in catalogue.go with the reason, rather than disappearing quietly.

Text is drawn in the four faces every viewer already has — Helvetica, Helvetica-Bold, Courier and Courier-Bold — so nothing is embedded and a watermark costs about a kilobyte. Stamp text may say {page}, {pages} and {n}, the last being a counter for serial numbering.

Verified against real files

Every operation is checked on the same corpus of 118 863 real PDFs the reader is measured against — Matplotlib, cairo, pdfTeX, Ghostscript, Adobe, R, Apache FOP, PDF 1.3 through 1.7. For each of the 118 833 that open, four properties have to hold, compared on the bytes of every page's content stream rather than on an exit status:

  • writing the document unchanged reproduces every page;
  • reversing twice is the identity;
  • merging a document with itself doubles it exactly;
  • selecting the last page yields exactly that page.

All four hold on all 118 833 files.

Laying pages out is checked the same way: for each of the 1 959 multi-page files in that corpus, two-up produces the right number of sheets and every form drawn on them carries exactly the bytes of the page it stands for, blanks included. All 1 959 hold.

Testing

go test -covermode=set ./...

CI gates on exact 100% statement coverage, go vet, and a cross-compile across linux/{amd64,arm64,riscv64,loong64,ppc64le,s390x}, js/wasm, darwin/arm64 and windows/amd64.

The rest of the family

reader reads and writes the format · ops is the verbs and the pdfops command · render turns a page into pixels · pdffont is what a document says about a font · extract reads a page back · coedit is a PDF several people edit at once · app is the browser workbench.

Documentation for all of it: https://go-pdfkit.github.io/docs/

License

BSD-3-Clause — see LICENSE. Copyright the go-pdfkit/ops authors.

Forms

pdfops fields lists what a form asks for and what it holds; pdfops fill fills it in and saves it.

$ pdfops fields fw9.pdf
note: the file also carries an XFA form, which is not read; the standard one is.
topmostSubform[0].Page1[0].f1_01[0]                       text      ""
topmostSubform[0].Page1[0].Boxes3a-b_ReadOrder[0].c1_1[0] checkbox  "Off"
                                                            buttons [1]

$ pdfops fill -set 'topmostSubform[0].Page1[0].f1_01[0]=Wolfgang Amadeus Mozart' \
              -set 'topmostSubform[0].Page1[0].Boxes3a-b_ReadOrder[0].c1_1[0]=1' \
              fw9.pdf filled.pdf

A filled form is written as an incremental update: the original file, byte for byte, with the objects that changed appended after it and a new cross-reference section pointing back at the old one. That is how every program that saves a form saves one, and it is the safest thing a program can do to somebody's document — nothing already there is rewritten, so whatever this does not understand survives untouched, and if the update is wrong the original is still the first part of the file.

The update says where its objects went the same way the file already does. A file whose cross-reference is a stream cannot be pointed back at by a plain table: a reader following /Prev would find an object where it expected the word xref. That is not a nicety — macOS's own renderer draws nothing at all for such a file, which is how the mistake was found.

Two things it will not do. A document that had to be repaired to be read has no cross-reference section worth pointing back at, so it is refused rather than added to. A document that is encrypted has every string and stream in it written through a key, and this does not yet write into one.

Measured on a real form: fw9.pdf filled in every field, written out, read back with every value in place, and rendered by macOS — which drew what we drew, in the same places, comb cells and ticks included.

Filling a form is the only verb that keeps one. Every other verb here takes the pages apart and builds a new document round them, and a form is tied into a document by object number in a dozen places at once — so merging, splitting or rotating a form's pages loses the form. Use fill on the file itself.

Documentation

Overview

Package ops is the verb layer of go-pdfkit: what people actually do to a PDF they already have. Merge two files, pull out pages three to seven, turn a page the right way up, drop the metadata, split a report into chapters.

A document here is an ordered list of pages, each borrowed from a source file, plus the document-level pieces. Every operation rearranges that list or annotates its entries, and nothing is applied until Doc.Bytes is called. An operation therefore costs nothing until it has to, pages from several files mix freely, and the list stays a plain value.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ParseRange

func ParseRange(spec string, pageCount int) ([]int, error)

ParseRange turns the page selection people write — "1-3,7,10-", "even", "last", "all" — into page numbers counting from one, in the order given and with duplicates kept, because "1,1,2" really does mean three pages.

A range with no start begins at the first page and one with no end runs to the last. A descending range like "5-2" counts down, which is how a reversed extract is written.

Types

type Bookmark added in v0.4.0

type Bookmark struct {
	Title    string
	Page     int
	Children []Bookmark
}

A Bookmark is an entry of an outline written from scratch: what it says, the page of this document it points at counting from one, and whatever sits under it.

It is what a document assembled rather than merged carries — a shared edit, a report built out of pieces — where there is no source outline to carry over because the outline is the caller's own.

type Doc

type Doc struct {
	// contains filtered or unexported fields
}

A Doc is a document being assembled: pages in the order they will be written, and the trailer's information dictionary.

func FromDocument

func FromDocument(src *reader.Document) *Doc

FromDocument wraps an already-parsed document. Every page the document reports can be read — the page tree walk has already established that — so there is nothing here that can fail.

func Merge

func Merge(docs ...*Doc) *Doc

Merge joins documents in the order given.

func New

func New() *Doc

New returns an empty document, for merging into.

func Open

func Open(b []byte) (*Doc, error)

Open reads a PDF file held in memory, with the empty password.

func OpenWithPassword

func OpenWithPassword(b []byte, password string) (*Doc, error)

OpenWithPassword reads a PDF file that may be encrypted.

func (*Doc) Append

func (d *Doc) Append(other *Doc)

Append adds the pages of another document to the end of this one. The other document is not changed, and the two may come from different files.

func (*Doc) Bates

func (d *Doc) Bates(spec, prefix string, start, digits int) error

Bates numbers the pages a range names with a running serial, the way a legal exhibit is marked: a fixed prefix and a zero-padded counter.

func (*Doc) Blank

func (d *Doc) Blank(width, height float64)

Blank adds an empty page of the given size at the end.

func (*Doc) Booklet

func (d *Doc) Booklet() error

Booklet reorders the pages for saddle-stitch printing — folded in half and stapled through the spine — and lays them two to a sheet. Blank pages are added so the count is a multiple of four, which is what folding needs.

func (*Doc) Bytes

func (d *Doc) Bytes() ([]byte, error)

Bytes writes the document out as a PDF file. Every borrowed page is copied out of the file it came from with the attributes it inherited written onto it, so pages from different documents keep their own geometry and resources.

func (*Doc) ClearInfo

func (d *Doc) ClearInfo()

ClearInfo drops the whole information dictionary, which is what "remove the metadata" means.

func (*Doc) Compress added in v0.2.0

func (d *Doc) Compress()

Compress asks for the file to be written with its objects packed into compressed streams and a cross-reference stream, which is what every writer since PDF 1.5 does and what makes a file a good deal smaller. It costs nothing but a version of 1.5, which every reader in use has understood for twenty years.

func (*Doc) Crop

func (d *Doc) Crop(spec string, box [4]float64) error

Crop sets the visible area of the pages the range names, in points, as [left bottom right top]. It sets the crop box; the media box, which is the paper the page is on, is left alone.

func (*Doc) Decrypt added in v0.2.0

func (d *Doc) Decrypt()

Decrypt writes the file without protection. A document opened with the right password is already decrypted, so this only undoes an earlier call to Doc.Encrypt; a file read with OpenWithPassword and written out is unprotected either way.

func (*Doc) Delete

func (d *Doc) Delete(spec string) error

Delete removes the pages the range names.

func (*Doc) DropOutlines

func (d *Doc) DropOutlines()

DropOutlines leaves the bookmarks behind. They are kept by default: a merge that loses every bookmark is a poor merge.

func (*Doc) Encrypt added in v0.2.0

func (d *Doc) Encrypt(e reader.Encryption)

Encrypt protects the file that will be written. Two people can open it: whoever knows the user password, subject to the permissions, and whoever knows the owner password, subject to nothing.

An encrypted file is not byte-for-byte reproducible — encryption needs randomness, by design — so a document written twice with the same call comes out different both times, and neither can be compared with the other.

func (*Doc) Flatten

func (d *Doc) Flatten()

Flatten draws each annotation's own appearance into the page and then drops the annotation, so what one reader sees is what every reader sees — which is what filling in a form and then flattening it means.

func (*Doc) Info

func (d *Doc) Info() reader.Dict

Info returns the document information dictionary, which may be nil.

func (*Doc) InsertBlank

func (d *Doc) InsertBlank(i int) error

InsertBlank puts an empty page before the page at i, counting from one; an index one past the end appends. The size is taken from the page it precedes, or from the one before it at the end of the document.

func (*Doc) Move

func (d *Doc) Move(from, to int) error

Move takes the page at from and puts it at to, both counting from one, the other pages closing up behind it.

func (*Doc) NUp

func (d *Doc) NUp(n int) error

NUp lays n pages on each sheet, in reading order. The sheet keeps the size of the first page, and the grid is whichever arrangement of at least n cells gives cells closest in shape to the pages going into them.

func (*Doc) Overlay

func (d *Doc) Overlay(other *Doc) error

Overlay draws each page of another document on top of the pages here, in order. When the other document has fewer pages its last page is not repeated: pages past its end are left alone.

func (*Doc) PageCount

func (d *Doc) PageCount() int

PageCount reports how many pages the document has.

func (*Doc) PageNumbers

func (d *Doc) PageNumbers(spec, format string) error

PageNumbers writes a number at the foot of the pages a range names. The format is a stamp's text, so "{page} of {pages}" and "— {page} —" both work.

func (*Doc) Protection added in v0.2.0

func (d *Doc) Protection() (reader.Protection, bool)

Protection reports how the file this document was read from was protected, and false when it was not protected at all — or when the document was not read from a file. It says nothing about how the document will be written: that is what was passed to Doc.Encrypt.

func (*Doc) RemoveAnnotations

func (d *Doc) RemoveAnnotations()

RemoveAnnotations drops every annotation: links, comments, form fields and all. What was drawn on the page stays; what sat on top of it goes.

func (*Doc) Resize

func (d *Doc) Resize(spec string, box [4]float64) error

Resize sets the media box — the paper — of the pages the range names.

func (*Doc) Reverse

func (d *Doc) Reverse()

Reverse puts the pages in the opposite order.

func (*Doc) Rotate

func (d *Doc) Rotate(spec string, degrees int) error

Rotate turns the pages the range names by the given number of degrees, which must be a multiple of ninety. The rotation is relative to what the page already had, so rotating twice by ninety turns a page upside down.

func (*Doc) Rotation

func (d *Doc) Rotation(i int) (int, error)

Rotation reports the rotation of the i'th page, counting from one.

func (*Doc) Sanitize

func (d *Doc) Sanitize()

Sanitize strips the parts of a file that do something rather than show something: page and annotation actions, JavaScript, launching, form submission, files travelling with a page, and the annotation types that exist to embed or play something.

A document written by this package always carries a catalogue of this package's own making, so document-level scripts, automatic actions on opening and embedded file trees are gone whatever this setting says; sanitising deals with what travels attached to a page.

func (*Doc) Select

func (d *Doc) Select(spec string) error

Select keeps only the pages the range names, in the order it names them, so it extracts, reorders and duplicates in one verb.

func (*Doc) SetInfo

func (d *Doc) SetInfo(key reader.Name, value string)

SetInfo replaces one entry of the information dictionary. An empty value removes it.

func (*Doc) SetOutline added in v0.4.0

func (d *Doc) SetOutline(marks []Bookmark)

SetOutline writes these bookmarks rather than carrying over the ones the sources had. An entry pointing at a page this document has not got is left out, and so is everything under it: a heading whose section has gone is not a heading any more.

Passing nothing puts the sources' own bookmarks back.

func (*Doc) SetRotation

func (d *Doc) SetRotation(spec string, degrees int) error

SetRotation turns the pages the range names to an absolute angle.

func (*Doc) SetVersion

func (d *Doc) SetVersion(v string)

SetVersion sets the version written in the header.

func (*Doc) Split

func (d *Doc) Split(n int) ([]*Doc, error)

Split cuts the document into pieces of at most n pages each.

func (*Doc) SplitAt

func (d *Doc) SplitAt(at ...int) ([]*Doc, error)

SplitAt cuts the document before each of the given page numbers, which is how a report is broken into chapters.

func (*Doc) Stamp

func (d *Doc) Stamp(spec string, s Stamp) error

Stamp draws text on the pages a range names.

func (*Doc) Underlay

func (d *Doc) Underlay(other *Doc) error

Underlay draws each page of another document underneath the pages here.

func (*Doc) Version

func (d *Doc) Version() string

Version reports the PDF version that will be written.

func (*Doc) Watermark

func (d *Doc) Watermark(spec, text string) error

Watermark draws large, pale, slanted text across the pages a range names — the thing the word usually means.

type Filling added in v0.5.0

type Filling struct {
	// contains filtered or unexported fields
}

A Filling is a document's form, opened so that it can be filled in and written back.

func OpenForm added in v0.5.0

func OpenForm(b []byte) (*Filling, bool, error)

OpenForm reads a document's form. It reports false, with no error, for a document that simply has none — including one carrying an AcroForm dictionary a producer left behind with an empty field list, which 561 of the figure corpus's 118 833 files do.

func OpenFormWithPassword added in v0.5.0

func OpenFormWithPassword(b []byte, password string) (*Filling, bool, error)

OpenFormWithPassword is the same for a document that is protected.

func (*Filling) Bytes added in v0.5.0

func (f *Filling) Bytes() ([]byte, error)

Bytes writes the original file with the changes appended to it.

func (*Filling) Fill added in v0.5.0

func (f *Filling) Fill(name, value string) error

Fill sets one field by name, which is what a command line or a map of answers wants.

func (*Filling) Form added in v0.5.0

func (f *Filling) Form() *forms.Form

Form is what was read, to be asked about its fields and told what they hold.

type Font

type Font string

A Font names one of the four faces this package can draw with.

const (
	Helvetica     Font = "Helvetica"
	HelveticaBold Font = "Helvetica-Bold"
	Courier       Font = "Courier"
	CourierBold   Font = "Courier-Bold"
)

The faces available. Any other name reads as Helvetica.

func (Font) Width

func (f Font) Width(s string, size float64) float64

Width reports how wide a string is at the given point size, once encoded.

type Page

type Page struct {
	// contains filtered or unexported fields
}

A Page is one page of a document, borrowed from the file it came from. The rotation is what will be written, not what the source said.

type Position

type Position uint8

A Position says where on a page something goes.

const (
	Center Position = iota
	TopLeft
	TopCenter
	TopRight
	BottomLeft
	BottomCenter
	BottomRight
	MiddleLeft
	MiddleRight
)

The nine places a stamp can sit.

type Stamp

type Stamp struct {
	Text     string
	Font     Font
	Size     float64    // in points; zero means twelve
	Colour   [3]float64 // red, green and blue, each from zero to one
	Opacity  float64    // zero means fully opaque
	Rotate   float64    // degrees, anticlockwise, about the text's own centre
	Position Position
	Margin   float64 // distance from the edge, in points; zero means 24
	Start    int     // the first value of {n}; zero means one
	Digits   int     // {n} padded to this many digits
}

A Stamp is a line of text drawn on a page.

The text may name what it is being drawn on: {page} is the page's number, {pages} the number of pages, and {n} a counter that starts wherever the stamp says and advances once per page stamped, which is what a numbering scheme like Bates needs.

Directories

Path Synopsis
cmd
pdfops command
Command pdfops does to a PDF the things people actually want done to one: merge, extract, delete, reorder, rotate, crop, split, and read or strip the metadata.
Command pdfops does to a PDF the things people actually want done to one: merge, extract, delete, reorder, rotate, crop, split, and read or strip the metadata.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL