engine

package module
v0.8.2 Latest Latest
Warning

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

Go to latest
Published: Oct 18, 2026 License: BSD-3-Clause Imports: 41 Imported by: 3

README

go-webengine/engine

go-webengine / engine

CI coverage gate Go Reference Docs

A pure-Go, CGO_ENABLED=0 headless web engine: it fetches a URL, parses the HTML into a DOM, applies a real CSS subset (cascade + inheritance + var() + modern colour + dark-mode + gradients), runs the page's JavaScript against a real DOM binding, lays the content out with a full box model (block, inline, float, flexbox, CSS grid, tables and position), and paints anti-aliased text, backgrounds, gradients, box-shadows, images and SVG to an image.RGBA — no Chromium, no cgo, no host web view. Give it a URL, get back an image of the page.

It is the rendering core of the browserproxy remote-browser service and the wasmdesk in-desktop browser: a server renders pages and streams frames (plus a click hit-map) to a thin client.

Quickstart

import (
    "context"
    "image"
    "os"

    "github.com/go-webengine/engine"
)

func main() {
    ctx := context.Background()

    // Render a live page to PNG bytes.
    png, err := engine.Screenshot(ctx, "https://example.com/", image.Rect(0, 0, 1024, 768))
    if err != nil {
        panic(err)
    }
    _ = os.WriteFile("out.png", png, 0o644)

    // Or get the raw image plus metadata (title, final URL, full content height).
    img, info, err := engine.Render(ctx, "https://example.com/", image.Rect(0, 0, 1024, 768))
    _ = img
    _ = info // info.Title, info.URL, info.ContentHeight
    _ = err
}

For more control use an *engine.Engine (from engine.New()): it adds RenderHTML (render a local HTML string offline), RenderWithLinks / RenderDocumentWithLinks (image plus a []Link hit-map for click-to-navigate), and a DisableJS field to render the static, no-JavaScript document.

The viewport width is fixed; the height grows to fit the whole page (at least the viewport height). There is also a CLI:

go run ./cmd/render -url https://example.com/ -out out.png -w 1024 -h 768
# or render a local file offline:
go run ./cmd/render -file page.html -base https://example.com/ -out out.png

Pipeline

Fetch (go-browserhttp)  →  Parse (x/net/html → dom)  →  Cascade (css: +var()/@media/dark-mode)
     →  JavaScript (js: goja + real DOM + fetch/XHR)
     →  Layout: block · inline · float · flex · grid · table · position (layout)
     ⟲   settle loop: JS reads laid-out metrics → re-cascade + re-layout to a bounded fixpoint
     →  Paint: AA text/SVG/gradients/shadows (paint) + backgrounds (go-widgets/painter) + images (go-images)
     →  image.RGBA  →  PNG (+ optional link hit-map)
Package Role
dom Owned DOM node tree built from golang.org/x/net/html.
css Real CSS subset: value model, stylesheet/declaration parser, tag/class/id + descendant/child/sibling combinators + :checked/:not() selectors with specificity, var() custom properties, @media width queries, modern colour (rgb()/hsl()), dark-mode, UA stylesheet, cascade + inheritance.
layout Full box model — block-and-inline flow, floats + clear, flexbox, CSS grid, tables, position (relative/absolute/fixed/sticky), margin collapsing, greedy word-wrap — driven by a Measurer interface (font-free, exactly testable).
js JavaScript execution via goja bound to a minimal real DOM, with fetch()/XHR and laid-out-geometry read-back (getBoundingClientRect, offset*, getComputedStyle).
paint Rasterises the box tree to *image.RGBA — AA text (real bold + italic), gradients, border-radius, box-shadow, opacity, images and SVG; also the real Measurer (go-opentype faces).
engine (root) Fetch, Render, Screenshot, RenderHTML, RenderWithLinks, the settle-then-render loop, image + SVG loading, and the anchor hit-map.
cmd/render CLI: render -url URL -out shot.png -w 1024 -h 768 (or -file page.html).

Everything reused is pure-Go and BSD/MIT — see SURVEY.md for the prior-art verdict (opossum/mycel studied, not built on) and the full reuse-vs-build decision.

What works / What doesn't

The full per-feature and per-page assessment (ten live pages, committed golden PNGs, measured vs headless Chrome) is in FIDELITY.md and bench/REPORT.md. Short version:

Works today

  • HTML → DOM → full box-model layout at a real viewport width: block/inline flow, floats + clear, flexbox, CSS grid, tables, position (relative/absolute/fixed/sticky), margin collapsing, greedy word-wrap.
  • CSS cascade with specificity (inline > id > class > tag) and inheritance; var() custom properties; @media width queries; dark-mode (prefers-color-scheme); external <link> stylesheet fetch; UA defaults.
  • Selectors: tag/class/id/compound, descendant + child + sibling (~/+) combinators, :checked and :not() (the checkbox-hack that collapses MediaWiki dropdowns), attribute selectors handled by a "reduce, don't drop" rule.
  • Colour & decoration: named/#rgb/#rrggbb, modern rgb()/hsl(), background-color, linear & radial gradients, background-image: url(), border + border-radius, box-shadow, group opacity.
  • Transforms and effects: translate and rotate (the function and the standalone property), CSS filter (blur and colour-matrix functions), backdrop-filter, mask-image (a single url() mask), group opacity.
  • Lists and columns: ul/ol markers (list-style discs, circles and squares, start/value), and multi-column layout for columns, column-count and column-width.
  • Text: anti-aliased proportional text (go-opentype) with real bold and italic faces (no faux-bold), serif / sans / mono, complex scripts (Cyrillic, Vietnamese, …); white-space: pre and nowrap; text-transform (uppercase/lowercase/capitalize, applied before measurement); letter-spacing, word-break/overflow-wrap, text-overflow: ellipsis, -webkit-line-clamp; @font-face — the document's own typefaces are fetched and registered before anything is measured, WOFF2 included, so a page is set in the face it asked for and not in a substitute at other metrics. A host can also install typefaces of its own with Engine.FontFaces (built with ParseFontFace), as a browser's system fonts; parsed fonts are shared across renders, so a page set in the same faces as the last one does not parse them again.
  • Tables: automatic table layout (CSS 2.1 §17.5.2.2) — every column keeps at least its longest unbreakable word, percentage and fixed cell widths are honoured, surplus goes to the auto columns; colspan, border-spacing, vertical-align on cells (with the UA middle default and the valign attribute), and a row's own CSS height as its minimum.
  • Form controls: inputs and buttons with UA chrome, and checkboxes/radios honouring appearance: none (custom toggle switches).
  • Containers and shadow DOM: @container size queries, and declarative shadow DOM (<template shadowrootmode>) with <slot> distribution.
  • Images: <img> over http(s) + data: (PNG/JPEG) and SVG (oksvg/rasterx) via <img *.svg>, data:image/svg+xml and inline <svg>.
  • JavaScript: page scripts run via goja against a real DOM, with fetch()/XHR and read-back of real laid-out geometry (getBoundingClientRect, offsetWidth/Height, getComputedStyle). A settle-then-render loop re-cascades and re-lays-out after scripts mutate the DOM (incl. dynamically injected <script>/<style>/<link>), to a bounded fixpoint — so mw.loader-style runtime chrome is reflected in the output. The same JS-settled DOM drives the click hit-map.

Honest limits (not overclaimed)

  • conic-gradient is recognised but not painted. filter and mask-image are modelled narrowly (see above): a mask is a single url() stretched over the box, with no mask-size/mask-position/mask-repeat. Transforms other than translate and rotate (scale, skew, matrix) are not supported. SVG has no <filter>/<mask>/<pattern>/embedded <image>/<text>, and a per-page image budget caps very icon-heavy pages.
  • ::before/::after generated content is not synthesised, so icon-font and visually-hidden chrome can render as text where a browser shows an icon.
  • vertical-align on a table cell models top, middle and bottom; the baseline alignment across a row is approximated as top.
  • Rate-limited image hosts (Wikimedia's CDN, measured round 154) answer some requests with HTTP 429. The engine retries them, and the renders are pixel-identical, but each retry costs about 2 seconds of wall time on that page (measured round 155).
  • Several pages render slower than Chrome (go.dev/blog, tailwindcss.com, pkg.go.dev): an open performance gap, not a fidelity one. The measured causes so far are the per-host request cap and the time spent on rate-limited responses.
  • This is not a standards-complete browser and not "as good as Chromium". Measured mean windowed-SSIM across the ten bench pages is ≈ 0.66, with clear diminishing returns; the Wikipedia number (≈ 0.42) is JS-confounded and noisy. See the numbers below.

Measured fidelity vs headless Chrome

From bench/REPORT.md: windowed SSIM over the common top-left region at 1024px width (1.0 = identical), pixel-diff %, and speed× = chrome_ms / webengine_ms (>1 = webengine faster). Single run against live pages, so the content of several pages moves between runs (see the notes in FIDELITY.md).

URL SSIM pixdiff % speed× note
example.com/ 0.831 5.6 18.3 near-parity; its JS cross-fade is not modelled
en.wikipedia.org/wiki/Go 0.417 30.3 1.6 live article; JS and rate-limited thumbnails
pkg.go.dev/net/http 0.716 11.3 0.8 large computed page
go.dev/blog/ 0.700 16.6 0.45 slower than Chrome
react.dev/ 0.725 33.0 1.3 SPA; hydration fails, so the static fallback renders
news.ycombinator.com/ 0.615 13.7 1.75 table layout; rotating front page
developer.mozilla.org/…/CSS 0.606 18.1 3.3 docs layout
github.com/golang/go 0.637 14.2 2.0 live repository counters
tailwindcss.com/ 0.724 12.2 0.5 sponsor carousel rotates; slower
caniuse.com/ 0.660 18.0 0.8 data grid

Mean SSIM over the ten pages is ≈ 0.66. Summed over the run, webengine took 26.1 s and headless Chrome 26.3 s, so the overall time is at parity, but the per-page spread is wide: example.com and the static docs pages are much faster, while go.dev, tailwindcss.com and the large pkg.go.dev page are slower. Re-run the harness with cd bench && go run ./cmd/compare -urls urls.txt (needs a Chrome/Chromium binary).

Larger corpora

Ten pages is a small, curated sample. Two larger, less-curated corpora — bench/urls-100.txt (100 pages, 100 distinct sites) and bench/urls-500.txt (500 pages across 268 sites, built from each site's own sitemap, ≤4 pages/site) — exist specifically to surface what a small hand-picked sample won't: round 166's and round 167's crash fixes (an unrecoverable stack overflow from a script-built DOM tree past 512 levels, and a CSS Grid panic on grid-row:1 / -1 with no explicit row template) were both found this way, on smashingmagazine.com and lego.com respectively — real pages no ten-page sample happened to include. Full results: bench/REPORT-100.md, bench/REPORT-500.md.

Mean windowed-SSIM is ≈ 0.53 on both (100-page median 0.55, 500-page median 0.56) — lower than the curated ten-page set, which is expected: this sample includes large, JS-heavy, highly dynamic sites (news homepages, cloud-vendor marketing pages) the ten-page set was never meant to represent, not a regression. 95 of 500 pages (19%) failed to render in BOTH webengine and headless Chrome — dead URLs, DNS failures and timeouts, not an engine gap; exactly one page failed in webengine alone (a transient DNS flake on the measuring machine). Round 168 found and fixed a real, separate performance bug this way too: several of the 500 pages' own origins (large organizations' own CDNs/WAFs, confirmed via www.cs.cmu.edu) rendered 10-20× slower than Chrome, traced to a font-face loading path that tried several candidate font files in series instead of concurrently (see FIDELITY.md's own round-168 entry) — fixed, but the 500-page numbers above predate that fix and have not yet been re-measured against it.

Security

The engine fetches every resource a page names (images, stylesheets, fonts, scripts and modules, fetch/XHR targets, form posts). It does not filter destinations itself, so rendering an untrusted page from a machine with access to private networks can reach internal addresses. If you embed the engine directly, add a dial-time guard through Engine.Client that refuses loopback, private and link-local addresses. browserproxy ships one (guard.go).

Since a renderer's whole job is turning arbitrary, untrusted page content into output, several unrelated kinds of pathological input have been found and capped (each confirmed live against a synthetic reproduction, not assumed):

  • Decoding: raster images declaring more than 25 megapixels are refused before decode; every fetch is size-capped.
  • Tree-depth recursion: a script-built DOM tree past 512 levels (bypassing the HTML parser's own 512-level parse limit via createElement+ appendChild) used to crash the whole process with an unrecoverable Go stack overflow — not a panic, recover() cannot catch it — in the CSS cascade, layout, cloneNode/isEqualNode, and three CSS parsers (calc(), :not()/:has()/:host(), :is()/:where()/:matches(), the last of these via a shared output budget rather than a depth cap, since it explodes in alternative COUNT, not nesting depth). All are now capped at 512 (matching the parser's own bound) or a generous output budget.
  • Memory exhaustion: a single innerHTML assignment, or a cloneNode call repeated in a tight loop, could previously allocate tens of gigabytes of real memory from a few lines of ordinary-looking script — no deep nesting or large string needed. cloneNode and the HTML→DOM conversion shared by Parse/ParseFragment (and so innerHTML/ outerHTML/insertAdjacentHTML) now cap total node count per call; the rendered canvas itself is capped by total pixel count.
  • Unresponsive render, no malicious input required: an ordinary, non-malicious page with several background-repeat sections (found live on a real production site, not a crafted page) drove the paint phase to roughly a billion per-pixel calls through image.Image's At() — each one heap-allocating to box the returned color.Color and re-deriving a premultiplied value from straight storage — taking tens of seconds of CPU with the caller's context.Context deadline never consulted, because nothing in that per-pixel loop checked it. Fixed by normalising a background-image tile to a concrete *image.NRGBA once, before it is stamped, instead of once per stamped pixel.

See FIDELITY.md's rounds 166–175 for the full measured writeups. None of these change how a well-formed page renders — only how a pathological one degrades (truncated output, never a crash or unbounded allocation).

Test

CGO_ENABLED=0 go build ./...          # cgo-free build
CGO_ENABLED=0 go vet ./...
CGO_ENABLED=0 go test -short ./...     # -short skips the live-network render
bash scripts/coverage-gate.sh          # ratchet coverage gate (see below)

The pure logic (cascade/inheritance, line-breaker, box metrics, DOM, selector engine) is asserted at exact geometry; committed golden PNGs — including offline JS/dynamic/gradient/position/SVG fixtures with a DisableJS control — cover the paint path. scripts/coverage-gate.sh enforces a ratchet coverage floor per pure-logic package (css/layout/paint/dom/paginate: css 99.5%, layout, paint and paginate 100%, dom 98.5%), which CI fails below and which is raised (never lowered) toward 100% as the engine matures. The live-network paths (root engine package, cmd/render) are excluded from the gate because their coverage is not reproducible in CI. The bench/ fidelity harness is a separate nested module (it pulls chromedp) and is not in the CGO=0 six-arch CI. go.mod floor is go 1.27.2; cross-built for all six 64-bit Go targets.

License

BSD-3-Clause — see LICENSE. Copyright (c) the go-webengine/engine authors.

Documentation

Overview

This file generalizes linkmap.go's anchor-only hit-map to any element, so a host can turn a click at pixel (x, y) into a specific button/input/etc, not only a link. Kept in its own file for the same reason linkmap.go is: the hit-test feature stays isolated from the core render pipeline.

Package engine is a pure-Go (CGO=0) web rendering engine: it fetches a URL, parses the HTML into a DOM, applies a real CSS subset (cascade, inheritance, var(), @media, dark-mode, gradients), runs the page's JavaScript against a real DOM binding, lays the content out with a full box model (block, inline, float, flex, grid, table, position), and paints anti-aliased text, backgrounds, gradients, box-shadows, images and SVG to an image.RGBA. A settle-then-render loop re-lays-out after scripts mutate the DOM, so script-driven changes are reflected in the output; set Engine.DisableJS to render the static, no-JavaScript document instead.

This file is the fallback path for a <form> whose submission ISN'T intercepted by the page's own JS: LiveDocument.Submit performs the real GET/POST a browser would, exactly like clicking a plain server-rendered form's submit button. A page that DOES handle submit with fetch()/XHR (the common SPA login shape) never reaches this — Submit checks defaultPrevented first and gets out of the way.

This file adds an IMAGE-DOCUMENT path to Fetch. A URL whose response is a raster image (Content-Type image/png|jpeg|gif|webp|bmp|…, i.e. raw image bytes rather than an HTML page — think a direct https://i.redd.it/abc.jpg link) has no markup to parse: feeding its bytes to the HTML parser yields a garbage/empty DOM and nothing renders. A browser instead wraps a bare image in a synthetic document that centres/sizes the image; the engine does the analogue here, synthesising `<img style="width:100%">` markup so the image flows through the exact same decode + layout + paint path as any other <img>, spanning the full viewport width (upscaled if it is smaller, so a standalone image is always at least the pane width). SVG is deliberately excluded: a fetched SVG document parses as inline <svg> and already renders through the SVG path, so it is left untouched.

This file adds a persistent counterpart to the one-shot Render family: a LiveDocument keeps a page's JS runtime alive across separate, later calls instead of tearing it down at the end of one render. It exists for a host that synthesizes real user interaction (focus a field, type a character, click a button) spread out over time — a plain Render per interaction would discard the JS runtime's own state (anything a script holds that isn't reflected back into the DOM/attributes, e.g. a controlled input's component state) between each one, silently diverging from what a real browser would do. See dynamic.go's settle, whose Session this reuses without re-running the page's initial scripts.

This file adds a hyperlink hit-map on top of the laid-out box tree: given a rendered page it reports the painted rectangle and resolved href of every <a href> anchor, so a consumer (e.g. the wasmdesk browserproxy) can turn a click at pixel (x, y) into a navigation target. It is kept deliberately in its own file — separate from engine.go and the js/ runtime work — so the hit-test can evolve without colliding with the rendering pipeline.

This file adds a last-resort OpenGraph/meta fallback: when a page renders to nothing — an un-hydratable JavaScript SPA whose content lives only in a React initial-state blob (Mastodon, X, many app-shell sites) — the engine synthesises a clean, readable card from the page's og:/meta tags and renders that instead of a blank frame. It is gated by Engine.MetaFallback and only ever replaces a genuinely empty render, so pages that render real content are completely unaffected.

This file reports the navigation structure of a laid-out document: where every <a href> is clickable, line by line; where every id'd element sits; and the heading outline. These are properties of the layout, not of any output format — a PDF exporter turns them into link annotations, named destinations and bookmarks; an outline panel lists the headings; an EPUB export writes them as its navigation document — so they live here beside the box tree instead of being re-derived, slightly differently, by each consumer. linkmap.go's LinksFromBox answers a narrower question (one bounding box per anchor, in image pixels, to hit-test a click) and is left exactly as it is; the two share anchorFor and itemBox.

This file adds STAGED progressive rendering on top of the batch pipeline. A browser paints before all resources/scripts are done and re-flows incrementally; the engine emulates that with a small number of fully-styled frames (external CSS already applied, so no unstyled flash):

  • "initial": the TEXT-FIRST frame — external sheets fetched and cascaded and the page laid out, but BEFORE any image is fetched (image boxes reserve from their attr/CSS sizes). The big perceived-latency win: the styled text paints without waiting on the slow, network-bound image fetch.
  • "images": after the images/background images are loaded and the page is re-laid-out with their real intrinsic sizes (deduped against "initial" by geometry — a page with no images that moved nothing yields just initial+final).
  • "settle": after each settle pass that visibly changed the geometry (deduped; bounded by the settle pass cap) — the interruptible-reflow analogue.
  • "final" (Final=true): the fully-settled render, byte-identical to RenderWithLinks for the same input.

Every frame carries an INDEPENDENT image snapshot (a fresh canvas per frame), so a consumer may retain it after onFrame returns.

This file is the actual "drive a login form" API: Focus/Blur/Type/Click synthesize the DOM events a real keystroke or mouse click produces, dispatched through the LiveDocument's persistent js.Session (so a stateful script — a controlled React input, say — sees exactly the sequence a real browser would deliver), each followed by a resettle so the returned frame reflects whatever the interaction changed. Built on js.Session.Dispatch (the event-firing seam) and ElementAt (elements.go, the click-coordinate-to-node resolver) landed in earlier phases.

Index

Constants

This section is empty.

Variables

View Source
var ErrClosed = errors.New("engine: LiveDocument is closed")

ErrClosed is returned by a LiveDocument method called after Close.

Functions

func DocumentIDs added in v0.4.0

func DocumentIDs(root *layout.Box) map[string]Anchor

DocumentIDs maps every element id (and legacy <a name>) that has a position to that position: a block element's box top-left, or for an inline element — which has no box of its own — the first atom it produced. Walked in document order, so the first occurrence of a duplicated id wins, as in a browser. An id whose element laid out nothing (display:none, an inline with no text) is absent, so LinkRuns drops a link to it rather than pointing at nowhere.

func ElementAt added in v0.4.0

func ElementAt(index map[*dom.Node]layout.Rect, pt image.Point) (*dom.Node, bool)

ElementAt returns the most specific element at pt: the smallest-area rectangle in index that contains it, which — since a descendant's box is always contained within its ancestors' — is the deepest element under the point for ordinary (non-overlapping-sibling) layouts. It does not model paint/z-order for OVERLAPPING siblings (e.g. one absolutely positioned over another): LinkAt's equally simple first-match has been sufficient for that case in practice, and a login-form-shaped page (the driving use case) does not stack interactive controls. ok is false when pt hits no element.

func ElementsFromBox added in v0.4.0

func ElementsFromBox(root *layout.Box) map[*dom.Node]layout.Rect

ElementsFromBox walks a laid-out box tree and returns every element's used border-box rectangle in full-page image pixels — the same coordinate space LinksFromBox's Link.Rect uses. It is a thin wrapper over layout.BuildIndex(root), exported at this package's level (like LinksFromBox wraps the box-walk for anchors) so a host does not need to import the layout package just to hit-test.

func EncodePNG

func EncodePNG(img image.Image) ([]byte, error)

EncodePNG encodes an image as PNG bytes.

func LinkAt

func LinkAt(links []Link, pt image.Point) (string, bool)

LinkAt returns the href of the first link whose rectangle contains pt (a full-page pixel point) and whether one was found. Links are tested in document order, so an earlier anchor wins when rectangles overlap.

func RasterizeSVG added in v0.4.0

func RasterizeSVG(data []byte, w, h int, currentColor string) (img image.Image, ok bool)

RasterizeSVG renders SVG source into a bitmap of exactly w×h pixels, whatever the document's own width/height say. The size is the caller's to pick, because an SVG has no pixels of its own — which is the whole point of this entry: the bitmap LoadImageSet hands back is sized for the RASTER CANVAS, where one CSS px is one device px, and on paper that is a hard 96 dpi ceiling. A PDF exporter is the case this exists for. Together with the serialisation LoadedImage.Data carries for an inline <svg>, it can render the same drawing again at the density the paper deserves.

currentColor is what `currentColor` resolves to, as a CSS colour string; empty leaves the document's own. ok is false for a non-positive size or source oksvg cannot parse — the same failure the internal path reports, and the same recover() around it, since oksvg panics on some malformed input. w and h are clamped to maxSVGDim.

func RegisterFontFaces added in v0.4.0

func RegisterFontFaces(fonts *paint.Fonts, faces []LoadedFontFace) int

RegisterFontFaces registers loaded faces with a Fonts registry, so both the measurement and whatever draws afterwards use them. A face that fails to parse is skipped and named in the returned count's shortfall rather than failing the render.

func Screenshot

func Screenshot(ctx context.Context, rawurl string, viewport image.Rectangle) ([]byte, error)

Screenshot renders url with a default engine and returns PNG bytes.

Types

type Anchor added in v0.4.0

type Anchor struct{ X, Y float64 }

Anchor is where an id'd element sits, in document CSS px: the top-left a viewer scrolls to for an in-document link — a PDF named destination, an EPUB fragment target, a scroll-to-hash in a browser shell.

type Document

type Document struct {
	URL   string    // final URL after redirects
	Title string    // <title> text
	Root  *dom.Node // owned DOM tree
	HTML  string    // decoded (UTF-8) source
}

Document is a fetched and parsed page.

type Engine

type Engine struct {
	Client    *http.Client
	UserAgent string
	// ImageCache, when non-nil, is consulted before every remote image download
	// and populated after each successful one, so a host can serve repeated
	// renders from a persistent cache. Nil (the default) fetches every time.
	ImageCache ImageCache
	// MaxImages bounds how many RASTER (non-SVG) <img>/background images are
	// fetched and decoded per render — the expensive network + large-decode work.
	MaxImages int
	// MaxVectorImages bounds how many VECTOR images (inline <svg> and
	// <img src="*.svg"> / data:image/svg+xml) are rasterised per render. Vector
	// chrome (nav/footer/social/UI icons) is cheap but plentiful — real design
	// systems place many dozens ahead of the content — so it gets its OWN, more
	// generous budget. Without this split a wall of decorative icons early in the
	// DOM exhausts a single shared budget and starves the actual content photos
	// (raster) further down (the DeepMind hero black-void bug).
	MaxVectorImages int

	// DisableJS turns off the JavaScript pass. Offline fixture tests that must
	// stay byte-deterministic set this; the default (false) runs page scripts.
	DisableJS bool
	// JSTimeout bounds the total script + timer budget per render. Zero selects
	// js.DefaultTimeout.
	JSTimeout time.Duration
	// MaxJSHeapBytes bounds process heap growth during the JavaScript settle
	// stage (script execution AND the esbuild module-bundle fetch/parse). Some
	// real pages ship huge module graphs (GitHub, large SPAs) whose bundling
	// balloons memory into the gigabytes before any time budget trips; a watchdog
	// samples the heap and, once growth from the stage's start exceeds this bound,
	// cancels the stage so it falls back to the already-computed pre-script layout
	// instead of driving the process toward OOM. Zero selects a sane default; a
	// negative value disables the guard.
	MaxJSHeapBytes int64
	// JSLog, if non-nil, receives console.* and diagnostic lines from the script
	// pass (used for debugging; nil discards them).
	JSLog func(string)
	// Backdrop is the base colour painted under a page before its own background.
	// The zero value (transparent) selects the default white paper. A themed host
	// (e.g. a reader in dark mode) sets it to its surface colour so a page that
	// declares no background of its own — most plainly a bare image — is framed to
	// match instead of flashing white. A page that DOES declare a background still
	// paints its own over this, so normal sites are unchanged.
	Backdrop css.Color
	// MetaFallback, when true, renders a clean readable card synthesised from a
	// page's OpenGraph/meta tags (og:title/og:description/og:image, <title>) as a
	// LAST RESORT — only when the real render is empty (a JavaScript SPA the engine
	// cannot hydrate: Mastodon, X, app-shell sites). Default false: it fabricates
	// content that is not in the page's rendered DOM, so it is opt-in; a
	// link-preview / reader consumer enables it, while callers wanting a faithful
	// render (or to detect the blank SPA themselves) are unaffected. It never
	// triggers on a page that renders any real content, so normal pages are
	// byte-identical whether it is on or off.
	MetaFallback bool
	// FileRoot, when non-empty, lets a render read file: URLs — images,
	// stylesheets (and their @imports) and @font-face files — from under this
	// directory, and nowhere else: a path that leaves it through "..", or
	// through a symbolic link resolving outside it, is refused, as is a file:
	// URL naming a host other than localhost. Empty (the default) refuses every
	// file: URL, as a browser does for a page that was not given
	// --allow-file-access-from-files (marp-cli: --allow-local-files). A host
	// rendering a local document (a slide deck referencing ./images/…) sets it
	// to that document's directory and passes a file: base URL.
	FileRoot string
	// DeviceScale is the number of device pixels per CSS pixel — a browser's
	// devicePixelRatio (CSSOM View), Chrome's deviceScaleFactor, marp-cli's
	// --image-scale. The viewport given to a render stays in CSS px; the
	// returned image is viewport × DeviceScale device pixels, painted at that
	// density (text rasterised at size × DeviceScale, vector images and
	// gradients at full resolution, raster images resampled from their CSS-px
	// intrinsic size). Scripts see window.devicePixelRatio and CSS-px
	// geometry; @media width features stay in CSS px and `resolution`
	// reflects the scale. 0 or 1 (the default) renders at one device pixel per
	// CSS px, exactly as before.
	DeviceScale float64
	// FontFaces are typefaces the host provides to every render, as a
	// browser's installed fonts: text whose font-family names one of these
	// families is set in it without the page declaring an @font-face. A
	// family the page does declare a face for keeps the page's. Only
	// Family, Weight, Italic and Data (SFNT, WOFF or WOFF2 bytes) are read;
	// a face that does not parse is skipped. The files are parsed once and
	// the parsed fonts reused across renders, which makes this far cheaper
	// than inlining the same fonts into every page as data: URIs — a host
	// rendering many pages in one typeface (a slide deck, page by page)
	// otherwise pays for decoding and parsing them on each.
	FontFaces []LoadedFontFace
}

func New

func New() *Engine

New returns an Engine with a browser-like HTTP client (Chrome TLS fingerprint, cookie jar, redirect following).

func (*Engine) Fetch

func (e *Engine) Fetch(ctx context.Context, rawurl string) (*Document, error)

Fetch retrieves and parses url into a Document.

func (*Engine) LoadFontFaces added in v0.4.0

func (e *Engine) LoadFontFaces(ctx context.Context, doc *Document, sheets []string, m css.Media) []LoadedFontFace

LoadFontFaces fetches the files the document's own @font-face rules name and returns the faces that could be used, in the order the rules declared them.

sheets are the stylesheet sources LoadStylesheets returned, plus the document's own inline CSS; a document's fonts live in whichever of them carries the @font-face rule, most often a <link> to a font service.

Without this, a page could only ever be typeset in the three families the paint package bundles, whatever it asked for — so a document written for a named typeface was rendered in a substitute AT DIFFERENT METRICS, and its lines broke somewhere its author never saw. Registering the faces it names (RegisterFontFaces) is what makes the measurement its own.

A rule whose every src is a format this build cannot decode is skipped, and the page keeps the bundled family for it: opentype.Parse reads SFNT (TrueType and CFF/OpenType), so a WOFF or WOFF2 src needs a decoder in go-opentype to become reachable. Fetch failures, oversized bodies and unparseable files are skipped the same way — a missing font degrades the typeface, it does not fail the render.

Slots load concurrently: each @font-face is one network round trip, and a page naming ten faces otherwise waits for all ten in turn (tailwindcss.com, round 151: ~4.4s of serial font downloads). Within a slot, every candidate rule ALSO fetches concurrently (round 168) rather than one at a time: this engine has no unicode-range model (css.FontFace carries no such field), so several genuinely distinct @font-face rules that a real browser would pick among BY CODEPOINT collapse into one slot here, as redundant-looking alternatives of each other — confirmed live on cs.cmu.edu's Font Awesome v4-compatibility shim, which declares five separate 'FontAwesome'-family, weight-400 rules (solid/brands/regular/compat, split only by unicode-range) that all land in ONE slot. Trying them one at a time cost ~52s when the origin's /webfonts/ path was erroring (each failed attempt still waits out the full ~6.4s server response): a page need not be malicious or even unusual for a single slot to carry several slow-to-fail candidates. The result keeps declaration order regardless of which candidate's fetch finishes first — same winner as the old sequential loop, just not waiting for each loser to fail before starting the next.

func (*Engine) LoadImageSet added in v0.4.0

func (e *Engine) LoadImageSet(ctx context.Context, doc *Document, sm css.StyleMap, viewportW int) map[*dom.Node]*LoadedImage

LoadImageSet is LoadImages keeping each image's source alongside its bitmap — same budgets, same fetch, same sizing, same accepted set.

func (*Engine) LoadImages added in v0.4.0

func (e *Engine) LoadImages(ctx context.Context, doc *Document, sm css.StyleMap, viewportW int) (map[*dom.Node][2]float64, map[*dom.Node]image.Image)

LoadImages fetches and decodes every replaced element in doc — raster <img>, <img src="*.svg"> and inline <svg> — best-effort and concurrently, returning the intrinsic sizes layout.LayoutDocument needs to size their boxes and the decoded bitmaps a painter needs to draw them, both keyed by element. A relative src resolves against doc.URL. Images wider than viewportW are scaled down proportionally, so a returned bitmap is already the size its box will be laid out at. A fetch or decode failure just leaves that element out of both maps; the raster/vector budgets are MaxImages and MaxVectorImages.

It is the exported entry point for a consumer that runs the engine's own cascade + layout but paints to something other than the built-in raster canvas (a PDF, say), so that consumer sizes and draws images exactly as the engine itself does instead of re-implementing fetch/decode/budgeting. RenderDocument uses the same code path internally.

func (*Engine) LoadStylesheets added in v0.4.0

func (e *Engine) LoadStylesheets(ctx context.Context, doc *Document, m css.Media) []string

LoadStylesheets fetches every <link rel="stylesheet"> of doc that applies under m (a <link media="print"> only under css.Print, media="screen" only under screen), plus their leading @import chains, and returns the sheet texts in cascade order — imports before importer, links in document order — ready for css.CascadeMedia(doc.Root, m, sheets). A relative href resolves against doc.URL; only http(s) targets are fetched; a sheet that fails to fetch is skipped and the rest still apply. The fan-out is bounded (64 sheets, 4 MB each, @import two levels deep, 10 s in all).

It is the exported entry point for a consumer that runs the engine's own cascade + layout but paints to something other than the built-in raster canvas (a PDF, say), so that consumer styles a page exactly as the engine itself does — external stylesheets being the dominant fidelity factor on a real site — instead of re-implementing fetch, @import and media selection. RenderDocument uses the same code path internally, for screen.

func (*Engine) Open added in v0.4.0

func (e *Engine) Open(ctx context.Context, rawurl string, viewport image.Rectangle) (*LiveDocument, error)

Open fetches url and renders it exactly like Render, but keeps the page's JS session alive afterward instead of closing it. The caller must call Close when done with the page (a real navigation opens a fresh LiveDocument via another Open — this type is for staying ON one page across interactions, not for the page itself changing).

func (*Engine) OpenDocument added in v0.4.0

func (e *Engine) OpenDocument(ctx context.Context, doc *Document, viewport image.Rectangle) (*LiveDocument, error)

OpenDocument is Open for an already-fetched Document — the interactive counterpart to RenderDocument, and what fixture tests use.

func (*Engine) Render

func (e *Engine) Render(ctx context.Context, rawurl string, viewport image.Rectangle) (*image.RGBA, *RenderInfo, error)

Render fetches url and renders it into an image at the given viewport width (the height is grown to fit the full page, at least the viewport height).

func (*Engine) RenderDocument

func (e *Engine) RenderDocument(ctx context.Context, doc *Document, viewport image.Rectangle) (*image.RGBA, *RenderInfo, error)

RenderDocument renders an already-fetched Document. It is the offline entry point used by fixtures and tests (image sub-resources are still fetched from the network if their src is absolute/resolvable).

func (*Engine) RenderDocumentProgressive added in v0.3.0

func (e *Engine) RenderDocumentProgressive(ctx context.Context, doc *Document, viewport image.Rectangle, onFrame func(ProgressiveFrame)) (*RenderInfo, error)

RenderDocumentProgressive renders an already-fetched Document progressively. See RenderProgressive.

func (e *Engine) RenderDocumentWithLinks(ctx context.Context, doc *Document, viewport image.Rectangle) (*image.RGBA, *RenderInfo, []Link, error)

RenderDocumentWithLinks renders an already-fetched Document and returns its image, render info and anchor hit-map from a single layout pass. It runs the same cascade → layout → paint pipeline as RenderDocument.

func (*Engine) RenderHTML

func (e *Engine) RenderHTML(ctx context.Context, htmlSrc, baseURL string, viewport image.Rectangle) (*image.RGBA, *RenderInfo, error)

RenderHTML renders an HTML string (with baseURL used to resolve relative image sources) into an image at the given viewport. It is the offline entry point for local fixtures and demos, running the same cascade/layout/paint pipeline as Render but without fetching the page itself.

func (*Engine) RenderProgressive added in v0.3.0

func (e *Engine) RenderProgressive(ctx context.Context, rawurl string, viewport image.Rectangle, onFrame func(ProgressiveFrame)) (*RenderInfo, error)

RenderProgressive fetches rawurl and renders it progressively, invoking onFrame for each staged frame. It runs the same cascade → JS settle → layout → paint pipeline as RenderWithLinks; the Final frame's image, links and RenderInfo are identical to what RenderWithLinks returns for the same input. onFrame is always called at least once and the last call has Final=true.

func (e *Engine) RenderWithLinks(ctx context.Context, rawurl string, viewport image.Rectangle) (*image.RGBA, *RenderInfo, []Link, error)

RenderWithLinks fetches url, renders it to a full-page image and, in the same layout pass, returns the anchor hit-map. It mirrors (*Engine).Render but also threads the laid-out box tree into LinksFromBox so the image and the link rectangles are guaranteed to describe the exact same layout. It is kept here (rather than folded into engine.go's RenderDocument) so the hit-test feature stays isolated from the core pipeline and the js/ runtime work.

func (*Engine) Screenshot

func (e *Engine) Screenshot(ctx context.Context, rawurl string, viewport image.Rectangle) ([]byte, error)

Screenshot renders url and encodes the result as PNG.

type Heading added in v0.4.0

type Heading struct {
	Level int       // 1 for <h1> … 6 for <h6>
	Title string    // the heading's atoms' texts, joined by single spaces
	Y     float64   // top of the heading's box, document CSS px
	Node  *dom.Node // the heading element
}

Heading is one <h1>…<h6> with its text and top, for the document outline: a PDF's bookmark tree, an outline panel, an EPUB's table of contents.

func Headings added in v0.4.0

func Headings(root *layout.Box) []Heading

Headings returns the headings in document order. A heading's title is the text of every atom inside its box, words joined by single spaces — so <h2>Part <b>Alpha</b></h2> is "Part Alpha" — and one with no text (an icon-only heading) is skipped, since a bookmark with no label helps nobody. Only Level is reported; whether an <h3> after an <h1> nests under it or stands beside it is the consumer's outline policy, not the layout's.

type ImageCache added in v0.3.11

type ImageCache interface {
	Get(url string) ([]byte, bool)
	Put(url string, data []byte)
}

Engine holds the HTTP client and render configuration. ImageCache is an optional byte cache the engine consults before downloading a remote image, and populates after a successful download. A host wires it to a persistent (e.g. on-disk) cache so repeated renders of the same page — or of any page sharing an image — reuse the bytes instead of re-fetching over the network. Keys are absolute image URLs. Implementations must be safe for concurrent use (the engine loads a page's images concurrently).

type Link struct {
	Rect image.Rectangle
	Href string
}

Link is one hyperlink's painted rectangle (in full-page image pixels, the same coordinate space as the image returned by Render) and its href resolved against the page URL to an absolute URL.

func LinksFromBox

func LinksFromBox(root *layout.Box, baseURL string) []Link

LinksFromBox walks a laid-out box tree and returns, in document order, one Link per <a href> anchor that painted at least one inline atom (a word or an image). Each anchor's Rect is the union of the rectangles of all inline atoms it produced, so a multi-line link yields a single bounding box covering every line. baseURL is the page's own URL: each anchor's raw href is resolved against it, and links that resolve to a non-navigable scheme (javascript:, mailto:, an empty or pure-fragment href) are dropped.

The rectangles are in the box tree's own coordinate space, which is the full-page image space (layout origin is 0,0 at the page top-left), so a caller can hit-test a full-page click directly against them.

type LinkRun added in v0.4.0

type LinkRun struct {
	X, Y, W, H float64
	Node       *dom.Node // the <a> element the run belongs to
	URI        string    // absolute http(s) target; empty for an in-document link
	Fragment   string    // in-document target: the id of the element it points to
}

LinkRun is one clickable rectangle in document CSS px: the atoms of one <a href> anchor on one line. A link wrapped over three lines is three runs, so the clickable area follows the text rather than covering the block between the lines the way a single bounding box would — a PDF link annotation or a hover highlight drawn from a run lands on the words and nowhere else. Exactly one of URI and Fragment is set: URI for an external http(s) target, Fragment for an in-document jump to an element's id.

func LinkRuns added in v0.4.0

func LinkRuns(root *layout.Box, base *url.URL, ids map[string]struct{}) []LinkRun

LinkRuns returns every anchor's clickable runs in document order, one per line the anchor's atoms appear on, in document CSS px (the box tree's own coordinate space). base is the document's URL: relative hrefs resolve against it, and it decides whether an absolute URL with a fragment is the document itself. ids is the set of element ids a fragment may jump to — the keys of DocumentIDs — so a fragment nobody anchors yields no run, as does a javascript:, mailto:, tel: or data: href.

An anchor that lays out no atom at all — a link whose only content is an empty styled block, Hacker News's vote arrows being the canonical case (30 of its 228 links) — gets the rectangle of the outermost box laid out under it instead, once, as a browser's print does. A consumer that paginates is left to clip a run straddling a page break to the page it starts on.

LinksFromBox is the other reading of the same tree: one rectangle per anchor in image pixels, for finding the link under a click. A run is for drawing the link — a PDF annotation, an EPUB export's hit area — not for finding it.

type LiveDocument added in v0.4.0

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

LiveDocument is a page whose JS session stays open across separate calls. Not safe for concurrent use — like js.Session, one goroutine drives one LiveDocument at a time. Callers interact through Interact (this file) and higher-level methods added by later phases (hit-testing, synthetic focus/type/click); Close releases the session and heap watchdog and must be called exactly once, whether by an explicit navigation-away or by the host window closing.

func (*LiveDocument) Backspace added in v0.4.0

func (d *LiveDocument) Backspace(ctx context.Context, n *dom.Node) (*image.RGBA, *RenderInfo, error)

Backspace removes the last character of n's value (a no-op if already empty), firing keydown("Backspace")→[if not prevented]delete+input→keyup — the same event shape Type's per-character typing uses, since a real browser fires exactly this sequence for a Backspace keystroke too. Ensures n is focused first, same as Type.

func (*LiveDocument) Blur added in v0.4.0

func (d *LiveDocument) Blur(ctx context.Context) (*image.RGBA, *RenderInfo, error)

Blur removes focus from whatever is currently focused (a no-op if nothing is). Firing it explicitly matters for a form that validates or submits-on-blur, and for the "did this field's value change" signal change carries.

func (*LiveDocument) Click added in v0.4.0

func (d *LiveDocument) Click(ctx context.Context, n *dom.Node) (defaultPrevented bool, img *image.RGBA, info *RenderInfo, err error)

Click synthesizes a real user click on n: mousedown, mouseup, then click — the sequence a page's onclick/addEventListener('click', …) expects, bubbling per real semantics (so a handler on a container, not just the clicked element itself, sees it too). Also moves focus to n first for a focusable control (an input, say), matching a real browser: clicking a field focuses it before the click event itself fires. It reports whether a listener called preventDefault() on the click, so a caller driving a submit button (a later phase) can tell whether the page's own script handled it (a fetch()-based login, say) rather than letting native form submission happen too.

func (*LiveDocument) Close added in v0.4.0

func (d *LiveDocument) Close()

Close releases the JS session's watchdog goroutine and the heap-guard goroutine. Idempotent.

func (*LiveDocument) Document added in v0.4.0

func (d *LiveDocument) Document() *Document

Document returns the live DOM's root document, for a caller that needs to walk or hit-test the current tree (e.g. a later phase's element index). The returned *Document is the SAME one Open/OpenDocument was given — its Root mutates in place as the page's own scripts and any synthetic interaction change it.

func (*LiveDocument) ElementAt added in v0.4.0

func (d *LiveDocument) ElementAt(pt image.Point) (*dom.Node, bool)

ElementAt is ElementAt(d.Elements(), pt) — the convenience a caller translating one click coordinate reaches for.

func (*LiveDocument) Elements added in v0.4.0

func (d *LiveDocument) Elements() map[*dom.Node]layout.Rect

Elements returns the CURRENT general element hit-map (every element's used border-box rect, in the same full-page image pixel space Frame's image uses) — call it fresh after Open or any Interact, since a mutation can move or resize anything. ElementAt resolves a specific point against it.

func (*LiveDocument) Eval added in v0.4.0

func (d *LiveDocument) Eval(code string) (interface{}, error)

Eval runs code in the document's live JS session and returns its value as a plain Go value (see js.Session.Eval) — for reading page-side JS state back into Go (a conformance harness reading testharness.js's own PASS/FAIL results, say), not for driving input (see Interact and synthetic.go's Focus/Type/Click for that). It does not re-settle layout/paint afterward; a caller whose eval mutated the DOM and wants a fresh Frame should follow it with Interact.

func (*LiveDocument) Focus added in v0.4.0

func (d *LiveDocument) Focus(ctx context.Context, n *dom.Node) (*image.RGBA, *RenderInfo, error)

Focus moves input focus to n: blurs whatever was focused before (firing blur/focusout and, if its value changed since ITS OWN focus, change — same rule Blur uses), then fires focus/focusin on n and records n's current value as the baseline for its own eventual blur. A page relies on focus to know which control a subsequent Type lands in — this models that explicitly rather than an implicit "last node touched" guess.

func (*LiveDocument) Frame added in v0.4.0

func (d *LiveDocument) Frame() (*image.RGBA, *RenderInfo, error)

Frame renders the document's CURRENT state to an image, exactly like Render's return value — call it after Open, and again after each Interact, to get what the user should now see.

func (*LiveDocument) Interact added in v0.4.0

func (d *LiveDocument) Interact(ctx context.Context, fn func()) (*image.RGBA, *RenderInfo, error)

Interact runs fn — which mutates the live DOM directly, or (in a later phase) dispatches a synthetic event through the live Session — and then resettles: re-cascades and re-lays-out from whatever fn changed, WITHOUT re-running the page's initial scripts. That last part is the entire point of LiveDocument: the JS runtime's own state (closures, timers, anything a script holds that the DOM alone doesn't capture) survives across repeated calls, exactly as it would across two real keystrokes in a browser. It returns the resulting frame.

func (*LiveDocument) KeyDown added in v0.4.0

func (d *LiveDocument) KeyDown(ctx context.Context, n *dom.Node, key string) (defaultPrevented bool, img *image.RGBA, info *RenderInfo, err error)

KeyDown fires a bare keydown+keyup pair for a NAMED key (e.g. "Enter", "Tab", "Escape") that, unlike a printable character or Backspace, this package does not itself interpret — it never mutates n's value or moves focus on the caller's behalf. It exists for the common real case of a login form whose OWN script listens for Enter on keydown to submit itself (dispatchEvent already lets that handler run and call preventDefault — see the reported bool); a host wanting real Tab-order traversal or an implicit Enter-submits-the-enclosing-form fallback implements that itself, on top of this, since neither is modeled here.

func (*LiveDocument) Submit added in v0.4.0

func (d *LiveDocument) Submit(ctx context.Context, form *dom.Node) (next *LiveDocument, navigated bool, err error)

Submit dispatches "submit" on form through the live session first (a real browser always does, even for a plain HTML form) and reports whether that navigated. If a listener called preventDefault() — the common SPA case, already reachable via Focus/Type/Click plus the engine's existing fetch()/XHR support — nothing further happens: navigated is false, next is nil, and d is still the current page. Otherwise Submit performs the real request per the form's action/method (enctype is always application/x-www-form-urlencoded/GET-querystring; multipart file upload is out of scope — not a shape a login form takes), parses the response as a new page, and returns it as a freshly Open'd *LiveDocument — d itself is Closed, matching how a real navigation ends the previous page's JS. The caller must switch to using next once navigated is true.

func (*LiveDocument) Type added in v0.4.0

func (d *LiveDocument) Type(ctx context.Context, n *dom.Node, text string) (img *image.RGBA, info *RenderInfo, err error)

Type synthesizes typing text into n, one rune at a time: keydown, then — unless a listener called preventDefault() on it (a real page's character-filtering validator, say) — n's value gains that character and "input" fires, then keyup always fires. Firing one full keydown/input/ keyup triple per character (not one bulk value assignment) is what lets a controlled React/Vue onChange see every keystroke, exactly as a real user typing would trigger it. Ensures n is focused first (moving focus there, with the normal blur/change fallout on whatever was focused before, if anything) — matching real usage: you cannot type into an unfocused field.

type LoadedFontFace added in v0.4.0

type LoadedFontFace struct {
	Family string // lowercased, as css.FontFace gives it
	Weight int
	Italic bool
	Data   []byte // SFNT bytes, ready for opentype.Parse
	URL    string // where they came from, resolved
	// contains filtered or unexported fields
}

LoadedFontFace is one @font-face rule whose file was fetched and accepted: the slot it fills and the SFNT bytes that fill it.

Data is what a consumer needs beyond measurement — a PDF exporter has to EMBED the same bytes the layout measured against, or the file it writes names a font it does not carry.

func ParseFontFace added in v0.8.0

func ParseFontFace(family string, weight int, italic bool, data []byte) (LoadedFontFace, error)

ParseFontFace parses a font file (SFNT, WOFF or WOFF2) into a face for Engine.FontFaces. A face built this way is parsed once, here; one built as a bare struct literal is parsed on first use and then found in the cache of parsed fonts by the hash of its bytes, on every render.

type LoadedImage added in v0.4.0

type LoadedImage struct {
	Size   [2]float64  // layout size, CSS px — LoadImages' first map
	Bitmap image.Image // decoded, CSS-sized, viewport-clamped — LoadImages' second map
	Data   []byte      // the bytes an <img> was fetched from, or an inline <svg>'s own serialisation
	Format string      // sniffed from Data — "jpeg", "png", "gif", "webp", "bmp", "svg" — or "" when unknown
	Lossy  bool        // Format is a lossy encoding: jpeg, or a webp whose bitstream is VP8 rather than VP8L
	// SourceW, SourceH is the decoded source's pixel size before any CSS or
	// viewport resize; Bitmap still has exactly the source's pixels iff its
	// bounds are this size, which is when Data can stand in for it.
	SourceW, SourceH int
}

LoadedImage is one replaced element as LoadImageSet returns it: the two values LoadImages splits into its maps, plus the source the bitmap was decoded from — for a consumer whose output can carry those bytes as they are (a PDF embeds a JPEG as a DCTDecode stream without re-encoding it), or that picks an encoding by the source's nature (a photograph from a lossy source is not line art from a lossless one).

type ProgressiveFrame added in v0.3.0

type ProgressiveFrame struct {
	Img   *image.RGBA
	Links []Link
	Info  *RenderInfo
	Stage string
	Final bool
}

ProgressiveFrame is one staged snapshot delivered to a RenderProgressive callback. Img is an independent allocation; Links matches RenderWithLinks's shape; Stage is "initial" | "images" | "settle" | "final"; Final is true exactly once, on the last frame.

type RenderInfo

type RenderInfo struct {
	Title         string
	URL           string
	ContentHeight int
}

RenderInfo carries metadata about a completed render.

func Render

func Render(ctx context.Context, rawurl string, viewport image.Rectangle) (*image.RGBA, *RenderInfo, error)

Render renders url with a default engine.

Directories

Path Synopsis
bench module
cmd
render command
Command render fetches a URL and writes a PNG screenshot of the statically rendered page.
Command render fetches a URL and writes a PNG screenshot of the statically rendered page.
Package css implements a deliberately small but real subset of CSS: a value model, a tokenizer/parser for stylesheets and declaration blocks, tag/class/ id selectors with specificity, and a cascade with inheritance over a dom tree.
Package css implements a deliberately small but real subset of CSS: a value model, a tokenizer/parser for stylesheets and declaration blocks, tag/class/ id selectors with specificity, and a cascade with inheritance over a dom tree.
Package dom is a small, owned DOM node tree built from golang.org/x/net/html.
Package dom is a small, owned DOM node tree built from golang.org/x/net/html.
Package js runs a page's JavaScript against a minimal but real DOM binding on a pure-Go goja ECMAScript runtime.
Package js runs a page's JavaScript against a minimal but real DOM binding on a pure-Go goja ECMAScript runtime.
Package layout is a block-and-inline flow engine.
Package layout is a block-and-inline flow engine.
Package paginate cuts a laid-out document into pages: it returns the document y at which each page after the first starts, honouring CSS Fragmentation Level 3 as far as a page medium needs it — forced breaks (break-before/after: page, left, right and the page-break-* aliases), break-inside: avoid, break-before/after: avoid (keep with the previous or the next content), orphans and widows — over the atoms a page can cut between.
Package paginate cuts a laid-out document into pages: it returns the document y at which each page after the first starts, honouring CSS Fragmentation Level 3 as far as a page medium needs it — forced breaks (break-before/after: page, left, right and the page-break-* aliases), break-inside: avoid, break-before/after: avoid (keep with the previous or the next content), orphans and widows — over the atoms a page can cut between.
Package paint rasterises a laid-out box tree onto an *image.RGBA using go-opentype for anti-aliased text, go-widgets/painter for backgrounds and go-gfx-decoded bitmaps for <img>.
Package paint rasterises a laid-out box tree onto an *image.RGBA using go-opentype for anti-aliased text, go-widgets/painter for backgrounds and go-gfx-decoded bitmaps for <img>.

Jump to

Keyboard shortcuts

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