use std::fmt::Write;
#[derive(Debug, Clone)]
pub enum PathCommand {
MoveTo { x: f64, y: f64 },
LineTo { x: f64, y: f64 },
CubicBezier { x1: f64, y1: f64, x2: f64, y2: f64, x: f64, y: f64 },
QuadraticBezier { x1: f64, y1: f64, x: f64, y: f64 },
Arc { rx: f64, ry: f64, x_axis_rotation: f64, large_arc: bool, sweep: bool, x: f64, y: f64 },
ClosePath,
}
impl PathCommand {
pub fn to_svg(&self) -> String {
match self {
Self::MoveTo { x, y } => format!("M {} {}", x, y),
Self::LineTo { x, y } => format!("L {} {}", x, y),
Self::CubicBezier { x1, y1, x2, y2, x, y } => {
format!("C {} {} {} {} {} {}", x1, y1, x2, y2, x, y)
}
Self::QuadraticBezier { x1, y1, x, y } => {
format!("Q {} {} {} {}", x1, y1, x, y)
}
Self::Arc { rx, ry, x_axis_rotation, large_arc, sweep, x, y } => {
format!(
"A {} {} {} {} {} {} {}",
rx,
ry,
x_axis_rotation,
if *large_arc { 1 } else { 0 },
if *sweep { 1 } else { 0 },
x,
y
)
}
Self::ClosePath => "Z".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct SvgPath {
pub commands: Vec<PathCommand>,
pub stroke: Option<String>,
pub stroke_width: f64,
pub fill: Option<String>,
pub fill_opacity: f64,
pub stroke_opacity: f64,
}
impl Default for SvgPath {
fn default() -> Self {
Self {
commands: Vec::new(),
stroke: Some("#000000".to_string()),
stroke_width: 1.0,
fill: None,
fill_opacity: 1.0,
stroke_opacity: 1.0,
}
}
}
impl SvgPath {
pub fn new(commands: Vec<PathCommand>) -> Self {
Self {
commands,
..Default::default()
}
}
pub fn with_stroke(mut self, color: String) -> Self {
self.stroke = Some(color);
self
}
pub fn with_stroke_width(mut self, width: f64) -> Self {
self.stroke_width = width;
self
}
pub fn with_fill(mut self, color: String) -> Self {
self.fill = Some(color);
self
}
pub fn with_fill_opacity(mut self, opacity: f64) -> Self {
self.fill_opacity = opacity;
self
}
pub fn with_stroke_opacity(mut self, opacity: f64) -> Self {
self.stroke_opacity = opacity;
self
}
pub fn to_svg(&self) -> String {
let mut path_data = String::new();
for cmd in &self.commands {
if !path_data.is_empty() {
path_data.push(' ');
}
path_data.push_str(&cmd.to_svg());
}
let mut attrs = format!(r#"d="{}""#, path_data);
if let Some(ref stroke) = self.stroke {
write!(attrs, r#" stroke="{}""#, stroke).unwrap();
} else {
attrs.push_str(r#" stroke="none""#);
}
write!(attrs, r#" stroke-width="{}""#, self.stroke_width).unwrap();
if let Some(ref fill) = self.fill {
write!(attrs, r#" fill="{}""#, fill).unwrap();
} else {
attrs.push_str(r#" fill="none""#);
}
if self.fill_opacity < 1.0 {
write!(attrs, r#" fill-opacity="{}""#, self.fill_opacity).unwrap();
}
if self.stroke_opacity < 1.0 {
write!(attrs, r#" stroke-opacity="{}""#, self.stroke_opacity).unwrap();
}
format!(r#"<path {} />"#, attrs)
}
}
#[derive(Debug, Clone)]
pub struct SvgRect {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
pub fill: Option<String>,
pub stroke: Option<String>,
pub stroke_width: f64,
}
impl SvgRect {
pub fn to_svg(&self) -> String {
let mut attrs = format!(
r#"x="{}" y="{}" width="{}" height="{}""#,
self.x, self.y, self.width, self.height
);
if let Some(ref fill) = self.fill {
write!(attrs, r#" fill="{}""#, fill).unwrap();
} else {
attrs.push_str(r#" fill="none""#);
}
if let Some(ref stroke) = self.stroke {
write!(attrs, r#" stroke="{}""#, stroke).unwrap();
write!(attrs, r#" stroke-width="{}""#, self.stroke_width).unwrap();
}
format!(r#"<rect {} />"#, attrs)
}
}
#[derive(Debug, Clone)]
pub struct SvgEllipse {
pub cx: f64,
pub cy: f64,
pub rx: f64,
pub ry: f64,
pub fill: Option<String>,
pub stroke: Option<String>,
pub stroke_width: f64,
}
impl SvgEllipse {
pub fn to_svg(&self) -> String {
let mut attrs = format!(
r#"cx="{}" cy="{}" rx="{}" ry="{}""#,
self.cx, self.cy, self.rx, self.ry
);
if let Some(ref fill) = self.fill {
write!(attrs, r#" fill="{}""#, fill).unwrap();
} else {
attrs.push_str(r#" fill="none""#);
}
if let Some(ref stroke) = self.stroke {
write!(attrs, r#" stroke="{}""#, stroke).unwrap();
write!(attrs, r#" stroke-width="{}""#, self.stroke_width).unwrap();
}
format!(r#"<ellipse {} />"#, attrs)
}
}
#[derive(Debug, Clone)]
pub struct SvgText {
pub x: f64,
pub y: f64,
pub text: String,
pub font_size: f64,
pub font_family: Option<String>,
pub fill: Option<String>,
}
impl SvgText {
pub fn to_svg(&self) -> String {
let mut attrs = format!(r#"x="{}" y="{}" font-size="{}""#, self.x, self.y, self.font_size);
if let Some(ref family) = self.font_family {
write!(attrs, r#" font-family="{}""#, family).unwrap();
}
if let Some(ref fill) = self.fill {
write!(attrs, r#" fill="{}""#, fill).unwrap();
}
format!(r#"<text {}>{}</text>"#, attrs, self.text)
}
}
#[derive(Debug, Clone)]
pub struct SvgImage {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
pub href: String,
}
impl SvgImage {
pub fn from_png_data(x: f64, y: f64, width: f64, height: f64, png_data: &[u8]) -> Self {
use base64::Engine;
let base64_engine = base64::engine::general_purpose::STANDARD;
let encoded = base64_engine.encode(png_data);
let href = format!("data:image/png;base64,{}", encoded);
Self {
x,
y,
width,
height,
href,
}
}
pub fn from_jpeg_data(x: f64, y: f64, width: f64, height: f64, jpeg_data: &[u8]) -> Self {
use base64::Engine;
let base64_engine = base64::engine::general_purpose::STANDARD;
let encoded = base64_engine.encode(jpeg_data);
let href = format!("data:image/jpeg;base64,{}", encoded);
Self {
x,
y,
width,
height,
href,
}
}
pub fn to_svg(&self) -> String {
format!(
r#"<image x="{}" y="{}" width="{}" height="{}" href="{}" />"#,
self.x, self.y, self.width, self.height, self.href
)
}
}
#[derive(Debug, Clone)]
pub enum SvgElement {
Path(SvgPath),
Rect(SvgRect),
Ellipse(SvgEllipse),
Text(SvgText),
Image(SvgImage),
}
impl SvgElement {
pub fn to_svg(&self) -> String {
match self {
Self::Path(p) => p.to_svg(),
Self::Rect(r) => r.to_svg(),
Self::Ellipse(e) => e.to_svg(),
Self::Text(t) => t.to_svg(),
Self::Image(i) => i.to_svg(),
}
}
}
#[derive(Debug, Clone)]
pub struct SvgBuilder {
pub width: f64,
pub height: f64,
pub viewbox: Option<(f64, f64, f64, f64)>,
pub elements: Vec<SvgElement>,
}
impl SvgBuilder {
pub fn new(width: f64, height: f64) -> Self {
Self {
width,
height,
viewbox: None,
elements: Vec::new(),
}
}
pub fn with_viewbox(mut self, x: f64, y: f64, width: f64, height: f64) -> Self {
self.viewbox = Some((x, y, width, height));
self
}
pub fn add_element(&mut self, element: SvgElement) {
self.elements.push(element);
}
pub fn add_path(&mut self, path: SvgPath) {
self.elements.push(SvgElement::Path(path));
}
pub fn add_rect(&mut self, rect: SvgRect) {
self.elements.push(SvgElement::Rect(rect));
}
pub fn add_ellipse(&mut self, ellipse: SvgEllipse) {
self.elements.push(SvgElement::Ellipse(ellipse));
}
pub fn add_text(&mut self, text: SvgText) {
self.elements.push(SvgElement::Text(text));
}
pub fn add_image(&mut self, image: SvgImage) {
self.elements.push(SvgElement::Image(image));
}
pub fn build(&self) -> String {
let mut svg = String::new();
svg.push_str(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
svg.push('\n');
svg.push_str(r#"<svg xmlns="http://www.w3.org/2000/svg" "#);
write!(svg, r#"width="{}" height="{}""#, self.width, self.height).unwrap();
if let Some((x, y, w, h)) = self.viewbox {
write!(svg, r#" viewBox="{} {} {} {}""#, x, y, w, h).unwrap();
}
svg.push_str(">\n");
for element in &self.elements {
svg.push_str(" ");
svg.push_str(&element.to_svg());
svg.push('\n');
}
svg.push_str("</svg>");
svg
}
pub fn build_bytes(&self) -> Vec<u8> {
self.build().into_bytes()
}
}
pub mod color {
pub fn rgb_to_hex(r: u8, g: u8, b: u8) -> String {
format!("#{:02X}{:02X}{:02X}", r, g, b)
}
pub fn colorref_to_hex(colorref: u32) -> String {
let r = (colorref & 0xFF) as u8;
let g = ((colorref >> 8) & 0xFF) as u8;
let b = ((colorref >> 16) & 0xFF) as u8;
rgb_to_hex(r, g, b)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_svg_path() {
let path = SvgPath::new(vec![
PathCommand::MoveTo { x: 0.0, y: 0.0 },
PathCommand::LineTo { x: 100.0, y: 100.0 },
])
.with_stroke("#000000".to_string());
let svg = path.to_svg();
assert!(svg.contains("M 0 0 L 100 100"));
}
#[test]
fn test_svg_builder() {
let mut builder = SvgBuilder::new(100.0, 100.0);
builder.add_rect(SvgRect {
x: 10.0,
y: 10.0,
width: 80.0,
height: 80.0,
fill: Some("#FF0000".to_string()),
stroke: None,
stroke_width: 0.0,
});
let svg = builder.build();
assert!(svg.contains("<svg"));
assert!(svg.contains("</svg>"));
assert!(svg.contains("<rect"));
}
#[test]
fn test_color_conversion() {
assert_eq!(color::rgb_to_hex(255, 0, 0), "#FF0000");
assert_eq!(color::colorref_to_hex(0x0000FF), "#FF0000");
}
}