svg-filters: rework codegen

This commit is contained in:
Schrottkatze 2024-03-16 20:52:45 +01:00
parent a42ec014e5
commit 5368951254
Signed by: schrottkatze
SSH key fingerprint: SHA256:hXb3t1vINBFCiDCmhRABHX5ocdbLiKyCdKI4HK2Rbbc
6 changed files with 361 additions and 267 deletions

View file

@ -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<String, FilterGraph>,
}
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<u8>>,
(id, graph): (&String, &FilterGraph),
) -> Result<&'w mut quick_xml::Writer<&'r mut Vec<u8>>, 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<u8>>,
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<String, String>);
impl Attrs {
fn write_into<'w, 'b>(
self,
el_writer: ElementWriter<'w, &'b mut Vec<u8>>,
) -> ElementWriter<'w, &'b mut Vec<u8>> {
let attrs = self.0.iter().map(|(k, v)| (k.as_str(), v.as_str()));
el_writer.with_attributes(attrs)
}
}
impl From<CommonAttrs> 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<Node, Edge>) -> Attrs {
let inputs: Vec<NodeIndex> = 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::<Vec<(&Node, EdgeIndex, &Edge)>>();
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<Node, Edge>) -> Attrs {
let outputs: Vec<NodeIndex> = 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<quick_xml::Error> 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()
}
}

View file

@ -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 {}

View file

@ -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();
// <filter id="chromabb" >
// <feColorMatrix values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" in="SourceGraphic" />
// <feOffset dx="25" dy="0" />
@ -21,37 +22,42 @@ fn main() {
// <feComposite in="rb" in2="grn" operator="arithmetic" k2="1" k3="1" />
// </filter>
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());
}

View file

@ -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<Node, Edge<'a>>,
use petgraph::{data::Build, prelude::*};
use crate::Node;
use self::edge::Edge;
use super::nodes::standard_input::StandardInput;
#[derive(Debug)]
pub struct FilterGraph {
pub dag: DiGraph<Node, Edge>,
source_graphic_idx: NodeIndex,
}
}
impl Filter<'_> {
impl FilterGraph {
pub fn new() -> Self {
let mut graph = DiGraph::new();
let source_graphic_idx = graph.add_node(Node::StdInput(
nodes::standard_input::StandardInput::SourceGraphic,
));
let mut dag = DiGraph::new();
let source_graphic_idx = dag.add_node(Node::StdInput(StandardInput::SourceGraphic));
Self {
graph,
dag,
source_graphic_idx,
}
}
pub fn add_node(&mut self, node: Node) -> NodeIndex {
self.graph.add_node(node)
self.dag.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()
pub mod edge {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Edge {
input_idx: u8,
}
}
impl Default for Filter<'_> {
impl Edge {
pub fn new(input_idx: u8) -> Self {
Self { input_idx }
}
}
impl Default for Edge {
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<u8>,
}
impl<'a> Edge<'a> {
pub fn new(name: &'a str) -> Self {
Self {
edge_type: EdgeType::Named(name),
in_idx: None,
Self { input_idx: 0 }
}
}
#[must_use]
pub fn with_idx(mut self, idx: u8) -> Self {
self.in_idx = Some(idx);
self
impl ToString for Edge {
fn to_string(&self) -> String {
match self.input_idx {
0 => "in".to_owned(),
n => format!("in{}", n + 1),
}
}
pub fn unnamed() -> Self {
Self {
edge_type: EdgeType::Unnamed,
in_idx: None,
}
}
}
impl Default for Edge<'_> {
fn default() -> Self {
Self::unnamed()
}
}
#[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<Node, Edge<'_>>,
mut el_writer: ElementWriter<'w, &'b mut Vec<u8>>,
node_idx: NodeIndex,
input_count: u8,
) -> ElementWriter<'w, &'b mut Vec<u8>> {
let inputs = g
.neighbors_directed(node_idx, petgraph::Direction::Incoming)
.collect::<Vec<NodeIndex>>();
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"
)
}
}
};
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<Node, Edge<'_>>,
mut el_writer: ElementWriter<'w, &'b mut Vec<u8>>,
node_idx: NodeIndex,
) -> ElementWriter<'w, &'b mut Vec<u8>> {
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::<HashSet<&str>>();
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::<Vec<&str>>().first().unwrap())
.to_string()
.into_bytes(),
),
})
} else {
panic!("Can't have more then one named output: {output:?}")
}
}

View file

@ -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"),
}
}
}

View file

@ -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 {