html2pdf

package module
v0.0.0-...-e65e4e8 Latest Latest
Warning

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

Go to latest
Published: Sep 7, 2026 License: BSD-3-Clause Imports: 22 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 are the engine's (paginate.Breaks): they land between atoms — a text line, a whole <tr>, a rule or spacer — never through one, and they honour the document's CSS Fragmentation: break-before / break-after: page (and the page-break-* aliases) force a page; break-inside: avoid keeps a table, a figure, a code block whole when it can fit a page; break-after: avoid on a heading keeps it with what follows; orphans / widows (2 by default) keep a paragraph from leaving a lone line at either edge — with the spec's own order of relaxation when a page cannot be cut any other way. The answer key is Chrome: corpus/cmd/breakcheck renders corpus/fixtures/breaks.html and puts every marker on the page headless Chrome put it on (18/18, nine pages).

@page { size: …; margin: … } sets the paper and margins, over Options.PageSize / MarginMm — A4, A5, letter, landscape, explicit lengths, per-side margins. Margin boxes (@top-center { content: counter(page) }) and :first / :left / :right pages are not read yet.

Layout width vs. print width

A web 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.

A document that declares @page is another matter: it was designed for its paper, its absolute units (a 12pt body, a 40mm figure) must print at their true size, and it is laid out 1:1, as a browser prints it — unless ViewportPx is set explicitly.

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. A character the family has no glyph for is set in the engine's last-resort DejaVu Sans (arrows, mathematical operators, box drawing, braille, Greek, Cyrillic, ①–⑩), embedded beside the family, the way a browser falls back per character to the system's fonts; CJK is beyond it and draws as nothing. The corpus report's Lost chars column counts what still vanishes.

Text is written glyph by glyph at each glyph's own advance — no kerning, no ligatures — because that is exactly how the engine measures and rasterises it; a kerned run would end short of its layout slot and the gap would read as a space. Kerning belongs in the engine's measure first. The corpus report's Glued column counts the words of the extracted text that are two of the page's words run together — a space lost in layout.

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 is the engine's (paginate.Breaks): it cuts between atoms — a text line, a whole table row — never through one, and honours the document's CSS fragmentation (break-before/after: page, break-inside: avoid, break-after: avoid, orphans, widows) and its @page size and margins.

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 — plus its last-resort DejaVu Sans for a character the family has no glyph for, 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; overridden by @page { size }
	MarginMm float64         // zero value: 20; overridden by @page { margin }

	// 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 — unless the document carries
	// an @page rule: a document that names its paper was designed for it,
	// its absolute units (a 12pt body, a 40mm figure) must print at their
	// true size, and it is laid out 1:1 as a browser prints it. 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 — unless the document says otherwise: an @page rule's size and margins win over PageSize and MarginMm, as they do in a browser's print (Chrome honours @page size in headless print-to-PDF; WeasyPrint always has).

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