///|
fn compute_bounds(node : SVGNode, parent_transform : Transform) -> BoundingBox {
let transform = parent_transform.multiply(node.transform)
let mut bbox = get_shape_bounds(node.shape)
if !bbox.is_empty() {
bbox = transform.apply_bbox(bbox)
}
for child in node.children {
let child_bbox = compute_bounds(child, transform)
bbox = bbox.union(child_bbox)
}
bbox
}
///|
/// Compute bounds without applying the node's own transform
fn compute_bounds_without_self_transform(
node : SVGNode,
parent_transform : Transform,
) -> BoundingBox {
let transform = parent_transform
let mut bbox = get_shape_bounds(node.shape)
if !bbox.is_empty() {
bbox = transform.apply_bbox(bbox)
}
for child in node.children {
let child_bbox = compute_bounds(child, transform)
bbox = bbox.union(child_bbox)
}
bbox
}
///|
fn get_shape_bounds(shape : Shape) -> BoundingBox {
match shape {
Rect(x~, y~, width~, height~, ..) =>
if width <= 0.0 || height <= 0.0 {
BoundingBox::empty()
} else {
BoundingBox::from_rect(x, y, width, height)
}
Circle(cx~, cy~, r~) =>
if r <= 0.0 {
BoundingBox::empty()
} else {
BoundingBox::from_rect(cx - r, cy - r, r * 2.0, r * 2.0)
}
Ellipse(cx~, cy~, rx~, ry~) =>
if rx <= 0.0 || ry <= 0.0 {
BoundingBox::empty()
} else {
BoundingBox::from_rect(cx - rx, cy - ry, rx * 2.0, ry * 2.0)
}
Line(x1~, y1~, x2~, y2~) => {
let mut bbox = BoundingBox::empty()
bbox = bbox.expand_by_point(x1, y1)
bbox = bbox.expand_by_point(x2, y2)
bbox
}
Polyline(points~) | Polygon(points~) => {
let mut bbox = BoundingBox::empty()
for p in points {
bbox = bbox.expand_by_point(p.0, p.1)
}
bbox
}
Path(commands~) => path_bbox(commands)
Text(x~, y~, text~, font_size~) => {
// Approximate text bounds using character count
// Each character is approximately 0.6 * font_size wide
let char_width = font_size * 0.6
let width = char_width * text.length().to_double()
let height = font_size
// y is baseline, so text extends upward
BoundingBox::from_rect(x, y - height, width, height)
}
Image(x~, y~, width~, height~, ..) =>
if width <= 0.0 || height <= 0.0 {
BoundingBox::empty()
} else {
BoundingBox::from_rect(x, y, width, height)
}
Group => BoundingBox::empty()
}
}
///|
/// Render context for drawing
priv struct RenderState {
setter : ColorSink
width : Int
height : Int
flatness : Double // For path flattening
clip : ClipRect? // Optional clipping rectangle for culling
/// Font callback: (codepoint, font_size) -> (path_commands, advance_width)
text_to_paths : ((Int, Double) -> (Array[PathCommand], Double))?
/// Host callback for resolving image references into decoded RGBA pixels.
image_resolver : ((String) -> Image?)?
target_image : Image?
blend_pixel : ((Int, Int, Color, BlendMode) -> Unit)?
diagnostics : Array[RenderDiagnostic]
}
///|
priv struct RenderResources {
clips : ClipPathRegistry
masks : MaskRegistry
filter_graphs : FilterGraphRegistry
patterns : PatternRegistry
gradients : GradientRegistry
markers : MarkerRegistry
active_patterns : Array[String]
pattern_tiles : Map[String, PatternTile]
surface_pool : RenderSurfacePool
}
///|
/// Render a parsed SVG document with registered resources
fn SVGDocument::render(self : SVGDocument, ctx : RenderState) -> Unit {
let resources = {
clips: self.clips,
masks: self.masks,
filter_graphs: self.filter_graphs,
patterns: self.patterns,
gradients: self.gradients,
markers: self.markers,
active_patterns: [],
pattern_tiles: Map([]),
surface_pool: RenderSurfacePool::new(),
}
match self.root.view_box {
Some(vb) => {
let viewbox_transform = vb.get_transform(
ctx.width.to_double(),
ctx.height.to_double(),
self.root.preserve_aspect_ratio,
)
render_node(
self.root,
viewbox_transform,
ctx,
resources,
true,
Color::black(),
)
}
None =>
render_node(
self.root,
Transform::identity(),
ctx,
resources,
true,
Color::black(),
)
}
}
///|
/// Check if a bounding box is visible within the render context
fn is_visible(bbox : BoundingBox, ctx : RenderState) -> Bool {
match ctx.clip {
Some(clip) => bbox.intersects(clip.to_bbox())
None => {
// Default: check against render context bounds
let ctx_bbox = BoundingBox::from_rect(
0.0,
0.0,
ctx.width.to_double(),
ctx.height.to_double(),
)
bbox.intersects(ctx_bbox)
}
}
}
///|
fn intersect_clip(a : ClipRect, b : ClipRect) -> ClipRect? {
let x0 = if a.x > b.x { a.x } else { b.x }
let y0 = if a.y > b.y { a.y } else { b.y }
let x1 = if a.x + a.width < b.x + b.width {
a.x + a.width
} else {
b.x + b.width
}
let y1 = if a.y + a.height < b.y + b.height {
a.y + a.height
} else {
b.y + b.height
}
let w = x1 - x0
let h = y1 - y0
if w <= 0 || h <= 0 {
None
} else {
Some(ClipRect::new(x0, y0, w, h))
}
}
///|
fn apply_transformed_rect_clip(
ctx : RenderState,
local_rect : BoundingBox,
transform : Transform,
) -> RenderState? {
if local_rect.is_empty() || !transform.is_invertible() {
return None
}
let world_bbox = transform.apply_bbox(local_rect)
let mut clip_x = floor_to_int(world_bbox.min_x)
let mut clip_y = floor_to_int(world_bbox.min_y)
let mut clip_w = ceil_to_int(world_bbox.max_x) - clip_x
let mut clip_h = ceil_to_int(world_bbox.max_y) - clip_y
if clip_x < 0 {
clip_w = clip_w + clip_x
clip_x = 0
}
if clip_y < 0 {
clip_h = clip_h + clip_y
clip_y = 0
}
if clip_x + clip_w > ctx.width {
clip_w = ctx.width - clip_x
}
if clip_y + clip_h > ctx.height {
clip_h = ctx.height - clip_y
}
if clip_w <= 0 || clip_h <= 0 {
return None
}
let bounds_clip = ClipRect::new(clip_x, clip_y, clip_w, clip_h)
let clip = match ctx.clip {
Some(existing) =>
match intersect_clip(existing, bounds_clip) {
Some(value) => value
None => return None
}
None => bounds_clip
}
let inverse = transform.inverse()
let inner = ctx.setter.with_clip(clip)
let setter : ColorSink = {
set: fn(x, y, color) {
let (local_x, local_y) = inverse.apply(
x.to_double() + 0.5,
y.to_double() + 0.5,
)
if local_x >= local_rect.min_x &&
local_x < local_rect.max_x &&
local_y >= local_rect.min_y &&
local_y < local_rect.max_y {
inner.pixel(x, y, color)
}
},
}
Some({ ..ctx, setter, clip: Some(clip) })
}
///|
fn render_node(
node : SVGNode,
parent_transform : Transform,
ctx : RenderState,
resources : RenderResources,
allow_mask : Bool,
parent_color : Color,
) -> Unit {
// Skip invisible nodes
if node.opacity <= 0.0 {
return
}
let needs_group_opacity = match node.shape {
Group => node.opacity < 1.0
_ => false
}
let has_mask = node.mask_id is Some(_)
let has_filter_graph = node.filter_graph_id is Some(_)
let needs_surface_mask = has_mask &&
(ctx.target_image is Some(_) || ctx.blend_pixel is Some(_))
if allow_mask &&
(
needs_group_opacity ||
!node.filters.is_empty() ||
has_filter_graph ||
needs_surface_mask ||
node.blend_mode != Normal ||
node.isolation == Isolate
) {
render_isolated_node(node, parent_transform, ctx, resources, parent_color)
return
}
let node_color = match node.color {
Some(c) => c
None => parent_color
}
let base_transform = parent_transform.multiply(node.transform)
let transform = match
(node.view_box, node.viewport_width, node.viewport_height) {
(Some(vb), Some(w), Some(h)) =>
base_transform.multiply(
vb.get_transform(w, h, node.preserve_aspect_ratio),
)
_ => base_transform
}
// Apply clip path if present
let ctx_for_node = match node.clip_path_id {
Some(id) =>
match resources.clips.get(id) {
Some(clip) => {
let bbox_local = match clip.units {
ObjectBoundingBox =>
compute_bounds_without_self_transform(node, Transform::identity())
UserSpaceOnUse => BoundingBox::empty()
}
apply_clip_path(ctx, clip, transform, bbox_local, resources)
}
None => ctx
}
None => ctx
}
let ctx_for_node = match
(node.clip_overflow, node.viewport_width, node.viewport_height) {
(true, Some(vw), Some(vh)) => {
let viewport_bbox = BoundingBox::from_rect(0.0, 0.0, vw, vh)
match
apply_transformed_rect_clip(ctx_for_node, viewport_bbox, base_transform) {
Some(clipped) => clipped
None => return
}
}
_ => ctx_for_node
}
// Apply mask if present
if allow_mask {
match node.mask_id {
Some(id) =>
match resources.masks.get(id) {
Some(mask) => {
render_masked_node(
node, parent_transform, ctx_for_node, resources, mask, node_color,
)
return
}
None => ()
}
None => ()
}
}
// Early culling: check if node's bounding box is visible
let shape_bbox = get_shape_bounds(node.shape)
if !shape_bbox.is_empty() {
let transformed_bbox = transform.apply_bbox(shape_bbox)
if !is_visible(transformed_bbox, ctx_for_node) {
// Node is outside visible area, but still need to check children
// (they might have different transforms that make them visible)
for child in node.children {
render_node(child, transform, ctx_for_node, resources, true, node_color)
}
return
}
}
// Render shape
match node.shape {
Rect(x~, y~, width~, height~, rx~, ry~) =>
render_rect(
x, y, width, height, rx, ry, node, transform, ctx_for_node, resources, node_color,
)
Circle(cx~, cy~, r~) =>
render_circle(
cx, cy, r, node, transform, ctx_for_node, resources, node_color,
)
Ellipse(cx~, cy~, rx~, ry~) =>
render_ellipse(
cx, cy, rx, ry, node, transform, ctx_for_node, resources, node_color,
)
Line(x1~, y1~, x2~, y2~) =>
render_line(
x1, y1, x2, y2, node, transform, ctx_for_node, resources, node_color,
)
Polyline(points~) =>
render_polyline(
points, node, transform, ctx_for_node, resources, node_color,
)
Polygon(points~) =>
render_polygon(
points, node, transform, ctx_for_node, node_color, resources,
)
Path(commands~) =>
render_path(
commands, node, transform, ctx_for_node, node_color, resources,
)
Text(x~, y~, text~, font_size~) =>
render_text(
x, y, text, font_size, node, transform, ctx_for_node, node_color, resources,
)
Image(x~, y~, width~, height~, href~) =>
render_image(
x,
y,
width,
height,
href,
transform,
ctx_for_node,
node.preserve_aspect_ratio,
node.preserve_aspect_ratio_is_set,
node.image_sampling,
node.opacity,
)
Group => ()
}
for child in node.children {
render_node(child, transform, ctx_for_node, resources, true, node_color)
}
}
///|
fn render_isolated_node(
node : SVGNode,
parent_transform : Transform,
ctx : RenderState,
resources : RenderResources,
parent_color : Color,
) -> Unit {
let bounds = isolated_node_region(node, parent_transform, ctx, resources)
let origin_x = max_int(0, floor_to_int(bounds.min_x))
let origin_y = max_int(0, floor_to_int(bounds.min_y))
let end_x = min_int(ctx.width, ceil_to_int(bounds.max_x))
let end_y = min_int(ctx.height, ceil_to_int(bounds.max_y))
if end_x <= origin_x || end_y <= origin_y {
return
}
let layer = resources.surface_pool.acquire(
origin_x,
origin_y,
end_x - origin_x,
end_y - origin_y,
SRGB,
)
let layer_setter : ColorSink = {
set: fn(x, y, color) {
layer.composite_device(x, y, PremulColor16::from_color(color))
},
}
let layer_blend = fn(x : Int, y : Int, color : Color, mode : BlendMode) {
if layer.contains_device(x, y) {
layer.set_device(
x,
y,
PremulColor16::from_color(
blend_with_mode(layer.get_device(x, y).to_color(), color, mode),
),
)
}
}
let layer_ctx = {
..ctx,
setter: layer_setter,
target_image: None,
blend_pixel: Some(layer_blend),
}
let layer_node = node.clone()
layer_node.opacity = 1.0
layer_node.blend_mode = Normal
layer_node.isolation = Auto
layer_node.clear_filters()
layer_node.filter_graph_id = None
layer_node.mask_id = None
render_node(
layer_node, parent_transform, layer_ctx, resources, true, parent_color,
)
for filter in node.filters {
let filtered = apply_filter(layer.to_image(), filter)
layer.replace_from_image(filtered)
}
match node.filter_graph_id {
Some(id) =>
match resources.filter_graphs.get(id) {
Some(graph) => {
let result = evaluate_filter_graph(
graph,
layer.to_image(),
compute_bounds(node, parent_transform),
ctx.image_resolver,
layer.origin_x,
layer.origin_y,
)
layer.replace_from_image(result)
}
None => ()
}
None => ()
}
match node.mask_id {
Some(id) =>
match resources.masks.get(id) {
Some(mask) =>
apply_mask_to_isolated_surface(
layer, node, parent_transform, ctx, resources, mask, parent_color,
)
None => ()
}
None => ()
}
layer.apply_opacity(node.opacity)
composite_surface_to_context(layer, ctx, node.blend_mode)
resources.surface_pool.release(layer)
}
///|
fn expand_effect_bounds(bounds : BoundingBox, amount : Double) -> BoundingBox {
if bounds.is_empty() {
bounds
} else {
{
min_x: bounds.min_x - amount,
min_y: bounds.min_y - amount,
max_x: bounds.max_x + amount,
max_y: bounds.max_y + amount,
}
}
}
///|
fn isolated_node_region(
node : SVGNode,
parent_transform : Transform,
ctx : RenderState,
resources : RenderResources,
) -> BoundingBox {
let mut bounds = compute_bounds(node, parent_transform)
let (scale_x, scale_y) = parent_transform.multiply(node.transform).get_scale()
if node.stroke.width > 0.0 {
bounds = expand_effect_bounds(
bounds,
node.stroke.width * max(scale_x.abs(), scale_y.abs()) * 0.5 + 1.0,
)
}
for filter in node.filters {
match filter {
Blur(radius) => bounds = expand_effect_bounds(bounds, radius * 3.0 + 1.0)
DropShadow(dx, dy, radius, _) => {
let shadow = expand_effect_bounds(bounds, radius * 3.0 + 1.0)
bounds = bounds.union({
min_x: shadow.min_x + dx,
min_y: shadow.min_y + dy,
max_x: shadow.max_x + dx,
max_y: shadow.max_y + dy,
})
}
_ => ()
}
}
match node.filter_graph_id {
Some(id) =>
match resources.filter_graphs.get(id) {
Some(graph) => bounds = bounds.union(graph.get_filter_bounds(bounds))
None => ()
}
None => ()
}
let canvas = BoundingBox::from_rect(
0.0,
0.0,
ctx.width.to_double(),
ctx.height.to_double(),
)
{
min_x: max(bounds.min_x, canvas.min_x),
min_y: max(bounds.min_y, canvas.min_y),
max_x: min(bounds.max_x, canvas.max_x),
max_y: min(bounds.max_y, canvas.max_y),
}
}
///|
fn apply_mask_to_isolated_surface(
source : RenderSurface,
node : SVGNode,
parent_transform : Transform,
ctx : RenderState,
resources : RenderResources,
mask : Mask,
parent_color : Color,
) -> Unit {
let bbox_local = compute_bounds_without_self_transform(
node,
Transform::identity(),
)
if bbox_local.is_empty() {
source.clear()
return
}
let element_transform = parent_transform.multiply(node.transform)
let mask_surface = resources.surface_pool.acquire(
source.origin_x,
source.origin_y,
source.width,
source.height,
SRGB,
)
let mask_setter : ColorSink = {
set: fn(x, y, color) {
mask_surface.composite_device(x, y, PremulColor16::from_color(color))
},
}
let mask_blend = fn(x : Int, y : Int, color : Color, mode : BlendMode) {
if mask_surface.contains_device(x, y) {
mask_surface.set_device(
x,
y,
PremulColor16::from_color(
blend_with_mode(mask_surface.get_device(x, y).to_color(), color, mode),
),
)
}
}
let base_mask_ctx = {
..ctx,
setter: mask_setter,
target_image: None,
blend_pixel: Some(mask_blend),
}
let mask_bounds_local = mask.get_mask_bounds(bbox_local)
let region_clip = ClipPath::with_transform(
"mask-region",
Rect(
x=mask_bounds_local.min_x,
y=mask_bounds_local.min_y,
width=mask_bounds_local.width(),
height=mask_bounds_local.height(),
rx=0.0,
ry=0.0,
),
element_transform,
)
let mask_ctx = apply_clip_path(
base_mask_ctx,
region_clip,
Transform::identity(),
BoundingBox::empty(),
resources,
)
let mask_parent = match mask.mask_content_units {
ObjectBoundingBox => {
let translate = Transform::translate(bbox_local.min_x, bbox_local.min_y)
let scale = Transform::scale(bbox_local.width(), bbox_local.height())
element_transform.multiply(translate.multiply(scale))
}
UserSpaceOnUse => element_transform
}
for content_node in mask.content {
render_node(
content_node, mask_parent, mask_ctx, resources, true, parent_color,
)
}
for index in 0.. mask_color.a
Luminance =>
clamp_channel16(
round_div_nonnegative(
mask_color.r.to_int64() * 2126L +
mask_color.g.to_int64() * 7152L +
mask_color.b.to_int64() * 722L,
10000L,
),
)
}
source.pixels[index] = source.pixels[index].scaled(factor)
}
resources.surface_pool.release(mask_surface)
}
///|
fn composite_surface_to_context(
surface : RenderSurface,
ctx : RenderState,
mode : BlendMode,
) -> Unit {
for local_y in 0.. clip.contains(x, y)
None => true
}
if !visible {
continue
}
let source = color.to_color()
if mode == Normal {
ctx.setter.pixel(x, y, source)
} else {
match ctx.blend_pixel {
Some(blend) => blend(x, y, source, mode)
None =>
match ctx.target_image {
Some(target) =>
target.set_pixel(
x,
y,
blend_with_mode(target.get_pixel(x, y), source, mode),
)
None => ctx.setter.pixel(x, y, source)
}
}
}
}
}
}
///|