html2pdf

package module
v0.0.0-...-88a869b Latest Latest
Warning

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

Go to latest
Published: Sep 6, 2026 License: BSD-3-Clause Imports: 21 Imported by: 0

README

html2pdf

CI Go Reference

Pure-Go, zero-C, static HTML to vector PDF. No headless browser, no screenshot-then-slice: it drives go-webengine's own layout tree straight into go-pdfkit text/rect/stroke calls, so the PDF's text is real text — selectable, searchable, small — not a raster of it.

Quick start

doc, err := html2pdf.Export(htmlSource, html2pdf.Options{})
if err != nil {
    log.Fatal(err)
}
f, _ := os.Create("out.pdf")
defer f.Close()
doc.Write(f)

Or from the shell — a local file, or a page fetched through the engine's own client (its final URL then resolves relative image sources; use -base for that with -in):

go run ./cmd/html2pdf -in report.html -out report.pdf
go run ./cmd/html2pdf -url https://example.org/report -out report.pdf

Pagination

Page breaks land between atoms — a text line, or a whole <tr> — never through one. A table row that would overflow the page moves to the next page whole; a paragraph may still break between its own lines, same as printed text always has.

Layout width vs. print width

A page is laid out at Options.ViewportPx (default 1024px), then scaled down to fit the print column — not laid out directly at the print column's own width (a plain A4 page is under 650px wide). Many real pages carry a fixed-width element sized for a desktop viewport (a sidebar, a multi-column nav) that a browser's own responsive CSS only collapses below some breakpoint; laying out narrower than that breakpoint just squeezes the rest of the page into a sliver instead of dropping the sidebar. Confirmed against RFC 9110's HTML edition, whose table-of-contents sidebar did exactly this — see corpus/CORPUS.md for the before/after page counts across all 8 corpus pages.

Navigation

An <a href> becomes a link annotation — a URI action for an http(s) target, a GoTo to a named destination for a fragment that points at an element id in the document — one clickable rectangle per line the anchor spans, so the clickable area follows the text; an anchor that lays out no text at all (a link around an empty, CSS-drawn block) gets the block's own rectangle, as a browser's print does. Every element id becomes a named destination, the headings become the viewer's bookmark tree (<h1> at the top level, deeper headings nested under the last shallower one), and the <title> fills the PDF's Title unless Options.Title is set (Author, Subject, Keywords likewise). javascript:, mailto:, tel: and fragments nobody anchors are dropped rather than written as dead links.

Scope

This renders static HTML: no JavaScript, no @font-face. Text is set in the three families go-webengine's own paint package bundles — Inter (sans), Lora (serif), Go Mono (mono) — so the glyphs drawn always match the metrics the layout pass measured against; there is no web-font fetch to fail silently.

External stylesheets — <link rel="stylesheet"> and their @import chains — are fetched through the engine's own bounded loader (Engine.LoadStylesheets: 64 sheets, 4 MB each, two @import levels, 10 s) and cascaded for the print medium by default (Options.Media, CLI -media): a page's @media print rules and print-only stylesheets apply, its screen-only ones do not — a browser's print preview, where a site's navigation, sidebars and footers are usually hidden. -media screen styles the page as displayed. Width features (min-width, max-width) are evaluated at ViewportPx under either.

Images — raster <img>, <img src="*.svg"> and inline <svg> — go through the engine's own fetch/decode/size pipeline (Engine.LoadImageSet), so they're laid out and drawn exactly as the engine's raster canvas would. How the pixels are stored follows the source, the way Chrome's PDF backend decides it: a JPEG the engine did not resize is embedded byte for byte (DCTDecode); any other lossy source (a resized JPEG, a lossy WebP) that is opaque is re-encoded as JPEG at quality 85; PNG, GIF, SVG rasters and anything with transparency are flate bitmaps with a soft mask, so line art and screenshots keep every pixel. Options.ImageDPI (CLI -image-dpi) caps a bitmap's density at its painted size — 0, the default, keeps every fetched pixel as Chrome and WeasyPrint do; WeasyPrint's --dpi is the same lever. A relative src resolves against BaseURL; an image that fails to fetch or decode is left out.

Inline-level decoration — a styled <span>, <code>, <a>… with a background, border or padding — paints too, fragmented per line the way CSS does (box-decoration-break: slice: the left border only on an element's first fragment, the right only on its last). The geometry comes from the engine's own LineBox.Inlines (go-webengine/engine#128), so a badge or pill lands exactly where the raster canvas puts it. Not painted on inline elements: border-radius, background-image/gradients, box-shadow — a fragment paints a flat background and straight borders.

Output

PDF 1.5. Content and font streams are flated; every non-stream object — the link annotations (one per clickable line), named destinations, outline items, the Info dictionary — is packed into flated object streams with a cross-reference stream (pdfkit's ObjectStreams), which is what keeps a document with thousands of links close to its text size (RFC 9110: 3 398 links, 1.06 MB; Chrome 4.5 MB). Fonts are embedded as subsets. Output is deterministic: the same input gives the same bytes.

Status

Validated three ways, all in corpus/ (in the spirit of go-webengine's own bench/):

  • a hand-built regression suite (html2pdf_test.go, ~94% statement coverage) and a corpus of 8 real public pages — corpus/CORPUS.md, with the bugs the corpus has found;
  • a timing/size bench against headless Chrome's own print-to-PDF — corpus/BENCH.md;
  • every reference reader on the machine — qpdf, poppler, MuPDF, Ghostscript, pdfium (Chrome's engine), pdf.js (Firefox's) and Quartz (Preview's) — run over every output, with Chrome's PDFs as the control: corpus/JUDGES.md. Being the smallest file means nothing if one reader in the field disagrees about it.

License

BSD-3-Clause, see LICENSE.

Documentation

Overview

Package html2pdf renders static HTML straight to a vector PDF: it drives go-webengine's own layout tree (no screenshot, no raster slicing) into go-pdfkit text/rect/stroke calls. Pagination breaks between atoms — a text line, or a whole table row — never through one; see atoms.go.

Scope

This is a static renderer: no JavaScript and no @font-face (text uses the three families go-webengine's own paint package bundles — Inter for sans, Lora for serif, Go Mono for mono — so the glyphs drawn always match the metrics layout measured against). External stylesheets — <link rel="stylesheet"> and their @import chains — are fetched through the engine's own bounded loader (Engine.LoadStylesheets) and cascaded for the print medium by default (Options.Media), so a page's @media print rules apply and its screen-only ones do not, as in a browser's print preview. Inline-level background and borders paint per line fragment from the engine's own LineBox.Inlines (box-decoration-break: slice); border-radius, background-image and box-shadow on an inline element do not.

Images — raster <img>, <img src="*.svg"> and inline <svg> — are fetched, decoded and sized by the engine's own pipeline (Engine.LoadImageSet) and embedded so they are laid out and drawn exactly as the engine's raster canvas would: a JPEG source as its own bytes (DCTDecode), any other lossy source re-encoded as JPEG when opaque, everything else as a flate bitmap with a soft mask for transparency — see images.go and Options.ImageDPI. A relative src resolves against Options.BaseURL; an image that fails to fetch or decode is simply left out. Stylesheets and images are the two places Export touches the network.

Navigation

An <a href> becomes a link annotation — a URI action for an http(s) target, a GoTo to a named destination for a fragment that points at an element id in the document — one clickable rectangle per line the anchor spans, or the box of an anchor that lays out no text (engine.LinkRuns). Every element id becomes a named destination, and the headings become the viewer's bookmark tree (<h1> at the top level, deeper headings nested under the last shallower one). The <title> fills the PDF's Title unless Options.Title is set.

Quick start

doc, err := html2pdf.Export(htmlSource, html2pdf.Options{})
if err != nil { ... }
f, _ := os.Create("out.pdf")
defer f.Close()
doc.Write(f)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Export

func Export(htmlSrc string, opts Options) (*pdfkit.Document, error)

Export parses htmlSrc, lays it out at opts.ViewportPx and returns a paginated pdfkit.Document — scaled to fit the page's printable width — ready to Write.

Types

type Options

type Options struct {
	PageSize pdfkit.PageSize // zero value: pdfkit.A4
	MarginMm float64         // zero value: 20

	// ViewportPx is the width (CSS px) the page is laid out against, then
	// uniformly scaled down to fit the print column. Many real pages carry a
	// fixed-width element sized for a desktop viewport — a sidebar, a
	// multi-column nav — that a browser's own responsive CSS only collapses
	// below some breakpoint. Laying out directly at the print column's actual
	// width (a plain A4 page is under 650px wide) sits below most such
	// breakpoints, so that fixed-width element squeezes the rest of the page
	// into a narrow remainder and the whole document wraps far taller than it
	// needs to — confirmed against RFC 9110's HTML edition, whose
	// table-of-contents sidebar did exactly this (428 pages laid out at the
	// print column's own ~642px width vs. 184 at 1024px). Zero value: 1024,
	// a common small-desktop/tablet breakpoint. Set below the print column's
	// own width (rare) to lay out 1:1 with no scaling.
	ViewportPx float64

	// BaseURL is the document's own URL, used to resolve a relative <img src>
	// and a relative <a href> (and to satisfy same-origin-shaped fetch logic
	// in the engine). A link whose href is this URL plus a fragment becomes an
	// in-document jump. Leave it empty for a document whose images and links
	// are all absolute or data: URIs.
	BaseURL string

	// Title, Author, Subject and Keywords fill the PDF's information
	// dictionary. An empty Title is taken from the document's <title>.
	Title, Author, Subject, Keywords string

	// ImageDPI caps the pixel density of an embedded bitmap at its painted
	// size: a bitmap that would exceed it — a 1024 px photograph painted
	// 60 mm wide is 430 dpi — is downsampled to it. Zero (the default) keeps
	// every pixel the engine fetched, which is what Chrome's print and
	// WeasyPrint do by default; WeasyPrint's --dpi is the same lever. 150
	// is a sound print value, 96 the screen's.
	ImageDPI float64

	// Media is the CSS medium the page is styled for: "print" (the zero
	// value) applies the page's @media print rules and print-only
	// stylesheets and skips its screen-only ones — a browser's print
	// preview, where a site's navigation, sidebars and footers are usually
	// hidden; "screen" styles the page as displayed. Width features
	// (min-width, max-width) are evaluated at ViewportPx under either.
	Media string
}

Options configures a single Export call. The zero value is A4, 20mm margins on all sides, and a 1024px layout viewport.

Directories

Path Synopsis
cmd
html2pdf command
Command html2pdf renders a static HTML file, or a fetched page, to a vector PDF.
Command html2pdf renders a static HTML file, or a fetched page, to a vector PDF.

Jump to

Keyboard shortcuts

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