svg

tape
@profile(t1);
import fs from "fs";
import io from "io";
import gfx from "gfx";
import svg from "svg";

fn main() -> i32 {
    let bytes: string = fs.read_file("icons/house.svg") or { return 1; };
    var icon: svg.Image = svg.parse(bytes) or {
        io.println("not an SVG image: {err}");
        return 2;
    };
    let size: svg.Size = icon.size();                 // 24 x 24 for a Lucide icon

    // Rendered at 48 px in dark grey: a new Argb surface, as image.decode gives
    var picture: gfx.Surface = icon.render(48, 48, #202020) or { return 3; };

    // ... and drawn with gfx like any image
    var screen: gfx.Surface = gfx.new_surface(320, 200, gfx.Format.Rgbx) or { return 4; };
    screen.clear(#F4F4F4);
    screen.draw(16, 16, &picture);
    io.println("{size.w} x {size.h}, checksum {screen.checksum()}");
    picture.free();
    screen.free();
    return 0;
}

svg draws SVG files at any size. render gives a gfx.Surface in the Argb format, exactly what image.decode gives for a PNG, so an icon set in SVG and one in PNG are drawn the same way. svg is a pure module: it reads no files, you hand it bytes.

FunctionDoes
svg.parse(bytes) -> Image or SvgErrorRead an SVG document
svg.from_doc(&doc) -> Image or SvgErrorAn image from an xml tree you read or built
image.size() -> SizeIts natural size in CSS pixels (w, h, f64)
image.render(w, h, current) -> gfx.Surface or SvgErrorDraw it at w x h on a new Argb surface; free it with free()
image.render_into(&surface, current)Draw it over a surface you have, at that surface’s size
svg.add_face(family, weight, italic, bytes) -> boolGive text a font (below)

current is the colour currentColor stands for: icon sets such as Lucide stroke and fill with it, so one file draws in any colour. An image with a viewBox is fitted into w x h keeping its aspect ratio (as its preserveAspectRatio says); ask size() to pick a w and h that match.

SvgErrorPrints
Malformednot well-formed XML
NotSvgnot an SVG document (no svg root element)
InvalidSizeinvalid image size
OutOfMemoryout of memory

A file with an XML error is Malformed, as browsers refuse it. To draw what can be read of one anyway:

tape
let doc: xml.Doc = xml.parse_lenient(bytes);
var im: svg.Image = svg.from_doc(&doc) or { return 1; };

What draws

Shapes (rect with rounded corners, circle, ellipse, line, polyline, polygon) and paths with every command, arcs included; groups, use and symbol, nested svg with viewBox; transforms; fills and strokes in colours, currentColor, linear and radial gradients; stroke widths, caps, joins, miter limits and dashes; opacity on shapes and groups; clip paths and masks; markers (arrowheads) at a path’s ends and corners; text and tspan with anchors and baselines.

Styles come from attributes (fill="red"), <style> sheets and style attributes, with inheritance and !important. The sheets understand the CSS that SVG files use: type, .class, #id, * and [attribute] selectors, the descendant, >, + and ~ combinators, comma lists, specificity; colours by name, #hex, rgb() and hsl(). A rule with a selector beyond that (:hover, :first-child) is skipped, and @media blocks are ignored.

Not drawn: image, pattern, filter, foreignObject, textPath, per-letter positions and rotations.

Text

svg draws text with TrueType fonts you register, since it reads no files itself. Read the font with fs and register it once; a family name in the file that has no face of its own falls back to sans-serif:

tape
let regular: string = fs.read_file("C:/Windows/Fonts/arial.ttf") or { return 1; };
svg.add_face("sans-serif", 400, false, regular);
let bold: string = fs.read_file("C:/Windows/Fonts/arialbd.ttf") or { return 1; };
svg.add_face("sans-serif", 700, false, bold);

The faces are kept for the whole program and every image uses them. A character a face lacks is taken from the first registered face that has it. Without any face, text is not drawn.

Last modified: