From 5368951254e89a92122c7e90e9cb02a8bdc727c3 Mon Sep 17 00:00:00 2001 From: Schrottkatze Date: Sat, 16 Mar 2024 20:52:45 +0100 Subject: [PATCH] svg-filters: rework codegen --- crates/svg-filters/src/codegen.rs | 229 ++++++++++++++++++++ crates/svg-filters/src/lib.rs | 6 +- crates/svg-filters/src/main.rs | 58 ++--- crates/svg-filters/src/types.rs | 284 +++++-------------------- crates/svg-filters/src/types/length.rs | 29 ++- crates/svg-filters/src/types/nodes.rs | 22 +- 6 files changed, 361 insertions(+), 267 deletions(-) create mode 100644 crates/svg-filters/src/codegen.rs diff --git a/crates/svg-filters/src/codegen.rs b/crates/svg-filters/src/codegen.rs new file mode 100644 index 0000000..ca0baa3 --- /dev/null +++ b/crates/svg-filters/src/codegen.rs @@ -0,0 +1,229 @@ +use std::{ + borrow::Cow, + cmp, + collections::{BTreeSet, HashMap, HashSet}, +}; + +use indexmap::IndexMap; +use petgraph::{ + algo::toposort, + graph::DiGraph, + prelude::{EdgeIndex, NodeIndex}, +}; +use quick_xml::{events::attributes::Attribute, name::QName, ElementWriter}; + +use crate::{ + types::{ + graph::{edge::Edge, FilterGraph}, + nodes::{primitives::WriteElement, CommonAttrs}, + }, + Node, +}; + +use self::error::CodegenError; + +pub struct SvgDocument { + filters: HashMap, +} + +impl SvgDocument { + pub fn new() -> Self { + Self { + filters: HashMap::new(), + } + } + + #[allow(clippy::unwrap_used, reason = "we literally just did the insertion")] + pub fn create_filter(&mut self, id: impl ToString) -> &mut FilterGraph { + let filter = FilterGraph::new(); + + self.filters.insert(id.to_string(), filter); + self.filters.get_mut(&id.to_string()).unwrap() + } + + pub fn generate_svg(&self) -> String { + let mut result = Vec::new(); + let mut doc_writer = quick_xml::Writer::new_with_indent(&mut result, b' ', 4); + + doc_writer + .create_element("svg") + .write_inner_content(|writer| { + self.filters + .iter() + .try_fold(writer, Self::gen_filter) + .map(|_| {}) + }); + + String::from_utf8_lossy(&result).to_string() + } + + fn gen_filter<'w, 'r>( + writer: &'w mut quick_xml::Writer<&'r mut Vec>, + (id, graph): (&String, &FilterGraph), + ) -> Result<&'w mut quick_xml::Writer<&'r mut Vec>, CodegenError> { + writer + .create_element("filter") + .with_attribute(("id", id.as_str())) + .write_inner_content(|writer| Self::graph_to_svg(writer, graph)) + } + + fn graph_to_svg( + writer: &mut quick_xml::Writer<&mut Vec>, + graph: &FilterGraph, + ) -> Result<(), CodegenError> { + let sorted = toposort(&graph.dag, None).expect("no cycles allowed in a DAG"); + sorted + .into_iter() + .filter_map(|node_idx| { + graph + .dag + .node_weight(node_idx) + .and_then(|node| node.primitive()) + .map(|(primitive, common_attrs)| (node_idx, primitive, common_attrs)) + }) + .try_fold(writer, |writer, (node_idx, primitive, common_attrs)| { + let mut el_writer = primitive.element_writer(writer); + el_writer = input_attrs(node_idx, &graph.dag).write_into(el_writer); + el_writer = output_attrs(node_idx, &graph.dag).write_into(el_writer); + el_writer = Attrs::from(*common_attrs).write_into(el_writer); + el_writer.write_empty() + })?; + + Ok(()) + } +} + +struct Attrs(IndexMap); + +impl Attrs { + fn write_into<'w, 'b>( + self, + el_writer: ElementWriter<'w, &'b mut Vec>, + ) -> ElementWriter<'w, &'b mut Vec> { + let attrs = self.0.iter().map(|(k, v)| (k.as_str(), v.as_str())); + el_writer.with_attributes(attrs) + } +} + +impl From for Attrs { + fn from(value: CommonAttrs) -> Self { + let CommonAttrs { + x, + y, + width, + height, + } = value; + Self(IndexMap::from([ + ("x".to_owned(), x.to_string()), + ("y".to_owned(), y.to_string()), + ("width".to_owned(), width.to_string()), + ("height".to_owned(), height.to_string()), + ])) + } +} + +#[allow( + clippy::unwrap_used, + reason = "in all cases of use of unwrap, it's values we got from the graph so we know it's safe" +)] +fn input_attrs(node_idx: NodeIndex, g: &DiGraph) -> Attrs { + let inputs: Vec = g + .neighbors_directed(node_idx, petgraph::Direction::Incoming) + .collect(); + let node = g.node_weight(node_idx).unwrap(); + + if node.input_count() as usize != inputs.len() { + todo!("proper error handling for wrong numbers of inputs") + } + + let mut inputs = inputs + .into_iter() + .map(|input_idx| { + let edge_idx = g.find_edge(input_idx, node_idx).unwrap(); + let edge = g.edge_weight(edge_idx).unwrap(); + let input_node = g.node_weight(input_idx).unwrap(); + + (input_node, edge_idx, edge) + }) + .collect::>(); + inputs.sort_by(|a, b| a.2.cmp(b.2)); + + let mut uniq = BTreeSet::new(); + let no_duplicates = inputs.iter().all(|(_, _, edge)| uniq.insert(edge)); + + if no_duplicates { + Attrs( + inputs + .into_iter() + .map(|(input, edge_idx, edge)| { + let name = match input { + Node::StdInput(s) => format!("{s:?}"), + Node::Primitive { .. } => format_edge_idx(edge_idx), + }; + (edge.to_string(), name) + }) + .collect(), + ) + } else { + todo!("better error handling for inputs with multiple values") + } +} + +#[allow( + clippy::unwrap_used, + reason = "in all cases of use of unwrap, it's values we got from the graph so we know it's safe" +)] +fn output_attrs(node_idx: NodeIndex, g: &DiGraph) -> Attrs { + let outputs: Vec = g + .neighbors_directed(node_idx, petgraph::Direction::Outgoing) + .collect(); + + match outputs.len().cmp(&1) { + cmp::Ordering::Less => Attrs(IndexMap::new()), + cmp::Ordering::Equal => { + let output = outputs.first().unwrap(); + let edge_idx = g.find_edge(node_idx, *output).unwrap(); + Attrs(IndexMap::from([( + "result".to_string(), + format_edge_idx(edge_idx), + )])) + } + cmp::Ordering::Greater => todo!("better error handling for too many outputs"), + } +} + +/// convenience method to avoid fuckups during future changes +fn format_edge_idx(idx: EdgeIndex) -> String { + format!("edge{}", idx.index()) +} + +mod error { + use std::{error::Error, fmt::Display}; + + #[derive(Debug)] + pub enum CodegenError { + QuickXmlError(quick_xml::Error), + } + + impl From for CodegenError { + fn from(value: quick_xml::Error) -> Self { + Self::QuickXmlError(value) + } + } + + impl Display for CodegenError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CodegenError::QuickXmlError(e) => e.fmt(f), + } + } + } + + impl Error for CodegenError {} +} + +impl Default for SvgDocument { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/svg-filters/src/lib.rs b/crates/svg-filters/src/lib.rs index a87a611..cc443d8 100644 --- a/crates/svg-filters/src/lib.rs +++ b/crates/svg-filters/src/lib.rs @@ -1,6 +1,8 @@ #![feature(lint_reasons)] +pub mod codegen; pub mod types; pub use types::nodes::Node; -pub use types::Edge; -pub use types::Filter; + +#[cfg(test)] +mod tests {} diff --git a/crates/svg-filters/src/main.rs b/crates/svg-filters/src/main.rs index fb6a782..7caef27 100644 --- a/crates/svg-filters/src/main.rs +++ b/crates/svg-filters/src/main.rs @@ -1,11 +1,12 @@ +use std::hint::black_box; + use svg_filters::{ - types::nodes::{primitives::color_matrix::ColorMatrixType, standard_input::StandardInput}, - Edge, Filter, Node, + codegen::SvgDocument, + types::{graph::edge::Edge, nodes::primitives::color_matrix::ColorMatrixType}, + Node, }; fn main() { - let mut filter = Filter::new(); - // // // @@ -21,37 +22,42 @@ fn main() { // // - let chan_r = filter.add_node(Node::color_matrix(ColorMatrixType::Matrix(Box::new([ + let mut doc = SvgDocument::new(); + let chromabb = doc.create_filter("chromabb_gen"); + + let chan_r = chromabb.add_node(Node::color_matrix(ColorMatrixType::Matrix(Box::new([ 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., ])))); - let chan_g = filter.add_node(Node::color_matrix(ColorMatrixType::Matrix(Box::new([ + let chan_g = chromabb.add_node(Node::color_matrix(ColorMatrixType::Matrix(Box::new([ 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., ])))); - let chan_b = filter.add_node(Node::color_matrix(ColorMatrixType::Matrix(Box::new([ + let chan_b = chromabb.add_node(Node::color_matrix(ColorMatrixType::Matrix(Box::new([ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 1., 0., ])))); - let offset_r = filter.add_node(Node::offset(25., 0.)); - let offset_b = filter.add_node(Node::offset(-25., 0.)); - let blur_r = filter.add_node(Node::gaussian_blur_xy(5, 0)); - let blur_b = filter.add_node(Node::gaussian_blur_xy(5, 0)); + let offset_r = chromabb.add_node(Node::offset(25., 0.)); + let offset_b = chromabb.add_node(Node::offset(-25., 0.)); + let blur_r = chromabb.add_node(Node::gaussian_blur_xy(5, 0)); + let blur_b = chromabb.add_node(Node::gaussian_blur_xy(5, 0)); - let composite_rb = filter.add_node(Node::composite_arithmetic(0., 1., 1., 0.)); - let composite_final = filter.add_node(Node::composite_arithmetic(0., 1., 1., 0.)); + let composite_rb = chromabb.add_node(Node::composite_arithmetic(0., 1., 1., 0.)); + let composite_final = chromabb.add_node(Node::composite_arithmetic(0., 1., 1., 0.)); - filter.graph.extend_with_edges(&[ - (filter.source_graphic(), chan_r, Edge::unnamed()), - (filter.source_graphic(), chan_b, Edge::unnamed()), - (filter.source_graphic(), chan_g, Edge::unnamed()), - (chan_r, offset_r, Edge::new("ro")), - (offset_r, blur_r, Edge::new("rob")), - (chan_b, offset_b, Edge::new("bo")), - (offset_b, blur_b, Edge::new("bob")), - (blur_r, composite_rb, Edge::new("robc").with_idx(0)), - (blur_b, composite_rb, Edge::new("bobc").with_idx(1)), - (composite_rb, composite_final, Edge::new("cf").with_idx(0)), - (chan_g, composite_final, Edge::new("gf").with_idx(1)), + chromabb.dag.extend_with_edges(&[ + (chromabb.source_graphic(), chan_r), + (chromabb.source_graphic(), chan_b), + (chromabb.source_graphic(), chan_g), + (chan_r, offset_r), + (offset_r, blur_r), + (chan_b, offset_b), + (offset_b, blur_b), + ]); + chromabb.dag.extend_with_edges(&[ + (blur_r, composite_rb, Edge::new(0)), + (blur_b, composite_rb, Edge::new(1)), + (composite_rb, composite_final, Edge::new(0)), + (chan_g, composite_final, Edge::new(1)), ]); - println!("{}", filter.to_svg()) + black_box(doc.generate_svg()); } diff --git a/crates/svg-filters/src/types.rs b/crates/svg-filters/src/types.rs index 325a869..fa3941e 100644 --- a/crates/svg-filters/src/types.rs +++ b/crates/svg-filters/src/types.rs @@ -1,250 +1,70 @@ -use core::panic; -use std::{ - borrow::Cow, - collections::{HashMap, HashSet}, - fmt::Debug, - io::BufWriter, - primitive, -}; - -use petgraph::{ - adj::EdgeIndex, - algo::{toposort, DfsSpace}, - data::{Build, DataMap}, - graph::DiGraph, - graphmap::DiGraphMap, - prelude::NodeIndex, - visit::NodeIndexable, -}; -use quick_xml::{events::attributes::Attribute, name::QName, ElementWriter, Error}; - pub mod length; pub mod nodes; -use crate::types::nodes::primitives::color_matrix::{ColorMatrix, ColorMatrixType}; +// pub mod old; -use self::{ - length::{Coordinate, Length}, - nodes::{ - primitives::{FePrimitive, WriteElement}, - Node, - }, -}; +pub mod graph { + use std::iter::Iterator; -#[derive(Debug)] -pub struct Filter<'a> { - pub graph: DiGraph>, - source_graphic_idx: NodeIndex, -} + use petgraph::{data::Build, prelude::*}; -impl Filter<'_> { - pub fn new() -> Self { - let mut graph = DiGraph::new(); - let source_graphic_idx = graph.add_node(Node::StdInput( - nodes::standard_input::StandardInput::SourceGraphic, - )); + use crate::Node; - Self { - graph, - source_graphic_idx, + use self::edge::Edge; + + use super::nodes::standard_input::StandardInput; + + #[derive(Debug)] + pub struct FilterGraph { + pub dag: DiGraph, + source_graphic_idx: NodeIndex, + } + + impl FilterGraph { + pub fn new() -> Self { + let mut dag = DiGraph::new(); + let source_graphic_idx = dag.add_node(Node::StdInput(StandardInput::SourceGraphic)); + + Self { + dag, + source_graphic_idx, + } + } + + pub fn add_node(&mut self, node: Node) -> NodeIndex { + self.dag.add_node(node) + } + + pub fn source_graphic(&self) -> NodeIndex { + self.source_graphic_idx } } - pub fn add_node(&mut self, node: Node) -> NodeIndex { - self.graph.add_node(node) - } - - pub fn source_graphic(&self) -> NodeIndex { - self.source_graphic_idx - } - - pub fn to_svg(&self) -> String { - let mut result = Vec::new(); - // let mut doc_writer = quick_xml::Writer::new(&mut result); - let mut doc_writer = quick_xml::Writer::new_with_indent(&mut result, b' ', 2); - - doc_writer - .create_element("filter") - .with_attribute(("id", "chromabb_gen")) - .write_inner_content(|writer| { - let mut dfs_space = DfsSpace::new(&self.graph); - let sorted = - toposort(&self.graph, Some(&mut dfs_space)).expect("No cycles! Bad user!"); - - let v = sorted - .into_iter() - .filter_map(|node_idx| { - let node = self - .graph - .node_weight(node_idx) - .expect("toposorting will not return invalid indices"); - - if let Node::Primitive { - primitive, - common_attrs, - } = node - { - Some((node_idx, primitive, common_attrs)) - } else { - None - } - }) - .try_fold(writer, |acc, (node_idx, primitive, common_attrs)| { - let mut el_writer = primitive.element_writer(&mut *acc); - el_writer = create_input_attr( - &self.graph, - el_writer, - node_idx, - self.graph - .node_weight(node_idx) - .expect("cannot get invalid node_idx from toposort") - .input_count(), - ); - create_output_attr(&self.graph, el_writer, node_idx).write_empty() - }) - .map(|_| ()); - - Ok::<(), Error>(()) - }) - .expect("shouldnt fail to write or something"); - String::from_utf8_lossy(&result).to_string() - } -} - -impl Default for Filter<'_> { - fn default() -> Self { - Self::new() - } -} - -#[derive(Debug, Clone)] -pub struct Edge<'a> { - pub edge_type: EdgeType<'a>, - /// the index of the `in` attribute on the target element - /// if None, just `in` - pub in_idx: Option, -} - -impl<'a> Edge<'a> { - pub fn new(name: &'a str) -> Self { - Self { - edge_type: EdgeType::Named(name), - in_idx: None, + pub mod edge { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] + pub struct Edge { + input_idx: u8, } - } - #[must_use] - pub fn with_idx(mut self, idx: u8) -> Self { - self.in_idx = Some(idx); - self - } - - pub fn unnamed() -> Self { - Self { - edge_type: EdgeType::Unnamed, - in_idx: None, + impl Edge { + pub fn new(input_idx: u8) -> Self { + Self { input_idx } + } } - } -} -impl Default for Edge<'_> { - fn default() -> Self { - Self::unnamed() - } -} + impl Default for Edge { + fn default() -> Self { + Self { input_idx: 0 } + } + } -#[derive(Debug, Clone)] -pub enum EdgeType<'a> { - Named(&'a str), - /// For standard inputs such as SourceGraphic etc., which we'll just be representing as nodes for simplicity - Unnamed, -} - -fn create_input_attr<'w, 'b>( - g: &'_ DiGraph>, - mut el_writer: ElementWriter<'w, &'b mut Vec>, - node_idx: NodeIndex, - input_count: u8, -) -> ElementWriter<'w, &'b mut Vec> { - let inputs = g - .neighbors_directed(node_idx, petgraph::Direction::Incoming) - .collect::>(); - - if inputs.len() != input_count as usize { - // TODO: better error handling - panic!("input couns didnt match"); - } - - inputs - .into_iter() - .enumerate() - .fold(el_writer, |el_writer, (i, incoming_idx)| { - let incoming_node = g.node_weight(incoming_idx).expect("cannot fail here"); - - // find incoming edge and get weight - let Edge { edge_type, in_idx } = g - .edge_weight( - g.find_edge(incoming_idx, node_idx) - .expect("there should always be an edge"), - ) - .expect("once again, should always exist"); - - let in_attr_name = match in_idx { - None | Some(0) => "in".to_owned(), - Some(n) => format!("in{}", n + 1), - }; - - let v = match incoming_node { - Node::StdInput(std_in) => format!("{std_in:?}"), - Node::Primitive { .. } => { - if let EdgeType::Named(name) = edge_type { - (*name).to_owned() - } else { - panic!( - "unnamed edges should not be used for connections between primitives" - ) - } + impl ToString for Edge { + fn to_string(&self) -> String { + match self.input_idx { + 0 => "in".to_owned(), + n => format!("in{}", n + 1), } - }; - - el_writer.with_attribute((in_attr_name.as_str(), v.as_str())) - }) -} - -#[allow(clippy::unwrap_used, reason = "all unwraps are for finding on options")] -fn create_output_attr<'w, 'b>( - g: &'_ DiGraph>, - mut el_writer: ElementWriter<'w, &'b mut Vec>, - node_idx: NodeIndex, -) -> ElementWriter<'w, &'b mut Vec> { - let output = g - .neighbors_directed(node_idx, petgraph::Direction::Outgoing) - .map(|neighbor_idx| { - let edge_idx = g.find_edge(node_idx, neighbor_idx).unwrap(); - - let Edge { - edge_type: EdgeType::Named(name), - .. - } = g.edge_weight(edge_idx).unwrap() - else { - panic!("Unnamed edge used for connection between primitives"); - }; - *name - }) - .collect::>(); - - if output.is_empty() { - el_writer - } else if output.len() == 1 { - el_writer.with_attribute(Attribute { - key: QName(b"result"), - value: Cow::from( - (*output.into_iter().collect::>().first().unwrap()) - .to_string() - .into_bytes(), - ), - }) - } else { - panic!("Can't have more then one named output: {output:?}") + } + } } } diff --git a/crates/svg-filters/src/types/length.rs b/crates/svg-filters/src/types/length.rs index 1186181..10ddfcc 100644 --- a/crates/svg-filters/src/types/length.rs +++ b/crates/svg-filters/src/types/length.rs @@ -1,8 +1,17 @@ -#[derive(Default, Debug)] +use std::fmt::Display; + +#[derive(Default, Debug, Clone, Copy)] pub struct Length(f32, Unit); + +impl Display for Length { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}{}", self.0, self.1) + } +} + pub type Coordinate = Length; -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone, Copy)] pub enum Unit { #[default] None, @@ -15,3 +24,19 @@ pub enum Unit { Pt, Pc, } + +impl Display for Unit { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Unit::None => f.write_str(""), + Unit::Em => f.write_str("em"), + Unit::Ex => f.write_str("ex"), + Unit::Px => f.write_str("px"), + Unit::In => f.write_str("in"), + Unit::Cm => f.write_str("cm"), + Unit::Mm => f.write_str("mm"), + Unit::Pt => f.write_str("pt"), + Unit::Pc => f.write_str("pc"), + } + } +} diff --git a/crates/svg-filters/src/types/nodes.rs b/crates/svg-filters/src/types/nodes.rs index 63e041c..80cb5d0 100644 --- a/crates/svg-filters/src/types/nodes.rs +++ b/crates/svg-filters/src/types/nodes.rs @@ -34,12 +34,12 @@ impl Default for Node { } } -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone, Copy)] pub(crate) struct CommonAttrs { - x: Coordinate, - y: Coordinate, - width: Length, - height: Length, + pub x: Coordinate, + pub y: Coordinate, + pub width: Length, + pub height: Length, } impl Node { @@ -50,6 +50,18 @@ impl Node { } } + pub fn primitive(&self) -> Option<(&FePrimitive, &CommonAttrs)> { + if let Node::Primitive { + primitive, + common_attrs, + } = self + { + Some((primitive, common_attrs)) + } else { + None + } + } + pub fn input_count(&self) -> u8 { match self { Node::Primitive {