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 ¶
- Variables
- func DocumentIDs(root *layout.Box) map[string]Anchor
- func ElementAt(index map[*dom.Node]layout.Rect, pt image.Point) (*dom.Node, bool)
- func ElementsFromBox(root *layout.Box) map[*dom.Node]layout.Rect
- func EncodePNG(img image.Image) ([]byte, error)
- func LinkAt(links []Link, pt image.Point) (string, bool)
- func RasterizeSVG(data []byte, w, h int, currentColor string) (img image.Image, ok bool)
- func RegisterFontFaces(fonts *paint.Fonts, faces []LoadedFontFace) int
- func Screenshot(ctx context.Context, rawurl string, viewport image.Rectangle) ([]byte, error)
- type Anchor
- type Document
- type Engine
- func (e *Engine) Fetch(ctx context.Context, rawurl string) (*Document, error)
- func (e *Engine) LoadFontFaces(ctx context.Context, doc *Document, sheets []string, m css.Media) []LoadedFontFace
- func (e *Engine) LoadImageSet(ctx context.Context, doc *Document, sm css.StyleMap, viewportW int) map[*dom.Node]*LoadedImage
- func (e *Engine) LoadImages(ctx context.Context, doc *Document, sm css.StyleMap, viewportW int) (map[*dom.Node][2]float64, map[*dom.Node]image.Image)
- func (e *Engine) LoadStylesheets(ctx context.Context, doc *Document, m css.Media) []string
- func (e *Engine) Open(ctx context.Context, rawurl string, viewport image.Rectangle) (*LiveDocument, error)
- func (e *Engine) OpenDocument(ctx context.Context, doc *Document, viewport image.Rectangle) (*LiveDocument, error)
- func (e *Engine) Render(ctx context.Context, rawurl string, viewport image.Rectangle) (*image.RGBA, *RenderInfo, error)
- func (e *Engine) RenderDocument(ctx context.Context, doc *Document, viewport image.Rectangle) (*image.RGBA, *RenderInfo, error)
- func (e *Engine) RenderDocumentProgressive(ctx context.Context, doc *Document, viewport image.Rectangle, ...) (*RenderInfo, error)
- func (e *Engine) RenderDocumentWithLinks(ctx context.Context, doc *Document, viewport image.Rectangle) (*image.RGBA, *RenderInfo, []Link, error)
- func (e *Engine) RenderHTML(ctx context.Context, htmlSrc, baseURL string, viewport image.Rectangle) (*image.RGBA, *RenderInfo, error)
- func (e *Engine) RenderProgressive(ctx context.Context, rawurl string, viewport image.Rectangle, ...) (*RenderInfo, error)
- func (e *Engine) RenderWithLinks(ctx context.Context, rawurl string, viewport image.Rectangle) (*image.RGBA, *RenderInfo, []Link, error)
- func (e *Engine) Screenshot(ctx context.Context, rawurl string, viewport image.Rectangle) ([]byte, error)
- type Heading
- type ImageCache
- type Link
- type LinkRun
- type LiveDocument
- func (d *LiveDocument) Backspace(ctx context.Context, n *dom.Node) (*image.RGBA, *RenderInfo, error)
- func (d *LiveDocument) Blur(ctx context.Context) (*image.RGBA, *RenderInfo, error)
- func (d *LiveDocument) Click(ctx context.Context, n *dom.Node) (defaultPrevented bool, img *image.RGBA, info *RenderInfo, err error)
- func (d *LiveDocument) Close()
- func (d *LiveDocument) Document() *Document
- func (d *LiveDocument) ElementAt(pt image.Point) (*dom.Node, bool)
- func (d *LiveDocument) Elements() map[*dom.Node]layout.Rect
- func (d *LiveDocument) Eval(code string) (interface{}, error)
- func (d *LiveDocument) Focus(ctx context.Context, n *dom.Node) (*image.RGBA, *RenderInfo, error)
- func (d *LiveDocument) Frame() (*image.RGBA, *RenderInfo, error)
- func (d *LiveDocument) Interact(ctx context.Context, fn func()) (*image.RGBA, *RenderInfo, error)
- func (d *LiveDocument) KeyDown(ctx context.Context, n *dom.Node, key string) (defaultPrevented bool, img *image.RGBA, info *RenderInfo, err error)
- func (d *LiveDocument) Submit(ctx context.Context, form *dom.Node) (next *LiveDocument, navigated bool, err error)
- func (d *LiveDocument) Type(ctx context.Context, n *dom.Node, text string) (img *image.RGBA, info *RenderInfo, err error)
- type LoadedFontFace
- type LoadedImage
- type ProgressiveFrame
- type RenderInfo
Constants ¶
This section is empty.
Variables ¶
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
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
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
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 LinkAt ¶
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
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.
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) 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
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 (*Engine) RenderDocumentWithLinks ¶
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 (*Engine) RenderWithLinks ¶
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.
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
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
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 ¶
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 ¶
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
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
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
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 ¶
RenderInfo carries metadata about a completed render.
Source Files
¶
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>. |
