Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 52 additions & 68 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -1,93 +1,77 @@
# Build the documentation, and publish it from main.
#
# ⛔ No Python. What this replaced was MkDocs Material plus mike: a
# requirements.txt, a pip install and a Python setup step, to render a static
# site for an organisation whose whole argument is that it needs no runtime but
# Go. Hugo is one binary, and the theme is a Go module.
name: docs

on:
push:
branches: [main]
# On a pull request too, so a change that breaks the build is caught before
# it lands rather than by the published site going stale.
pull_request:
workflow_dispatch:

# The build needs nothing beyond the source. Write access to the repository
# is granted to the deploy job alone, below, so a pull request build cannot
# push to gh-pages.
permissions:
contents: read
pages: read

concurrency:
group: pages-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
# Build on every pull request as well as on main, so a change to the docs or to
# the toolchain in requirements.txt has to prove it still builds before it
# lands. Without this the workflow ran only on main, which meant a dependency
# bump had no signal at all and nothing could say whether it was safe to merge.
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- uses: actions/checkout@v7
with:
# The theme reads the last-modified date of each page from git, so a
# shallow clone would date every page to the checkout.
fetch-depth: 0

- uses: actions/setup-go@v7
with:
# Pinned: Hugo modules are Go modules, and the resolver is the one in
# this toolchain.
go-version: '1.27.1'

- name: Set up Python
uses: actions/setup-python@v7
- uses: peaceiris/actions-hugo@v3
with:
python-version: "3.14"
cache: pip
cache-dependency-path: requirements.txt
hugo-version: '0.167.0'
extended: true

- uses: actions/configure-pages@v6
id: pages

- name: Install MkDocs Material + mike
run: |
pip install --upgrade pip
pip install -r requirements.txt
- name: Build
# --minify and the base URL Pages computes, the same way the landing
# repository does it.
run: hugo --minify --baseURL "${{ steps.pages.outputs.base_url }}/"

# --strict turns a broken link or an unknown config key into a failure
# rather than a warning nobody reads.
- name: Build the site (strict)
run: mkdocs build --strict
- name: Say which theme built it
# The theme is a FORK reached through a replace directive. Printing the
# resolved module means a build that silently fell back to upstream is
# visible in the log rather than in the rendering.
run: hugo mod graph | head -5

- uses: actions/upload-pages-artifact@v5
with:
path: public

deploy:
# Only main publishes. A pull request stops after build above: it has no
# business pushing to gh-pages, and `permissions: contents: write` is not
# granted to a fork's PR anyway.
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
if: github.event_name != 'pull_request'
needs: build
# Two deploys must never run at once: `mike deploy --push` pushes gh-pages,
# and a second job doing the same is rejected outright --
#
# ! [rejected] gh-pages -> gh-pages (fetch first)
#
# cancel-in-progress stays FALSE deliberately: killing a publish half way
# through is worse than making it wait, and the queued run then fetches the
# gh-pages the previous one just wrote.
concurrency:
group: gh-pages-deploy
cancel-in-progress: false
runs-on: ubuntu-latest
permissions:
contents: write
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: "3.14"

- name: Install MkDocs Material + mike
run: |
pip install --upgrade pip
pip install -r requirements.txt

- name: Configure git for mike
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

# mike publishes versioned docs to the gh-pages branch; GitHub Pages serves
# that branch at https://go-pdfkit.github.io/docs/. The version label
# tracks main as pdfkit's current minor release ("0.4", matching tag
# v0.4.0) aliased "latest"; bump the label here on every minor release.
# /docs/ redirects to the default ("latest").
- name: Deploy versioned docs
run: |
git fetch origin gh-pages --depth=1 || true
mike deploy --push --update-aliases 0.4 latest
mike set-default --push latest
- id: deployment
uses: actions/deploy-pages@v5
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
site/
.cache/
# Hugo's output and its caches.
public/
resources/
.hugo_build.lock
53 changes: 42 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,54 @@
# go-pdfkit docs

Source for **<https://go-pdfkit.github.io/docs/>** — MkDocs Material,
versioned with [mike](https://github.com/jimporter/mike).

Preview locally:
Hugo source for **<https://go-pdfkit.github.io/docs/>**.

```sh
pip install -r requirements.txt
mkdocs serve
hugo server # preview
hugo --minify # build into public/
```

Build:
## The theme is our fork of Hextra

```sh
mkdocs build --strict
```toml
# go.mod
require github.com/imfing/hextra v0.13.0
replace github.com/imfing/hextra => github.com/tannevaled/hextra v0.16.0
```

Deployment runs on push to `main` (`.github/workflows/docs.yml`): it
`mike deploy`s to the `gh-pages` branch, which GitHub Pages serves.
The fork keeps upstream's module path, so it is reached through a `replace`
rather than by importing a different path, and it is tagged independently and
ahead — `v0.16.0` against upstream's `v0.13.0`. **This repository is the first
in the fleet to consume it**, so the two lines above are the pattern the others
copy.

The build prints `hugo mod graph`, so a build that silently fell back to
upstream shows in the log rather than in the rendering.

## What this replaced, and why

MkDocs Material plus `mike`: a `requirements.txt`, a `pip install` and a Python
setup step in CI, to render a static site for an organisation whose whole
argument is that it needs no runtime but Go. **There is no Python here.**

The thirteen pages were converted by a small Go tool rather than by hand. It
checks the nav table against the tree **in both directions before writing
anything** — a page on disk the nav does not name, or a nav entry with no page,
is an error — then adds front matter, drops a leading `# Heading` that only
repeats the title, rewrites internal `.md` links to the sections the pages
moved into, and turns MkDocs admonitions into Hextra callouts.

⚠ That last one is why the conversion was read rather than trusted: an
admonition is delimited by **indentation**, so rewriting only its opening line
leaves the body indented four spaces — and Markdown renders that as a **code
block**. The first version did exactly that, and the opening line had converted
perfectly.

## Versioning

Not yet. `mike` published `/latest/` and a directory per version; this
publishes one current set. The fork carries a versioned-documentation kit added
on 2026-10-05, and adopting it is its own change: moving the generator and the
versioning scheme in one step would make a failure impossible to attribute.

## License

Expand Down
24 changes: 24 additions & 0 deletions content/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
title: go-pdfkit
layout: hextra-home
---

{{< hextra/hero-badge link="https://github.com/go-pdfkit" >}}
<span>Pure Go · CGO=0 · BSD-3-Clause</span>
{{< /hextra/hero-badge >}}

{{< hextra/hero-headline >}}
The whole of PDF in Go,&nbsp;<br class="hx:sm:block hx:hidden" />with no C anywhere
{{< /hextra/hero-headline >}}

{{< hextra/hero-subtitle >}}
Read a file, write one, rearrange it, draw it, read it back as words and&nbsp;<br class="hx:sm:block hx:hidden" />pictures, and edit it with other people — in a browser tab if you like.
{{< /hextra/hero-subtitle >}}

{{< hextra/hero-button text="Read the documentation" link="docs" >}}

{{< hextra/feature-grid >}}
{{< hextra/feature-card title="Measured, not asserted" subtitle="Everything here is checked against 118 863 real PDFs — arXiv's figures, Matplotlib, Mathematica, pdfTeX, Ghostscript and Adobe — rather than against files written to pass a test." >}}
{{< hextra/feature-card title="Runs on eight architectures" subtitle="amd64, arm64, riscv64, loong64, ppc64le, s390x, 386 and arm — the whole suite runs on each, big-endian and 32-bit included, and on FreeBSD and OpenBSD too." >}}
{{< hextra/feature-card title="In a browser tab" subtitle="Every library builds for js/wasm. The workbench opens a PDF, rearranges it and saves it back without any of it leaving the machine." >}}
{{< /hextra/feature-grid >}}
61 changes: 35 additions & 26 deletions docs/index.md → content/docs/_index.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
---
title: "Overview"
weight: 10
---

# go-pdfkit documentation

**The whole of PDF in Go, with `CGO_ENABLED=0` and no C anywhere**: read a
Expand All @@ -10,24 +15,28 @@ Fonts are parsed and shaped with
rasterised by [go-gfx](https://github.com/go-gfx/gfx); nothing outside the Go
standard library and our own pure-Go libraries is required.

!!! quote "Measured against 118 863 real files"
Every claim on this site is measured against a corpus of real PDFs —
arXiv's figures, from Matplotlib and Mathematica and pdfTeX and Ghostscript
and Adobe — rather than against files written to pass a test. Each wave of
work is checked by **hashing the pixels of every page before and after** and
putting the biggest changes beside what the operating system's own renderer
draws. That 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.

!!! success "Zero C dependencies"
No cgo, no bundled `libpoppler`/`libharfbuzz`/`libfreetype`. Glyph
subsetting, font-descriptor metrics and per-glyph advances all come from
[go-opentype](https://github.com/go-opentype/opentype)'s
`Font.SubsetTrueType` / `Font.SubsetCFF` — `pdfkit` keeps no private sfnt
re-parse or subsetter of its own. See
[Text & fonts](text-and-fonts.md#font-embedding-architecture).

{{< callout type="info" >}}
**Measured against 118 863 real files**

Every claim on this site is measured against a corpus of real PDFs —
arXiv's figures, from Matplotlib and Mathematica and pdfTeX and Ghostscript
and Adobe — rather than against files written to pass a test. Each wave of
work is checked by **hashing the pixels of every page before and after** and
putting the biggest changes beside what the operating system's own renderer
draws. That 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.
{{< /callout >}}
{{< callout type="info" >}}
**Zero C dependencies**

No cgo, no bundled `libpoppler`/`libharfbuzz`/`libfreetype`. Glyph
subsetting, font-descriptor metrics and per-glyph advances all come from
[go-opentype](https://github.com/go-opentype/opentype)'s
`Font.SubsetTrueType` / `Font.SubsetCFF` — `pdfkit` keeps no private sfnt
re-parse or subsetter of its own. See
[Text & fonts](/docs/writing-a-document/text-and-fonts/#font-embedding-architecture).
{{< /callout >}}
## The family

| Repo | What it is |
Expand Down Expand Up @@ -70,7 +79,7 @@ Ruby-idiomatic writer built on the same font stack.
- **Widget bridge**: `Page.AddWidget` and `Page.AddWidgetVector` "print" a
[go-widgets/toolkit](https://github.com/go-widgets/toolkit) widget tree onto
a page — as a rasterised image, or as PDF vector operators with selectable
text. See [Widgets](widgets.md).
text. See [Widgets](/docs/writing-a-document/widgets/).
- **Deterministic output**: with the zero `Options`, a document has no
timestamps and a content-derived `/ID` (a SHA-256 of the body), so
identical inputs produce byte-identical PDFs.
Expand All @@ -84,7 +93,7 @@ Ruby-idiomatic writer built on the same font stack.
- Encryption, tagged PDF / PDF-A, interactive forms and annotations are not
yet implemented.

See [Scope and limitations](text-and-fonts.md#scope-and-limitations) for the
See [Scope and limitations](/docs/writing-a-document/text-and-fonts/#scope-and-limitations) for the
full picture, and the upstream
[README](https://github.com/go-pdfkit/pdfkit#scope-and-limitations) for the
canonical statement.
Expand All @@ -97,15 +106,15 @@ go get github.com/go-pdfkit/pdfkit

## Where to go next

- [Getting started](getting-started.md) — the five calls that make a PDF:
- [Getting started](/docs/writing-a-document/getting-started/) — the five calls that make a PDF:
`New` → `AddPage` → `SetFont` → `Text` → `Write`.
- [Graphics](graphics.md) — paths, colour, line state, transforms, clipping
- [Graphics](/docs/writing-a-document/graphics/) — paths, colour, line state, transforms, clipping
and transparency.
- [Text & fonts](text-and-fonts.md) — loading fonts, drawing text, measuring
- [Text & fonts](/docs/writing-a-document/text-and-fonts/) — loading fonts, drawing text, measuring
and wrapping, and the shaped-text API for complex scripts.
- [Images](images.md) — `DrawImage`, `DrawPNG` and `DrawJPEG`.
- [Widgets](widgets.md) — printing a go-widgets/toolkit widget tree to a page.
- [API reference](api.md) — the full public surface with Go examples.
- [Images](/docs/writing-a-document/images/) — `DrawImage`, `DrawPNG` and `DrawJPEG`.
- [Widgets](/docs/writing-a-document/widgets/) — printing a go-widgets/toolkit widget tree to a page.
- [API reference](/docs/api/) — the full public surface with Go examples.

Source lives at
[github.com/go-pdfkit/pdfkit](https://github.com/go-pdfkit/pdfkit).
7 changes: 5 additions & 2 deletions docs/api.md → content/docs/api.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# API reference
---
title: "API reference"
weight: 130
---

The full public surface of `github.com/go-pdfkit/pdfkit`, the document
builder. See [pkg.go.dev](https://pkg.go.dev/github.com/go-pdfkit/pdfkit) for
Expand Down Expand Up @@ -157,7 +160,7 @@ lays the same tree out and instead emits PDF vector operators, so fills and
strokes stay crisp and text (including a TrueType-font widget label's own
embedded face) becomes real, selectable PDF text; it requires
`WidgetOptions.Font` and returns an error without one. See
[Widgets](widgets.md) for a full walkthrough.
[Widgets](/docs/writing-a-document/widgets/) for a full walkthrough.

## Example

Expand Down
4 changes: 4 additions & 0 deletions content/docs/working-with-a-file/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
title: "Working with a file"
weight: 100
---
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# Editing together
---
title: "Editing together"
weight: 60
---

[`coedit`](https://github.com/go-pdfkit/coedit) is a PDF several people are
editing at once.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# Reading a page back
---
title: "Reading a page back"
weight: 50
---

[`extract`](https://github.com/go-pdfkit/extract) reads a page as words and
pictures.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# Rearranging a file
---
title: "Rearranging a file"
weight: 30
---

[`ops`](https://github.com/go-pdfkit/ops) is the verb layer: what people
actually do to a PDF they already have. A document here is an ordered list of
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# Reading a file
---
title: "Reading a file"
weight: 20
---

[`reader`](https://github.com/go-pdfkit/reader) is the parsing and writing
half: everything about the format itself, and nothing about what a page
Expand Down
Loading
Loading