the aliasing svg renderer
bendn 2023-10-18
commit c19c4a2
-rw-r--r--.gitignore2
-rw-r--r--Cargo.toml13
-rw-r--r--README.md3
-rw-r--r--src/lib.rs2
-rw-r--r--src/main.rs53
-rw-r--r--src/render.rs62
-rw-r--r--src/tree.rs195
7 files changed, 330 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..96ef6c0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/target
+Cargo.lock
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..7a7bc9a
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "psvg"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyhow = "1.0.75"
+clap = { version = "4.4.6", features = ["derive"] }
+fimg = "0.4.16"
+tiny-skia-path = "0.11.2"
+usvg = { version = "0.36.0", default-features = false }
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a3b9d14
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# psvg
+
+aliasing svg renderer
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..8aa0377
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,2 @@
+mod render;
+pub mod tree;
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..e85bc71
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,53 @@
+use std::{path::PathBuf, str::FromStr};
+
+use anyhow::{anyhow, Result};
+use clap::Parser;
+use psvg::tree::Tree;
+
+#[derive(Parser)]
+
+/// PSVG: the curveless aliasing svg renderer
+struct Args {
+ #[arg(short = 's')]
+ /// Specify the size of the output png.
+ /// If not supplied, will render at the svg's set width and height.
+ /// Specify as: '144x124'
+ size: Option<Size>,
+ /// Svg to render.
+ file: PathBuf,
+ /// File to output rendered svg (png).
+ out: PathBuf,
+}
+
+#[derive(Copy, Clone, Debug, Default)]
+struct Size {
+ w: u32,
+ h: u32,
+}
+
+impl FromStr for Size {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let (w, h) = s.split_once('x').ok_or(anyhow!(
+ "please delimit width and height witih a 'x': 128x142"
+ ))?;
+ Ok(Size {
+ w: w.parse()?,
+ h: h.parse()?,
+ })
+ }
+}
+
+fn main() -> Result<()> {
+ let args = Args::parse();
+ let mut svg = Tree::new(&std::fs::read_to_string(args.file)?)?;
+ match args.size {
+ Some(Size { w, h }) => {
+ svg.resize(w as f32, h as f32);
+ }
+ None => svg.resize(svg.osize.width(), svg.osize.height()),
+ }
+ svg.render().save(args.out);
+ Ok(())
+}
diff --git a/src/render.rs b/src/render.rs
new file mode 100644
index 0000000..705a8ce
--- /dev/null
+++ b/src/render.rs
@@ -0,0 +1,62 @@
+use crate::tree::{Node, PathNode, Tree};
+use fimg::Image;
+use tiny_skia_path::{NormalizedF32, Path, Point};
+use usvg::Color;
+
+impl Tree {
+ #[must_use]
+ pub fn render(&self) -> Image<Box<[u8]>, 4> {
+ let mut canvas =
+ Image::alloc(self.width.round() as u32, self.height.round() as u32).boxed();
+ for node in &*self.children {
+ render(node, &mut canvas);
+ }
+ canvas
+ }
+}
+
+trait Col {
+ fn col(self, opacity: NormalizedF32) -> [u8; 4];
+}
+impl Col for Color {
+ fn col(self, opacity: NormalizedF32) -> [u8; 4] {
+ [self.red, self.green, self.blue, opacity.to_u8()]
+ }
+}
+
+fn point(p: &Path) -> Vec<(i32, i32)> {
+ let mut points = Vec::with_capacity(p.len() + 1);
+ let r = |p: Point| (p.x.round() as i32, p.y.round() as i32);
+ for &point in p.points() {
+ points.push(r(point));
+ }
+ points.push(r(*p.points().first().unwrap()));
+ points
+}
+
+fn render(node: &Node, img: &mut Image<Box<[u8]>, 4>) {
+ match node {
+ Node::Path(PathNode::Fill {
+ color,
+ opacity,
+ path,
+ }) => {
+ img.points(&point(path), color.col(*opacity));
+ }
+ Node::Path(PathNode::Stroke {
+ color,
+ opacity,
+ path,
+ ..
+ // TODO: stroek
+ }) => img.points(&point(path), color.col(*opacity)),
+ Node::Group {
+ ..
+ } => {
+ // for child in &**children {
+ // render(child, img);
+ // }
+ }
+ t => unimplemented!("{t:?}"),
+ }
+}
diff --git a/src/tree.rs b/src/tree.rs
new file mode 100644
index 0000000..4aa2aab
--- /dev/null
+++ b/src/tree.rs
@@ -0,0 +1,195 @@
+use anyhow::Result;
+use tiny_skia_path::{NonZeroPositiveF32, NormalizedF32, Path, Rect, Size, Transform};
+use usvg::{BBox, Color, Fill, Image, Opacity, Options, Paint, TreeParsing};
+
+#[derive(Debug)]
+pub struct Tree {
+ pub width: f32,
+ pub height: f32,
+ pub children: Box<[Node]>,
+ pub osize: Size,
+}
+
+#[derive(Debug)]
+pub enum PathNode {
+ Fill {
+ color: Color,
+ opacity: Opacity,
+ path: Path,
+ },
+ Stroke {
+ color: Color,
+ opacity: Opacity,
+ stroke_color: Color,
+ stroke_opacity: Opacity,
+ stroke: NonZeroPositiveF32,
+ path: Path,
+ },
+}
+
+#[derive(Debug)]
+pub enum Node {
+ Group {
+ opacity: Opacity,
+ bbox: BBox,
+ children: Box<[Node]>,
+ },
+ Path(PathNode),
+ Image(Image),
+}
+
+impl Node {
+ fn transform(&mut self, t: Transform) {
+ match self {
+ Self::Path(PathNode::Fill { path, .. } | PathNode::Stroke { path, .. }) => {
+ *path = path.clone().transform(t).unwrap(); // idc
+ }
+ // TODO
+ _ => {}
+ }
+ }
+}
+
+impl From<usvg::Tree> for Tree {
+ fn from(tree: usvg::Tree) -> Self {
+ let mut children = vec![];
+ collect(tree.root, &mut children);
+ Self {
+ osize: tree.size,
+ width: tree.view_box.rect.width(),
+ height: tree.view_box.rect.height(),
+ children: children.into(),
+ }
+ }
+}
+
+trait PColor {
+ fn col(&self) -> Color;
+}
+
+impl PColor for Paint {
+ fn col(&self) -> Color {
+ match self {
+ Self::Color(c) => *c,
+ _ => unimplemented!(),
+ }
+ }
+}
+fn convert(node: usvg::Node, to: &mut Vec<Node>) -> Option<Rect> {
+ match &*node.clone().borrow() {
+ usvg::NodeKind::Group(g) => {
+ let mut children = vec![];
+ let bbox = collect(node, &mut children);
+ let mut children = children.into_boxed_slice();
+ for child in &mut *children {
+ child.transform(g.transform);
+ }
+ to.push(Node::Group {
+ opacity: g.opacity,
+ bbox,
+ children,
+ });
+ bbox.to_rect()
+ }
+ usvg::NodeKind::Path(usvg::Path {
+ stroke:
+ Some(usvg::Stroke {
+ paint: stroke_paint,
+ opacity: stroke_opacity,
+ width: stroke,
+ ..
+ }),
+ transform,
+ fill: Some(Fill { opacity, paint, .. }),
+ data: path,
+ ..
+ }) => {
+ to.push(Node::Path(PathNode::Stroke {
+ color: paint.col(),
+ opacity: *opacity,
+ stroke: *stroke,
+ stroke_opacity: *stroke_opacity,
+ stroke_color: stroke_paint.col(),
+ path: (**path).clone().transform(*transform).unwrap(),
+ }));
+ Some(path.bounds())
+ }
+ usvg::NodeKind::Path(usvg::Path {
+ stroke:
+ Some(usvg::Stroke {
+ paint: stroke_paint,
+ opacity: stroke_opacity,
+ width: stroke,
+ ..
+ }),
+ transform,
+ fill: None,
+ data: path,
+ ..
+ }) => {
+ to.push(Node::Path(PathNode::Stroke {
+ color: Color::black(),
+ opacity: NormalizedF32::new(0.0).unwrap(),
+ stroke: *stroke,
+ stroke_opacity: *stroke_opacity,
+ stroke_color: stroke_paint.col(),
+ path: (**path).clone().transform(*transform).unwrap(),
+ }));
+ Some(path.bounds())
+ }
+ usvg::NodeKind::Path(usvg::Path {
+ transform,
+ stroke: None,
+ fill: Some(Fill { opacity, paint, .. }),
+ data: path,
+ ..
+ }) => {
+ to.push(Node::Path(PathNode::Fill {
+ color: paint.col(),
+ opacity: *opacity,
+ path: (**path).clone().transform(*transform).unwrap(),
+ }));
+ Some(path.bounds())
+ }
+ usvg::NodeKind::Path(usvg::Path {
+ transform,
+ stroke: None,
+ fill: None,
+ data: path,
+ ..
+ }) => {
+ to.push(Node::Path(PathNode::Fill {
+ color: Color::black(),
+ opacity: NormalizedF32::new(0.0).unwrap(),
+ path: (**path).clone().transform(*transform).unwrap(),
+ }));
+ Some(path.bounds())
+ }
+ usvg::NodeKind::Image(_) => todo!(),
+ usvg::NodeKind::Text(_) => unimplemented!(),
+ }
+}
+
+fn collect(node: usvg::Node, to: &mut Vec<Node>) -> BBox {
+ let mut bbox = BBox::default();
+ for child in node.children() {
+ bbox = bbox.expand(collect(child.clone(), to));
+ bbox = bbox.expand(convert(child, to).map_or(BBox::default(), BBox::from));
+ }
+ bbox
+}
+
+impl Tree {
+ pub fn new(svg: &str) -> Result<Self> {
+ Ok(Tree::from(usvg::Tree::from_str(svg, &Options::default())?))
+ }
+
+ pub fn resize(&mut self, w: f32, h: f32) {
+ let t = Transform::from_scale(w / self.width, h / self.height);
+ for child in &mut *self.children {
+ child.transform(t);
+ }
+ self.width = w;
+ self.height = h;
+ }
+}