prettypdf

package module
v0.11.0 Latest Latest
Warning

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

Go to latest
Published: Aug 12, 2026 License: MIT Imports: 13 Imported by: 0

README

go-pretty-pdf

Go Reference CI Go Report Card

Transform a directory of MDX files into a beautiful, print-ready PDF via headless Chrome.

Library + CLI. Use it as a composable Go library or as a standalone command-line tool.

Fast. A 3,000-document book becomes a 3,535-page print-ready PDF in ~23 seconds — and validating those 3,000 documents takes 142 ms. Measured, reproducible numbers in BENCHMARKS.md.

Install

CLI (binary)
go install github.com/sazardev/go-pretty-pdf/cmd/pretty-pdf@latest
Library
go get github.com/sazardev/go-pretty-pdf
Requirements
  • Go 1.26+
  • Chrome or Chromium — optional. If none is found on your system, pretty-pdf automatically downloads and caches a small headless-only Chrome build the first time you run it (like Playwright/Puppeteer do). Already have Chrome installed? It's used as-is, nothing is downloaded. Prefer to control this yourself? Pass --chrome-path /path/to/chrome or set PRETTY_PDF_CHROME_PATH. Auto-download currently covers linux/amd64, darwin/amd64, darwin/arm64, and windows/amd64 — on linux/arm64 (no official build exists yet) install Chromium via your package manager and point --chrome-path at it.

Quick start

CLI
# Scaffold a new book project (interactive wizard)
pretty-pdf init my-book

# Build a PDF
pretty-pdf build --source my-book --out my-book.pdf

# Watch for changes and rebuild
pretty-pdf watch --source my-book --out my-book.pdf

# Validate MDX files
pretty-pdf check --source my-book
Library
package main

import (
	"context"
	"log"

	prettypdf "github.com/sazardev/go-pretty-pdf"
)

func main() {
	pdf, err := prettypdf.New(
		prettypdf.WithSourceDir("./docs"),
		prettypdf.WithOutputFile("output.pdf"),
		prettypdf.WithTitle("My Documentation"),
		prettypdf.WithAuthor("Jane Doe"),
	)
	if err != nil {
		log.Fatal(err)
	}

	if err := pdf.Build(context.Background()); err != nil {
		log.Fatal(err)
	}
}

How it works

MD/MDX files → Parse frontmatter & markdown → Transpile components → Compose HTML → Render PDF
  1. Parse — goldmark parses .md/.mdx files with YAML frontmatter. Fenced code blocks (```go, ```python, ...) are syntax-highlighted via Chroma, using a style paired to each theme's tone (e.g. Dracula for the dark theme, the Gruvbox style for the gruvbox theme, GitHub's light style everywhere else)
  2. Transpile — custom components (<DeepDive>, <Warning>, <Axiom>) become styled HTML
  3. Compose — HTML assembled with embedded template + CSS + auto-generated Table of Contents
  4. Render — headless Chrome prints to PDF with headers, footers, and PDF bookmarks, then an automatic quality audit checks the result for overflowing content, broken images, low-contrast text, near-empty output, dead links, duplicate ids, broken TOC entries, unloaded fonts, at-risk page breaks, and headings at risk of being clipped by the print engine (see pretty-pdf build's Warnings output, or render.RenderToPDFWithAudit in the library API)

Documents are sorted by their [X.Y.Z] frontmatter ID, not filename.

A .md/.mdx file doesn't strictly need a --- frontmatter block either: if one is missing entirely, id and title are generated automatically from the filename, the same convention .txt uses below (02-getting -started.mdx → id [2.0.0], title "Getting Started"; no numeric prefix → the next free major version, so it never collides with an explicitly numbered doc). The content itself still gets full markdown rendering — components, raw HTML, everything — unlike .txt. A --- block that is present but fails to parse as YAML is still a hard error, so a typo in real frontmatter doesn't silently get treated as "no frontmatter".

.txt files are also accepted for freeform writing with zero setup: they have no frontmatter, so id and title are generated automatically from the filename (03-field-notes.txt → id [3.0.0], title "Field Notes"; no numeric prefix → the next free major version, so it never collides with an explicitly numbered doc). Content is treated as literal text — blank lines become paragraphs, single line breaks become <br> — and is always HTML-escaped, so .txt loses component/raw-HTML customization but in exchange never executes anything the author typed. Good for a quick note dropped into an otherwise structured .mdx book.

Trust model

MDX is parsed with raw HTML passthrough enabled, and custom components don't escape their inner content — this lets authors embed arbitrary HTML/CSS for rich documents, but it also means a .md or .mdx file can contain a <script> tag that will execute during rendering. By default, headless Chrome's network access is blocked while rendering (see WithNetworkAccess), so scripts can't exfiltrate data or fetch remote content — but they still run. Only build PDFs from source files you trust. See SECURITY.md for details.

Performance

go-pretty-pdf keeps the Go-side pipeline (parse + validate + compose) sub-second even on large books; headless Chrome's print step is what drives PDF wall time. Measured on an i5-13500 (WSL2) with Chromium 150:

Source docs check epub PDF pages PDF wall time PDF throughput
100 30 ms 27 ms 121 0.57 s 211 pages/s
1,000 62 ms 88 ms 1,180 2.99 s 394 pages/s
3,000 142 ms 248 ms 3,535 23.02 s 154 pages/s

Full table, hardware, and a reproducible recipe: BENCHMARKS.md. To measure your own machine:

PRETTY_PDF_CHROME_PATH=/usr/bin/chromium go run ./scripts/benchmark --color=false

MDX format

---
id: "[1.0.0]"
title: "Getting Started"
subtitle: "A simple introduction"
tags: [example, intro]
difficulty: "beginner"
status: complete
completeness: 100
depends_on: []
---

# Welcome to Your Book

This is the first chapter.

## Variables

You can use {{key}} syntax for variable substitution: running {{product}} v{{version}}.

Required frontmatter fields: id (format [X.Y.Z]), title.

Built-in components

Component Usage Appearance
<DeepDive> <DeepDive title="Details">...</DeepDive> Blue info panel
<Warning> <Warning title="Note">...</Warning> Orange warning panel
<Axiom> <Axiom>...</Axiom> Green italic quote

Register custom components via WithComponent():

prettypdf.WithComponent("Callout", func(attrs map[string]string, inner string) string {
	level := attrs["level"]
	return fmt.Sprintf(`<div class="callout callout-%s">%s</div>`, level, inner)
})

Configuration

Create a go-pretty-pdf.yml in your project:

title: "My Book"
subtitle: "A Complete Guide"
author: "Jane Doe"
source: book
output: out.pdf
theme: default

css: custom.css
template: custom-template.html

vars:
  product: "go-pretty-pdf"
  version: "1.0"

lint:
  require_frontmatter: [id, title]
  require_id_format: "[X.Y.Z]"
  no_duplicate_ids: true
  max_heading_depth: 3

render:
  timeout: 60s
  paper: a4
  margin_top: 20mm
  margin_bottom: 20mm
  margin_left: 15mm
  margin_right: 15mm
  header_title: "{{title}}"

Library API

// Constructor with functional options
pdf, err := prettypdf.New(opts...)

// All-in-one build pipeline
pdf.Build(ctx)

// Step-by-step pipeline
docs, _ := pdf.ParseDir()
errs := pdf.ValidateDoc(doc)
html, _ := pdf.ComposeHTML(docs)
pdf.Render(html)

// Quality audit from the most recent Build/Render call (nil if neither ran yet)
audit := pdf.LastAudit()

// Validation-only
errs, _ := pdf.Validate(ctx)

// Lower-level: render straight to PDF and get the audit report back
report, err := render.RenderToPDFWithAudit(html, "out.pdf", render.DefaultOptions())
Available options
Option Description
WithSourceDir(dir) MDX source directory (default: book)
WithOutputFile(path) Output PDF path (default: out.pdf)
WithTitle(title) Document title
WithSubtitle(sub) Document subtitle
WithAuthor(author) Document author
WithCSS(css) Custom CSS content string
WithTemplate(html) Custom HTML template string
WithTheme(t) Apply a raw theme.Theme (no customization/section toggles)
WithThemeName(name, opts) Resolve a theme by name (builtin, custom, or file path) with color/font/section customization
WithComponent(name, handler) Register custom MDX component
WithValidator(v) Custom validation logic
WithTimeout(d) Chrome render timeout (default: 60s)
WithHeaderTitle(t) PDF header title
WithVerbose(bool) Enable verbose logging
WithVars(map) Variable substitution map
WithRenderMargins(t,b,l,r) PDF margins in inches
WithPaperSize(w,h) Paper size in inches
WithConfig(cfg) Apply source/output/title/subtitle/author from config
WithConfigCSSAndTemplate(cfg) Load CSS/template from config file paths
WithFullConfig(cfg) Apply the entire config struct (source, CSS/template, theme, vars, render settings) in one call
WithNetworkAccess(bool) Allow headless Chrome to make network requests while rendering (default: false, blocked)

Themes

Seventeen built-in themes, each a palette/typography layer over one shared structural stylesheet — clean and professional by default, easy to customize without writing CSS, and extendable with your own custom themes:

default · minimal · modern · classic · corporate · dark · academic · editorial · sepia · terminal · blueprint · ivy · government · resume · legal · latex · gruvbox

# Pick a theme, tweak colors/fonts/density, drop sections you don't want
pretty-pdf build --theme corporate \
  --color-primary "#0ea5e9" --font-heading "Georgia, serif" \
  --no-cover --no-page-numbers --density compact

# Scaffold your own reusable theme
pretty-pdf theme new my-report --from corporate
pretty-pdf theme list
prettypdf.WithThemeName("corporate", theme.Options{
	Colors:   theme.Colors{Primary: "#0ea5e9"},
	Sections: theme.Sections{Cover: theme.BoolPtr(false)},
})

Custom themes live in <name>.theme.yml files (project-local ./themes/ or a global themes directory) and extends a builtin theme. Full reference, all customization fields, and the pretty-pdf theme command family: see docs/cli.md#themes.

CLI reference

pretty-pdf build     Build a PDF from MDX source files
pretty-pdf epub      Build an EPUB from MDX source files (no Chrome required)
pretty-pdf check     Validate MDX files without building
pretty-pdf theme     List, inspect, and manage themes
pretty-pdf init      Scaffold a new book project (interactive wizard)
pretty-pdf watch     Watch for changes and rebuild automatically
pretty-pdf serve     Preview MDX as HTML with live reload (no Chrome required)
pretty-pdf version   Print the version number

Run pretty-pdf <command> --help for the full flag list of any command.

Global flags: --config, --source, --verbose, --no-color, --quiet

License

MIT — see LICENSE.

Documentation

Overview

Package prettypdf transforms MDX source files into print-ready PDFs via headless Chrome.

It is both a composable Go library and a CLI tool. The library exposes a pipeline with step-by-step methods (ParseDir, ValidateDoc, ComposeHTML, Render) and 18 functional options for full customization.

Quick start (library)

pdf, err := prettypdf.New(
	prettypdf.WithSourceDir("./docs"),
	prettypdf.WithOutputFile("output.pdf"),
	prettypdf.WithTitle("My Book"),
)
if err != nil {
	log.Fatal(err)
}
if err := pdf.Build(context.Background()); err != nil {
	log.Fatal(err)
}

Quick start (CLI)

go install github.com/sazardev/go-pretty-pdf/cmd/pretty-pdf@latest
pretty-pdf build --source ./docs --out out.pdf

MDX files require frontmatter with an `id` field in [X.Y.Z] format (e.g. `[1.0.0]`). Documents are sorted by ID, not filename. Variables in {{key}} syntax are substituted before parsing.

Built-in custom components: <DeepDive>, <Warning>, <Axiom>. Additional components can be registered via WithComponent().

Trust model

MDX is parsed with raw HTML passthrough enabled, and component transpilation does not escape inner content. Only build PDFs from MDX you trust — see SECURITY.md for details. Network access during rendering is blocked by default (see WithNetworkAccess).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ComposeHTML

func ComposeHTML(docs []*mdx.Document, opts ComposeOptions) (string, error)

Types

type ComposeOptions

type ComposeOptions = compose.Options

func DefaultComposeOptions

func DefaultComposeOptions() ComposeOptions

type Option

type Option func(*PDF)

func WithAuthor

func WithAuthor(author string) Option

func WithCSS

func WithCSS(css string) Option

func WithChromeExecPath added in v0.5.0

func WithChromeExecPath(path string) Option

WithChromeExecPath pins rendering to a specific Chrome/Chromium binary instead of chromedp's default system discovery. Leave unset (or pass "") to keep the default behavior. See the chromemgr package for resolving this automatically, including downloading a browser when none is installed.

func WithComponent

func WithComponent(name string, handler mdx.ComponentHandler) Option

func WithConfig

func WithConfig(cfg *config.Config) Option

func WithConfigCSSAndTemplate

func WithConfigCSSAndTemplate(cfg *config.Config) Option

WithConfigCSSAndTemplate resolves cfg.Theme (with cfg.ThemeOptions customization) and then loads CSS/template content from cfg.CSS/ cfg.Template, which — being explicit file overrides — take priority over the theme and replace its CSS/template outright. Read/resolve failures are recorded as warnings and flushed to stderr by New() once all options have been applied, so ordering relative to WithVerbose does not matter.

func WithCoverImage added in v0.8.0

func WithCoverImage(imagePath string) Option

WithCoverImage replaces the theme's text cover with a full-bleed page built from imagePath (.png/.jpg/.jpeg). That cover page is sized to the image's own pixel dimensions exactly — a square image gets a square cover page — while every other page keeps its configured paper size. The theme's own text cover (title/subtitle/metadata) is suppressed regardless of theme/section settings and regardless of the order options are applied in — see New(), which enforces this once after every Option has run.

func WithEpubLanguage added in v0.10.0

func WithEpubLanguage(lang string) Option

func WithFormats added in v0.10.0

func WithFormats(formats ...OutputFormat) Option

func WithFullConfig added in v0.3.0

func WithFullConfig(cfg *config.Config) Option

WithFullConfig applies every field of cfg: source/output/title/subtitle /author (via WithConfig), CSS/template/theme (via WithConfigCSSAndTemplate), variable substitution (cfg.Vars), and render settings (cfg.Render: timeout, paper size, margins, header title). Unlike WithConfig and WithConfigCSSAndTemplate, which only cover a subset of Config, this is the single option needed to fully apply a loaded go-pretty-pdf.yml.

func WithGenerateDocumentOutline added in v0.11.0

func WithGenerateDocumentOutline(enabled bool) Option

WithGenerateDocumentOutline toggles whether PDF bookmarks/outline are built from the document's headings. Building the outline is post-print work over the whole PDF; disable for a meaningful speedup on very large documents at the cost of losing in-PDF navigation bookmarks.

func WithGenerateTaggedPDF added in v0.11.0

func WithGenerateTaggedPDF(enabled bool) Option

WithGenerateTaggedPDF toggles PDF accessibility tagging (PDF/UA). Tagging is the most expensive post-print step on large documents; disable for a real speedup when accessibility metadata isn't needed.

func WithHeaderTitle

func WithHeaderTitle(title string) Option

WithHeaderTitle sets the PDF page header text. If never called, New() defaults it to the document title (WithTitle/composeOpts.Title).

func WithNetworkAccess added in v0.3.0

func WithNetworkAccess(enabled bool) Option

WithNetworkAccess controls whether headless Chrome may make outbound network requests while rendering. It defaults to false: the composed HTML is a self-contained data URI, so network access is blocked to prevent SSRF/exfiltration from untrusted MDX content (e.g. a malicious <img> or <script> tag). Enable it only if your documents intentionally reference remote images, fonts, or other resources by URL.

func WithOutputFile

func WithOutputFile(path string) Option

func WithPaperSize

func WithPaperSize(width, height float64) Option

func WithRenderMargins

func WithRenderMargins(top, bottom, left, right float64) Option

func WithSharedBrowser added in v0.11.0

func WithSharedBrowser(browserCtx context.Context) Option

WithSharedBrowser makes subsequent Build calls render PDFs on the given Chrome allocator instead of booting a fresh Chrome process each time. The allocator must come from render.NewBrowser, stay alive across all Build calls, and be canceled by the caller once done. Each Build opens its own tab on the allocator, so concurrent Build calls share one Chrome process safely. This amortizes the per-launch Chrome startup cost across many renders — most useful when a process renders many documents (docsgen's per-theme PDFs, a batch job, a test harness).

func WithSourceDir

func WithSourceDir(dir string) Option

func WithSubtitle

func WithSubtitle(subtitle string) Option

func WithTemplate

func WithTemplate(html string) Option

func WithTheme

func WithTheme(t theme.Theme) Option

WithTheme applies a raw builtin/synthetic Theme's CSS as-is, with no customization (colors/fonts/sections/density) and no section toggles applied. It shares composeOpts.CSS with WithCSS and WithThemeName — whichever of these options is applied last wins, since New() applies options in the order they're passed. Most callers should prefer WithThemeName, which resolves section toggles (cover/TOC/page numbers/header) into composeOpts/renderOpts too.

func WithThemeName added in v0.4.0

func WithThemeName(name string, opts theme.Options) Option

WithThemeName resolves a theme by name — a builtin ("default", "corporate", ...), a custom theme discovered in ./themes/ or the global themes directory, or a direct path to a .theme.yml/.css file — applies opts customization (colors, fonts, density, network fonts), and wires the resulting section toggles (cover, TOC, page numbers, header) into composeOpts/renderOpts. The theme is resolved through both the PDF and the EPUB pipelines, so a later EPUB build (Build with FormatEPUB or RenderEpub) carries the same theme, not the default stylesheet.

func WithTimeout

func WithTimeout(d time.Duration) Option

func WithTitle

func WithTitle(title string) Option

func WithValidator

func WithValidator(v mdx.Validator) Option

func WithVars

func WithVars(vars map[string]string) Option

func WithVerbose

func WithVerbose(v bool) Option

type OutputFormat added in v0.10.0

type OutputFormat string
const (
	FormatPDF  OutputFormat = "pdf"
	FormatEPUB OutputFormat = "epub"
)

func ParseFormats added in v0.10.0

func ParseFormats(s string) ([]OutputFormat, error)

type PDF

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

func New

func New(opts ...Option) (*PDF, error)

func (*PDF) Build

func (p *PDF) Build(ctx context.Context) error

func (*PDF) ComposeHTML

func (p *PDF) ComposeHTML(docs []*mdx.Document) (string, error)

func (*PDF) Formats added in v0.10.0

func (p *PDF) Formats() []OutputFormat

func (*PDF) LastAudit added in v0.8.0

func (p *PDF) LastAudit() *render.AuditReport

LastAudit returns the visual/structural audit report from the most recent Build or Render call, or nil if neither has run yet. See render.AuditReport and render/audit.go for what it checks.

func (*PDF) NeedsChrome added in v0.10.0

func (p *PDF) NeedsChrome() bool

func (*PDF) ParseDir

func (p *PDF) ParseDir() ([]*mdx.Document, error)

func (*PDF) Render

func (p *PDF) Render(html string) error

func (*PDF) RenderEpub added in v0.10.0

func (p *PDF) RenderEpub(docs []*mdx.Document, outputPath string) error

func (*PDF) RenderWithContext added in v0.11.0

func (p *PDF) RenderWithContext(ctx context.Context, html string) error

RenderWithContext is Render with the browser rooted in ctx instead of context.Background(): canceling ctx (client disconnect, SIGINT wired to context cancellation) tears down the in-flight Chrome render rather than running to completion or to opts.Timeout regardless. opts.Timeout still applies as an upper bound layered on top of ctx.

func (*PDF) Validate

func (p *PDF) Validate(ctx context.Context) ([]mdx.ValidationError, error)

func (*PDF) ValidateAll added in v0.2.0

func (p *PDF) ValidateAll(docs []*mdx.Document) []mdx.ValidationError

func (*PDF) ValidateDoc

func (p *PDF) ValidateDoc(doc *mdx.Document) []mdx.ValidationError

func (*PDF) Warnings added in v0.9.0

func (p *PDF) Warnings() []string

Warnings returns the non-fatal configuration warnings recorded by New — e.g. an unresolvable theme name or an unreadable --css/--template file — each of which caused that option to fall back rather than apply. They are also printed to stderr by New itself; this accessor exists for callers that want to detect the condition programmatically instead.

Directories

Path Synopsis
Package chromemgr resolves a working Chrome/Chromium executable for headless rendering without requiring the user to install one by hand.
Package chromemgr resolves a working Chrome/Chromium executable for headless rendering without requiring the user to install one by hand.
cmd
pretty-pdf command
Package epub converts parsed MDX documents into a single EPUB 3 file — no headless Chrome involved, unlike the PDF pipeline in render.
Package epub converts parsed MDX documents into a single EPUB 3 file — no headless Chrome involved, unlike the PDF pipeline in render.
scripts
benchmark command
Command benchmark runs an automated performance verification of go-pretty-pdf across multiple book sizes.
Command benchmark runs an automated performance verification of go-pretty-pdf across multiple book sizes.
bump command
docsgen command
Package theme implements go-pretty-pdf's theme engine: a set of professional built-in themes plus a customization layer (colors, fonts, section toggles, density) that composes on top of them via CSS custom properties, and a YAML format for user-defined custom themes.
Package theme implements go-pretty-pdf's theme engine: a set of professional built-in themes plus a customization layer (colors, fonts, section toggles, density) that composes on top of them via CSS custom properties, and a YAML format for user-defined custom themes.

Jump to

Keyboard shortcuts

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