///|
fn create_node_for_tag(tag_name : String) -> SVGNode {
let node = match tag_name {
"rect" =>
SVGNode::new(Rect(x=0.0, y=0.0, width=0.0, height=0.0, rx=0.0, ry=0.0))
"circle" => SVGNode::new(Circle(cx=0.0, cy=0.0, r=0.0))
"ellipse" => SVGNode::new(Ellipse(cx=0.0, cy=0.0, rx=0.0, ry=0.0))
"line" => SVGNode::new(Line(x1=0.0, y1=0.0, x2=0.0, y2=0.0))
"polyline" => SVGNode::new(Polyline(points=[]))
"polygon" => SVGNode::new(Polygon(points=[]))
"path" => SVGNode::new(Path(commands=[]))
_ => SVGNode::new(Group) // svg, g, defs, etc. are groups
}
node
}
///|
fn apply_attributes(node : SVGNode, attrs : Array[(String, String)]) -> Unit {
for attr in attrs {
if attr.0 != "style" {
apply_presentation_attribute(node, attr.0, attr.1)
}
}
for attr in attrs {
if attr.0 == "style" {
apply_style_attributes(node, attr.1)
}
}
match node.shape {
Rect(..) as rect => {
let has_rx = get_attr(attrs, "rx") is Some(_)
let has_ry = get_attr(attrs, "ry") is Some(_)
if has_rx && !has_ry {
node.shape = Rect(
x=rect.x,
y=rect.y,
width=rect.width,
height=rect.height,
rx=rect.rx,
ry=rect.rx,
)
} else if has_ry && !has_rx {
node.shape = Rect(
x=rect.x,
y=rect.y,
width=rect.width,
height=rect.height,
rx=rect.ry,
ry=rect.ry,
)
}
}
_ => ()
}
}
///|
fn apply_attribute(
node : SVGNode,
name : String,
value : String,
length_context? : LengthContext,
) -> Unit {
fn length(axis : LengthAxis) -> Double {
match length_context {
Some(context) => parse_length_with_context(value, axis, context)
None => parse_length(value)
}
}
match name {
"id" => node.id = value
"style" => apply_style_attributes(node, value)
"viewBox" => node.view_box = parse_view_box(value)
"preserveAspectRatio" => {
node.preserve_aspect_ratio = parse_preserve_aspect_ratio(value)
node.preserve_aspect_ratio_is_set = true
}
"clip-path" =>
if parse_url_ref(value) is Some(id) {
node.set_clip_path(id)
}
"mask" => if parse_url_ref(value) is Some(id) { node.set_mask(id) }
"requiredExtensions" => node.opacity = 0.0
"x" =>
match node.shape {
Rect(..) as r =>
node.shape = Rect(
x=length(Horizontal),
y=r.y,
width=r.width,
height=r.height,
rx=r.rx,
ry=r.ry,
)
Image(..) as i =>
node.shape = Image(
x=length(Horizontal),
y=i.y,
width=i.width,
height=i.height,
href=i.href,
)
_ => ()
}
"y" =>
match node.shape {
Rect(..) as r =>
node.shape = Rect(
x=r.x,
y=length(Vertical),
width=r.width,
height=r.height,
rx=r.rx,
ry=r.ry,
)
Image(..) as i =>
node.shape = Image(
x=i.x,
y=length(Vertical),
width=i.width,
height=i.height,
href=i.href,
)
_ => ()
}
"width" =>
match node.shape {
Rect(..) as r =>
node.shape = Rect(
x=r.x,
y=r.y,
width=length(Horizontal),
height=r.height,
rx=r.rx,
ry=r.ry,
)
Image(..) as i =>
node.shape = Image(
x=i.x,
y=i.y,
width=length(Horizontal),
height=i.height,
href=i.href,
)
_ => ()
}
"height" =>
match node.shape {
Rect(..) as r =>
node.shape = Rect(
x=r.x,
y=r.y,
width=r.width,
height=length(Vertical),
rx=r.rx,
ry=r.ry,
)
Image(..) as i =>
node.shape = Image(
x=i.x,
y=i.y,
width=i.width,
height=length(Vertical),
href=i.href,
)
_ => ()
}
"rx" =>
match node.shape {
Rect(..) as r =>
node.shape = Rect(
x=r.x,
y=r.y,
width=r.width,
height=r.height,
rx=length(Horizontal),
ry=r.ry,
)
Ellipse(..) as e =>
node.shape = Ellipse(cx=e.cx, cy=e.cy, rx=length(Horizontal), ry=e.ry)
_ => ()
}
"ry" =>
match node.shape {
Rect(..) as r =>
node.shape = Rect(
x=r.x,
y=r.y,
width=r.width,
height=r.height,
rx=r.rx,
ry=length(Vertical),
)
Ellipse(..) as e =>
node.shape = Ellipse(cx=e.cx, cy=e.cy, rx=e.rx, ry=length(Vertical))
_ => ()
}
"cx" =>
match node.shape {
Circle(..) as c =>
node.shape = Circle(cx=length(Horizontal), cy=c.cy, r=c.r)
Ellipse(..) as e =>
node.shape = Ellipse(cx=length(Horizontal), cy=e.cy, rx=e.rx, ry=e.ry)
_ => ()
}
"cy" =>
match node.shape {
Circle(..) as c =>
node.shape = Circle(cx=c.cx, cy=length(Vertical), r=c.r)
Ellipse(..) as e =>
node.shape = Ellipse(cx=e.cx, cy=length(Vertical), rx=e.rx, ry=e.ry)
_ => ()
}
"r" =>
match node.shape {
Circle(..) as c =>
node.shape = Circle(cx=c.cx, cy=c.cy, r=length(Diagonal))
_ => ()
}
"x1" =>
match node.shape {
Line(..) as l =>
node.shape = Line(x1=length(Horizontal), y1=l.y1, x2=l.x2, y2=l.y2)
_ => ()
}
"y1" =>
match node.shape {
Line(..) as l =>
node.shape = Line(x1=l.x1, y1=length(Vertical), x2=l.x2, y2=l.y2)
_ => ()
}
"x2" =>
match node.shape {
Line(..) as l =>
node.shape = Line(x1=l.x1, y1=l.y1, x2=length(Horizontal), y2=l.y2)
_ => ()
}
"y2" =>
match node.shape {
Line(..) as l =>
node.shape = Line(x1=l.x1, y1=l.y1, x2=l.x2, y2=length(Vertical))
_ => ()
}
"d" =>
match node.shape {
Path(..) => node.shape = Path(commands=parse_path(value))
_ => ()
}
"points" =>
match node.shape {
Polyline(..) => node.shape = Polyline(points=parse_points(value))
Polygon(..) => node.shape = Polygon(points=parse_points(value))
_ => ()
}
"fill" => {
node.fill = parse_paint(value)
node.fill_is_set = true
}
"color" => {
node.color = Some(parse_color(value))
node.color_is_set = true
}
"paint-order" => node.paint_order = parse_paint_order(value)
"image-rendering" => node.image_sampling = parse_image_sampling(value)
"stroke" => {
node.stroke = {
paint: parse_paint(value),
width: node.stroke.width,
linecap: node.stroke.linecap,
linejoin: node.stroke.linejoin,
miterlimit: node.stroke.miterlimit,
dasharray: node.stroke.dasharray,
dashoffset: node.stroke.dashoffset,
non_scaling: node.stroke.non_scaling,
}
node.stroke_paint_is_set = true
}
"stroke-width" => {
node.stroke_width_is_set = true
node.stroke = {
paint: node.stroke.paint,
width: length(Diagonal),
linecap: node.stroke.linecap,
linejoin: node.stroke.linejoin,
miterlimit: node.stroke.miterlimit,
dasharray: node.stroke.dasharray,
dashoffset: node.stroke.dashoffset,
non_scaling: node.stroke.non_scaling,
}
}
"opacity" => node.opacity = parse_number(value)
"fill-opacity" => node.fill_opacity = parse_number(value)
"stroke-opacity" => node.stroke_opacity = parse_number(value)
"filter" => node.filter_graph_id = parse_url_ref(value)
"mix-blend-mode" => node.blend_mode = parse_blend_mode(value)
"isolation" => node.isolation = parse_isolation(value)
"transform" => node.transform = parse_transform(value)
// Stroke detail attributes
"stroke-linecap" =>
node.stroke = { ..node.stroke, linecap: parse_linecap(value) }
"stroke-linejoin" =>
node.stroke = { ..node.stroke, linejoin: parse_linejoin(value) }
"stroke-miterlimit" =>
node.stroke = { ..node.stroke, miterlimit: parse_number(value) }
"stroke-dasharray" =>
node.stroke = {
..node.stroke,
dasharray: match length_context {
Some(context) => parse_dasharray(value, length_context=context)
None => parse_dasharray(value)
},
}
"stroke-dashoffset" =>
node.stroke = { ..node.stroke, dashoffset: length(Diagonal) }
"vector-effect" =>
node.stroke = {
..node.stroke,
non_scaling: trim_string(value).to_lower() == "non-scaling-stroke",
}
"marker-start" => {
node.marker_start = parse_marker_ref(value)
node.marker_start_is_set = true
}
"marker-mid" => {
node.marker_mid = parse_marker_ref(value)
node.marker_mid_is_set = true
}
"marker-end" => {
node.marker_end = parse_marker_ref(value)
node.marker_end_is_set = true
}
"marker" =>
match parse_marker_ref(value) {
Some(id) => {
node.marker_start = Some(id)
node.marker_mid = Some(id)
node.marker_end = Some(id)
node.marker_start_is_set = true
node.marker_mid_is_set = true
node.marker_end_is_set = true
}
None => {
node.marker_start = None
node.marker_mid = None
node.marker_end = None
node.marker_start_is_set = true
node.marker_mid_is_set = true
node.marker_end_is_set = true
}
}
// Fill rule
"fill-rule" => node.fill_rule = parse_fill_rule(value)
"clip-rule" => node.clip_rule = parse_fill_rule(value)
_ => () // Ignore unknown attributes
}
}
///|
fn parse_blend_mode(value : String) -> BlendMode {
match trim_string(value).to_lower() {
"multiply" => Multiply
"screen" => Screen
"overlay" => Overlay
"darken" => Darken
"lighten" => Lighten
"color-dodge" => ColorDodge
"color-burn" => ColorBurn
"hard-light" => HardLight
"soft-light" => SoftLight
"difference" => Difference
"exclusion" => Exclusion
"hue" => Hue
"saturation" => Saturation
"color" => ColorMode
"luminosity" => Luminosity
_ => Normal
}
}
///|
fn parse_isolation(value : String) -> Isolation {
if trim_string(value).to_lower() == "isolate" {
Isolate
} else {
Auto
}
}
///|
fn apply_presentation_attribute(
node : SVGNode,
name : String,
value : String,
) -> Unit {
if name == "marker" {
return
}
apply_attribute(node, name, value)
}
///|
fn parse_marker_ref(value : String) -> String? {
let v = trim_string(value)
if v.length() == 0 || v == "none" {
return None
}
parse_url_ref(v)
}
///|
fn apply_style_attributes(node : SVGNode, value : String) -> Unit {
let declarations = parse_inline_style_declarations(value)
for declaration in declarations {
let name = declaration.name
let winner = find_winning_declaration(declarations, name)
if name == "style" || is_custom_property(name) {
continue
}
match winner {
Some(_) => apply_attribute(node, name, declaration.value)
_ => ()
}
}
}
///|
fn apply_style_attributes_filtered(node : SVGNode, value : String) -> Unit {
let declarations = parse_inline_style_declarations(value)
for declaration in declarations {
let name = declaration.name
let winner = find_winning_declaration(declarations, name)
if name == "style" ||
is_custom_property(name) ||
name == "transform" ||
name == "x" ||
name == "y" ||
name == "width" ||
name == "height" {
continue
}
match winner {
Some(_) => apply_attribute(node, name, declaration.value)
_ => ()
}
}
}
///|
fn apply_presentation_attributes(
node : SVGNode,
attrs : Array[(String, String)],
) -> Unit {
for attr in attrs {
let name = attr.0
if name == "id" ||
name == "x" ||
name == "y" ||
name == "width" ||
name == "height" ||
name == "href" ||
name == xlink_href_attribute_name ||
name == "transform" ||
name == "style" {
continue
}
apply_presentation_attribute(node, name, attr.1)
}
for attr in attrs {
if attr.0 == "style" {
apply_style_attributes_filtered(node, attr.1)
}
}
}
///|
fn apply_paint_order_recursive(node : SVGNode, order : PaintOrder) -> Unit {
node.paint_order = order
for child in node.children {
apply_paint_order_recursive(child, order)
}
}
///|
fn get_attr(attrs : Array[(String, String)], name : String) -> String? {
for attr in attrs {
if attr.0 == name {
return Some(attr.1)
}
}
None
}
///|
fn get_href_attr(attrs : Array[(String, String)]) -> String? {
match get_attr(attrs, "href") {
Some(value) => Some(value)
None => get_attr(attrs, xlink_href_attribute_name)
}
}
///|
fn parse_url_ref(value : String) -> String? {
let v = trim_string(value)
if v.length() == 0 {
return None
}
if string_starts_with(v, "url(") && string_ends_with(v, ")") {
let inner = build_substring(v, 4, v.length() - 1)
let trimmed = trim_string(inner)
if trimmed.length() > 0 && Int::unsafe_to_char(trimmed[0].to_int()) == '#' {
return Some(build_substring(trimmed, 1, trimmed.length()))
}
return Some(trimmed)
}
if Int::unsafe_to_char(v[0].to_int()) == '#' {
return Some(build_substring(v, 1, v.length()))
}
None
}
///|
fn parse_number_list(value : String) -> Array[Double] {
let nums : Array[Double] = []
let mut buf = StringBuilder::new()
for i in 0.. 0 {
nums.push(parse_number(buf.to_string()))
buf = StringBuilder::new()
}
} else {
buf.write_char(c)
}
}
if buf.to_string().length() > 0 {
nums.push(parse_number(buf.to_string()))
}
nums
}
///|
fn split_tokens(value : String) -> Array[String] {
let tokens : Array[String] = []
let mut buf = StringBuilder::new()
for i in 0.. 0 {
tokens.push(buf.to_string())
buf = StringBuilder::new()
}
} else {
buf.write_char(c)
}
}
if buf.to_string().length() > 0 {
tokens.push(buf.to_string())
}
tokens
}
///|
fn parse_view_box(value : String) -> ViewBox? {
let nums = parse_number_list(value)
if nums.length() < 4 {
return None
}
Some({ min_x: nums[0], min_y: nums[1], width: nums[2], height: nums[3] })
}
///|
fn parse_preserve_aspect_ratio(value : String) -> PreserveAspectRatio {
let v = trim_string(value)
if v.length() == 0 {
return PreserveAspectRatio::default()
}
let tokens = split_tokens(v)
if tokens.length() == 0 {
return PreserveAspectRatio::default()
}
if tokens[0] == "none" {
return { align: None, meet_or_slice: Meet }
}
let align = match tokens[0] {
"xMinYMin" => XMinYMin
"xMidYMin" => XMidYMin
"xMaxYMin" => XMaxYMin
"xMinYMid" => XMinYMid
"xMidYMid" => XMidYMid
"xMaxYMid" => XMaxYMid
"xMinYMax" => XMinYMax
"xMidYMax" => XMidYMax
"xMaxYMax" => XMaxYMax
_ => XMidYMid
}
let meet_or_slice = if tokens.length() > 1 {
match tokens[1] {
"slice" => Slice
_ => Meet
}
} else {
Meet
}
{ align, meet_or_slice }
}
///|
fn parse_mask_units(value : String) -> MaskUnits {
match trim_string(value) {
"userSpaceOnUse" => UserSpaceOnUse
"objectBoundingBox" => ObjectBoundingBox
_ => ObjectBoundingBox
}
}
///|
fn parse_clip_path_units(value : String) -> ClipPathUnits {
match trim_string(value) {
"objectBoundingBox" => ObjectBoundingBox
_ => UserSpaceOnUse
}
}
///|
fn parse_mask_type(value : String) -> MaskType {
match trim_string(value) {
"alpha" => Alpha
_ => Luminance
}
}