diff --git a/crates/usvg/src/writer.rs b/crates/usvg/src/writer.rs index 2c455cbc1..947d3a80b 100644 --- a/crates/usvg/src/writer.rs +++ b/crates/usvg/src/writer.rs @@ -1,7 +1,7 @@ // Copyright 2023 the Resvg Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use std::fmt::Display; +use std::fmt::{Arguments, Display}; use std::io::Write; use svgtypes::{parse_font_families, FontFamily}; @@ -138,6 +138,74 @@ impl Default for WriteOptions { } } +pub struct LazyXmlWriter<'a> { + xml: &'a mut xmlwriter::XmlWriter, + current_node: Option, + force_write_node: bool, +} + +impl<'a> LazyXmlWriter<'a> { + pub fn new(xml: &'a mut xmlwriter::XmlWriter, node: EId) -> Self { + Self { + xml, + current_node: Some(node), + force_write_node: false, + } + } + + fn force_write(&mut self) -> &mut xmlwriter::XmlWriter { + self.init_node(); + self.force_write_node = true; + self.xml + } + + fn init_node(&mut self) { + if let Some(node) = self.current_node { + self.xml.start_svg_element(node); + self.current_node = None; + } + } + + pub fn write_id_attribute(&mut self, id: &str, opt: &WriteOptions) { + self.init_node(); + self.xml.write_id_attribute(id, opt); + } + + pub fn write_func_iri(&mut self, aid: AId, id: &str, opt: &WriteOptions) { + self.init_node(); + self.xml.write_func_iri(aid, id, opt); + } + + fn write_svg_attribute(&mut self, id: AId, value: &V) { + self.init_node(); + self.xml.write_svg_attribute(id, value); + } + + fn write_transform(&mut self, id: AId, ts: Transform, opt: &WriteOptions) { + if !ts.is_default() { + self.init_node(); + self.xml.write_transform(id, ts, opt); + } + } + + pub fn write_attribute_fmt(&mut self, name: &str, fmt: Arguments) { + self.init_node(); + self.xml.write_attribute_fmt(name, fmt); + } + + pub fn end_element(&mut self) { + if self.force_write_node { + // simulate that node was written + self.current_node = None; + } + if let Some(_node) = self.current_node { + // Node was never written. + return; + } + self.xml.end_element(); + } +} + pub(crate) fn convert(tree: &Tree, opt: &WriteOptions) -> String { let mut xml = XmlWriter::new(xmlwriter::Options { use_single_quote: opt.use_single_quote, @@ -846,17 +914,17 @@ fn write_group_element(g: &Group, is_clip_path: bool, opt: &WriteOptions, xml: & return; } - xml.start_svg_element(EId::G); + let mut lazy_writer = LazyXmlWriter::new(xml, EId::G); if !g.id.is_empty() { - xml.write_id_attribute(&g.id, opt); + lazy_writer.write_id_attribute(&g.id, opt); }; if let Some(ref clip) = g.clip_path { - xml.write_func_iri(AId::ClipPath, clip.id(), opt); + lazy_writer.write_func_iri(AId::ClipPath, clip.id(), opt); } if let Some(ref mask) = g.mask { - xml.write_func_iri(AId::Mask, mask.id(), opt); + lazy_writer.write_func_iri(AId::Mask, mask.id(), opt); } if !g.filters.is_empty() { @@ -866,28 +934,30 @@ fn write_group_element(g: &Group, is_clip_path: bool, opt: &WriteOptions, xml: & .iter() .map(|filter| format!("url(#{}{})", prefix, filter.id())) .collect(); - xml.write_svg_attribute(AId::Filter, &ids.join(" ")); + lazy_writer.write_svg_attribute(AId::Filter, &ids.join(" ")); } if g.opacity != Opacity::ONE { - xml.write_svg_attribute(AId::Opacity, &g.opacity.get()); + lazy_writer.write_svg_attribute(AId::Opacity, &g.opacity.get()); } - xml.write_transform(AId::Transform, g.transform, opt); + lazy_writer.write_transform(AId::Transform, g.transform, opt); if g.blend_mode != BlendMode::Normal || g.isolate { // For reasons unknown, `mix-blend-mode` and `isolation` must be written // as `style` attribute. let isolation = if g.isolate { "isolate" } else { "auto" }; - xml.write_attribute_fmt( + lazy_writer.write_attribute_fmt( AId::Style.to_str(), format_args!("mix-blend-mode:{};isolation:{}", g.blend_mode, isolation), ); } - write_elements(g, false, opt, xml); - - xml.end_element(); + if !g.children.is_empty() { + let xml = lazy_writer.force_write(); + write_elements(g, false, opt, xml); + } + lazy_writer.end_element(); } trait XmlWriterExt { diff --git a/crates/usvg/tests/files/path-simple-case-empty-group-expected.svg b/crates/usvg/tests/files/path-simple-case-empty-group-expected.svg new file mode 100644 index 000000000..3180288a7 --- /dev/null +++ b/crates/usvg/tests/files/path-simple-case-empty-group-expected.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/usvg/tests/files/path-simple-case-empty-group.svg b/crates/usvg/tests/files/path-simple-case-empty-group.svg new file mode 100644 index 000000000..4ba910e6e --- /dev/null +++ b/crates/usvg/tests/files/path-simple-case-empty-group.svg @@ -0,0 +1,4 @@ + + + + diff --git a/crates/usvg/tests/files/path-simple-case-empty-path-expected.svg b/crates/usvg/tests/files/path-simple-case-empty-path-expected.svg new file mode 100644 index 000000000..3180288a7 --- /dev/null +++ b/crates/usvg/tests/files/path-simple-case-empty-path-expected.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/usvg/tests/files/path-simple-case-empty-path.svg b/crates/usvg/tests/files/path-simple-case-empty-path.svg new file mode 100644 index 000000000..95c4d31d4 --- /dev/null +++ b/crates/usvg/tests/files/path-simple-case-empty-path.svg @@ -0,0 +1,4 @@ + + + + diff --git a/crates/usvg/tests/write.rs b/crates/usvg/tests/write.rs index 14edfbb92..576e7bc85 100644 --- a/crates/usvg/tests/write.rs +++ b/crates/usvg/tests/write.rs @@ -67,6 +67,17 @@ fn path_simple_case() { resave("path-simple-case"); } +#[test] +fn path_simple_case_empty_group() { + resave("path-simple-case-empty-group"); +} + +#[test] +fn path_simple_case_empty_path() { + // this test will succeed because the parser removes empty paths (not the writer) + resave("path-simple-case-empty-path"); +} + #[test] fn ellipse_simple_case() { resave("ellipse-simple-case");