|
libwebsockets
Lightweight C library for HTML5 websockets
|
Lws includes a stateful, line-at-a-time renderer for the fill-only subset of SVG. Where the PNG and JPEG support decode a raster that already has a fixed size, this parses the vector document into a small retained scene and then computes each output line on demand by intersecting the geometry with the scanline. Like the raster decoders, there is no requirement for a framebuffer, only one line's spans exist at a time; the whole rendered output never has to be in memory.
Because the vector scene is retained, a rendered line can be produced in any order and repeatedly, at any requested output scale. Two differences from the raster decoders follow from the format:
All arithmetic is pure integer: coordinates, transforms and raster math in saturating Q16.16 (range +/-32768, resolution 1/65536), and parse values, angles and opacity in 1e-8 fixed point through the lws_fx operators. There is no floating point anywhere in the renderer, no libm dependency, no FPU requirement, and rendered output is bit-identical across platforms. Saturation replaces overflow, so hostile transform stacks clip the geometry instead of producing out-of-range values.
Fixed allocations at lws_svg_new():
| Allocation | Size |
|---|---|
| Parse context (includes the 24-deep element style stack) | 1272B |
| Attribute value accumulation buffer | 256B, grows by doubling to the largest attribute |
During parsing, a transient working array holds the current shape's geometry at 16B/point in user space, growing to the largest single shape and reused for the rest of the document, and the attribute value buffer grows to accommodate the largest attribute (typically a path d string), capped at 64KB.
The whole object — context, working buffers, stylesheets and retained scene — lives in a single lwsac allocated in 4KB chunks, freed with one call. The growable working buffers (points, subpaths, crossings, aa columns, attribute values) grow by chained doubling generations inside the lwsac, so superseded generations remain until the object is destroyed; the caps below bound the worst case. The peak simultaneous footprint (as the lwsac total, including chunk overheads and superseded generations) is tracked and logged at destroy time at info level.
The retained scene items are:
| Retained item | Size |
|---|---|
| Per shape | 56B |
| Per subpath | 40B |
| Per flattened point | 8B |
So a typical icon or chart SVG resolves to a few KB of retained scene. For hostile input, scene growth is capped at 2048 shapes, 1024 subpaths per shape and 262,144 flattened points in total (2MB of point data, or an absolute ceiling of around 6MB including per-subpath overhead when the points are arranged as minimal subpaths); exceeding any cap is a FATAL parse result, as is nesting deeper than 24 elements.
The rasterization scratch (crossings, and one int64 per output column for antialiasing) is a single pool shared by every live svg object: it carries no state between lines, so one allocation serves any number of simultaneously-live documents (a page full of visible svgs costs one 4KB scratch, not one each). The pool is refcounted on the live objects, grows to the largest demand seen, shrinks by doubling steps when the object that needed the larger size is destroyed, and is freed with the last object. Rendering is single-threaded, as the display list already requires. Spans are delivered to a caller callback, so the renderer itself holds no output buffer at all; the line buffer is the caller's (in the dlo integration, the existing display line composition buffer).
With lws_svg_render_t.aa set, coverage is the exact covered fraction of each pixel rather than a centre sample: for each output row, every boundary edge is clipped to the row band and contributes clipped-linear "ramp" areas to a per-column winding integral, swept left to right into per-pixel fractions (the cell/area approach used by FreeType-class rasterizers, in pure integer arithmetic — no supersampling anywhere). Corners inside a pixel, thin features and slivers between scanlines are handled exactly. Spans are split where the fraction changes and the alpha byte carries fill alpha x coverage, so it composites through the existing alpha path. Binary coverage remains the default; the dlo integration renders antialiased.
Stroked geometry is converted to fill geometry at parse time, so the rasterizer and antialiasing need no stroke-specific code. For each flattened centerline segment a quad of the segment expanded by half the stroke width is emitted; at vertices the join fills the outer notch with a bevel triangle, a miter wedge (clipped by stroke-miterlimit, falling back to a bevel), or an arc fan; open subpath ends get butt, round (semicircle fan) or square caps. Every polygon is emitted with consistent winding, so under the nonzero fill rule overlapping pieces sum to exactly the union of the stroke: no cancellation artifacts at concave corners or self-crossing outlines.
The stroke of a shape is committed as its own scene shape in the stroke colour (after the fill, when both are painted), and a background image never delays the layout for it. Outline generation is in user space with the user-space width, then transformed by the CTM, so a scale() group thickens strokes exactly as it scales the geometry they outline.
Costs: a stroked N-segment path becomes about 4N + join and cap polygons in the scene, sharing the same scene lwsac and the same caps. A wide stroke on a path that doubles back on itself paints correctly, since the overlap adds rather than cancels.
Not rendered in this phase of the work: gradients and other paint servers, text, masks, filters, patterns, <use>/<symbol> instancing, stroke-dasharray / stroke-dashoffset (strokes render continuous). Subtrees that are only containers for these (<defs>, <text>, gradients, unknown elements) are parsed but suppressed; a fill referencing an unsupported paint server (url(#...)) paints nothing. <style> blocks are parsed for the minimal css described above, but selectors beyond the simple forms given are ignored.
The apis to create and destroy a context are very simple...
The document is fed to the stateful parser in arbitrarily-sized chunks; the parse results are insensitive to the chunking.
If input is consumed, *buf and *len are adjusted accordingly. The return consists of:
| Return value bit | Meaning |
|---|---|
| LWS_SRET_OK (0, no bits set) | The document completed (root element closed), the scene is ready; with hold_at_metadata, the root tag parsed and dimensions are available |
| LWS_SRET_WANT_INPUT | The input so far was consumed and more is needed to progress |
| LWS_SRET_FATAL | The document exceeded a resource cap or allocation failed |
With hold_at_metadata set, the parser stops at the end of the root <svg> tag even if more input is available at *buf, so image dimensions for layout are available before the whole document is retained, in the same spirit as the raster decoders' header-only phase. Continue the full parse later by feeding the remaining input with the flag clear.
A document whose input ends without closing the root element is not an error; whatever parsed is renderable (and renders nothing further once the input ends).
Intrinsic dimensions for layout come from lws_svg_get_width() / lws_svg_get_height(): the width/height attributes when in px, otherwise the viewBox extents, otherwise the CSS default replaced element size of 300x150. They return 0 until the root tag has parsed.
ri gives the output raster size in px (and whether to antialias, see above); the document's viewBox and preserveAspectRatio policy map the user-space geometry into it. The filled spans are delivered to cb in ascending x, in document order, as half-open [x0, x1) pixel ranges with the span's composed RGBA (alpha already includes opacity composition, and with aa set, the per-pixel coverage). Lines may be requested in any order and repeatedly; the only mutation of the context is growth of the scratch buffers.
LWS_WITH_SVG (on by default) adds LWSDLOSS_TYPE_SVG alongside the PNG and JPEG image dlos: <img> fetches of URLs ending .svg are parsed streaming from the Secure Streams buflist with the same metadata-first flow as the raster images, then rendered line-at-a-time into the dlo line composition buffer at the box size the layout chose. Building with LWS_WITH_SVG=0 removes it and the .svg url handling cleanly.
lws-api-test-svg generates a corpus of around 200 documents covering each supported feature with analytic checks (exact span geometry, independent pixel-centre oracles, streaming chunk equivalence, transform invariances and robustness sweeps); --dump <dir> writes the corpus as .svg/.pbm pairs for eyeballing.
It can also render a single file for eyeballing, in the same way as the lhp dlo tool renders to a 24bpp top-down .bmp:
The spans are composited in document order on to the background colour line-at-a-time and written as the .bmp; the exit status is nonzero if the file could not be opened or parsed. Running the tool with no --svg performs the corpus selftest.