the aliasing svg renderer
init
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | Cargo.toml | 13 | ||||
| -rw-r--r-- | README.md | 3 | ||||
| -rw-r--r-- | src/lib.rs | 2 | ||||
| -rw-r--r-- | src/main.rs | 53 | ||||
| -rw-r--r-- | src/render.rs | 62 | ||||
| -rw-r--r-- | src/tree.rs | 195 |
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; + } +} |