///|
/// Public API helpers
/// Convenience functions for rendering to Image and bridging external trees.
///|
/// Render an SVGDocument into an Image.
pub fn render_svg_document_to_image(
doc : SVGDocument,
width : Int,
height : Int,
) -> Image {
let image = Image::new(width, height)
let setter : PixelSetter = {
set: (x, y, color) => image.set_pixel(x, y, color),
}
let ctx = RenderContext::new(setter, width, height)
doc.render(ctx)
image
}
///|
/// Render a Scene into an Image.
pub fn render_svg_scene_to_image(
scene : Scene,
width : Int,
height : Int,
) -> Image {
let image = Image::new(width, height)
let setter : PixelSetter = {
set: (x, y, color) => image.set_pixel(x, y, color),
}
let ctx = RenderContext::new(setter, width, height)
scene.render(ctx)
image
}
///|
/// Render a raw SVGNode tree into an Image.
pub fn render_svg_node_to_image(
node : SVGNode,
width : Int,
height : Int,
) -> Image {
let scene = Scene::new(node)
render_svg_scene_to_image(scene, width, height)
}
///|
/// Parse and render an SVG markup string into an Image.
pub fn render_svg_to_image(
svg_str : String,
width : Int,
height : Int,
) -> Image? {
parse_svg_document(svg_str).map(fn(doc) {
render_svg_document_to_image(doc, width, height)
})
}
///|
/// Render PathCommand array directly to an Image (no SVG string parsing).
/// This is much faster than render_svg_to_image for programmatic paths
/// (e.g., font glyph outlines) because it skips SVG serialization and parsing.
///
/// `transform` is a 6-element affine transform [a, b, c, d, e, f] or empty for identity.
pub fn render_path_commands_to_image(
commands : Array[PathCommand],
width : Int,
height : Int,
fill_color : Color,
transform? : Array[Double] = [],
) -> Image {
let image = Image::new(width, height)
if commands.is_empty() || width <= 0 || height <= 0 {
return image
}
let flatness = 0.5
let polylines = path_to_polylines(commands, flatness)
let tf = if transform.length() >= 6 {
Transform::matrix(
transform[0],
transform[1],
transform[2],
transform[3],
transform[4],
transform[5],
)
} else {
Transform::identity()
}
let transformed_polylines : Array[Array[(Int, Int)]] = []
for polyline in polylines {
let transformed = polyline.map(fn(p) {
let (x, y) = tf.apply(p.0, p.1)
(x.to_int(), y.to_int())
})
if transformed.length() >= 3 {
transformed_polylines.push(transformed)
}
}
if transformed_polylines.length() > 0 {
raster_polygons_fill_direct(transformed_polylines, fill_color, image)
}
image
}
///|
/// Optimized scanline fill that writes directly to Image pixels.
/// Avoids per-pixel callback overhead and reuses intersection buffer.
fn raster_polygons_fill_direct(
polygons : Array[Array[(Int, Int)]],
color : Color,
image : Image,
) -> Unit {
if polygons.length() == 0 {
return
}
let w = image.width
let h = image.height
// Find bounding box
let mut min_y = h
let mut max_y = 0
for poly in polygons {
for pt in poly {
let y = pt.1
if y < min_y {
min_y = y
}
if y > max_y {
max_y = y
}
}
}
if min_y < 0 {
min_y = 0
}
if max_y >= h {
max_y = h - 1
}
if min_y > max_y {
return
}
// Build edge table: collect all edges sorted by min_y
let edges : Array[(Int, Int, Int, Int, Int)] = [] // (y_min, y_max, x1, x2, winding_sign)
for poly in polygons {
let n = poly.length()
if n < 3 {
continue
}
for i in 0.. y {
break
}
if y >= y_min && y < y_max {
let x = x1 + (y - y_min) * (x2 - x1) / (y_max - y_min)
intersections.push(x)
}
}
if intersections.length() < 2 {
continue
}
// Sort intersections
for i = 1; i < intersections.length(); i = i + 1 {
let key = intersections[i]
let mut j = i - 1
while j >= 0 && intersections[j] > key {
intersections[j + 1] = intersections[j]
j -= 1
}
intersections[j + 1] = key
}
// Fill between pairs — direct pixel array write
let mut i = 0
while i + 1 < intersections.length() {
let mut x_start = intersections[i]
let mut x_end = intersections[i + 1]
if x_start < 0 {
x_start = 0
}
if x_end >= w {
x_end = w - 1
}
if x_start <= x_end && y >= 0 && y < h {
let row_offset = y * w
for x = x_start; x <= x_end; x = x + 1 {
image.pixels[row_offset + x] = color
}
}
i += 2
}
}
}