#![allow(clippy::similar_names)]
use std::cmp::Ordering;
pub trait PartialSvgExt {
fn as_svg_component(&self) -> String;
}
pub trait SvgExt {
#[must_use]
fn to_svg(&self) -> String;
#[cfg(feature = "extended-svg")]
#[cfg_attr(docsrs, doc(cfg(feature = "extended-svg")))]
fn to_svgz(&self) -> std::io::Result<Vec<u8>> {
use flate2::write::GzEncoder;
use std::io::Write;
let mut buffer = Vec::new();
let outline = self.to_svg();
let mut encoder = GzEncoder::new(&mut buffer, flate2::Compression::best());
encoder.write_all(outline.as_bytes())?;
encoder.finish()?;
Ok(buffer)
}
#[cfg(feature = "extended-svg")]
#[cfg_attr(docsrs, doc(cfg(feature = "extended-svg")))]
fn to_svg_dataimage_url(&self) -> std::io::Result<String> {
use base64::{engine::general_purpose::STANDARD, write::EncoderStringWriter};
use std::io::Write;
let buffer = self.to_svg().into_bytes();
let mut encoder = EncoderStringWriter::new(&STANDARD);
encoder.write_all(&buffer)?;
let data = encoder.into_inner();
let url = format!("data:image/svg+xml;base64,{data}",);
Ok(url)
}
}
#[derive(Debug, Clone, Copy)]
pub struct SvgProperties {
pub viewbox_position: (f32, f32),
pub viewbox_size: (f32, f32),
pub scale_to: Option<f32>,
pub margin: Option<f32>,
}
pub enum SvgPathComponent {
MoveTo(i16, i16),
HorizontalTo(i16),
VerticalTo(i16),
LineTo(i16, i16),
QuadraticBezier(i16, i16, i16, i16),
RelativeLineTo(i16, i16),
RelativeQuadraticBezier(i16, i16, i16, i16),
RelativeSmoothQuadraticBezier(i16, i16),
RelativeVerticalTo(i16),
RelativeHorizontalTo(i16),
Close,
}
impl SvgPathComponent {
pub fn render(path: &[Self]) -> String {
let mut out = String::with_capacity(path.len() * 12); let mut ctrl = ' ';
for component in path {
let (cmd, args) = component.components();
let mut skip_next = false;
if ctrl != cmd {
out.push(cmd);
ctrl = cmd;
skip_next = true;
}
let mut buffer = itoa::Buffer::new();
for c in args {
if c >= 0 && !skip_next {
out.push(' ');
}
out.push_str(buffer.format(c)); skip_next = false;
}
}
out
}
pub fn minify(path: &mut [Self]) {
if path.len() < 2 {
return;
}
let mut i = 1;
while i < path.len() {
let prev = &path[i - 1];
let curr = &path[i];
let (Some(prev_line), Some(curr_line)) =
(prev.line_components(), curr.line_components())
else {
i += 1;
continue;
};
let (dx, dy) = (prev_line.0.cmp(&curr_line.0), prev_line.1.cmp(&curr_line.1));
match (dx, dy) {
(Ordering::Equal, Ordering::Equal) => {
}
(Ordering::Equal, _) => {
path[i] = SvgPathComponent::VerticalTo(curr_line.1);
}
(_, Ordering::Equal) => {
path[i] = SvgPathComponent::HorizontalTo(curr_line.0);
}
_ => {}
}
i += 1;
}
let mut px = 0;
let mut py = 0;
let mut last_q = None; for component in path.iter_mut() {
match component {
Self::MoveTo(x, y) => {
px = *x;
py = *y;
}
Self::LineTo(x, y) => {
let (x, y) = (*x, *y);
let (dx, dy) = (x - px, y - py);
*component = Self::RelativeLineTo(dx, dy);
px = x;
py = y;
}
Self::QuadraticBezier(x1, y1, x, y) => {
let (x, y) = (*x, *y);
let (dx1, dy1, dx, dy) = (*x1 - px, *y1 - py, x - px, y - py);
*component = Self::RelativeQuadraticBezier(dx1, dy1, dx, dy);
px = x;
py = y;
}
Self::HorizontalTo(x) => {
let x = *x;
let dx = x - px;
*component = Self::RelativeHorizontalTo(dx);
px = x;
}
Self::VerticalTo(y) => {
let y = *y;
let dy = y - py;
*component = Self::RelativeVerticalTo(dy);
py = y;
}
_ => {}
}
match component {
Self::RelativeQuadraticBezier(x1, y1, x, y) => {
let (x1, y1, x, y) = (*x1, *y1, *x, *y);
if let Some((_, _, px, py)) = last_q {
if x1 == px && y1 == py {
*component = Self::RelativeSmoothQuadraticBezier(x, y);
}
}
last_q = Some((x1, y1, x, y));
}
_ => {
last_q = None;
}
}
}
}
pub fn line_components(&self) -> Option<(i16, i16)> {
match self {
Self::MoveTo(x, y) | Self::LineTo(x, y) => Some((*x, *y)),
Self::HorizontalTo(x) => Some((*x, i16::MAX)),
Self::VerticalTo(y) => Some((i16::MAX, *y)),
_ => None,
}
}
pub fn components(&self) -> (char, Vec<i16>) {
match self {
Self::MoveTo(x, y) => ('M', vec![*x, *y]),
Self::HorizontalTo(x) => ('H', vec![*x]),
Self::VerticalTo(y) => ('V', vec![*y]),
Self::LineTo(x, y) => ('L', vec![*x, *y]),
Self::QuadraticBezier(x1, y1, x2, y2) => ('Q', vec![*x1, *y1, *x2, *y2]),
Self::RelativeLineTo(x, y) => ('l', vec![*x, *y]),
Self::RelativeQuadraticBezier(x1, y1, x2, y2) => ('q', vec![*x1, *y1, *x2, *y2]),
Self::RelativeSmoothQuadraticBezier(x, y) => ('t', vec![*x, *y]),
Self::RelativeVerticalTo(y) => ('v', vec![*y]),
Self::RelativeHorizontalTo(x) => ('h', vec![*x]),
Self::Close => ('Z', vec![]),
}
}
}
pub fn wrap_svg_component(properties: &SvgProperties, component: &str) -> String {
let (width, height) = properties.viewbox_size;
let (xmin, ymin) = properties.viewbox_position;
let aspect_ratio = width / height;
let x_margin = properties.margin.unwrap_or_default();
let y_margin = x_margin / aspect_ratio;
let (xmin, ymin) = (xmin - x_margin, ymin - y_margin);
let (width, height) = (width + 2.0 * x_margin, height + 2.0 * y_margin);
let vwidth = properties.scale_to.unwrap_or(width);
let vheight = vwidth / aspect_ratio;
let vsize = format!("width='{vwidth}' height='{vheight}'");
let viewbox = format!("viewBox='{xmin} {ymin} {width} {height}'",);
format!("<svg xmlns='http://www.w3.org/2000/svg' style='background-color:#FFF' {vsize} {viewbox}>{component}</svg>")
}