// SVG backend: interpret the backend-neutral `DrawCmd` list (see draw.mbt) into
// compact native SVG. A circle becomes one `` rather than thousands of
// sampled ``s. The same draw list drives the PDF and canvas backends.
///|
/// Render the image to a standalone SVG document of the given size.
pub fn Image::to_svg(self : Image, width : Double, height : Double) -> String {
svg_of(self.to_draw_list(width, height), width, height)
}
///|
fn svg_of(cmds : Array[DrawCmd], width : Double, height : Double) -> String {
let defs = StringBuilder::new()
let body = StringBuilder::new()
let id = [0] // shared counter for gradient/clip ids
for cmd in cmds {
match cmd {
FillPath(path, paint, area) => {
let paint_str = paint_svg(paint, defs, id)
match area {
Outline(w) =>
body.write_string(
"",
)
_ =>
body.write_string(
"",
)
}
}
FillViewport(paint) =>
body.write_string(
"",
)
RasterCell(x, y, w, h, c) =>
body.write_string(
"",
)
PushClip(path, area) => {
let cid = "clip\{id[0]}"
id[0] = id[0] + 1
defs.write_string(
"",
)
body.write_string("")
}
PopClip => body.write_string("")
PushOpacity(a) => body.write_string("")
PopOpacity => body.write_string("")
DrawText(content, x, y, size, color) =>
body.write_string(
"\{svg_xml_escape(content)}",
)
}
}
let defs_str = defs.to_string()
let defs_block = if defs_str.length() > 0 {
"\{defs_str}"
} else {
""
}
"\n" +
""
}
///|
fn svg_rule(area : Area) -> String {
match area {
Anz => "nonzero"
Aeo => "evenodd"
Outline(_) => "nonzero" // strokes are handled before svg_rule
}
}
///|
/// SVG paint for a baked `Paint`; registers a gradient in `defs` if needed.
fn paint_svg(paint : Paint, defs : StringBuilder, id : Array[Int]) -> String {
match paint {
Solid(c) => svg_color(c)
Linear(stops, p0, p1) => {
let gid = "grad\{id[0]}"
id[0] = id[0] + 1
defs.write_string(
"\{svg_stops(stops)}",
)
"url(#\{gid})"
}
Radial(stops, c, r) => {
let gid = "grad\{id[0]}"
id[0] = id[0] + 1
defs.write_string(
"\{svg_stops(stops)}",
)
"url(#\{gid})"
}
}
}
///|
fn svg_stops(stops : Array[Stop]) -> String {
let parts : Array[String] = []
for s in stops {
let opacity = if s.color.a < 1.0 {
" stop-opacity=\"\{s.color.a}\""
} else {
""
}
parts.push(
"",
)
}
parts.join("")
}
///|
fn svg_color(c : Color) -> String {
if c.a < 1.0 {
let r = (c.r * 255.0).to_int()
let g = (c.g * 255.0).to_int()
let b = (c.b * 255.0).to_int()
"rgba(\{r},\{g},\{b},\{c.a})"
} else {
@color.to_hex(c)
}
}
///|
/// SVG `d` attribute for a (baked) path.
fn svg_path_d(path : Path) -> String {
let parts : Array[String] = []
for seg in path.0 {
match seg {
MoveTo(p) => parts.push("M \{p.x},\{p.y}")
LineTo(p) => parts.push("L \{p.x},\{p.y}")
CurveTo(c1, c2, e) =>
parts.push("C \{c1.x},\{c1.y} \{c2.x},\{c2.y} \{e.x},\{e.y}")
QCurveTo(c, e) => parts.push("Q \{c.x},\{c.y} \{e.x},\{e.y}")
EArcTo(rx, ry, rot, large, sweep, e) => {
let lf = if large { "1" } else { "0" }
let sf = if sweep { "1" } else { "0" }
parts.push("A \{rx} \{ry} \{rot} \{lf} \{sf} \{e.x},\{e.y}")
}
Close => parts.push("Z")
}
}
parts.join(" ")
}
///|
/// Escape text for XML content/attributes.
fn svg_xml_escape(s : String) -> String {
let parts : Array[String] = []
for ch in s {
parts.push(
match ch {
'&' => "&"
'<' => "<"
'>' => ">"
'"' => """
'\'' => "'"
_ => ch.to_string()
},
)
}
parts.join("")
}