go-pdfkit

Pure-Go, CGO-free PDF 1.7 writer with a Go-idiomatic API.

pure Go ยท CGO=0 PDF 1.7 100% statement coverage deterministic output built on go-opentype widget bridge BSD-3-Clause
pdfkit repo docs org
pkg.go.dev Go License

go-pdfkit is the whole of PDF in Go, with no C anywhere: read a file, write one, rearrange it, draw it, read it back as words and pictures, and edit it with other people โ€” in a browser tab if you like, since every one of these builds for js/wasm. Fonts are parsed and shaped by go-opentype and pages are rasterised by go-gfx.

Everything here is measured against 118 863 real PDFs โ€” arXiv's figures, Matplotlib and Mathematica and pdfTeX and Ghostscript and Adobe โ€” rather than against a handful of files somebody wrote to pass a test. That corpus is what found a stroke that came out at half its colour, a font that took the dots off every i, and a colour transform that turned every plot's paper yellow. Each of them is in the release notes of the library it was found in.

// Take three pages out of a report, turn one the right way up,
// stamp the lot, and write it out.
doc, _ := ops.Open(bytes)
doc.Select("4-6")
doc.SetRotation("2", 90)
doc.Watermark("all", "DRAFT")
out, _ := doc.Bytes()

// Draw a page, and read one back.
img, _ := render.Page(src, 1, render.Options{DPI: 150})
text, _ := extract.Text(src, 1)
$ pdfops merge out.pdf a.pdf b.pdf
$ pdfops nup -n 4 slides.pdf handout.pdf
$ pdfops encrypt -user letmein -allow print,copy plain.pdf locked.pdf
$ pdfops text -layout -pages 1 paper.pdf
$ pdfops images paper.pdf pictures/

reader ready

Reads and writes the format itself. Lexer and object model, cross-reference tables and streams with hybrid files and /Prev chains, object streams, repair by scanning when the table is wrong, every filter (Flate, LZW with early change, ASCIIHex, ASCII85, RunLength, PNG and TIFF predictors), the standard security handler from RC4-40 to AES-256, content streams and inline images, the page tree with inherited attributes โ€” and a writer that produces the same bytes twice. Of 118 863 real PDFs, 118 833 open; the thirty that do not are 27 PNGs named .pdf, one PostScript file and two truncated past recovery.

ops ready

The verbs, and the pdfops command. Merge, select, delete, reverse, move, rotate, crop, resize, split, n-up, booklet, overlay, blank pages, watermarks, page numbers, Bates stamps, sanitize, flatten, compress, encrypt, decrypt, permissions, and reading a page back as text or pictures. Links and bookmarks are carried over and pointed at the pages they became. Every verb is measured on the whole corpus: 118 833 files compressed and read back page for page identical, 35.2 GB down to 33.6 GB.

render ready

Turns a page into pixels. The transform stack, every painting operator, both winding rules, strokes with caps/joins/miter/dashes, clipping, every colour space, transparency, form XObjects; images at every depth with soft and stencil masks; text from TrueType, OpenType, bare CFF, PostScript Type 1, composite and Type 3 fonts; PDF functions of all four kinds, all seven shadings โ€” axial, radial, function-based, and the four mesh kinds, triangles and Coons and tensor patches โ€” and shading and tiling patterns, placed in the space of the form that names them. Rasterises with go-gfx and reads outlines with go-opentype. Builds for js/wasm.

pdffont ready

What a document says about a font โ€” not the outlines, which are the program’s business. Which codes a string holds, how wide each is, what each is called, and what text each stands for, through /ToUnicode and the encodings. It refuses one guess: a symbolic font read through an assumed encoding gives the wrong letter with nothing to say so. The glyph-name table is checked against the documents themselves โ€” 63 515 names, 99.31% agreement.

extract ready

Reads a page back: the text with where each piece of it sits, and the pictures with the box each covers. A page holds instructions for drawing glyphs, not text โ€” so where the document says nothing about what they mean, this says so rather than guessing. Over the whole corpus: 121 946 pages, 34 million characters, 1.89% of runs that nothing in the document could name, 300 685 pictures located, no panics.

coedit ready

A PDF several people edit at once. What is shared is not the file โ€” merging two piles of bytes cannot be done โ€” but the plan: which pages, from which files, in which order, turned and cropped how, with which bookmarks over them. A list, a tree and a handful of fields, which a CRDT merges without asking anybody. Checked with 25 editors making random edits for 40 rounds, merged in a random order, all agreeing on the same document.

app ready

A PDF workbench that runs in a browser tab and nowhere else. Open a file, turn its pages, rotate one, drop one, lay it out two to a sheet, write across it, save it back out โ€” and none of it leaves the machine, because there is nowhere for it to go. The whole tool is one wasm binary the browser downloads once and a service worker then keeps. Driven in CI by a real Chrome over the DevTools protocol, reading the canvas pixels back.

pdfkit ready

The document builder: pages, vector graphics, and text set in embedded, subsetted fonts. TrueType and OpenType/CFF embed as Type0 (Identity-H) composite fonts with a /ToUnicode CMap; JPEG and raster images place directly; a widget bridge prints a go-widgets tree onto a page, rasterised or as selectable vector text. With the zero Options the output has no timestamps and a content-derived /ID, so identical inputs produce byte-identical PDFs.

What the corpus says. Every wave of work here is measured on the output rather than on the exit status: the pages are rendered before and after and their pixels hashed, and the biggest changes are put beside what the operating system's own renderer draws. 118 833 of 118 835 real files open; 1 536 769 753 content operations read; every one of them rewrites to an identical fingerprint; 118 833 encrypt, decrypt and compress with every page byte for byte the same; 4 108 pages draw with no panics and six blank; 121 946 pages read back as 34 million characters.

Text and fonts: every embedded font is written as a subset with Identity-H encoding, a per-glyph /W width array and a /ToUnicode CMap for copy/paste. TrueType glyf outlines embed as a subsetted FontFile2 / CIDFontType2 with a /CIDToGIDMap stream; CFF/OpenType outlines embed as a charstring-subsetted FontFile3 / CIDFontType0. Both outline flavours are fully glyph-subsetted; only a CID-keyed CFF or a CFF2 (variable) font falls back to embedding the whole CFF/CFF2 table. An optional shaped-text API (TextShaped) runs the go-opentype shaper's GSUB/GPOS tables for Arabic, Indic and CJK.

Widget bridge: Page.AddWidget and Page.AddWidgetVector "print" a go-widgets/toolkit widget tree onto a page โ€” AddWidget as a rasterised image XObject (pixel-identical to the screen), AddWidgetVector as PDF vector operators, so fills/strokes stay crisp and text stays selectable, including a TrueType-font widget label's own face embedded as real Type0 text.

Every library here runs at 100 % statement coverage, including the branches that report a file saying something it should not. Every one builds for the six 64-bit architectures the fleet targets plus js/wasm, macOS and Windows. Pure Go, cgo disabled, BSD-3-Clause.