Logo by Sanya β t.me/SanyochekDev
A tiny, fast browser engine written in Rust.
Renders HTML, CSS, JavaScript, SVG and images β to a PNG, or to a live interactive window.
Quick start Β· Interactive mode Β· Features Β· Architecture Β· Contributing
Falco is a real browser engine in roughly 36,000 lines of Rust. It parses HTML, applies CSS, executes JavaScript, loads images, computes layout, and paints to a canvas β either as a PNG file or a live interactive window where you can scroll, click links, fill out forms, and navigate.
HTML βββΆ DOM βββΆ Style tree βββΆ Layout tree βββΆ Paint commands βββΆ Canvas βββΆ PNG / Window
β² β² β²
β β β
HTML5 tokenizer CSS cascade Flex / Grid / Table / Float / Absolute
+ tree builder + Selectors 4 + Inline / Block flow
β
JS (custom VM with closures, generators, Promise, BigInt, Symbol)
β
Image βββΆ HTTP / data: URL / local file
Falco is not a wrapper around WebKit, Gecko, or Chromium. Every module β HTML tokenizer, CSS parser, layout engine, JS VM, font rasterizer, PNG encoder β is written from scratch in Rust.
To set expectations clearly β this is v0.1.0, an early release by a single developer. Not everything listed in this README is production-ready. Here's what is actually wired into the render pipeline and what is structurally complete but not yet called:
- HTML parser (
html/
) β legacy parser, not spec-compliant but functional - DOM types (
dom/
) β minimal node types, no observers/shadow - CSS parser (
css/
) β selectors, properties, cascade, color parsing - Style cascade (
style/
) β UA styles + inheritance + flex/grid props - Layout (
layout/
) β block / inline / flex /CSS Grid/** table**/ float / absolute - Painting (
paint/
) β fonts (ab_glyph), gradients, shadows, alpha compositing - SVG renderer (
svg/
) β paths, basic shapes, gradients, stroke + fill - Hand-written PNG encoder (
png/
) - Image (
image/
) β HTTP, data: URLs, local files - JS VM (
tjs/
) β bytecode interpreter + JIT (x86_64, Linux-only) - JS-DOM bindings (
js_tjs/
,js_runner/
) βdocument.getElementById
,console.log
,alert
,onclick
- Networking (
net/
) β HTTP/1.1 (ureq), cookies, cache, websocket, redirect - Interactive
--window
mode β scrolling, forms, navigation, history
These exist as spec-compliant replacements for the legacy modules. They
compile and have their own unit tests, but render_with_base_url
does not call into them yet. This is the v0.2.0 milestone.
html::spec
β WHATWG Β§13.2 tokenizer (all 80 states) + tree builder (all 22 insertion modes) + serializer + XML parser + encoding detectiondom::spec
β spec DOM with MutationObserver, Shadow DOM, custom elements, accessibility treecss::spec
β Selectors Level 4 (:has()
,:is()
,:where()
, cascade layers, container queries)tjs_ext/
β Symbol, BigInt, Promise, microtasks, Map/Set, WeakMap/WeakSet, Reflect
security/
β SOP, multi-process, seccomp sandbox, CSP, TLS cert chain validation, permissions, extensions, DevTools protocol. All algorithms are in place and unit-tested, but the renderer doesn't enforce them yet.web_runtime/
βfetch()
,XMLHttpRequest
, event loop, Promise. The Promise / event loop integration is real and unit-tested, butfetch
andXHR
are stubbed (no real network behind them in the JS context). WebGL, video, MSE, EME, NDSD areheadless stubsβ they implement the API surface but don't actually render WebGL frames, decode H.264 video, or do DRM. They exist as scaffolding for future work.media/
β@media
queries are parsed but always applied (no conditional cascade yet)
real-http2
,real-webgl
,sandbox
Cargo features do not compile with--all-features
because their upstream APIs have drifted (h2::Body removed, glow API changed, seccomp pre_exec is Unix-only). They are disabled by default and the CI clippy step does not check them.- The JIT (
tjs/jit.rs
) works on Linux x86_64 but fails on macOS CI runners becausemmap(MAP_JIT)
requires code signing with thecom.apple.security.cs.allow-jit
entitlement. JIT tests are marked#[ignore]
on macOS. - DOM mutation from JS (
element.innerHTML = ...
,element.style.color = ...
) does not trigger re-render. - HTML5 spec-compliant tree repair (adoption agency, foster parenting) is in
html/spec/tree_builder.rs
but the legacy parser is what actually runs.
Bottom line: if you cargo build && ./falco https://example.com --out out.png
, you get a real PNG render. The HTML/CSS/layout/paint path works end-to-end. The spec-compliant parsers, security enforcement, and advanced web runtime (WebGL/video/DRM) are scaffolding for future milestones, not working features. Please read the code before claiming otherwise.
cargo build --release
./target/release/falco page.html --out page.png --width 1200
./target/release/falco https://example.com --out example.png --width 800
./target/release/falco page.html --window
./target/release/falco README.html --css style.css --out readme.png
<style>
tags inside the HTML are automatically extracted and
applied. External CSS via --css
is merged on top.
When you pass --window
, Falco opens a real browser window with an address bar at the top, the page content in the middle, and a status bar at the bottom. You can:
Click links(<a href>
) β Falco fetches the new URL and re-rendersAddress barβ click it or pressF6
, type a URL, pressEnter
Reload withr
β re-fetches the current pageBack/Forward withAlt+β
/Alt+β
β full history navigation
Click inputs to focus them, thentype to enter text (live update)Tab / Shift+Tabβ cycle focus between interactive elements** Backspace**β delete the last character** Enter**β submit form / click the focused button / follow focused link- Supported types:
text
,email
,password
(masked),checkbox
,submit
,button
,textarea
Mouse wheel,** arrow keys**,** Page Up/Down**,** Home/End****Focus ring**β 2px blue outline around the focused element** Blinking caret**β 500ms blink in text inputs and the address bar** Hover hint**β when hovering a link, its href appears in the status bar
The window only re-paints when state actually changes (scroll, input text, focus, hover). On idle frames, only the address bar / status bar overlays are redrawn β the page canvas is cached. This keeps CPU usage low and scrolling smooth.
If no display is available (headless server, no X11/Wayland), Falco automatically falls back to PNG output with a warning.
falco β a tiny browser engine
USAGE
falco <input> [OPTIONS]
INPUT
A URL (http://...) or a path to a local .html file.
OPTIONS
--css <path> External CSS file (merged with <style> tags in HTML).
--out <path> Output PNG path. Default: falco.png
--width <px> Viewport width. Default: 1200
--height <px> Viewport height (canvas grows if content is taller). Default: 800
--bg <hex> Background color (0xRRGGBBAA). Default: 0xFFFFFFFF
--window Open a live interactive window instead of writing PNG.
-h, --help Show this help
-V, --version Print version
INTERACTIVE MODE KEYS (when --window is used)
q / Esc quit
r reload (prints message β restart Falco to actually reload)
β / β scroll line
PgUp / PgDn scroll page
Home / End jump to top / bottom
g / G top / bottom (vim-style)
mouse wheel scroll
mouse click follow `<a href>` link (prints URL to stderr)
php
use falco::{render_to_png, RenderOptions};
fn main() -> anyhow::Result<()> {
let html = std::fs::read_to_string("page.html")?;
let css = std::fs::read_to_string("style.css")?;
let opts = RenderOptions {
width: 1200,
height: 800,
background: 0xFFFFFFFF,
};
render_to_png(&html, &css, opts, "out.png")?;
Ok(())
}
HTML5 tokenizer(WHATWG Β§13.2.5) β all 80 states, script-data escape/double-escape, attribute parsing with duplicate detection, named + numeric character references with Windows-1252 quirksHTML5 tree builder(WHATWG Β§13.2.6) β all 22 insertion modes, stack of open elements with scope algorithms, active formatting elements list, reconstruct active formatting,adoption agency algorithm,** foster parenting**for table content,<template>
with separate DocumentFragment contentsEntity referencesβ&
,A
,Β©
, 50+ named entitiesWhitespace collapsing(browser-style)with separate DocumentFragment contents<template>
elementXML/XHTML parserβ strict, with namespace bindings, CDATA, PIs** Encoding detection**β BOM, HTTPContent-Type
charset,<meta charset>
,<meta http-equiv>
, heuristic UTF-8/UTF-16 detection, decoders for UTF-8 / UTF-16LE / UTF-16BE / Windows-1252Tree builder auto-closeβ<li>
,<p>
,<td>
,<tr>
,<option>
,<dt>
/<dd>
innerHTML / outerHTML serializationβ void elements,<template>
contents fragment, raw text elements, full attribute value escaping
NodeRef = Rc<RefCell<Node>>
withparent
,firstChild
,lastChild
,previousSibling
,nextSibling
pointers per specDocumentHandle = Rc<RefCell<Document>>
with weak back-ref from each NodeMutation records queued on every append/insert/remove/setAttribute/removeAttributeMutationObserver withobserve()
,disconnect()
,take_records()
,MutationObserverInit
(childList/attributes/characterData/subtree/attributeOldValue/ characterDataOldValue/attributeFilter), subtree ancestor matchingShadow DOMβattachShadow()
with open/closed modes, host validation, named + default slots, fallback content, slot distribution (flatten tree algorithm),assignedSlot
lookupCustom elementsβcustomElements.define()
with name validation,observedAttributes
tracking, lifecycle callbacks (connected / disconnected / adopted / attributeChanged / form-associated), pending upgrades, customized built-in elements (is="..."
)Accessibility treeβ parallel tree with role/name/description/ state/actions, implicit ARIA roles for ~50 HTML tags, honorsrole=""
,aria-hidden
,hidden
,display:none
, accessible name computation (aria-label > aria-labelledby > element-specific > title)
Selectors Level 4β type, class, id, universal (*
), descendant, child (>
), adjacent sibling (+
), general sibling (~
), attribute ([attr]
,=
,~=
,|=
,^=
,$=
,*=
)Pseudo-classesβ:hover
,:focus
,:focus-visible
,:focus-within
,:active
,:visited
,:checked
,:disabled
,:enabled
,:readonly
,:readwrite
,:required
,:optional
,:valid
,:invalid
,:empty
,:root
,:first-child
,:last-child
,:only-child
,:first-of-type
,:last-of-type
,:only-of-type
,:nth-child(an+b)
,:nth-last-child
,:nth-of-type
,:nth-last-of-type
,:nth-child(an+b of S)
,:is()
,:where()
,:not()
,:has()
,:lang()
,:dir()
Cascade & specificityβ (a, b, c) tuple,:where()
zero,:is()
/:not()
/:has()
most specific arg, CascadeOrigin (UA / User / Author) with reversed order for!important
, CascadeLayers (None wins over layered; later wins over earlier)Propertiesβdisplay
,position
,color
,background
(includinglinear-gradient
andradial-gradient
),font-*
,margin
,padding
,border
,border-radius
,width
,height
,min/max-width
,top/right/bottom/left
,z-index
,overflow
,opacity
,box-shadow
,white-space
,box-sizing
,gap
,flex*
,grid*
,writing-mode
, logical properties (margin-inline-start
, etc.)Valuesβ keywords, hex/rgb/rgba/named colors, lengths (px, em, rem, pt, %, vw, vh), percentages, numbers,!important
Functionsβlinear-gradient()
,radial-gradient()
,url()
,var()
,calc()
(simplified),rgb()
,rgba()
Shorthandsβmargin
,padding
,border
,background
,flex
@-rulesβ@media
(parsed and applied),@keyframes
/@animation
with cubic-bezier & steps timing functions,@font-face
with family/weight lookup,@layer
(cascade layers),@container
queries withevaluate_container_query()
Logical propertiesβmargin-inline-start
etc. resolved to physical properties based onwriting-mode
CSS countersβcounter-reset
,counter-increment
,counter-set
Containmentβcontain: layout/paint/size/style/inline-size/ block-size
,strict
,content
Filtersβblur
,brightness
,contrast
,drop-shadow
,grayscale
,hue-rotate
,invert
,opacity
,saturate
,sepia
Clip-pathβpolygon
,circle
,ellipse
,inset
,path
,url()
Block flowβ vertical stacking** Inline flow**β horizontal text wrapping with proper baseline** Flexbox**βflex-direction
,justify-content
,align-items
,flex-wrap
,gap
,flex-grow
,flex-shrink
,flex-basis
CSS Gridβgrid-template-columns
/grid-template-rows
(withfr
,auto
,minmax()
,repeat()
),grid-column
/grid-row
placement,gap
/column-gap
/row-gap
,auto-flow
Table layoutβ<table>
,<tr>
,<td>
,<th>
,<thead>
,<tbody>
,<tfoot>
,<caption>
, column width distribution, border collapseFloatβfloat: left/right
, simple clearInline-blockβ inline elements with block-like width/height** Box model**β margin, border, padding, content withbox-sizing: border-box
supportPositionβstatic
,relative
,absolute
,fixed
(parsed, relative + absolute positioning applied)Unitsβ px, em, rem, pt, %, vw, vh** Writing modes**βhorizontal-tb
,vertical-rl/lr
,sideways-rl/lr
Backgroundsβ solid colors and linear / radial gradients** Borders**β all four sides with custom colors and styles** Border-radius**β rounded corners (all four corners)** Box-shadow**β outer shadows with blur** Opacity**β alpha blending for entire elements** Text**β TrueType font rasterization viaab_glyph
, bold and italic synthesisAlpha compositingβ proper RGBA blending** SVG**β paths, basic shapes (rect
,circle
,ellipse
,line
,polyline
,polygon
), gradients, stroke + fillPNG encoderβ hand-written, noflate2
dependency
Falco ships its own JavaScript VM (the tjs
module) β pure Rust, no
V8/SpiderMonkey/boa
. It is a bytecode VM with a generational GC, inline caching, hidden classes, and a JIT tier-up.
ES2015+ syntaxβlet
/const
, arrow functions, template literals, destructuring (object + array), default + rest params, spread,for...of
,for...in
, computed property names, shorthand methods/properties, optional chaining, nullish coalescing, exponentiation operator, async/await (parsed)Functionsβfunction foo() {}
, closures with proper upvalue capture, generators (function*
/yield
/yield*
),async function
/await
(parser-level)TypesβSymbol
with 13 well-known symbols,BigInt
with arbitrary precision (u32 limbs, signed),Promise
withthen
/catch
/finally
- state machine,
Iterator
protocol,Generator
as state machineBuilt-insβMath
,JSON
,Array
(push
,pop
,map
,filter
,reduce
,forEach
,find
,findIndex
,includes
,slice
,splice
,flat
,flatMap
),String
(split
,replace
,match
,padStart
,padEnd
,trim
,trimStart
,trimEnd
,startsWith
,endsWith
,includes
,repeat
),Object
(keys
,values
,entries
,assign
,freeze
,fromEntries
),Reflect
(get
,set
,has
,deleteProperty
,ownKeys
),WeakMap
,WeakSet
,Map
,Set
,Proxy
Microtask queueβPromise
reactions drained as microtasksDOM bindingsβdocument.getElementById
,document.querySelector
/querySelectorAll
,console.log
,alert
,addEventListener
(basic)Event loop integrationβsetTimeout
,setInterval
,requestAnimationFrame
,fetch()
(returnsPromise<Response>
),XMLHttpRequest
, all driven by the event loop inweb_runtime/event_loop.rs
console.log(...)β prints to stderr** alert(msg)**β shows in the status bar
- HTTP/1.1 fetch (via
ureq
) - HTTP/2 parser (
http2.rs
) - Cookie jar (
cookies.rs
) with proper domain/path matching - Redirect handling (
redirect.rs
) with redirect-loop detection - Cache (
cache.rs
) β HTTP cache with conditional requests - WebSocket (
websocket.rs
) β frame parser, masking, ping/pong
fetch()
(fetch.rs
) β Promise-based, integrates with event loopXMLHttpRequest
(xhr.rs
) β sync + async modes- Event loop (
event_loop.rs
) β task queues, microtasks, RAF Promise
(promise.rs
) β state machine, then/catch/finally- WebGL (
webgl.rs
) β shader compilation, buffer management, draw calls (headless) - Video (
video.rs
) β<video>
element demux + decode stub - MSE (
mse.rs
) β Media Source Extensions - EME (
eme.rs
) β Encrypted Media Extensions - NDSD (
ndsd.rs
) β Native Device Service Discovery
Origin / SOP(origin.rs
) β Origin struct,is_same_origin
,is_same_site
,registrable_domain
(with 2-part TLD list),check_cors
(with credentials / wildcard handling),check_navigation
Multi-process / site isolation(process.rs
) β Process kinds (Browser / Renderer / GPU / Utility / Plugin), site-to-process map, ProcessPerSite / ProcessPerTab policies, crash recovery with max-restarts / sad-tab / fatal modesSandbox(sandbox.rs
) β seccomp-bpf filter (Linux), renderer allowlist (~30 syscalls), blocksexecve
/fork
/ptrace
/open
/socket
/connect
/mount
,PR_SET_NO_NEW_PRIVS
,drop_capabilities
CSP(csp.rs
) β directive map,default-src
fallback, source expression matching ('self'
,'none'
,'unsafe-inline'
,'unsafe-eval'
,data:
,blob:
, host,*.wildcard
,scheme:
), nonce/hash support,allows_inline_script
,allows_eval
,allows_javascript_url
,is_safe_attribute
(blocksonclick
,onerror
,javascript:
in href/src), violation reportsTLS certificates(cert.rs
) β Certificate struct, validity, hostname matching (with wildcards), TrustStore with Mozilla defaults,validate_chain
(chain building, signature check, hostname, EKU, path length), OCSP stub, HPKP pinning, Certificate TransparencyPermissions(permissions.rs
) β 20 permission types (Geolocation, Camera, Microphone, Notifications, ...), per-(origin, permission) state, pluggable prompt handler, iframe allow parsingExtensions(extensions.rs
) β Manifest V3, content scripts, match patterns (<all_urls>
,*://*.host/*
), glob matching, permissions, generate extension ID, ChromeApi enumDevTools protocol(devtools.rs
) β JSON value type, Request/Response/Event/RpcError, Inspector/Page/Runtime/DOM/Network/ Console methods, event subscribers, console message buffering
HTTP/HTTPS URLsβ fetched viaureq
β base64-encoded inline imagesdata:
URLsLocal filesβ relative paths resolved against the page URL** Formats**β PNG, JPEG, GIF (first frame), BMP (via theimage
crate)Sizingβwidth
/height
HTML attributes take precedence, CSSwidth
/height
respected, default 300Γ200px, nearest-neighbor scalingBroken imagesβ grey placeholder box with thealt
textCachingβ global cache by URL
- The
html::spec
,dom::spec
,css::spec
,tjs_ext/
,security/
modules arestructurally complete but not yet wired into the render pipeline. Falco still uses the legacyhtml
/dom
/css
modules for actual rendering. The new modules exist as the spec-compliant replacements and pass their own unit tests, but the renderer has not been switched over yet. - No CSS animations / transitions in the renderer (the data structures
exist in
css/spec/advanced.rs
, but the paint loop does not interpolate them). - No
@media
query value matching in the renderer (queries are parsed; the cascade does not yet apply them conditionally). - No real DOM mutation from JS in the renderer (
element.innerHTML = ...
,element.style.color = ...
are stubs). - No HTML5 spec-compliant tree repair in the renderer (the algorithm
exists in
html/spec/tree_builder.rs
but the legacy parser is used).
| Module | Lines | Description |
|---|---|---|
html::spec |
||
| ~4,900 | WHATWG HTML5 tokenizer + tree builder + serializer + XML parser + encoding | |
html.rs |
||
| ~540 | Legacy HTML parser (still used in render pipeline) | |
dom::spec |
||
| ~2,280 | Spec-compliant DOM, MutationObserver, Shadow DOM, custom elements, a11y | |
dom.rs |
||
| ~140 | Legacy DOM (still used in render pipeline) | |
css::spec |
||
| ~2,020 | Selectors L4, cascade specificity, @-rules, animations, containment, filters | |
css/ |
||
| ~1,660 | Legacy CSS parser + selector matching + color parsing | |
style/ |
||
| ~1,720 | Style cascade + UA styles + inheritance + flex/grid properties | |
layout/ |
||
| ~1,950 | Block / inline / flex / grid / table / float / absolute layout | |
paint/ |
||
| ~470 | Canvas + font rasterizer + alpha compositing + gradients + shadows | |
svg/ |
||
| ~1,130 | SVG parser + renderer (paths, shapes, gradients) | |
tjs/ |
||
| ~4,340 | Custom JS VM: lexer, parser, interpreter, bytecode VM, JIT, value, builtins | |
tjs_ext/ |
||
| ~780 | Symbol, BigInt, Promise, microtasks, Map/Set, WeakMap/WeakSet, Reflect | |
js_tjs.rs |
||
| ~700 | JS-to-DOM bindings (document, console, alert, onclick) | |
web_runtime/ |
||
| ~4,300 | fetch, XHR, event loop, Promise, WebGL, video, MSE, EME, NDSD, HTTP/2 | |
net/ |
||
| ~930 | HTTP fetch, cookies, cache, websocket, redirect | |
security/ |
||
| ~3,590 | SOP, multi-process, sandbox, CSP, certs, permissions, extensions, DevTools | |
window/ |
||
| ~1,110 | Interactive window: scrolling, forms, navigation, history, address bar | |
image/ |
||
| ~200 | Image (HTTP, data: URLs, local files) + cache + scaling | |
png/ |
||
| ~100 | Hand-written PNG encoder (no flate2 dependency) | |
main.rs , lib.rs |
||
| ~550 | CLI parsing + library entry points | |
| Total | ||
| ~36,000 |
falco/
βββ .github/ # CI, issue templates, contributing, security policy
β βββ workflows/
β β βββ ci.yml # fmt + clippy + build + test on 3 OSes Γ 2 toolchains
β β βββ release.yml # Build per-OS release binaries on tag push
β βββ ISSUE_TEMPLATE/ # bug_report.md, feature_request.md, config.yml
β βββ CONTRIBUTING.md
β βββ CODE_OF_CONDUCT.md
β βββ SECURITY.md
β βββ PULL_REQUEST_TEMPLATE.md
β βββ FUNDING.yml # MonoBank donation link
β βββ dependabot.yml
βββ docs/ # Logo
β βββ falco.svg # project logo (by Sanya)
βββ src/
β βββ main.rs
β βββ lib.rs
β βββ html/spec/ # WHATWG HTML5 (tokenizer, tree builder, ...)
β βββ html.rs # legacy HTML parser
β βββ dom/spec/ # spec DOM (observer, shadow, custom elements, a11y)
β βββ dom.rs # legacy DOM
β βββ css/spec/ # selectors L4, cascade, @-rules
β βββ css/ # legacy CSS parser
β βββ style/ # cascade + inheritance + UA styles
β βββ layout/ # block/inline/flex/grid/table/float/absolute
β βββ paint/ # canvas + fonts + compositing
β βββ svg/ # SVG parser + renderer
β βββ tjs/ # custom JS VM (lexer, parser, VM, JIT)
β βββ tjs_ext/ # Symbol, BigInt, Promise, ...
β βββ web_runtime/ # fetch, XHR, event loop, WebGL, video, MSE, EME
β βββ net/ # HTTP, cookies, cache, websocket, redirect
β βββ security/ # SOP, sandbox, CSP, certs, permissions, DevTools
β βββ window/ # interactive window mode
β βββ image/ # image
β βββ png/ # PNG encoder
βββ Cargo.toml
βββ Cargo.lock
βββ LICENSE
βββ README.md
The next big pieces of work, roughly in priority order:
Wireβ switch off the legacyhtml::spec
+dom::spec
into the render pipelinehtml
+dom
modules. This unlocks spec-compliant tree repair, full MutationObserver, and real custom elements.Wireβ getcss::spec
into the cascade:is
/:where
/:has
working in real rendering, plus cascade layers and container queries.DOM mutation from JSβelement.innerHTML = ...
,element.style.color = ...
, full re-render on mutation.CSS animations / transitionsβ interpolate keyframes in the paint loop, run them through the event loop.** Wire**β SOP enforcement in DOM access, CSP in the script runner, certificate validation on HTTPS fetches, multi-process sandbox.security/
into the rendererβ apply queries conditionally based on viewport size.@media
query value matching
If Falco is useful to you, consider buying the author a coffee:
MIT β see LICENSE.