diff --git a/CHANGELOG.md b/CHANGELOG.md index cb2e52f17..989c279f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ This changelog also contains important changes in dependencies. This release has an MSRV of 1.87.0 for `usvg` and `resvg` and the C API. +### Fixed +- `dominant-baseline` is now correctly inherited by nested `` elements, + so a nested span stays on the same baseline as its siblings. (#864) + ## [0.47.0] 2026-02-05 This release has an MSRV of 1.87.0 for `usvg` and `resvg` and the C API. diff --git a/crates/resvg/tests/integration/render.rs b/crates/resvg/tests/integration/render.rs index dbeeac0b1..bc1763030 100644 --- a/crates/resvg/tests/integration/render.rs +++ b/crates/resvg/tests/integration/render.rs @@ -1402,6 +1402,7 @@ use crate::render; #[test] fn text_dominant_baseline_hanging() { assert_eq!(render("tests/text/dominant-baseline/hanging"), 0); } #[test] fn text_dominant_baseline_ideographic() { assert_eq!(render("tests/text/dominant-baseline/ideographic"), 0); } #[test] fn text_dominant_baseline_inherit() { assert_eq!(render("tests/text/dominant-baseline/inherit"), 0); } +#[test] fn text_dominant_baseline_inherited_through_nested_tspan() { assert_eq!(render("tests/text/dominant-baseline/inherited-through-nested-tspan"), 0); } #[test] fn text_dominant_baseline_mathematical() { assert_eq!(render("tests/text/dominant-baseline/mathematical"), 0); } #[test] fn text_dominant_baseline_middle() { assert_eq!(render("tests/text/dominant-baseline/middle"), 0); } #[test] fn text_dominant_baseline_nested() { assert_eq!(render("tests/text/dominant-baseline/nested"), 0); } diff --git a/crates/resvg/tests/tests/text/dominant-baseline/inherited-through-nested-tspan.png b/crates/resvg/tests/tests/text/dominant-baseline/inherited-through-nested-tspan.png new file mode 100644 index 000000000..1ab68f262 Binary files /dev/null and b/crates/resvg/tests/tests/text/dominant-baseline/inherited-through-nested-tspan.png differ diff --git a/crates/resvg/tests/tests/text/dominant-baseline/inherited-through-nested-tspan.svg b/crates/resvg/tests/tests/text/dominant-baseline/inherited-through-nested-tspan.svg new file mode 100644 index 000000000..f50cb8c2f --- /dev/null +++ b/crates/resvg/tests/tests/text/dominant-baseline/inherited-through-nested-tspan.svg @@ -0,0 +1,16 @@ + + Inherited through a nested tspan + + + + + + + + + + hello world + + diff --git a/crates/usvg/src/parser/svgtree/mod.rs b/crates/usvg/src/parser/svgtree/mod.rs index 1618f3225..9c427f343 100644 --- a/crates/usvg/src/parser/svgtree/mod.rs +++ b/crates/usvg/src/parser/svgtree/mod.rs @@ -823,11 +823,15 @@ impl AId { fn is_non_inheritable(id: AId) -> bool { matches!( id, + // `dominant-baseline` is intentionally *not* listed here: unlike + // `alignment-baseline` and `baseline-shift`, it is an inherited property + // (CSS Inline Layout 3 / SVG 2, matching Chromium and Inkscape). Treating + // it as non-inheritable broke nested ``s, whose direct parent does + // not carry the value (https://github.com/linebender/resvg/issues/864). AId::AlignmentBaseline | AId::BaselineShift | AId::ClipPath | AId::Display - | AId::DominantBaseline | AId::Filter | AId::FloodColor | AId::FloodOpacity diff --git a/crates/usvg/tests/text.rs b/crates/usvg/tests/text.rs new file mode 100644 index 000000000..118a4fb01 --- /dev/null +++ b/crates/usvg/tests/text.rs @@ -0,0 +1,100 @@ +// Copyright 2024 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::sync::Arc; + +use once_cell::sync::Lazy; +use usvg::{DominantBaseline, Group, Node, Text}; + +static GLOBAL_FONTDB: Lazy> = Lazy::new(|| { + let mut fontdb = usvg::fontdb::Database::new(); + fontdb.load_fonts_dir("../resvg/tests/fonts"); + fontdb.set_serif_family("Noto Serif"); + fontdb.set_sans_serif_family("Noto Sans"); + fontdb.set_cursive_family("Yellowtail"); + fontdb.set_fantasy_family("Sedgwick Ave Display"); + fontdb.set_monospace_family("Noto Mono"); + Arc::new(fontdb) +}); + +fn parse(svg: &str) -> usvg::Tree { + let opt = usvg::Options { + fontdb: GLOBAL_FONTDB.clone(), + ..Default::default() + }; + usvg::Tree::from_str(svg, &opt).unwrap() +} + +fn first_text(group: &Group) -> Option<&Text> { + for node in group.children() { + match node { + Node::Text(t) => return Some(t), + Node::Group(g) => { + if let Some(t) = first_text(g) { + return Some(t); + } + } + _ => {} + } + } + None +} + +// Regression test for https://github.com/linebender/resvg/issues/864 +// +// `dominant-baseline` is an inherited property (CSS Inline Layout 3 / SVG 2), +// so a nested `` that does not set it must inherit the value from an +// ancestor `` — even when its direct parent `` doesn't carry it. +// Previously the nested span fell back to `Auto`, placing it on the alphabetic +// baseline while its sibling text used `text-after-edge`, so the two were no +// longer on the same line. +#[test] +fn nested_tspan_inherits_dominant_baseline() { + let svg = " + + hello world + + "; + + let tree = parse(svg); + let text = first_text(tree.root()).expect("a text node"); + + let chunk = text + .chunks() + .iter() + .find(|c| c.text() == "hello world") + .expect("the 'hello world' chunk"); + + // Two spans: the nested `hello` and the trailing ` world`. + assert_eq!(chunk.spans().len(), 2); + for span in chunk.spans() { + assert_eq!( + span.dominant_baseline(), + DominantBaseline::TextAfterEdge, + "span {:?} did not inherit dominant-baseline from ", + &chunk.text()[span.start()..span.end()] + ); + } +} + +// A nested `` setting its own `dominant-baseline` still overrides the +// inherited one, and deeper descendants inherit the nested value. +#[test] +fn nested_tspan_dominant_baseline_override() { + let svg = " + + hi + + "; + + let tree = parse(svg); + let text = first_text(tree.root()).expect("a text node"); + let chunk = text + .chunks() + .iter() + .find(|c| c.text() == "hi") + .expect("the 'hi' chunk"); + + let span = &chunk.spans()[0]; + assert_eq!(span.dominant_baseline(), DominantBaseline::Hanging); +}