diff --git a/.typos.toml b/.typos.toml index 389e359e0..418faf839 100644 --- a/.typos.toml +++ b/.typos.toml @@ -16,6 +16,7 @@ SVGinOT = "SVGinOT" # Match Inside a Word - Case Insensitive [default.extend-words] +wdth = "wdth" [files] # Include .github, .cargo, etc. diff --git a/Cargo.lock b/Cargo.lock index 6dcbc486d..2984b3d4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,15 +49,29 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.8.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bytemuck" -version = "1.21.0" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "byteorder-lite" @@ -158,6 +172,15 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +[[package]] +name = "font-types" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4d2d0cf79d38430cc9dc9aadec84774bff2e1ba30ae2bf6c16cfce9385a23" +dependencies = [ + "bytemuck", +] + [[package]] name = "fontconfig-parser" version = "0.5.7" @@ -197,6 +220,19 @@ dependencies = [ "weezl", ] +[[package]] +name = "harfrust" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9f40651a03bc0f7316bd75267ff5767e93017ef3cfffe76c6aa7252cc5a31c" +dependencies = [ + "bitflags 2.10.0", + "bytemuck", + "core_maths", + "read-fonts", + "smallvec", +] + [[package]] name = "image-webp" version = "0.2.0" @@ -333,12 +369,30 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "proc-macro2" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +dependencies = [ + "unicode-ident", +] + [[package]] name = "quick-error" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +dependencies = [ + "proc-macro2", +] + [[package]] name = "rand" version = "0.6.5" @@ -454,6 +508,17 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "read-fonts" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" +dependencies = [ + "bytemuck", + "core_maths", + "font-types", +] + [[package]] name = "resvg" version = "0.45.1" @@ -494,24 +559,6 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" -[[package]] -name = "rustybuzz" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" -dependencies = [ - "bitflags 2.8.0", - "bytemuck", - "core_maths", - "log", - "smallvec", - "ttf-parser", - "unicode-bidi-mirroring", - "unicode-ccc", - "unicode-properties", - "unicode-script", -] - [[package]] name = "simd-adler32" version = "0.3.7" @@ -539,6 +586,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "skrifa" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" +dependencies = [ + "bytemuck", + "read-fonts", +] + [[package]] name = "slotmap" version = "1.0.7" @@ -573,6 +630,17 @@ dependencies = [ "siphasher 1.0.1", ] +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "tiny-skia" version = "0.11.4" @@ -630,22 +698,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] -name = "unicode-bidi-mirroring" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" - -[[package]] -name = "unicode-ccc" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" - -[[package]] -name = "unicode-properties" -version = "0.1.3" +name = "unicode-ident" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-script" @@ -667,15 +723,16 @@ dependencies = [ "data-url", "flate2", "fontdb", + "harfrust", "imagesize", "kurbo", "log", "once_cell", "pico-args", "roxmltree", - "rustybuzz", "simplecss", "siphasher 1.0.1", + "skrifa", "strict-num", "svgtypes", "tiny-skia-path", diff --git a/crates/resvg/Cargo.toml b/crates/resvg/Cargo.toml index e29a4160a..cd89b1735 100644 --- a/crates/resvg/Cargo.toml +++ b/crates/resvg/Cargo.toml @@ -14,6 +14,10 @@ workspace = "../.." name = "resvg" required-features = ["text", "system-fonts", "memmap-fonts"] +[[example]] +name = "generate_hinting_comparison" +required-features = ["text", "hinting"] + [dependencies] gif = { version = "0.13", optional = true } image-webp = { version = "0.2.0", optional = true } @@ -42,3 +46,5 @@ memmap-fonts = ["usvg/memmap-fonts"] # When disabled, `image` elements with SVG data will still be rendered. # Adds around 200KiB to your binary. raster-images = ["gif", "image-webp", "dep:zune-jpeg"] +# Enables font hinting via skrifa (requires `text`). +hinting = ["usvg/hinting"] diff --git a/crates/resvg/examples/generate_hinting_comparison.rs b/crates/resvg/examples/generate_hinting_comparison.rs new file mode 100644 index 000000000..ea8a0fb4b --- /dev/null +++ b/crates/resvg/examples/generate_hinting_comparison.rs @@ -0,0 +1,78 @@ +//! Run with: cargo run --example generate_hinting_comparison --features text,hinting + +use std::sync::Arc; +use usvg::fontdb; + +fn main() { + // Load fonts + let mut fontdb = fontdb::Database::new(); + fontdb.load_fonts_dir("crates/resvg/tests/fonts"); + fontdb.set_sans_serif_family("Noto Sans"); + let fontdb = Arc::new(fontdb); + + // SVG with small text where hinting is most visible + let svg = br#" + + + The quick brown fox jumps over the lazy dog. (12px) + + + The quick brown fox jumps over the lazy dog. (14px) + + + The quick brown fox jumps over the lazy dog. (16px) + + + The quick brown fox jumps over. (20px) + + + The quick brown fox. (24px) + + + "#; + + // Render with hinting + let opt_hinted = usvg::Options { + fontdb: fontdb.clone(), + hinting: usvg::HintingOptions { + enabled: true, + dpi: Some(96.0), + }, + ..usvg::Options::default() + }; + + let tree = usvg::Tree::from_data(svg, &opt_hinted).unwrap(); + let size = tree.size().to_int_size(); + let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height()).unwrap(); + pixmap.fill(tiny_skia::Color::WHITE); + resvg::render( + &tree, + tiny_skia::Transform::identity(), + &mut pixmap.as_mut(), + ); + pixmap.save_png("hinted.png").unwrap(); + println!("Saved hinted.png"); + + // Render without hinting + let opt_unhinted = usvg::Options { + fontdb: fontdb.clone(), + hinting: usvg::HintingOptions { + enabled: false, + dpi: Some(96.0), + }, + ..usvg::Options::default() + }; + + let tree = usvg::Tree::from_data(svg, &opt_unhinted).unwrap(); + let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height()).unwrap(); + pixmap.fill(tiny_skia::Color::WHITE); + resvg::render( + &tree, + tiny_skia::Transform::identity(), + &mut pixmap.as_mut(), + ); + pixmap.save_png("unhinted.png").unwrap(); + println!("Saved unhinted.png"); + + println!("Done! Compare hinted.png and unhinted.png"); +} diff --git a/crates/resvg/src/main.rs b/crates/resvg/src/main.rs index 830eb8d4f..5b72fb772 100644 --- a/crates/resvg/src/main.rs +++ b/crates/resvg/src/main.rs @@ -577,6 +577,8 @@ fn parse_args() -> Result { image_href_resolver: usvg::ImageHrefResolver::default(), font_resolver: usvg::FontResolver::default(), fontdb: Arc::new(fontdb::Database::new()), + #[cfg(feature = "text")] + hinting: usvg::HintingOptions::default(), style_sheet, }; diff --git a/crates/resvg/tests-hinted/tests/text/alignment-baseline/after-edge.png b/crates/resvg/tests-hinted/tests/text/alignment-baseline/after-edge.png new file mode 100644 index 000000000..0d9462116 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/alignment-baseline/after-edge.png differ diff --git a/crates/resvg/tests-hinted/tests/text/alignment-baseline/alphabetic.png b/crates/resvg/tests-hinted/tests/text/alignment-baseline/alphabetic.png new file mode 100644 index 000000000..148548b89 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/alignment-baseline/alphabetic.png differ diff --git a/crates/resvg/tests-hinted/tests/text/alignment-baseline/auto.png b/crates/resvg/tests-hinted/tests/text/alignment-baseline/auto.png new file mode 100644 index 000000000..148548b89 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/alignment-baseline/auto.png differ diff --git a/crates/resvg/tests-hinted/tests/text/alignment-baseline/baseline.png b/crates/resvg/tests-hinted/tests/text/alignment-baseline/baseline.png new file mode 100644 index 000000000..148548b89 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/alignment-baseline/baseline.png differ diff --git a/crates/resvg/tests-hinted/tests/text/alignment-baseline/before-edge.png b/crates/resvg/tests-hinted/tests/text/alignment-baseline/before-edge.png new file mode 100644 index 000000000..8aa703e37 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/alignment-baseline/before-edge.png differ diff --git a/crates/resvg/tests-hinted/tests/text/alignment-baseline/central.png b/crates/resvg/tests-hinted/tests/text/alignment-baseline/central.png new file mode 100644 index 000000000..3c63a94ce Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/alignment-baseline/central.png differ diff --git a/crates/resvg/tests-hinted/tests/text/alignment-baseline/hanging-and-baseline-shift-eq-20-on-tspan.png b/crates/resvg/tests-hinted/tests/text/alignment-baseline/hanging-and-baseline-shift-eq-20-on-tspan.png new file mode 100644 index 000000000..3dc7c0d40 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/alignment-baseline/hanging-and-baseline-shift-eq-20-on-tspan.png differ diff --git a/crates/resvg/tests-hinted/tests/text/alignment-baseline/hanging-on-tspan.png b/crates/resvg/tests-hinted/tests/text/alignment-baseline/hanging-on-tspan.png new file mode 100644 index 000000000..dc99d2e1a Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/alignment-baseline/hanging-on-tspan.png differ diff --git a/crates/resvg/tests-hinted/tests/text/alignment-baseline/hanging-on-vertical.png b/crates/resvg/tests-hinted/tests/text/alignment-baseline/hanging-on-vertical.png new file mode 100644 index 000000000..02aac9476 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/alignment-baseline/hanging-on-vertical.png differ diff --git a/crates/resvg/tests-hinted/tests/text/alignment-baseline/hanging-with-underline.png b/crates/resvg/tests-hinted/tests/text/alignment-baseline/hanging-with-underline.png new file mode 100644 index 000000000..f6ffc4254 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/alignment-baseline/hanging-with-underline.png differ diff --git a/crates/resvg/tests-hinted/tests/text/alignment-baseline/hanging.png b/crates/resvg/tests-hinted/tests/text/alignment-baseline/hanging.png new file mode 100644 index 000000000..080a0012c Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/alignment-baseline/hanging.png differ diff --git a/crates/resvg/tests-hinted/tests/text/alignment-baseline/ideographic.png b/crates/resvg/tests-hinted/tests/text/alignment-baseline/ideographic.png new file mode 100644 index 000000000..0d9462116 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/alignment-baseline/ideographic.png differ diff --git a/crates/resvg/tests-hinted/tests/text/alignment-baseline/inherit.png b/crates/resvg/tests-hinted/tests/text/alignment-baseline/inherit.png new file mode 100644 index 000000000..080a0012c Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/alignment-baseline/inherit.png differ diff --git a/crates/resvg/tests-hinted/tests/text/alignment-baseline/mathematical.png b/crates/resvg/tests-hinted/tests/text/alignment-baseline/mathematical.png new file mode 100644 index 000000000..0f3d0b0cb Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/alignment-baseline/mathematical.png differ diff --git a/crates/resvg/tests-hinted/tests/text/alignment-baseline/middle-on-textPath.png b/crates/resvg/tests-hinted/tests/text/alignment-baseline/middle-on-textPath.png new file mode 100644 index 000000000..69cb52bfa Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/alignment-baseline/middle-on-textPath.png differ diff --git a/crates/resvg/tests-hinted/tests/text/alignment-baseline/middle.png b/crates/resvg/tests-hinted/tests/text/alignment-baseline/middle.png new file mode 100644 index 000000000..1a7af5871 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/alignment-baseline/middle.png differ diff --git a/crates/resvg/tests-hinted/tests/text/alignment-baseline/text-after-edge.png b/crates/resvg/tests-hinted/tests/text/alignment-baseline/text-after-edge.png new file mode 100644 index 000000000..0d9462116 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/alignment-baseline/text-after-edge.png differ diff --git a/crates/resvg/tests-hinted/tests/text/alignment-baseline/text-before-edge.png b/crates/resvg/tests-hinted/tests/text/alignment-baseline/text-before-edge.png new file mode 100644 index 000000000..8aa703e37 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/alignment-baseline/text-before-edge.png differ diff --git a/crates/resvg/tests-hinted/tests/text/alignment-baseline/two-textPath-with-middle-on-first.png b/crates/resvg/tests-hinted/tests/text/alignment-baseline/two-textPath-with-middle-on-first.png new file mode 100644 index 000000000..43e93ffeb Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/alignment-baseline/two-textPath-with-middle-on-first.png differ diff --git a/crates/resvg/tests-hinted/tests/text/baseline-shift/-10.png b/crates/resvg/tests-hinted/tests/text/baseline-shift/-10.png new file mode 100644 index 000000000..c80578101 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/baseline-shift/-10.png differ diff --git a/crates/resvg/tests-hinted/tests/text/baseline-shift/-50percent.png b/crates/resvg/tests-hinted/tests/text/baseline-shift/-50percent.png new file mode 100644 index 000000000..cf19529a6 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/baseline-shift/-50percent.png differ diff --git a/crates/resvg/tests-hinted/tests/text/baseline-shift/0.png b/crates/resvg/tests-hinted/tests/text/baseline-shift/0.png new file mode 100644 index 000000000..56ce39d6b Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/baseline-shift/0.png differ diff --git a/crates/resvg/tests-hinted/tests/text/baseline-shift/10.png b/crates/resvg/tests-hinted/tests/text/baseline-shift/10.png new file mode 100644 index 000000000..be2d2f135 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/baseline-shift/10.png differ diff --git a/crates/resvg/tests-hinted/tests/text/baseline-shift/2mm.png b/crates/resvg/tests-hinted/tests/text/baseline-shift/2mm.png new file mode 100644 index 000000000..6610843ed Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/baseline-shift/2mm.png differ diff --git a/crates/resvg/tests-hinted/tests/text/baseline-shift/50percent.png b/crates/resvg/tests-hinted/tests/text/baseline-shift/50percent.png new file mode 100644 index 000000000..8a4ce42d9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/baseline-shift/50percent.png differ diff --git a/crates/resvg/tests-hinted/tests/text/baseline-shift/baseline.png b/crates/resvg/tests-hinted/tests/text/baseline-shift/baseline.png new file mode 100644 index 000000000..56ce39d6b Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/baseline-shift/baseline.png differ diff --git a/crates/resvg/tests-hinted/tests/text/baseline-shift/deeply-nested-super.png b/crates/resvg/tests-hinted/tests/text/baseline-shift/deeply-nested-super.png new file mode 100644 index 000000000..dcd0e6990 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/baseline-shift/deeply-nested-super.png differ diff --git a/crates/resvg/tests-hinted/tests/text/baseline-shift/inheritance-1.png b/crates/resvg/tests-hinted/tests/text/baseline-shift/inheritance-1.png new file mode 100644 index 000000000..1a6398aa9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/baseline-shift/inheritance-1.png differ diff --git a/crates/resvg/tests-hinted/tests/text/baseline-shift/inheritance-2.png b/crates/resvg/tests-hinted/tests/text/baseline-shift/inheritance-2.png new file mode 100644 index 000000000..830e6aee2 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/baseline-shift/inheritance-2.png differ diff --git a/crates/resvg/tests-hinted/tests/text/baseline-shift/inheritance-3.png b/crates/resvg/tests-hinted/tests/text/baseline-shift/inheritance-3.png new file mode 100644 index 000000000..830e6aee2 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/baseline-shift/inheritance-3.png differ diff --git a/crates/resvg/tests-hinted/tests/text/baseline-shift/inheritance-4.png b/crates/resvg/tests-hinted/tests/text/baseline-shift/inheritance-4.png new file mode 100644 index 000000000..830e6aee2 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/baseline-shift/inheritance-4.png differ diff --git a/crates/resvg/tests-hinted/tests/text/baseline-shift/inheritance-5.png b/crates/resvg/tests-hinted/tests/text/baseline-shift/inheritance-5.png new file mode 100644 index 000000000..830e6aee2 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/baseline-shift/inheritance-5.png differ diff --git a/crates/resvg/tests-hinted/tests/text/baseline-shift/invalid-value.png b/crates/resvg/tests-hinted/tests/text/baseline-shift/invalid-value.png new file mode 100644 index 000000000..56ce39d6b Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/baseline-shift/invalid-value.png differ diff --git a/crates/resvg/tests-hinted/tests/text/baseline-shift/mixed-nested.png b/crates/resvg/tests-hinted/tests/text/baseline-shift/mixed-nested.png new file mode 100644 index 000000000..fb84f4307 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/baseline-shift/mixed-nested.png differ diff --git a/crates/resvg/tests-hinted/tests/text/baseline-shift/nested-length.png b/crates/resvg/tests-hinted/tests/text/baseline-shift/nested-length.png new file mode 100644 index 000000000..c2db88572 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/baseline-shift/nested-length.png differ diff --git a/crates/resvg/tests-hinted/tests/text/baseline-shift/nested-super.png b/crates/resvg/tests-hinted/tests/text/baseline-shift/nested-super.png new file mode 100644 index 000000000..b8f06326f Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/baseline-shift/nested-super.png differ diff --git a/crates/resvg/tests-hinted/tests/text/baseline-shift/nested-with-baseline-1.png b/crates/resvg/tests-hinted/tests/text/baseline-shift/nested-with-baseline-1.png new file mode 100644 index 000000000..a2b5c54f2 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/baseline-shift/nested-with-baseline-1.png differ diff --git a/crates/resvg/tests-hinted/tests/text/baseline-shift/nested-with-baseline-2.png b/crates/resvg/tests-hinted/tests/text/baseline-shift/nested-with-baseline-2.png new file mode 100644 index 000000000..a2b5c54f2 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/baseline-shift/nested-with-baseline-2.png differ diff --git a/crates/resvg/tests-hinted/tests/text/baseline-shift/sub.png b/crates/resvg/tests-hinted/tests/text/baseline-shift/sub.png new file mode 100644 index 000000000..ae9edf359 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/baseline-shift/sub.png differ diff --git a/crates/resvg/tests-hinted/tests/text/baseline-shift/super.png b/crates/resvg/tests-hinted/tests/text/baseline-shift/super.png new file mode 100644 index 000000000..182790202 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/baseline-shift/super.png differ diff --git a/crates/resvg/tests-hinted/tests/text/baseline-shift/with-rotate.png b/crates/resvg/tests-hinted/tests/text/baseline-shift/with-rotate.png new file mode 100644 index 000000000..eda16b210 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/baseline-shift/with-rotate.png differ diff --git a/crates/resvg/tests-hinted/tests/text/color-font/cbdt.png b/crates/resvg/tests-hinted/tests/text/color-font/cbdt.png new file mode 100644 index 000000000..adcdfa237 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/color-font/cbdt.png differ diff --git a/crates/resvg/tests-hinted/tests/text/color-font/colrv0.png b/crates/resvg/tests-hinted/tests/text/color-font/colrv0.png new file mode 100644 index 000000000..785f1ca03 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/color-font/colrv0.png differ diff --git a/crates/resvg/tests-hinted/tests/text/color-font/colrv1.png b/crates/resvg/tests-hinted/tests/text/color-font/colrv1.png new file mode 100644 index 000000000..23bb63a05 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/color-font/colrv1.png differ diff --git a/crates/resvg/tests-hinted/tests/text/color-font/compound-emojis-and-coordinates-list.png b/crates/resvg/tests-hinted/tests/text/color-font/compound-emojis-and-coordinates-list.png new file mode 100644 index 000000000..b6ecffc1f Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/color-font/compound-emojis-and-coordinates-list.png differ diff --git a/crates/resvg/tests-hinted/tests/text/color-font/compound-emojis.png b/crates/resvg/tests-hinted/tests/text/color-font/compound-emojis.png new file mode 100644 index 000000000..6c6724ece Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/color-font/compound-emojis.png differ diff --git a/crates/resvg/tests-hinted/tests/text/color-font/mixed-text-rtl.png b/crates/resvg/tests-hinted/tests/text/color-font/mixed-text-rtl.png new file mode 100644 index 000000000..ee64397bd Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/color-font/mixed-text-rtl.png differ diff --git a/crates/resvg/tests-hinted/tests/text/color-font/mixed-text.png b/crates/resvg/tests-hinted/tests/text/color-font/mixed-text.png new file mode 100644 index 000000000..02cf9a8a4 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/color-font/mixed-text.png differ diff --git a/crates/resvg/tests-hinted/tests/text/color-font/sbix.png b/crates/resvg/tests-hinted/tests/text/color-font/sbix.png new file mode 100644 index 000000000..f7839ac35 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/color-font/sbix.png differ diff --git a/crates/resvg/tests-hinted/tests/text/color-font/svg.png b/crates/resvg/tests-hinted/tests/text/color-font/svg.png new file mode 100644 index 000000000..476deb877 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/color-font/svg.png differ diff --git a/crates/resvg/tests-hinted/tests/text/color-font/writing-mode=tb.png b/crates/resvg/tests-hinted/tests/text/color-font/writing-mode=tb.png new file mode 100644 index 000000000..98fdf53ba Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/color-font/writing-mode=tb.png differ diff --git a/crates/resvg/tests-hinted/tests/text/direction/rtl-with-vertical-writing-mode.png b/crates/resvg/tests-hinted/tests/text/direction/rtl-with-vertical-writing-mode.png new file mode 100644 index 000000000..506f03f9f Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/direction/rtl-with-vertical-writing-mode.png differ diff --git a/crates/resvg/tests-hinted/tests/text/direction/rtl.png b/crates/resvg/tests-hinted/tests/text/direction/rtl.png new file mode 100644 index 000000000..f36467fd4 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/direction/rtl.png differ diff --git a/crates/resvg/tests-hinted/tests/text/dominant-baseline/alignment-baseline-and-baseline-shift-on-tspans.png b/crates/resvg/tests-hinted/tests/text/dominant-baseline/alignment-baseline-and-baseline-shift-on-tspans.png new file mode 100644 index 000000000..67707dcf8 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/dominant-baseline/alignment-baseline-and-baseline-shift-on-tspans.png differ diff --git a/crates/resvg/tests-hinted/tests/text/dominant-baseline/alignment-baseline=baseline-on-tspan.png b/crates/resvg/tests-hinted/tests/text/dominant-baseline/alignment-baseline=baseline-on-tspan.png new file mode 100644 index 000000000..4e187100d Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/dominant-baseline/alignment-baseline=baseline-on-tspan.png differ diff --git a/crates/resvg/tests-hinted/tests/text/dominant-baseline/alphabetic.png b/crates/resvg/tests-hinted/tests/text/dominant-baseline/alphabetic.png new file mode 100644 index 000000000..254182f03 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/dominant-baseline/alphabetic.png differ diff --git a/crates/resvg/tests-hinted/tests/text/dominant-baseline/auto.png b/crates/resvg/tests-hinted/tests/text/dominant-baseline/auto.png new file mode 100644 index 000000000..254182f03 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/dominant-baseline/auto.png differ diff --git a/crates/resvg/tests-hinted/tests/text/dominant-baseline/central.png b/crates/resvg/tests-hinted/tests/text/dominant-baseline/central.png new file mode 100644 index 000000000..ab4b50d5e Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/dominant-baseline/central.png differ diff --git a/crates/resvg/tests-hinted/tests/text/dominant-baseline/complex.png b/crates/resvg/tests-hinted/tests/text/dominant-baseline/complex.png new file mode 100644 index 000000000..617d0aef7 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/dominant-baseline/complex.png differ diff --git a/crates/resvg/tests-hinted/tests/text/dominant-baseline/different-alignment-baseline-on-tspan.png b/crates/resvg/tests-hinted/tests/text/dominant-baseline/different-alignment-baseline-on-tspan.png new file mode 100644 index 000000000..ae0691262 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/dominant-baseline/different-alignment-baseline-on-tspan.png differ diff --git a/crates/resvg/tests-hinted/tests/text/dominant-baseline/dummy-tspan.png b/crates/resvg/tests-hinted/tests/text/dominant-baseline/dummy-tspan.png new file mode 100644 index 000000000..4e187100d Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/dominant-baseline/dummy-tspan.png differ diff --git a/crates/resvg/tests-hinted/tests/text/dominant-baseline/equal-alignment-baseline-on-tspan.png b/crates/resvg/tests-hinted/tests/text/dominant-baseline/equal-alignment-baseline-on-tspan.png new file mode 100644 index 000000000..52dde4ae1 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/dominant-baseline/equal-alignment-baseline-on-tspan.png differ diff --git a/crates/resvg/tests-hinted/tests/text/dominant-baseline/hanging.png b/crates/resvg/tests-hinted/tests/text/dominant-baseline/hanging.png new file mode 100644 index 000000000..a6a8476ad Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/dominant-baseline/hanging.png differ diff --git a/crates/resvg/tests-hinted/tests/text/dominant-baseline/ideographic.png b/crates/resvg/tests-hinted/tests/text/dominant-baseline/ideographic.png new file mode 100644 index 000000000..2c76d2a76 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/dominant-baseline/ideographic.png differ diff --git a/crates/resvg/tests-hinted/tests/text/dominant-baseline/inherit.png b/crates/resvg/tests-hinted/tests/text/dominant-baseline/inherit.png new file mode 100644 index 000000000..2959616f6 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/dominant-baseline/inherit.png differ diff --git a/crates/resvg/tests-hinted/tests/text/dominant-baseline/mathematical.png b/crates/resvg/tests-hinted/tests/text/dominant-baseline/mathematical.png new file mode 100644 index 000000000..6d6bef244 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/dominant-baseline/mathematical.png differ diff --git a/crates/resvg/tests-hinted/tests/text/dominant-baseline/middle.png b/crates/resvg/tests-hinted/tests/text/dominant-baseline/middle.png new file mode 100644 index 000000000..2959616f6 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/dominant-baseline/middle.png differ diff --git a/crates/resvg/tests-hinted/tests/text/dominant-baseline/nested.png b/crates/resvg/tests-hinted/tests/text/dominant-baseline/nested.png new file mode 100644 index 000000000..bb5b79149 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/dominant-baseline/nested.png differ diff --git a/crates/resvg/tests-hinted/tests/text/dominant-baseline/no-change.png b/crates/resvg/tests-hinted/tests/text/dominant-baseline/no-change.png new file mode 100644 index 000000000..c0dc34816 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/dominant-baseline/no-change.png differ diff --git a/crates/resvg/tests-hinted/tests/text/dominant-baseline/reset-size.png b/crates/resvg/tests-hinted/tests/text/dominant-baseline/reset-size.png new file mode 100644 index 000000000..254182f03 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/dominant-baseline/reset-size.png differ diff --git a/crates/resvg/tests-hinted/tests/text/dominant-baseline/sequential.png b/crates/resvg/tests-hinted/tests/text/dominant-baseline/sequential.png new file mode 100644 index 000000000..519544004 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/dominant-baseline/sequential.png differ diff --git a/crates/resvg/tests-hinted/tests/text/dominant-baseline/text-after-edge.png b/crates/resvg/tests-hinted/tests/text/dominant-baseline/text-after-edge.png new file mode 100644 index 000000000..2c76d2a76 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/dominant-baseline/text-after-edge.png differ diff --git a/crates/resvg/tests-hinted/tests/text/dominant-baseline/text-before-edge.png b/crates/resvg/tests-hinted/tests/text/dominant-baseline/text-before-edge.png new file mode 100644 index 000000000..65fb0f7ab Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/dominant-baseline/text-before-edge.png differ diff --git a/crates/resvg/tests-hinted/tests/text/dominant-baseline/use-script.png b/crates/resvg/tests-hinted/tests/text/dominant-baseline/use-script.png new file mode 100644 index 000000000..2efb9c954 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/dominant-baseline/use-script.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-family/bold-sans-serif.png b/crates/resvg/tests-hinted/tests/text/font-family/bold-sans-serif.png new file mode 100644 index 000000000..8487acc42 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-family/bold-sans-serif.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-family/cursive.png b/crates/resvg/tests-hinted/tests/text/font-family/cursive.png new file mode 100644 index 000000000..8e9e4f35a Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-family/cursive.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-family/double-quoted.png b/crates/resvg/tests-hinted/tests/text/font-family/double-quoted.png new file mode 100644 index 000000000..e4a712cd9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-family/double-quoted.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-family/fallback-1.png b/crates/resvg/tests-hinted/tests/text/font-family/fallback-1.png new file mode 100644 index 000000000..01ed41297 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-family/fallback-1.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-family/fallback-2.png b/crates/resvg/tests-hinted/tests/text/font-family/fallback-2.png new file mode 100644 index 000000000..e4a712cd9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-family/fallback-2.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-family/fantasy.png b/crates/resvg/tests-hinted/tests/text/font-family/fantasy.png new file mode 100644 index 000000000..052077d50 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-family/fantasy.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-family/font-list.png b/crates/resvg/tests-hinted/tests/text/font-family/font-list.png new file mode 100644 index 000000000..af2cd3720 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-family/font-list.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-family/monospace.png b/crates/resvg/tests-hinted/tests/text/font-family/monospace.png new file mode 100644 index 000000000..80ea7dcf0 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-family/monospace.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-family/noto-sans.png b/crates/resvg/tests-hinted/tests/text/font-family/noto-sans.png new file mode 100644 index 000000000..e4a712cd9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-family/noto-sans.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-family/sans-serif.png b/crates/resvg/tests-hinted/tests/text/font-family/sans-serif.png new file mode 100644 index 000000000..e4a712cd9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-family/sans-serif.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-family/serif.png b/crates/resvg/tests-hinted/tests/text/font-family/serif.png new file mode 100644 index 000000000..a8a599ec8 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-family/serif.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-family/source-sans-pro.png b/crates/resvg/tests-hinted/tests/text/font-family/source-sans-pro.png new file mode 100644 index 000000000..af2cd3720 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-family/source-sans-pro.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-kerning/arabic-script.png b/crates/resvg/tests-hinted/tests/text/font-kerning/arabic-script.png new file mode 100644 index 000000000..5a99b3eba Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-kerning/arabic-script.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-kerning/as-property.png b/crates/resvg/tests-hinted/tests/text/font-kerning/as-property.png new file mode 100644 index 000000000..035723f7f Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-kerning/as-property.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-kerning/none.png b/crates/resvg/tests-hinted/tests/text/font-kerning/none.png new file mode 100644 index 000000000..d9848739d Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-kerning/none.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-size-adjust/simple-case.png b/crates/resvg/tests-hinted/tests/text/font-size-adjust/simple-case.png new file mode 100644 index 000000000..11165a864 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-size-adjust/simple-case.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-size/em-nested-and-mixed.png b/crates/resvg/tests-hinted/tests/text/font-size/em-nested-and-mixed.png new file mode 100644 index 000000000..672d43509 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-size/em-nested-and-mixed.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-size/em-on-the-root-element.png b/crates/resvg/tests-hinted/tests/text/font-size/em-on-the-root-element.png new file mode 100644 index 000000000..672d43509 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-size/em-on-the-root-element.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-size/em.png b/crates/resvg/tests-hinted/tests/text/font-size/em.png new file mode 100644 index 000000000..672d43509 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-size/em.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-size/ex-nested-and-mixed.png b/crates/resvg/tests-hinted/tests/text/font-size/ex-nested-and-mixed.png new file mode 100644 index 000000000..6fb0c6beb Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-size/ex-nested-and-mixed.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-size/ex-on-the-root-element.png b/crates/resvg/tests-hinted/tests/text/font-size/ex-on-the-root-element.png new file mode 100644 index 000000000..5daac5671 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-size/ex-on-the-root-element.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-size/ex.png b/crates/resvg/tests-hinted/tests/text/font-size/ex.png new file mode 100644 index 000000000..5daac5671 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-size/ex.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-size/inheritance.png b/crates/resvg/tests-hinted/tests/text/font-size/inheritance.png new file mode 100644 index 000000000..e4a712cd9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-size/inheritance.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-size/mixed-values.png b/crates/resvg/tests-hinted/tests/text/font-size/mixed-values.png new file mode 100644 index 000000000..d0252f5b3 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-size/mixed-values.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-size/named-value-without-a-parent.png b/crates/resvg/tests-hinted/tests/text/font-size/named-value-without-a-parent.png new file mode 100644 index 000000000..c44a486ff Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-size/named-value-without-a-parent.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-size/named-value.png b/crates/resvg/tests-hinted/tests/text/font-size/named-value.png new file mode 100644 index 000000000..c5b95b568 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-size/named-value.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-size/negative-size.png b/crates/resvg/tests-hinted/tests/text/font-size/negative-size.png new file mode 100644 index 000000000..0c96ac6a1 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-size/negative-size.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-size/nested-percent-values-1.png b/crates/resvg/tests-hinted/tests/text/font-size/nested-percent-values-1.png new file mode 100644 index 000000000..b2c29cf83 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-size/nested-percent-values-1.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-size/nested-percent-values-2.png b/crates/resvg/tests-hinted/tests/text/font-size/nested-percent-values-2.png new file mode 100644 index 000000000..b2c29cf83 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-size/nested-percent-values-2.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-size/percent-value-without-a-parent.png b/crates/resvg/tests-hinted/tests/text/font-size/percent-value-without-a-parent.png new file mode 100644 index 000000000..1a333005a Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-size/percent-value-without-a-parent.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-size/percent-value.png b/crates/resvg/tests-hinted/tests/text/font-size/percent-value.png new file mode 100644 index 000000000..b2c29cf83 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-size/percent-value.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-size/simple-case.png b/crates/resvg/tests-hinted/tests/text/font-size/simple-case.png new file mode 100644 index 000000000..e4a712cd9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-size/simple-case.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-size/zero-size-on-parent-1.png b/crates/resvg/tests-hinted/tests/text/font-size/zero-size-on-parent-1.png new file mode 100644 index 000000000..e4a712cd9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-size/zero-size-on-parent-1.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-size/zero-size-on-parent-2.png b/crates/resvg/tests-hinted/tests/text/font-size/zero-size-on-parent-2.png new file mode 100644 index 000000000..e4a712cd9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-size/zero-size-on-parent-2.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-size/zero-size-on-parent-3.png b/crates/resvg/tests-hinted/tests/text/font-size/zero-size-on-parent-3.png new file mode 100644 index 000000000..0c96ac6a1 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-size/zero-size-on-parent-3.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-size/zero-size.png b/crates/resvg/tests-hinted/tests/text/font-size/zero-size.png new file mode 100644 index 000000000..0c96ac6a1 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-size/zero-size.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-stretch/extra-condensed.png b/crates/resvg/tests-hinted/tests/text/font-stretch/extra-condensed.png new file mode 100644 index 000000000..e4a712cd9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-stretch/extra-condensed.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-stretch/inherit.png b/crates/resvg/tests-hinted/tests/text/font-stretch/inherit.png new file mode 100644 index 000000000..e4a712cd9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-stretch/inherit.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-stretch/narrower.png b/crates/resvg/tests-hinted/tests/text/font-stretch/narrower.png new file mode 100644 index 000000000..e4a712cd9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-stretch/narrower.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-style/inherit.png b/crates/resvg/tests-hinted/tests/text/font-style/inherit.png new file mode 100644 index 000000000..692da732d Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-style/inherit.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-style/italic.png b/crates/resvg/tests-hinted/tests/text/font-style/italic.png new file mode 100644 index 000000000..692da732d Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-style/italic.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-style/oblique.png b/crates/resvg/tests-hinted/tests/text/font-style/oblique.png new file mode 100644 index 000000000..692da732d Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-style/oblique.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-variant/inherit.png b/crates/resvg/tests-hinted/tests/text/font-variant/inherit.png new file mode 100644 index 000000000..447ba5874 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-variant/inherit.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-variant/small-caps.png b/crates/resvg/tests-hinted/tests/text/font-variant/small-caps.png new file mode 100644 index 000000000..447ba5874 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-variant/small-caps.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-variation-settings/all-axes-combined.png b/crates/resvg/tests-hinted/tests/text/font-variation-settings/all-axes-combined.png new file mode 100644 index 000000000..3d0f560c2 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-variation-settings/all-axes-combined.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-variation-settings/auto-font-stretch-condensed.png b/crates/resvg/tests-hinted/tests/text/font-variation-settings/auto-font-stretch-condensed.png new file mode 100644 index 000000000..0619002a5 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-variation-settings/auto-font-stretch-condensed.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-variation-settings/auto-font-style-oblique.png b/crates/resvg/tests-hinted/tests/text/font-variation-settings/auto-font-style-oblique.png new file mode 100644 index 000000000..8659a0b78 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-variation-settings/auto-font-style-oblique.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-variation-settings/auto-font-weight-700.png b/crates/resvg/tests-hinted/tests/text/font-variation-settings/auto-font-weight-700.png new file mode 100644 index 000000000..5514ba565 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-variation-settings/auto-font-weight-700.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-variation-settings/explicit-overrides-auto.png b/crates/resvg/tests-hinted/tests/text/font-variation-settings/explicit-overrides-auto.png new file mode 100644 index 000000000..4a26241cc Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-variation-settings/explicit-overrides-auto.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-variation-settings/grad-negative.png b/crates/resvg/tests-hinted/tests/text/font-variation-settings/grad-negative.png new file mode 100644 index 000000000..61edccf5d Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-variation-settings/grad-negative.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-variation-settings/multiple-axes.png b/crates/resvg/tests-hinted/tests/text/font-variation-settings/multiple-axes.png new file mode 100644 index 000000000..dda375bc5 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-variation-settings/multiple-axes.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-variation-settings/opsz-144.png b/crates/resvg/tests-hinted/tests/text/font-variation-settings/opsz-144.png new file mode 100644 index 000000000..642e1e3ac Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-variation-settings/opsz-144.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-variation-settings/slnt-negative.png b/crates/resvg/tests-hinted/tests/text/font-variation-settings/slnt-negative.png new file mode 100644 index 000000000..8659a0b78 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-variation-settings/slnt-negative.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-variation-settings/wdth-151.png b/crates/resvg/tests-hinted/tests/text/font-variation-settings/wdth-151.png new file mode 100644 index 000000000..6cf95d3fa Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-variation-settings/wdth-151.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-variation-settings/wdth-25.png b/crates/resvg/tests-hinted/tests/text/font-variation-settings/wdth-25.png new file mode 100644 index 000000000..4595317b3 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-variation-settings/wdth-25.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-variation-settings/wght-100.png b/crates/resvg/tests-hinted/tests/text/font-variation-settings/wght-100.png new file mode 100644 index 000000000..4a26241cc Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-variation-settings/wght-100.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-variation-settings/wght-700.png b/crates/resvg/tests-hinted/tests/text/font-variation-settings/wght-700.png new file mode 100644 index 000000000..5514ba565 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-variation-settings/wght-700.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-variation-settings/xtra-extreme.png b/crates/resvg/tests-hinted/tests/text/font-variation-settings/xtra-extreme.png new file mode 100644 index 000000000..96244e875 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-variation-settings/xtra-extreme.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-weight/650.png b/crates/resvg/tests-hinted/tests/text/font-weight/650.png new file mode 100644 index 000000000..29d739899 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-weight/650.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-weight/700.png b/crates/resvg/tests-hinted/tests/text/font-weight/700.png new file mode 100644 index 000000000..8487acc42 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-weight/700.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-weight/bold.png b/crates/resvg/tests-hinted/tests/text/font-weight/bold.png new file mode 100644 index 000000000..03f5926ed Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-weight/bold.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-weight/bolder-with-clamping.png b/crates/resvg/tests-hinted/tests/text/font-weight/bolder-with-clamping.png new file mode 100644 index 000000000..564835c83 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-weight/bolder-with-clamping.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-weight/bolder-without-parent.png b/crates/resvg/tests-hinted/tests/text/font-weight/bolder-without-parent.png new file mode 100644 index 000000000..03f5926ed Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-weight/bolder-without-parent.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-weight/bolder.png b/crates/resvg/tests-hinted/tests/text/font-weight/bolder.png new file mode 100644 index 000000000..03f5926ed Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-weight/bolder.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-weight/inherit.png b/crates/resvg/tests-hinted/tests/text/font-weight/inherit.png new file mode 100644 index 000000000..29d739899 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-weight/inherit.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-weight/invalid-number-1.png b/crates/resvg/tests-hinted/tests/text/font-weight/invalid-number-1.png new file mode 100644 index 000000000..29d739899 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-weight/invalid-number-1.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-weight/lighter-with-clamping.png b/crates/resvg/tests-hinted/tests/text/font-weight/lighter-with-clamping.png new file mode 100644 index 000000000..8b375d903 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-weight/lighter-with-clamping.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-weight/lighter-without-parent.png b/crates/resvg/tests-hinted/tests/text/font-weight/lighter-without-parent.png new file mode 100644 index 000000000..8b375d903 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-weight/lighter-without-parent.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-weight/lighter.png b/crates/resvg/tests-hinted/tests/text/font-weight/lighter.png new file mode 100644 index 000000000..03f5926ed Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-weight/lighter.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font-weight/normal.png b/crates/resvg/tests-hinted/tests/text/font-weight/normal.png new file mode 100644 index 000000000..29d739899 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font-weight/normal.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font/font-shorthand.png b/crates/resvg/tests-hinted/tests/text/font/font-shorthand.png new file mode 100644 index 000000000..298cfb6b4 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font/font-shorthand.png differ diff --git a/crates/resvg/tests-hinted/tests/text/font/simple-case.png b/crates/resvg/tests-hinted/tests/text/font/simple-case.png new file mode 100644 index 000000000..53fb1842e Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/font/simple-case.png differ diff --git a/crates/resvg/tests-hinted/tests/text/glyph-orientation-horizontal/simple-case.png b/crates/resvg/tests-hinted/tests/text/glyph-orientation-horizontal/simple-case.png new file mode 100644 index 000000000..f86448cc3 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/glyph-orientation-horizontal/simple-case.png differ diff --git a/crates/resvg/tests-hinted/tests/text/glyph-orientation-vertical/simple-case.png b/crates/resvg/tests-hinted/tests/text/glyph-orientation-vertical/simple-case.png new file mode 100644 index 000000000..8389c2b45 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/glyph-orientation-vertical/simple-case.png differ diff --git a/crates/resvg/tests-hinted/tests/text/kerning/0.png b/crates/resvg/tests-hinted/tests/text/kerning/0.png new file mode 100644 index 000000000..60dc13a67 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/kerning/0.png differ diff --git a/crates/resvg/tests-hinted/tests/text/kerning/10percent.png b/crates/resvg/tests-hinted/tests/text/kerning/10percent.png new file mode 100644 index 000000000..eeb6f2dac Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/kerning/10percent.png differ diff --git a/crates/resvg/tests-hinted/tests/text/lengthAdjust/spacingAndGlyphs.png b/crates/resvg/tests-hinted/tests/text/lengthAdjust/spacingAndGlyphs.png new file mode 100644 index 000000000..76b95453a Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/lengthAdjust/spacingAndGlyphs.png differ diff --git a/crates/resvg/tests-hinted/tests/text/lengthAdjust/text-on-path.png b/crates/resvg/tests-hinted/tests/text/lengthAdjust/text-on-path.png new file mode 100644 index 000000000..9ac0d84f1 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/lengthAdjust/text-on-path.png differ diff --git a/crates/resvg/tests-hinted/tests/text/lengthAdjust/vertical.png b/crates/resvg/tests-hinted/tests/text/lengthAdjust/vertical.png new file mode 100644 index 000000000..801ec787b Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/lengthAdjust/vertical.png differ diff --git a/crates/resvg/tests-hinted/tests/text/lengthAdjust/with-underline.png b/crates/resvg/tests-hinted/tests/text/lengthAdjust/with-underline.png new file mode 100644 index 000000000..25f2dc627 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/lengthAdjust/with-underline.png differ diff --git a/crates/resvg/tests-hinted/tests/text/letter-spacing/-3.png b/crates/resvg/tests-hinted/tests/text/letter-spacing/-3.png new file mode 100644 index 000000000..be8812a00 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/letter-spacing/-3.png differ diff --git a/crates/resvg/tests-hinted/tests/text/letter-spacing/0.png b/crates/resvg/tests-hinted/tests/text/letter-spacing/0.png new file mode 100644 index 000000000..fd8649fc5 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/letter-spacing/0.png differ diff --git a/crates/resvg/tests-hinted/tests/text/letter-spacing/1mm.png b/crates/resvg/tests-hinted/tests/text/letter-spacing/1mm.png new file mode 100644 index 000000000..18e668cc5 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/letter-spacing/1mm.png differ diff --git a/crates/resvg/tests-hinted/tests/text/letter-spacing/3.png b/crates/resvg/tests-hinted/tests/text/letter-spacing/3.png new file mode 100644 index 000000000..a7d7ae076 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/letter-spacing/3.png differ diff --git a/crates/resvg/tests-hinted/tests/text/letter-spacing/5percent.png b/crates/resvg/tests-hinted/tests/text/letter-spacing/5percent.png new file mode 100644 index 000000000..e2091d179 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/letter-spacing/5percent.png differ diff --git a/crates/resvg/tests-hinted/tests/text/letter-spacing/filter-bbox.png b/crates/resvg/tests-hinted/tests/text/letter-spacing/filter-bbox.png new file mode 100644 index 000000000..fca6ebe93 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/letter-spacing/filter-bbox.png differ diff --git a/crates/resvg/tests-hinted/tests/text/letter-spacing/large-negative.png b/crates/resvg/tests-hinted/tests/text/letter-spacing/large-negative.png new file mode 100644 index 000000000..1b8172c23 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/letter-spacing/large-negative.png differ diff --git a/crates/resvg/tests-hinted/tests/text/letter-spacing/mixed-scripts.png b/crates/resvg/tests-hinted/tests/text/letter-spacing/mixed-scripts.png new file mode 100644 index 000000000..d90dae59b Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/letter-spacing/mixed-scripts.png differ diff --git a/crates/resvg/tests-hinted/tests/text/letter-spacing/mixed-spacing.png b/crates/resvg/tests-hinted/tests/text/letter-spacing/mixed-spacing.png new file mode 100644 index 000000000..7c7db8f5b Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/letter-spacing/mixed-spacing.png differ diff --git a/crates/resvg/tests-hinted/tests/text/letter-spacing/non-ASCII-character.png b/crates/resvg/tests-hinted/tests/text/letter-spacing/non-ASCII-character.png new file mode 100644 index 000000000..ea4c7f2e3 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/letter-spacing/non-ASCII-character.png differ diff --git a/crates/resvg/tests-hinted/tests/text/letter-spacing/normal.png b/crates/resvg/tests-hinted/tests/text/letter-spacing/normal.png new file mode 100644 index 000000000..fd8649fc5 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/letter-spacing/normal.png differ diff --git a/crates/resvg/tests-hinted/tests/text/letter-spacing/on-Arabic.png b/crates/resvg/tests-hinted/tests/text/letter-spacing/on-Arabic.png new file mode 100644 index 000000000..7e39c013f Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/letter-spacing/on-Arabic.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-anchor/coordinates-list.png b/crates/resvg/tests-hinted/tests/text/text-anchor/coordinates-list.png new file mode 100644 index 000000000..8fd778eda Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-anchor/coordinates-list.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-anchor/end-on-text.png b/crates/resvg/tests-hinted/tests/text/text-anchor/end-on-text.png new file mode 100644 index 000000000..bbd2f5cea Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-anchor/end-on-text.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-anchor/end-with-letter-spacing.png b/crates/resvg/tests-hinted/tests/text/text-anchor/end-with-letter-spacing.png new file mode 100644 index 000000000..595317e8a Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-anchor/end-with-letter-spacing.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-anchor/inheritance-1.png b/crates/resvg/tests-hinted/tests/text/text-anchor/inheritance-1.png new file mode 100644 index 000000000..76c599107 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-anchor/inheritance-1.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-anchor/inheritance-2.png b/crates/resvg/tests-hinted/tests/text/text-anchor/inheritance-2.png new file mode 100644 index 000000000..576a1a04d Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-anchor/inheritance-2.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-anchor/inheritance-3.png b/crates/resvg/tests-hinted/tests/text/text-anchor/inheritance-3.png new file mode 100644 index 000000000..39d61c3c7 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-anchor/inheritance-3.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-anchor/invalid-value-on-text.png b/crates/resvg/tests-hinted/tests/text/text-anchor/invalid-value-on-text.png new file mode 100644 index 000000000..704550448 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-anchor/invalid-value-on-text.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-anchor/middle-on-text.png b/crates/resvg/tests-hinted/tests/text/text-anchor/middle-on-text.png new file mode 100644 index 000000000..eeb6f2dac Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-anchor/middle-on-text.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-anchor/on-the-first-tspan.png b/crates/resvg/tests-hinted/tests/text/text-anchor/on-the-first-tspan.png new file mode 100644 index 000000000..84c7c27d7 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-anchor/on-the-first-tspan.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-anchor/on-tspan-with-arabic.png b/crates/resvg/tests-hinted/tests/text/text-anchor/on-tspan-with-arabic.png new file mode 100644 index 000000000..28a2a589a Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-anchor/on-tspan-with-arabic.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-anchor/on-tspan.png b/crates/resvg/tests-hinted/tests/text/text-anchor/on-tspan.png new file mode 100644 index 000000000..3a1382736 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-anchor/on-tspan.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-anchor/start-on-text.png b/crates/resvg/tests-hinted/tests/text/text-anchor/start-on-text.png new file mode 100644 index 000000000..704550448 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-anchor/start-on-text.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-anchor/text-anchor-not-on-text-chunk.png b/crates/resvg/tests-hinted/tests/text/text-anchor/text-anchor-not-on-text-chunk.png new file mode 100644 index 000000000..cbc15e897 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-anchor/text-anchor-not-on-text-chunk.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-decoration/all-types-inline-comma-separated.png b/crates/resvg/tests-hinted/tests/text/text-decoration/all-types-inline-comma-separated.png new file mode 100644 index 000000000..83c0ff98f Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-decoration/all-types-inline-comma-separated.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-decoration/all-types-inline-no-spaces.png b/crates/resvg/tests-hinted/tests/text/text-decoration/all-types-inline-no-spaces.png new file mode 100644 index 000000000..83c0ff98f Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-decoration/all-types-inline-no-spaces.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-decoration/all-types-inline.png b/crates/resvg/tests-hinted/tests/text/text-decoration/all-types-inline.png new file mode 100644 index 000000000..142b6a4a9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-decoration/all-types-inline.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-decoration/all-types-nested.png b/crates/resvg/tests-hinted/tests/text/text-decoration/all-types-nested.png new file mode 100644 index 000000000..142b6a4a9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-decoration/all-types-nested.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-decoration/indirect-with-multiple-colors.png b/crates/resvg/tests-hinted/tests/text/text-decoration/indirect-with-multiple-colors.png new file mode 100644 index 000000000..be330c74e Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-decoration/indirect-with-multiple-colors.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-decoration/indirect.png b/crates/resvg/tests-hinted/tests/text/text-decoration/indirect.png new file mode 100644 index 000000000..8106c9827 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-decoration/indirect.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-decoration/line-through.png b/crates/resvg/tests-hinted/tests/text/text-decoration/line-through.png new file mode 100644 index 000000000..829521b23 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-decoration/line-through.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-decoration/outside-the-text-element.png b/crates/resvg/tests-hinted/tests/text/text-decoration/outside-the-text-element.png new file mode 100644 index 000000000..8106c9827 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-decoration/outside-the-text-element.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-decoration/overline.png b/crates/resvg/tests-hinted/tests/text/text-decoration/overline.png new file mode 100644 index 000000000..ff79f34ea Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-decoration/overline.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-decoration/style-resolving-1.png b/crates/resvg/tests-hinted/tests/text/text-decoration/style-resolving-1.png new file mode 100644 index 000000000..05d18a90b Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-decoration/style-resolving-1.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-decoration/style-resolving-2.png b/crates/resvg/tests-hinted/tests/text/text-decoration/style-resolving-2.png new file mode 100644 index 000000000..05d18a90b Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-decoration/style-resolving-2.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-decoration/style-resolving-3.png b/crates/resvg/tests-hinted/tests/text/text-decoration/style-resolving-3.png new file mode 100644 index 000000000..1a7dc5044 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-decoration/style-resolving-3.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-decoration/style-resolving-4.png b/crates/resvg/tests-hinted/tests/text/text-decoration/style-resolving-4.png new file mode 100644 index 000000000..f3966be60 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-decoration/style-resolving-4.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-decoration/tspan-decoration.png b/crates/resvg/tests-hinted/tests/text/text-decoration/tspan-decoration.png new file mode 100644 index 000000000..84a11b46a Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-decoration/tspan-decoration.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-decoration/underline-with-dy-list-1.png b/crates/resvg/tests-hinted/tests/text/text-decoration/underline-with-dy-list-1.png new file mode 100644 index 000000000..9b3a737df Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-decoration/underline-with-dy-list-1.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-decoration/underline-with-dy-list-2.png b/crates/resvg/tests-hinted/tests/text/text-decoration/underline-with-dy-list-2.png new file mode 100644 index 000000000..74ad89d4c Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-decoration/underline-with-dy-list-2.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-decoration/underline-with-rotate-list-3.png b/crates/resvg/tests-hinted/tests/text/text-decoration/underline-with-rotate-list-3.png new file mode 100644 index 000000000..b817a93d7 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-decoration/underline-with-rotate-list-3.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-decoration/underline-with-rotate-list-4.png b/crates/resvg/tests-hinted/tests/text/text-decoration/underline-with-rotate-list-4.png new file mode 100644 index 000000000..fb043910e Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-decoration/underline-with-rotate-list-4.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-decoration/underline-with-y-list.png b/crates/resvg/tests-hinted/tests/text/text-decoration/underline-with-y-list.png new file mode 100644 index 000000000..141de3407 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-decoration/underline-with-y-list.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-decoration/underline.png b/crates/resvg/tests-hinted/tests/text/text-decoration/underline.png new file mode 100644 index 000000000..b010c0ebf Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-decoration/underline.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-decoration/with-textLength-on-a-single-character.png b/crates/resvg/tests-hinted/tests/text/text-decoration/with-textLength-on-a-single-character.png new file mode 100644 index 000000000..545ea2502 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-decoration/with-textLength-on-a-single-character.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-rendering/geometricPrecision.png b/crates/resvg/tests-hinted/tests/text/text-rendering/geometricPrecision.png new file mode 100644 index 000000000..d1ad2b441 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-rendering/geometricPrecision.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-rendering/on-tspan.png b/crates/resvg/tests-hinted/tests/text/text-rendering/on-tspan.png new file mode 100644 index 000000000..eeb6f2dac Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-rendering/on-tspan.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-rendering/optimizeLegibility.png b/crates/resvg/tests-hinted/tests/text/text-rendering/optimizeLegibility.png new file mode 100644 index 000000000..c2f40f8c9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-rendering/optimizeLegibility.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-rendering/optimizeSpeed.png b/crates/resvg/tests-hinted/tests/text/text-rendering/optimizeSpeed.png new file mode 100644 index 000000000..0f9e55116 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-rendering/optimizeSpeed.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text-rendering/with-underline.png b/crates/resvg/tests-hinted/tests/text/text-rendering/with-underline.png new file mode 100644 index 000000000..5f3d9f55d Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-rendering/with-underline.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/bidi-reordering.png b/crates/resvg/tests-hinted/tests/text/text/bidi-reordering.png new file mode 100644 index 000000000..22424bfc4 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/bidi-reordering.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/complex-grapheme-split-by-tspan.png b/crates/resvg/tests-hinted/tests/text/text/complex-grapheme-split-by-tspan.png new file mode 100644 index 000000000..6c704ceb3 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/complex-grapheme-split-by-tspan.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/complex-graphemes-and-coordinates-list.png b/crates/resvg/tests-hinted/tests/text/text/complex-graphemes-and-coordinates-list.png new file mode 100644 index 000000000..bbc4c58d6 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/complex-graphemes-and-coordinates-list.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/complex-graphemes.png b/crates/resvg/tests-hinted/tests/text/text/complex-graphemes.png new file mode 100644 index 000000000..905cedece Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/complex-graphemes.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/dx-and-dy-instead-of-x-and-y.png b/crates/resvg/tests-hinted/tests/text/text/dx-and-dy-instead-of-x-and-y.png new file mode 100644 index 000000000..ef9f34131 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/dx-and-dy-instead-of-x-and-y.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/dx-and-dy-with-less-values-than-characters.png b/crates/resvg/tests-hinted/tests/text/text/dx-and-dy-with-less-values-than-characters.png new file mode 100644 index 000000000..989d203b8 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/dx-and-dy-with-less-values-than-characters.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/dx-and-dy-with-more-values-than-characters.png b/crates/resvg/tests-hinted/tests/text/text/dx-and-dy-with-more-values-than-characters.png new file mode 100644 index 000000000..6cab0765c Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/dx-and-dy-with-more-values-than-characters.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/dx-and-dy-with-multiple-values.png b/crates/resvg/tests-hinted/tests/text/text/dx-and-dy-with-multiple-values.png new file mode 100644 index 000000000..6cab0765c Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/dx-and-dy-with-multiple-values.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/em-and-ex-coordinates.png b/crates/resvg/tests-hinted/tests/text/text/em-and-ex-coordinates.png new file mode 100644 index 000000000..de33d047d Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/em-and-ex-coordinates.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/escaped-text-1.png b/crates/resvg/tests-hinted/tests/text/text/escaped-text-1.png new file mode 100644 index 000000000..c77ef22f7 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/escaped-text-1.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/escaped-text-2.png b/crates/resvg/tests-hinted/tests/text/text/escaped-text-2.png new file mode 100644 index 000000000..7d7279ae7 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/escaped-text-2.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/escaped-text-3.png b/crates/resvg/tests-hinted/tests/text/text/escaped-text-3.png new file mode 100644 index 000000000..77a0fb19a Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/escaped-text-3.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/escaped-text-4.png b/crates/resvg/tests-hinted/tests/text/text/escaped-text-4.png new file mode 100644 index 000000000..64da98e1c Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/escaped-text-4.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/fill-rule=evenodd.png b/crates/resvg/tests-hinted/tests/text/text/fill-rule=evenodd.png new file mode 100644 index 000000000..44d6f118e Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/fill-rule=evenodd.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/filter-bbox.png b/crates/resvg/tests-hinted/tests/text/text/filter-bbox.png new file mode 100644 index 000000000..90a08c413 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/filter-bbox.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/glyph-splitting.png b/crates/resvg/tests-hinted/tests/text/text/glyph-splitting.png new file mode 100644 index 000000000..f30a932fa Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/glyph-splitting.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/ligatures-handling-in-mixed-fonts-1.png b/crates/resvg/tests-hinted/tests/text/text/ligatures-handling-in-mixed-fonts-1.png new file mode 100644 index 000000000..9be04f992 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/ligatures-handling-in-mixed-fonts-1.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/ligatures-handling-in-mixed-fonts-2.png b/crates/resvg/tests-hinted/tests/text/text/ligatures-handling-in-mixed-fonts-2.png new file mode 100644 index 000000000..93c8be5b7 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/ligatures-handling-in-mixed-fonts-2.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/mm-coordinates.png b/crates/resvg/tests-hinted/tests/text/text/mm-coordinates.png new file mode 100644 index 000000000..05ca508a3 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/mm-coordinates.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/nested.png b/crates/resvg/tests-hinted/tests/text/text/nested.png new file mode 100644 index 000000000..f192fbb98 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/nested.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/no-coordinates.png b/crates/resvg/tests-hinted/tests/text/text/no-coordinates.png new file mode 100644 index 000000000..8de5c7465 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/no-coordinates.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/percent-value-on-dx-and-dy.png b/crates/resvg/tests-hinted/tests/text/text/percent-value-on-dx-and-dy.png new file mode 100644 index 000000000..64aaa83ae Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/percent-value-on-dx-and-dy.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/percent-value-on-x-and-y.png b/crates/resvg/tests-hinted/tests/text/text/percent-value-on-x-and-y.png new file mode 100644 index 000000000..64aaa83ae Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/percent-value-on-x-and-y.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/real-text-height.png b/crates/resvg/tests-hinted/tests/text/text/real-text-height.png new file mode 100644 index 000000000..411fb3828 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/real-text-height.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/rotate-on-Arabic.png b/crates/resvg/tests-hinted/tests/text/text/rotate-on-Arabic.png new file mode 100644 index 000000000..9ac4f9632 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/rotate-on-Arabic.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/rotate-with-an-invalid-angle.png b/crates/resvg/tests-hinted/tests/text/text/rotate-with-an-invalid-angle.png new file mode 100644 index 000000000..aaaff76ff Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/rotate-with-an-invalid-angle.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/rotate-with-less-values-than-characters.png b/crates/resvg/tests-hinted/tests/text/text/rotate-with-less-values-than-characters.png new file mode 100644 index 000000000..c29af579b Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/rotate-with-less-values-than-characters.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/rotate-with-more-values-than-characters.png b/crates/resvg/tests-hinted/tests/text/text/rotate-with-more-values-than-characters.png new file mode 100644 index 000000000..4b89b642d Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/rotate-with-more-values-than-characters.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/rotate-with-multiple-values-and-complex-text.png b/crates/resvg/tests-hinted/tests/text/text/rotate-with-multiple-values-and-complex-text.png new file mode 100644 index 000000000..f1bcacf8a Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/rotate-with-multiple-values-and-complex-text.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/rotate-with-multiple-values-underline-and-pattern.png b/crates/resvg/tests-hinted/tests/text/text/rotate-with-multiple-values-underline-and-pattern.png new file mode 100644 index 000000000..3d645baa5 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/rotate-with-multiple-values-underline-and-pattern.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/rotate-with-multiple-values.png b/crates/resvg/tests-hinted/tests/text/text/rotate-with-multiple-values.png new file mode 100644 index 000000000..4b89b642d Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/rotate-with-multiple-values.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/rotate.png b/crates/resvg/tests-hinted/tests/text/text/rotate.png new file mode 100644 index 000000000..62912100b Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/rotate.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/simple-case.png b/crates/resvg/tests-hinted/tests/text/text/simple-case.png new file mode 100644 index 000000000..8de5c7465 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/simple-case.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/transform.png b/crates/resvg/tests-hinted/tests/text/text/transform.png new file mode 100644 index 000000000..efd4d7db8 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/transform.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/x-and-y-with-dx-and-dy-lists.png b/crates/resvg/tests-hinted/tests/text/text/x-and-y-with-dx-and-dy-lists.png new file mode 100644 index 000000000..03d211b24 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/x-and-y-with-dx-and-dy-lists.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/x-and-y-with-dx-and-dy.png b/crates/resvg/tests-hinted/tests/text/text/x-and-y-with-dx-and-dy.png new file mode 100644 index 000000000..ef9f34131 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/x-and-y-with-dx-and-dy.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/x-and-y-with-less-values-than-characters.png b/crates/resvg/tests-hinted/tests/text/text/x-and-y-with-less-values-than-characters.png new file mode 100644 index 000000000..6a0bebcb7 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/x-and-y-with-less-values-than-characters.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/x-and-y-with-more-values-than-characters.png b/crates/resvg/tests-hinted/tests/text/text/x-and-y-with-more-values-than-characters.png new file mode 100644 index 000000000..23e8df100 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/x-and-y-with-more-values-than-characters.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/x-and-y-with-multiple-values-and-arabic-text.png b/crates/resvg/tests-hinted/tests/text/text/x-and-y-with-multiple-values-and-arabic-text.png new file mode 100644 index 000000000..5c8c3e6ae Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/x-and-y-with-multiple-values-and-arabic-text.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/x-and-y-with-multiple-values-and-tspan.png b/crates/resvg/tests-hinted/tests/text/text/x-and-y-with-multiple-values-and-tspan.png new file mode 100644 index 000000000..059541219 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/x-and-y-with-multiple-values-and-tspan.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/x-and-y-with-multiple-values.png b/crates/resvg/tests-hinted/tests/text/text/x-and-y-with-multiple-values.png new file mode 100644 index 000000000..23e8df100 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/x-and-y-with-multiple-values.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/xml-lang=ja.png b/crates/resvg/tests-hinted/tests/text/text/xml-lang=ja.png new file mode 100644 index 000000000..6424dc632 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/xml-lang=ja.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/xml-space.png b/crates/resvg/tests-hinted/tests/text/text/xml-space.png new file mode 100644 index 000000000..5d85e4d97 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/xml-space.png differ diff --git a/crates/resvg/tests-hinted/tests/text/text/zalgo.png b/crates/resvg/tests-hinted/tests/text/text/zalgo.png new file mode 100644 index 000000000..9c1de758d Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text/zalgo.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textLength/150-on-parent.png b/crates/resvg/tests-hinted/tests/text/textLength/150-on-parent.png new file mode 100644 index 000000000..461c20732 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textLength/150-on-parent.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textLength/150-on-tspan.png b/crates/resvg/tests-hinted/tests/text/textLength/150-on-tspan.png new file mode 100644 index 000000000..711de47ec Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textLength/150-on-tspan.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textLength/150.png b/crates/resvg/tests-hinted/tests/text/textLength/150.png new file mode 100644 index 000000000..711de47ec Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textLength/150.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textLength/40mm.png b/crates/resvg/tests-hinted/tests/text/textLength/40mm.png new file mode 100644 index 000000000..d77d4cf79 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textLength/40mm.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textLength/75percent.png b/crates/resvg/tests-hinted/tests/text/textLength/75percent.png new file mode 100644 index 000000000..711de47ec Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textLength/75percent.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textLength/arabic-with-lengthAdjust.png b/crates/resvg/tests-hinted/tests/text/textLength/arabic-with-lengthAdjust.png new file mode 100644 index 000000000..25c1a600b Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textLength/arabic-with-lengthAdjust.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textLength/arabic.png b/crates/resvg/tests-hinted/tests/text/textLength/arabic.png new file mode 100644 index 000000000..f931ab8cb Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textLength/arabic.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textLength/inherit.png b/crates/resvg/tests-hinted/tests/text/textLength/inherit.png new file mode 100644 index 000000000..461c20732 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textLength/inherit.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textLength/negative.png b/crates/resvg/tests-hinted/tests/text/textLength/negative.png new file mode 100644 index 000000000..461c20732 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textLength/negative.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textLength/on-a-single-tspan.png b/crates/resvg/tests-hinted/tests/text/textLength/on-a-single-tspan.png new file mode 100644 index 000000000..69d1db4df Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textLength/on-a-single-tspan.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textLength/on-text-and-tspan.png b/crates/resvg/tests-hinted/tests/text/textLength/on-text-and-tspan.png new file mode 100644 index 000000000..e816ee471 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textLength/on-text-and-tspan.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textLength/zero.png b/crates/resvg/tests-hinted/tests/text/textLength/zero.png new file mode 100644 index 000000000..aadbaee6f Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textLength/zero.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/closed-path.png b/crates/resvg/tests-hinted/tests/text/textPath/closed-path.png new file mode 100644 index 000000000..cf5786988 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/closed-path.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/complex.png b/crates/resvg/tests-hinted/tests/text/textPath/complex.png new file mode 100644 index 000000000..51b6b6f27 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/complex.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/dy-with-tiny-coordinates.png b/crates/resvg/tests-hinted/tests/text/textPath/dy-with-tiny-coordinates.png new file mode 100644 index 000000000..c720d53ea Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/dy-with-tiny-coordinates.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/invalid-link.png b/crates/resvg/tests-hinted/tests/text/textPath/invalid-link.png new file mode 100644 index 000000000..0c96ac6a1 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/invalid-link.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/invalid-textPath-in-the-middle.png b/crates/resvg/tests-hinted/tests/text/textPath/invalid-textPath-in-the-middle.png new file mode 100644 index 000000000..775304401 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/invalid-textPath-in-the-middle.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/link-to-rect.png b/crates/resvg/tests-hinted/tests/text/textPath/link-to-rect.png new file mode 100644 index 000000000..1ce803654 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/link-to-rect.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/m-A-path.png b/crates/resvg/tests-hinted/tests/text/textPath/m-A-path.png new file mode 100644 index 000000000..d7241a66c Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/m-A-path.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/m-L-Z-path.png b/crates/resvg/tests-hinted/tests/text/textPath/m-L-Z-path.png new file mode 100644 index 000000000..1139f1c39 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/m-L-Z-path.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/method=stretch.png b/crates/resvg/tests-hinted/tests/text/textPath/method=stretch.png new file mode 100644 index 000000000..d84bbeb00 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/method=stretch.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/mixed-children-1.png b/crates/resvg/tests-hinted/tests/text/textPath/mixed-children-1.png new file mode 100644 index 000000000..ba04dc178 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/mixed-children-1.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/mixed-children-2.png b/crates/resvg/tests-hinted/tests/text/textPath/mixed-children-2.png new file mode 100644 index 000000000..a5e96e06b Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/mixed-children-2.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/nested.png b/crates/resvg/tests-hinted/tests/text/textPath/nested.png new file mode 100644 index 000000000..d64b6261a Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/nested.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/no-link.png b/crates/resvg/tests-hinted/tests/text/textPath/no-link.png new file mode 100644 index 000000000..746d45cd7 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/no-link.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/path-with-ClosePath.png b/crates/resvg/tests-hinted/tests/text/textPath/path-with-ClosePath.png new file mode 100644 index 000000000..0af37467a Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/path-with-ClosePath.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/path-with-subpaths-and-startOffset.png b/crates/resvg/tests-hinted/tests/text/textPath/path-with-subpaths-and-startOffset.png new file mode 100644 index 000000000..f79188722 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/path-with-subpaths-and-startOffset.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/path-with-subpaths.png b/crates/resvg/tests-hinted/tests/text/textPath/path-with-subpaths.png new file mode 100644 index 000000000..89480805c Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/path-with-subpaths.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/side=right.png b/crates/resvg/tests-hinted/tests/text/textPath/side=right.png new file mode 100644 index 000000000..d84bbeb00 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/side=right.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/simple-case.png b/crates/resvg/tests-hinted/tests/text/textPath/simple-case.png new file mode 100644 index 000000000..d84bbeb00 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/simple-case.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/spacing=auto.png b/crates/resvg/tests-hinted/tests/text/textPath/spacing=auto.png new file mode 100644 index 000000000..d84bbeb00 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/spacing=auto.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/startOffset=-100.png b/crates/resvg/tests-hinted/tests/text/textPath/startOffset=-100.png new file mode 100644 index 000000000..0506922b7 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/startOffset=-100.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/startOffset=10percent.png b/crates/resvg/tests-hinted/tests/text/textPath/startOffset=10percent.png new file mode 100644 index 000000000..fa84f0091 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/startOffset=10percent.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/startOffset=30.png b/crates/resvg/tests-hinted/tests/text/textPath/startOffset=30.png new file mode 100644 index 000000000..b7d27fdd9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/startOffset=30.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/startOffset=5mm.png b/crates/resvg/tests-hinted/tests/text/textPath/startOffset=5mm.png new file mode 100644 index 000000000..81fe398f3 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/startOffset=5mm.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/startOffset=9999.png b/crates/resvg/tests-hinted/tests/text/textPath/startOffset=9999.png new file mode 100644 index 000000000..746d45cd7 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/startOffset=9999.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/tspan-with-absolute-position.png b/crates/resvg/tests-hinted/tests/text/textPath/tspan-with-absolute-position.png new file mode 100644 index 000000000..4c372caf0 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/tspan-with-absolute-position.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/tspan-with-relative-position.png b/crates/resvg/tests-hinted/tests/text/textPath/tspan-with-relative-position.png new file mode 100644 index 000000000..f0fa1c507 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/tspan-with-relative-position.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/two-paths.png b/crates/resvg/tests-hinted/tests/text/textPath/two-paths.png new file mode 100644 index 000000000..8ff22ea10 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/two-paths.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/very-long-text.png b/crates/resvg/tests-hinted/tests/text/textPath/very-long-text.png new file mode 100644 index 000000000..09a576cf4 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/very-long-text.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/with-baseline-shift-and-rotate.png b/crates/resvg/tests-hinted/tests/text/textPath/with-baseline-shift-and-rotate.png new file mode 100644 index 000000000..3dcc9aa16 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/with-baseline-shift-and-rotate.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/with-baseline-shift.png b/crates/resvg/tests-hinted/tests/text/textPath/with-baseline-shift.png new file mode 100644 index 000000000..b962afde2 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/with-baseline-shift.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/with-big-letter-spacing.png b/crates/resvg/tests-hinted/tests/text/textPath/with-big-letter-spacing.png new file mode 100644 index 000000000..9a1173800 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/with-big-letter-spacing.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/with-coordinates-on-text.png b/crates/resvg/tests-hinted/tests/text/textPath/with-coordinates-on-text.png new file mode 100644 index 000000000..d9e1cac8f Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/with-coordinates-on-text.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/with-coordinates-on-textPath.png b/crates/resvg/tests-hinted/tests/text/textPath/with-coordinates-on-textPath.png new file mode 100644 index 000000000..fefb64c88 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/with-coordinates-on-textPath.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/with-filter.png b/crates/resvg/tests-hinted/tests/text/textPath/with-filter.png new file mode 100644 index 000000000..8ff22ea10 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/with-filter.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/with-invalid-path-and-xlink-href.png b/crates/resvg/tests-hinted/tests/text/textPath/with-invalid-path-and-xlink-href.png new file mode 100644 index 000000000..8052da3ae Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/with-invalid-path-and-xlink-href.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/with-letter-spacing.png b/crates/resvg/tests-hinted/tests/text/textPath/with-letter-spacing.png new file mode 100644 index 000000000..5c27cf97a Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/with-letter-spacing.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/with-path-and-xlink-href.png b/crates/resvg/tests-hinted/tests/text/textPath/with-path-and-xlink-href.png new file mode 100644 index 000000000..8052da3ae Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/with-path-and-xlink-href.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/with-path.png b/crates/resvg/tests-hinted/tests/text/textPath/with-path.png new file mode 100644 index 000000000..8052da3ae Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/with-path.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/with-rotate.png b/crates/resvg/tests-hinted/tests/text/textPath/with-rotate.png new file mode 100644 index 000000000..7360da828 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/with-rotate.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/with-text-anchor.png b/crates/resvg/tests-hinted/tests/text/textPath/with-text-anchor.png new file mode 100644 index 000000000..925971bbb Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/with-text-anchor.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/with-transform-on-a-referenced-path.png b/crates/resvg/tests-hinted/tests/text/textPath/with-transform-on-a-referenced-path.png new file mode 100644 index 000000000..215041c5e Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/with-transform-on-a-referenced-path.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/with-transform-outside-a-referenced-path.png b/crates/resvg/tests-hinted/tests/text/textPath/with-transform-outside-a-referenced-path.png new file mode 100644 index 000000000..76be77e91 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/with-transform-outside-a-referenced-path.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/with-underline.png b/crates/resvg/tests-hinted/tests/text/textPath/with-underline.png new file mode 100644 index 000000000..d3aed712d Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/with-underline.png differ diff --git a/crates/resvg/tests-hinted/tests/text/textPath/writing-mode=tb.png b/crates/resvg/tests-hinted/tests/text/textPath/writing-mode=tb.png new file mode 100644 index 000000000..3e1c98107 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/textPath/writing-mode=tb.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tref/link-to-a-complex-text.png b/crates/resvg/tests-hinted/tests/text/tref/link-to-a-complex-text.png new file mode 100644 index 000000000..feddad059 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tref/link-to-a-complex-text.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tref/link-to-a-non-SVG-element.png b/crates/resvg/tests-hinted/tests/text/tref/link-to-a-non-SVG-element.png new file mode 100644 index 000000000..f192fbb98 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tref/link-to-a-non-SVG-element.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tref/link-to-a-non-text-element.png b/crates/resvg/tests-hinted/tests/text/tref/link-to-a-non-text-element.png new file mode 100644 index 000000000..c77ef22f7 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tref/link-to-a-non-text-element.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tref/link-to-an-external-file-element.png b/crates/resvg/tests-hinted/tests/text/tref/link-to-an-external-file-element.png new file mode 100644 index 000000000..0e0b36898 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tref/link-to-an-external-file-element.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tref/link-to-text.png b/crates/resvg/tests-hinted/tests/text/tref/link-to-text.png new file mode 100644 index 000000000..c77ef22f7 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tref/link-to-text.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tref/nested.png b/crates/resvg/tests-hinted/tests/text/tref/nested.png new file mode 100644 index 000000000..f192fbb98 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tref/nested.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tref/position-attributes.png b/crates/resvg/tests-hinted/tests/text/tref/position-attributes.png new file mode 100644 index 000000000..c77ef22f7 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tref/position-attributes.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tref/style-attributes.png b/crates/resvg/tests-hinted/tests/text/tref/style-attributes.png new file mode 100644 index 000000000..c77ef22f7 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tref/style-attributes.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tref/with-a-title-child.png b/crates/resvg/tests-hinted/tests/text/tref/with-a-title-child.png new file mode 100644 index 000000000..3383b4211 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tref/with-a-title-child.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tref/with-text.png b/crates/resvg/tests-hinted/tests/text/tref/with-text.png new file mode 100644 index 000000000..3383b4211 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tref/with-text.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tref/xml-space.png b/crates/resvg/tests-hinted/tests/text/tref/xml-space.png new file mode 100644 index 000000000..acd613b55 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tref/xml-space.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/bidi-reordering.png b/crates/resvg/tests-hinted/tests/text/tspan/bidi-reordering.png new file mode 100644 index 000000000..0dd91d52f Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/bidi-reordering.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/mixed-font-size.png b/crates/resvg/tests-hinted/tests/text/tspan/mixed-font-size.png new file mode 100644 index 000000000..b3ebfc4ff Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/mixed-font-size.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/mixed-xml-space-1.png b/crates/resvg/tests-hinted/tests/text/tspan/mixed-xml-space-1.png new file mode 100644 index 000000000..aceb7c921 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/mixed-xml-space-1.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/mixed-xml-space-2.png b/crates/resvg/tests-hinted/tests/text/tspan/mixed-xml-space-2.png new file mode 100644 index 000000000..a5a0de16f Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/mixed-xml-space-2.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/mixed-xml-space-3.png b/crates/resvg/tests-hinted/tests/text/tspan/mixed-xml-space-3.png new file mode 100644 index 000000000..cd4394f0d Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/mixed-xml-space-3.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/mixed.png b/crates/resvg/tests-hinted/tests/text/tspan/mixed.png new file mode 100644 index 000000000..a307cb9c2 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/mixed.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/multiple-coordinates.png b/crates/resvg/tests-hinted/tests/text/tspan/multiple-coordinates.png new file mode 100644 index 000000000..95e556a6d Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/multiple-coordinates.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/nested-rotate.png b/crates/resvg/tests-hinted/tests/text/tspan/nested-rotate.png new file mode 100644 index 000000000..607313a8b Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/nested-rotate.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/nested-whitespaces.png b/crates/resvg/tests-hinted/tests/text/tspan/nested-whitespaces.png new file mode 100644 index 000000000..8de5c7465 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/nested-whitespaces.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/nested.png b/crates/resvg/tests-hinted/tests/text/tspan/nested.png new file mode 100644 index 000000000..c44914518 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/nested.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/only-with-y.png b/crates/resvg/tests-hinted/tests/text/tspan/only-with-y.png new file mode 100644 index 000000000..ef9f34131 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/only-with-y.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/outside-the-text.png b/crates/resvg/tests-hinted/tests/text/tspan/outside-the-text.png new file mode 100644 index 000000000..f192fbb98 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/outside-the-text.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/pseudo-multi-line.png b/crates/resvg/tests-hinted/tests/text/tspan/pseudo-multi-line.png new file mode 100644 index 000000000..5d2784df6 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/pseudo-multi-line.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/rotate-and-display-none.png b/crates/resvg/tests-hinted/tests/text/tspan/rotate-and-display-none.png new file mode 100644 index 000000000..d7c797962 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/rotate-and-display-none.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/rotate-on-child.png b/crates/resvg/tests-hinted/tests/text/tspan/rotate-on-child.png new file mode 100644 index 000000000..f6fb88e66 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/rotate-on-child.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/sequential.png b/crates/resvg/tests-hinted/tests/text/tspan/sequential.png new file mode 100644 index 000000000..a307cb9c2 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/sequential.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/style-override.png b/crates/resvg/tests-hinted/tests/text/tspan/style-override.png new file mode 100644 index 000000000..c1a2b7732 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/style-override.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/text-shaping-across-multiple-tspan-1.png b/crates/resvg/tests-hinted/tests/text/tspan/text-shaping-across-multiple-tspan-1.png new file mode 100644 index 000000000..ce0c2ee97 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/text-shaping-across-multiple-tspan-1.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/text-shaping-across-multiple-tspan-2.png b/crates/resvg/tests-hinted/tests/text/tspan/text-shaping-across-multiple-tspan-2.png new file mode 100644 index 000000000..035e35b7e Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/text-shaping-across-multiple-tspan-2.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/transform.png b/crates/resvg/tests-hinted/tests/text/tspan/transform.png new file mode 100644 index 000000000..ef9f34131 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/transform.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/tspan-bbox-1.png b/crates/resvg/tests-hinted/tests/text/tspan/tspan-bbox-1.png new file mode 100644 index 000000000..6cb2f1fa9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/tspan-bbox-1.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/tspan-bbox-2.png b/crates/resvg/tests-hinted/tests/text/tspan/tspan-bbox-2.png new file mode 100644 index 000000000..478c7f069 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/tspan-bbox-2.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/tspan-span-and-BIDI-reordering.png b/crates/resvg/tests-hinted/tests/text/tspan/tspan-span-and-BIDI-reordering.png new file mode 100644 index 000000000..777d2c988 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/tspan-span-and-BIDI-reordering.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/with-clip-path.png b/crates/resvg/tests-hinted/tests/text/tspan/with-clip-path.png new file mode 100644 index 000000000..ef9f34131 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/with-clip-path.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/with-dy.png b/crates/resvg/tests-hinted/tests/text/tspan/with-dy.png new file mode 100644 index 000000000..9fff45e24 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/with-dy.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/with-filter.png b/crates/resvg/tests-hinted/tests/text/tspan/with-filter.png new file mode 100644 index 000000000..ef9f34131 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/with-filter.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/with-mask.png b/crates/resvg/tests-hinted/tests/text/tspan/with-mask.png new file mode 100644 index 000000000..ef9f34131 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/with-mask.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/with-opacity.png b/crates/resvg/tests-hinted/tests/text/tspan/with-opacity.png new file mode 100644 index 000000000..4da5aea0e Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/with-opacity.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/with-x-and-y.png b/crates/resvg/tests-hinted/tests/text/tspan/with-x-and-y.png new file mode 100644 index 000000000..ef9f34131 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/with-x-and-y.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/without-attributes.png b/crates/resvg/tests-hinted/tests/text/tspan/without-attributes.png new file mode 100644 index 000000000..ef9f34131 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/without-attributes.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/xml-space-1.png b/crates/resvg/tests-hinted/tests/text/tspan/xml-space-1.png new file mode 100644 index 000000000..7f9ab70ae Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/xml-space-1.png differ diff --git a/crates/resvg/tests-hinted/tests/text/tspan/xml-space-2.png b/crates/resvg/tests-hinted/tests/text/tspan/xml-space-2.png new file mode 100644 index 000000000..7f9ab70ae Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/tspan/xml-space-2.png differ diff --git a/crates/resvg/tests-hinted/tests/text/unicode-bidi/bidi-override.png b/crates/resvg/tests-hinted/tests/text/unicode-bidi/bidi-override.png new file mode 100644 index 000000000..8a474e793 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/unicode-bidi/bidi-override.png differ diff --git a/crates/resvg/tests-hinted/tests/text/word-spacing/-5.png b/crates/resvg/tests-hinted/tests/text/word-spacing/-5.png new file mode 100644 index 000000000..44c765b17 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/word-spacing/-5.png differ diff --git a/crates/resvg/tests-hinted/tests/text/word-spacing/0.png b/crates/resvg/tests-hinted/tests/text/word-spacing/0.png new file mode 100644 index 000000000..218adbf5c Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/word-spacing/0.png differ diff --git a/crates/resvg/tests-hinted/tests/text/word-spacing/10.png b/crates/resvg/tests-hinted/tests/text/word-spacing/10.png new file mode 100644 index 000000000..35c4431c6 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/word-spacing/10.png differ diff --git a/crates/resvg/tests-hinted/tests/text/word-spacing/2mm.png b/crates/resvg/tests-hinted/tests/text/word-spacing/2mm.png new file mode 100644 index 000000000..9c796a0da Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/word-spacing/2mm.png differ diff --git a/crates/resvg/tests-hinted/tests/text/word-spacing/5percent.png b/crates/resvg/tests-hinted/tests/text/word-spacing/5percent.png new file mode 100644 index 000000000..35c4431c6 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/word-spacing/5percent.png differ diff --git a/crates/resvg/tests-hinted/tests/text/word-spacing/large-negative.png b/crates/resvg/tests-hinted/tests/text/word-spacing/large-negative.png new file mode 100644 index 000000000..f192fbb98 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/word-spacing/large-negative.png differ diff --git a/crates/resvg/tests-hinted/tests/text/word-spacing/normal.png b/crates/resvg/tests-hinted/tests/text/word-spacing/normal.png new file mode 100644 index 000000000..218adbf5c Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/word-spacing/normal.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/arabic-with-rl.png b/crates/resvg/tests-hinted/tests/text/writing-mode/arabic-with-rl.png new file mode 100644 index 000000000..c9c41e555 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/arabic-with-rl.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/horizontal-tb.png b/crates/resvg/tests-hinted/tests/text/writing-mode/horizontal-tb.png new file mode 100644 index 000000000..1a6398aa9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/horizontal-tb.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/inheritance.png b/crates/resvg/tests-hinted/tests/text/writing-mode/inheritance.png new file mode 100644 index 000000000..7cfad8bd7 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/inheritance.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/invalid-value.png b/crates/resvg/tests-hinted/tests/text/writing-mode/invalid-value.png new file mode 100644 index 000000000..1a6398aa9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/invalid-value.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/japanese-with-tb.png b/crates/resvg/tests-hinted/tests/text/writing-mode/japanese-with-tb.png new file mode 100644 index 000000000..7f84384c0 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/japanese-with-tb.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/lr-tb.png b/crates/resvg/tests-hinted/tests/text/writing-mode/lr-tb.png new file mode 100644 index 000000000..1a6398aa9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/lr-tb.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/lr.png b/crates/resvg/tests-hinted/tests/text/writing-mode/lr.png new file mode 100644 index 000000000..1a6398aa9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/lr.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/mixed-languages-with-tb-and-underline.png b/crates/resvg/tests-hinted/tests/text/writing-mode/mixed-languages-with-tb-and-underline.png new file mode 100644 index 000000000..e885cc1ff Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/mixed-languages-with-tb-and-underline.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/mixed-languages-with-tb.png b/crates/resvg/tests-hinted/tests/text/writing-mode/mixed-languages-with-tb.png new file mode 100644 index 000000000..a7ca7c988 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/mixed-languages-with-tb.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/on-tspan.png b/crates/resvg/tests-hinted/tests/text/writing-mode/on-tspan.png new file mode 100644 index 000000000..1a6398aa9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/on-tspan.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/rl-tb.png b/crates/resvg/tests-hinted/tests/text/writing-mode/rl-tb.png new file mode 100644 index 000000000..1a6398aa9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/rl-tb.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/rl.png b/crates/resvg/tests-hinted/tests/text/writing-mode/rl.png new file mode 100644 index 000000000..1a6398aa9 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/rl.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/tb-and-punctuation.png b/crates/resvg/tests-hinted/tests/text/writing-mode/tb-and-punctuation.png new file mode 100644 index 000000000..bf62f2cea Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/tb-and-punctuation.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/tb-rl.png b/crates/resvg/tests-hinted/tests/text/writing-mode/tb-rl.png new file mode 100644 index 000000000..7cfad8bd7 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/tb-rl.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/tb-with-alignment.png b/crates/resvg/tests-hinted/tests/text/writing-mode/tb-with-alignment.png new file mode 100644 index 000000000..6c10b1d29 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/tb-with-alignment.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/tb-with-dx-on-second-tspan.png b/crates/resvg/tests-hinted/tests/text/writing-mode/tb-with-dx-on-second-tspan.png new file mode 100644 index 000000000..4b911f374 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/tb-with-dx-on-second-tspan.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/tb-with-dx-on-tspan.png b/crates/resvg/tests-hinted/tests/text/writing-mode/tb-with-dx-on-tspan.png new file mode 100644 index 000000000..aa564bd17 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/tb-with-dx-on-tspan.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/tb-with-dy-on-second-tspan.png b/crates/resvg/tests-hinted/tests/text/writing-mode/tb-with-dy-on-second-tspan.png new file mode 100644 index 000000000..ced51b913 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/tb-with-dy-on-second-tspan.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/tb-with-rotate-and-underline.png b/crates/resvg/tests-hinted/tests/text/writing-mode/tb-with-rotate-and-underline.png new file mode 100644 index 000000000..204522bcc Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/tb-with-rotate-and-underline.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/tb-with-rotate.png b/crates/resvg/tests-hinted/tests/text/writing-mode/tb-with-rotate.png new file mode 100644 index 000000000..59fffe168 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/tb-with-rotate.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/tb.png b/crates/resvg/tests-hinted/tests/text/writing-mode/tb.png new file mode 100644 index 000000000..7cfad8bd7 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/tb.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/vertical-lr.png b/crates/resvg/tests-hinted/tests/text/writing-mode/vertical-lr.png new file mode 100644 index 000000000..7cfad8bd7 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/vertical-lr.png differ diff --git a/crates/resvg/tests-hinted/tests/text/writing-mode/vertical-rl.png b/crates/resvg/tests-hinted/tests/text/writing-mode/vertical-rl.png new file mode 100644 index 000000000..7cfad8bd7 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/writing-mode/vertical-rl.png differ diff --git a/crates/resvg/tests-hinted/text/color-font/cbdt.png b/crates/resvg/tests-hinted/text/color-font/cbdt.png new file mode 100644 index 000000000..adcdfa237 Binary files /dev/null and b/crates/resvg/tests-hinted/text/color-font/cbdt.png differ diff --git a/crates/resvg/tests-hinted/text/color-font/colrv0.png b/crates/resvg/tests-hinted/text/color-font/colrv0.png new file mode 100644 index 000000000..e4b090d24 Binary files /dev/null and b/crates/resvg/tests-hinted/text/color-font/colrv0.png differ diff --git a/crates/resvg/tests-hinted/text/color-font/colrv1.png b/crates/resvg/tests-hinted/text/color-font/colrv1.png new file mode 100644 index 000000000..7537da902 Binary files /dev/null and b/crates/resvg/tests-hinted/text/color-font/colrv1.png differ diff --git a/crates/resvg/tests-hinted/text/color-font/compound-emojis-and-coordinates-list.png b/crates/resvg/tests-hinted/text/color-font/compound-emojis-and-coordinates-list.png new file mode 100644 index 000000000..b6ecffc1f Binary files /dev/null and b/crates/resvg/tests-hinted/text/color-font/compound-emojis-and-coordinates-list.png differ diff --git a/crates/resvg/tests-hinted/text/color-font/compound-emojis.png b/crates/resvg/tests-hinted/text/color-font/compound-emojis.png new file mode 100644 index 000000000..6c6724ece Binary files /dev/null and b/crates/resvg/tests-hinted/text/color-font/compound-emojis.png differ diff --git a/crates/resvg/tests-hinted/text/color-font/mixed-text-rtl.png b/crates/resvg/tests-hinted/text/color-font/mixed-text-rtl.png new file mode 100644 index 000000000..4be0084c6 Binary files /dev/null and b/crates/resvg/tests-hinted/text/color-font/mixed-text-rtl.png differ diff --git a/crates/resvg/tests-hinted/text/color-font/mixed-text.png b/crates/resvg/tests-hinted/text/color-font/mixed-text.png new file mode 100644 index 000000000..6e31b50da Binary files /dev/null and b/crates/resvg/tests-hinted/text/color-font/mixed-text.png differ diff --git a/crates/resvg/tests-hinted/text/color-font/sbix.png b/crates/resvg/tests-hinted/text/color-font/sbix.png new file mode 100644 index 000000000..f7839ac35 Binary files /dev/null and b/crates/resvg/tests-hinted/text/color-font/sbix.png differ diff --git a/crates/resvg/tests-hinted/text/color-font/svg.png b/crates/resvg/tests-hinted/text/color-font/svg.png new file mode 100644 index 000000000..476deb877 Binary files /dev/null and b/crates/resvg/tests-hinted/text/color-font/svg.png differ diff --git a/crates/resvg/tests-hinted/text/color-font/writing-mode=tb.png b/crates/resvg/tests-hinted/text/color-font/writing-mode=tb.png new file mode 100644 index 000000000..98fdf53ba Binary files /dev/null and b/crates/resvg/tests-hinted/text/color-font/writing-mode=tb.png differ diff --git a/crates/resvg/tests/fonts/README.md b/crates/resvg/tests/fonts/README.md index 7108843b6..18d6d1515 100644 --- a/crates/resvg/tests/fonts/README.md +++ b/crates/resvg/tests/fonts/README.md @@ -14,4 +14,9 @@ Noto COLOR Emoji (COLRv1) 3. Run `fonttools ttx NotoColorEmojiCOLR.subset.ttf` 4. Go to the section and rename all instances of "Noto Color Emoji" to "Noto Color Emoji COLR" (so that we can distinguish them from CBDT in tests). -5. Run `fonttools ttx -f NotoColorEmojiCOLR.subset.ttx` \ No newline at end of file +5. Run `fonttools ttx -f NotoColorEmojiCOLR.subset.ttx` + +Roboto Flex (Variable Font) +1. Download: https://github.com/googlefonts/roboto-flex/raw/main/fonts/RobotoFlex%5BGRAD%2CXOPQ%2CXTRA%2CYOPQ%2CYTAS%2CYTDE%2CYTFI%2CYTLC%2CYTUC%2Copsz%2Cslnt%2Cwdth%2Cwght%5D.ttf +2. Run `pyftsubset RobotoFlex*.ttf --unicodes="U+0020-007E" --layout-features='*' --drop-tables= --output-file=RobotoFlex.subset.ttf` +3. Copy OFL license from https://github.com/googlefonts/roboto-flex/blob/main/OFL.txt diff --git a/crates/resvg/tests/fonts/RobotoFlex-LICENSE-OFL.txt b/crates/resvg/tests/fonts/RobotoFlex-LICENSE-OFL.txt new file mode 100644 index 000000000..5530c5720 --- /dev/null +++ b/crates/resvg/tests/fonts/RobotoFlex-LICENSE-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Roboto Flex Project Authors (https://github.com/googlefonts/roboto-flex) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/crates/resvg/tests/fonts/RobotoFlex.subset.ttf b/crates/resvg/tests/fonts/RobotoFlex.subset.ttf new file mode 100644 index 000000000..fc9440d2e Binary files /dev/null and b/crates/resvg/tests/fonts/RobotoFlex.subset.ttf differ diff --git a/crates/resvg/tests/gen-tests.py b/crates/resvg/tests/gen-tests.py index a0b8bd0ba..8a8f12930 100755 --- a/crates/resvg/tests/gen-tests.py +++ b/crates/resvg/tests/gen-tests.py @@ -1,6 +1,17 @@ #!/usr/bin/env python3 -import os +"""Generate integration test files for resvg. + +This script generates two test files: +- render.rs: Tests for all SVG files (unhinted rendering) +- render_hinted.rs: Tests for text SVG files (hinted rendering) + +Usage: + python3 gen-tests.py # Uses default output paths + python3 gen-tests.py -o custom/render.rs --output-hinted custom/render_hinted.rs +""" + +import argparse from pathlib import Path IGNORE = [ @@ -14,28 +25,89 @@ 'tests/paint-servers/radialGradient/focal-point-correction', ] -print('// Copyright 2020 the Resvg Authors') -print('// SPDX-License-Identifier: Apache-2.0 OR MIT') -print() -print('// This file is auto-generated by gen-tests.py') -print() -print('#![allow(non_snake_case)]') -print() -print('use crate::render;') -print() - -files = sorted(list(Path('tests').rglob('*.svg'))) -for file in files: - file = str(file).replace('.svg', '') - - if file in IGNORE: - continue - fn_name = file.replace('tests/', '') +def make_fn_name(file_path): + """Convert a file path to a valid Rust function name.""" + fn_name = file_path.replace('tests/', '') fn_name = fn_name.replace('/', '_') fn_name = fn_name.replace('-', '_') fn_name = fn_name.replace('=', '_eq_') fn_name = fn_name.replace('.', '_') fn_name = fn_name.replace('#', '') + return fn_name + + +def generate_render_rs(output_file): + """Generate render.rs with unhinted tests for all SVG files.""" + with open(output_file, 'w') as f: + f.write('// Copyright 2020 the Resvg Authors\n') + f.write('// SPDX-License-Identifier: Apache-2.0 OR MIT\n') + f.write('\n') + f.write('// This file is auto-generated by gen-tests.py\n') + f.write('\n') + f.write('#![allow(non_snake_case)]\n') + f.write('\n') + f.write('use crate::render;\n') + f.write('\n') + + files = sorted(list(Path('tests').rglob('*.svg'))) + for file in files: + file_str = str(file).replace('.svg', '') + + if file_str in IGNORE: + continue + + fn_name = make_fn_name(file_str) + f.write(f'#[test] fn {fn_name}() {{ assert_eq!(render("{file_str}"), 0); }}\n') + + +def generate_render_hinted_rs(output_file): + """Generate render_hinted.rs with hinted tests for text SVG files.""" + with open(output_file, 'w') as f: + f.write('// Copyright 2020 the Resvg Authors\n') + f.write('// SPDX-License-Identifier: Apache-2.0 OR MIT\n') + f.write('\n') + f.write('// This file is auto-generated by gen-tests.py\n') + f.write('\n') + f.write('#![allow(non_snake_case)]\n') + f.write('\n') + f.write('use crate::render_hinted;\n') + f.write('\n') + + # Only generate hinted tests for text-related tests + text_files = sorted(list(Path('tests/text').rglob('*.svg'))) + for file in text_files: + file_str = str(file).replace('.svg', '') + + if file_str in IGNORE: + continue + + fn_name = 'hinted_' + make_fn_name(file_str) + f.write(f'#[test] fn {fn_name}() {{ assert_eq!(render_hinted("{file_str}"), 0); }}\n') + + +def main(): + parser = argparse.ArgumentParser( + description='Generate integration test files for resvg' + ) + parser.add_argument( + '--output', '-o', + default='integration/render.rs', + help='Output file for unhinted tests (default: integration/render.rs)' + ) + parser.add_argument( + '--output-hinted', + default='integration/render_hinted.rs', + help='Output file for hinted tests (default: integration/render_hinted.rs)' + ) + args = parser.parse_args() + + generate_render_rs(args.output) + print(f'Generated {args.output}') + + generate_render_hinted_rs(args.output_hinted) + print(f'Generated {args.output_hinted}') + - print(f'#[test] fn {fn_name}() {{ assert_eq!(render("{file}"), 0); }}') +if __name__ == '__main__': + main() diff --git a/crates/resvg/tests/integration/hinting.rs b/crates/resvg/tests/integration/hinting.rs new file mode 100644 index 000000000..c3f4590e2 --- /dev/null +++ b/crates/resvg/tests/integration/hinting.rs @@ -0,0 +1,279 @@ +// Copyright 2025 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Tests for font hinting functionality. +//! +//! These tests verify that: +//! 1. Hinting produces visibly different output than non-hinted rendering +//! 2. The `text-rendering` CSS property correctly controls hinting behavior +//! 3. Hinting works correctly at various font sizes + +use crate::GLOBAL_FONTDB; + +/// Renders an SVG with the specified hinting settings and returns the pixel data. +fn render_with_hinting(svg_data: &[u8], hinting_enabled: bool) -> Vec { + let opt = usvg::Options { + fontdb: GLOBAL_FONTDB.clone(), + hinting: usvg::HintingOptions { + enabled: hinting_enabled, + dpi: Some(96.0), + }, + ..usvg::Options::default() + }; + + let tree = usvg::Tree::from_data(svg_data, &opt).unwrap(); + let size = tree.size().to_int_size(); + let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height()).unwrap(); + resvg::render( + &tree, + tiny_skia::Transform::identity(), + &mut pixmap.as_mut(), + ); + + pixmap.take() +} + +/// Count the number of pixels that differ between two images. +fn count_different_pixels(img1: &[u8], img2: &[u8]) -> usize { + assert_eq!(img1.len(), img2.len()); + img1.chunks(4) + .zip(img2.chunks(4)) + .filter(|(p1, p2)| p1 != p2) + .count() +} + +/// Test that hinting produces different output than non-hinted rendering. +/// This demonstrates that hinting is actually being applied. +#[test] +fn hinting_produces_different_output() { + // Small text at 12px where hinting effects are most visible + let svg = br#" + + + Hinting Test + + + "#; + + let hinted = render_with_hinting(svg, true); + let unhinted = render_with_hinting(svg, false); + + let diff_count = count_different_pixels(&hinted, &unhinted); + + // Hinted and unhinted output should differ + // The exact number of different pixels depends on the font and size, + // but there should be a noticeable difference + assert!( + diff_count > 0, + "Hinted and unhinted output should differ, but they are identical" + ); + + // Log the difference for debugging + eprintln!( + "hinting_produces_different_output: {} pixels differ", + diff_count + ); +} + +/// Test that geometric-precision disables hinting even when hinting is enabled. +#[test] +fn geometric_precision_disables_hinting() { + let svg_geometric = br#" + + + Geometric Precision + + + "#; + + // With geometricPrecision, hinting should be disabled regardless of the option + let with_hinting_option = render_with_hinting(svg_geometric, true); + let without_hinting_option = render_with_hinting(svg_geometric, false); + + let diff_count = count_different_pixels(&with_hinting_option, &without_hinting_option); + + // Both should produce the same output since geometricPrecision disables hinting + assert_eq!( + diff_count, 0, + "geometricPrecision should produce identical output regardless of hinting option" + ); +} + +/// Test that optimizeLegibility enables hinting when the option is set. +#[test] +fn optimize_legibility_enables_hinting() { + let svg = br#" + + + Optimize Legibility + + + "#; + + let hinted = render_with_hinting(svg, true); + let unhinted = render_with_hinting(svg, false); + + let diff_count = count_different_pixels(&hinted, &unhinted); + + // optimizeLegibility with hinting enabled should differ from unhinted + assert!( + diff_count > 0, + "optimizeLegibility should produce different output when hinting is enabled" + ); +} + +/// Test hinting at various font sizes to demonstrate size-dependent effects. +#[test] +fn hinting_at_various_sizes() { + let sizes = [8, 10, 12, 14, 16, 20, 24, 32, 48]; + let mut results = Vec::new(); + + for size in sizes { + let svg = format!( + r#" + + + Size {} pixels + + + "#, + size, size + ); + + let hinted = render_with_hinting(svg.as_bytes(), true); + let unhinted = render_with_hinting(svg.as_bytes(), false); + + let diff_count = count_different_pixels(&hinted, &unhinted); + results.push((size, diff_count)); + + eprintln!("Size {}px: {} pixels differ", size, diff_count); + } + + // Verify that at least some sizes show hinting differences + let sizes_with_differences = results.iter().filter(|(_, diff)| *diff > 0).count(); + assert!( + sizes_with_differences > 0, + "Hinting should produce differences at various sizes" + ); +} + +/// Test that hinting works with different DPI settings. +#[test] +fn hinting_with_different_dpi() { + let svg = br#" + + + DPI Test + + + "#; + + let render_at_dpi = |dpi: f32| -> Vec { + let opt = usvg::Options { + fontdb: GLOBAL_FONTDB.clone(), + dpi, + hinting: usvg::HintingOptions { + enabled: true, + dpi: Some(dpi), + }, + ..usvg::Options::default() + }; + + let tree = usvg::Tree::from_data(svg, &opt).unwrap(); + let size = tree.size().to_int_size(); + let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height()).unwrap(); + resvg::render( + &tree, + tiny_skia::Transform::identity(), + &mut pixmap.as_mut(), + ); + pixmap.take() + }; + + let at_72dpi = render_at_dpi(72.0); + let at_96dpi = render_at_dpi(96.0); + let at_144dpi = render_at_dpi(144.0); + + // Different DPI values should produce different ppem values and thus different hinting. + // ppem = font_size * dpi / 72, so: + // - 72 DPI: ppem = 12 * 72 / 72 = 12 + // - 96 DPI: ppem = 12 * 96 / 72 = 16 + // - 144 DPI: ppem = 12 * 144 / 72 = 24 + let diff_72_96 = count_different_pixels(&at_72dpi, &at_96dpi); + let diff_96_144 = count_different_pixels(&at_96dpi, &at_144dpi); + + eprintln!("72 vs 96 DPI: {} pixels differ (ppem 12 vs 16)", diff_72_96); + eprintln!("96 vs 144 DPI: {} pixels differ (ppem 16 vs 24)", diff_96_144); + + // At least one pair should show differences due to different hinting grid alignment. + // Note: The exact differences depend on font hinting instructions, so we verify + // that different DPI values produce different rendering rather than requiring + // specific pixel counts. + assert!( + diff_72_96 > 0 || diff_96_144 > 0, + "Different DPI values should produce at least some hinting differences" + ); +} + +/// Test hinting with variable fonts (Roboto Flex). +#[test] +fn hinting_with_variable_font() { + let svg = br#" + + + Variable Font Hinting + + + "#; + + let hinted = render_with_hinting(svg, true); + let unhinted = render_with_hinting(svg, false); + + let diff_count = count_different_pixels(&hinted, &unhinted); + + eprintln!("Variable font hinting: {} pixels differ", diff_count); + + // Variable fonts should also show hinting differences + // (though the exact behavior depends on the font's hinting data) +} + +/// Test that auto text-rendering defaults to optimizeLegibility behavior. +#[test] +fn auto_text_rendering_uses_hinting() { + // SVG with auto (default) text-rendering + let svg_auto = br#" + + + Auto Text Rendering + + + "#; + + // SVG with explicit optimizeLegibility + let svg_legibility = br#" + + + Auto Text Rendering + + + "#; + + let auto_hinted = render_with_hinting(svg_auto, true); + let legibility_hinted = render_with_hinting(svg_legibility, true); + + let diff_count = count_different_pixels(&auto_hinted, &legibility_hinted); + + // Both should produce the same output since auto defaults to optimizeLegibility + assert_eq!( + diff_count, 0, + "auto and optimizeLegibility should produce identical output" + ); +} diff --git a/crates/resvg/tests/integration/main.rs b/crates/resvg/tests/integration/main.rs index b90eb5c6c..698952f1c 100644 --- a/crates/resvg/tests/integration/main.rs +++ b/crates/resvg/tests/integration/main.rs @@ -14,8 +14,13 @@ use usvg::fontdb; #[rustfmt::skip] mod render; +#[rustfmt::skip] +mod render_hinted; + mod extra; +mod hinting; + const IMAGE_SIZE: u32 = 300; static GLOBAL_FONTDB: Lazy> = Lazy::new(|| { @@ -34,11 +39,15 @@ static GLOBAL_FONTDB: Lazy> = Lazy::new(|| { }); pub fn render(name: &str) -> usize { - render_inner(name, TestMode::Normal) + render_inner(name, TestMode::Normal, HintingMode::Disabled) +} + +pub fn render_hinted(name: &str) -> usize { + render_inner(name, TestMode::Normal, HintingMode::Enabled) } pub fn render_extra_with_scale(name: &str, scale: f32) -> usize { - render_inner(name, TestMode::Extra(scale)) + render_inner(name, TestMode::Extra(scale), HintingMode::Disabled) } pub fn render_extra(name: &str) -> usize { @@ -46,14 +55,41 @@ pub fn render_extra(name: &str) -> usize { } pub fn render_node(name: &str, id: &str) -> usize { - render_inner(name, TestMode::Node(id)) + render_inner(name, TestMode::Node(id), HintingMode::Disabled) +} + +#[derive(Clone, Copy)] +pub enum HintingMode { + Disabled, + Enabled, } -pub fn render_inner(name: &str, test_mode: TestMode) -> usize { - let svg_path = format!("tests/{}.svg", name); - let png_path = format!("tests/{}.png", name); +pub fn render_inner(name: &str, test_mode: TestMode, hinting_mode: HintingMode) -> usize { + let (svg_path, png_path, diff_dir) = match hinting_mode { + HintingMode::Disabled => ( + format!("tests/{}.svg", name), + format!("tests/{}.png", name), + "tests/diffs", + ), + HintingMode::Enabled => ( + format!("tests/{}.svg", name), + format!("tests-hinted/{}.png", name), + "tests/diffs-hinted", + ), + }; let make_ref = std::env::var("MAKE_REF").is_ok(); + let hinting_options = match hinting_mode { + HintingMode::Disabled => usvg::HintingOptions { + enabled: false, + dpi: None, + }, + HintingMode::Enabled => usvg::HintingOptions { + enabled: true, + dpi: Some(96.0), + }, + }; + let opt = usvg::Options { fontdb: GLOBAL_FONTDB.clone(), resources_dir: Some( @@ -62,6 +98,8 @@ pub fn render_inner(name: &str, test_mode: TestMode) -> usize { .unwrap() .to_owned(), ), + #[cfg(feature = "text")] + hinting: hinting_options, ..usvg::Options::default() }; @@ -110,6 +148,12 @@ pub fn render_inner(name: &str, test_mode: TestMode) -> usize { }; let make_ref_fn = || -> ! { + // Create parent directory if needed (for tests-hinted/) + if let Some(parent) = std::path::Path::new(&png_path).parent() { + if let Err(e) = std::fs::create_dir_all(parent) { + eprintln!("Warning: failed to create directory {:?}: {}", parent, e); + } + } pixmap.save_png(&png_path).unwrap(); Command::new("oxipng") .args([ @@ -129,7 +173,7 @@ pub fn render_inner(name: &str, test_mode: TestMode) -> usize { if make_ref { make_ref_fn(); } else { - panic!("missing reference image"); + panic!("missing reference image: {}", png_path); } }; @@ -137,8 +181,8 @@ pub fn render_inner(name: &str, test_mode: TestMode) -> usize { if make_ref { make_ref_fn(); } else { - let _ = std::fs::create_dir_all("tests/diffs"); - diff_image.save_png(&format!("tests/diffs/{}.png", name.replace("/", "_"))); + let _ = std::fs::create_dir_all(diff_dir); + diff_image.save_png(&format!("{}/{}.png", diff_dir, name.replace("/", "_"))); pixel_diff } @@ -149,6 +193,10 @@ pub fn render_inner(name: &str, test_mode: TestMode) -> usize { /// Returns `Some` if there is at least one different pixel, and `None` if the images match. fn get_diff(expected_image: &TestImage, actual_image: &TestImage) -> Option<(TestImage, usize)> { + /// Pixel difference threshold for image comparison. + /// Value of 1 means any channel difference > 1 is considered a mismatch. + /// This is strict but necessary for detecting subtle font rendering changes. + /// Note: May need platform-specific adjustments if tests become flaky. const DIFF_THRESHOLD: u8 = 1; let width = max(expected_image.width, actual_image.width); diff --git a/crates/resvg/tests/integration/render.rs b/crates/resvg/tests/integration/render.rs index d5d651c8d..ad79dd7e5 100644 --- a/crates/resvg/tests/integration/render.rs +++ b/crates/resvg/tests/integration/render.rs @@ -1457,6 +1457,20 @@ use crate::render; #[test] fn text_font_style_oblique() { assert_eq!(render("tests/text/font-style/oblique"), 0); } #[test] fn text_font_variant_inherit() { assert_eq!(render("tests/text/font-variant/inherit"), 0); } #[test] fn text_font_variant_small_caps() { assert_eq!(render("tests/text/font-variant/small-caps"), 0); } +#[test] fn text_font_variation_settings_all_axes_combined() { assert_eq!(render("tests/text/font-variation-settings/all-axes-combined"), 0); } +#[test] fn text_font_variation_settings_auto_font_stretch_condensed() { assert_eq!(render("tests/text/font-variation-settings/auto-font-stretch-condensed"), 0); } +#[test] fn text_font_variation_settings_auto_font_style_oblique() { assert_eq!(render("tests/text/font-variation-settings/auto-font-style-oblique"), 0); } +#[test] fn text_font_variation_settings_auto_font_weight_700() { assert_eq!(render("tests/text/font-variation-settings/auto-font-weight-700"), 0); } +#[test] fn text_font_variation_settings_explicit_overrides_auto() { assert_eq!(render("tests/text/font-variation-settings/explicit-overrides-auto"), 0); } +#[test] fn text_font_variation_settings_grad_negative() { assert_eq!(render("tests/text/font-variation-settings/grad-negative"), 0); } +#[test] fn text_font_variation_settings_multiple_axes() { assert_eq!(render("tests/text/font-variation-settings/multiple-axes"), 0); } +#[test] fn text_font_variation_settings_opsz_144() { assert_eq!(render("tests/text/font-variation-settings/opsz-144"), 0); } +#[test] fn text_font_variation_settings_slnt_negative() { assert_eq!(render("tests/text/font-variation-settings/slnt-negative"), 0); } +#[test] fn text_font_variation_settings_wdth_151() { assert_eq!(render("tests/text/font-variation-settings/wdth-151"), 0); } +#[test] fn text_font_variation_settings_wdth_25() { assert_eq!(render("tests/text/font-variation-settings/wdth-25"), 0); } +#[test] fn text_font_variation_settings_wght_100() { assert_eq!(render("tests/text/font-variation-settings/wght-100"), 0); } +#[test] fn text_font_variation_settings_wght_700() { assert_eq!(render("tests/text/font-variation-settings/wght-700"), 0); } +#[test] fn text_font_variation_settings_xtra_extreme() { assert_eq!(render("tests/text/font-variation-settings/xtra-extreme"), 0); } #[test] fn text_font_weight_650() { assert_eq!(render("tests/text/font-weight/650"), 0); } #[test] fn text_font_weight_700() { assert_eq!(render("tests/text/font-weight/700"), 0); } #[test] fn text_font_weight_bold() { assert_eq!(render("tests/text/font-weight/bold"), 0); } diff --git a/crates/resvg/tests/integration/render_hinted.rs b/crates/resvg/tests/integration/render_hinted.rs new file mode 100644 index 000000000..cf4b06a06 --- /dev/null +++ b/crates/resvg/tests/integration/render_hinted.rs @@ -0,0 +1,387 @@ +// Copyright 2020 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +// This file is auto-generated by gen-tests.py + +#![allow(non_snake_case)] + +use crate::render_hinted; + +#[test] fn hinted_text_alignment_baseline_after_edge() { assert_eq!(render_hinted("tests/text/alignment-baseline/after-edge"), 0); } +#[test] fn hinted_text_alignment_baseline_alphabetic() { assert_eq!(render_hinted("tests/text/alignment-baseline/alphabetic"), 0); } +#[test] fn hinted_text_alignment_baseline_auto() { assert_eq!(render_hinted("tests/text/alignment-baseline/auto"), 0); } +#[test] fn hinted_text_alignment_baseline_baseline() { assert_eq!(render_hinted("tests/text/alignment-baseline/baseline"), 0); } +#[test] fn hinted_text_alignment_baseline_before_edge() { assert_eq!(render_hinted("tests/text/alignment-baseline/before-edge"), 0); } +#[test] fn hinted_text_alignment_baseline_central() { assert_eq!(render_hinted("tests/text/alignment-baseline/central"), 0); } +#[test] fn hinted_text_alignment_baseline_hanging_and_baseline_shift_eq_20_on_tspan() { assert_eq!(render_hinted("tests/text/alignment-baseline/hanging-and-baseline-shift-eq-20-on-tspan"), 0); } +#[test] fn hinted_text_alignment_baseline_hanging_on_tspan() { assert_eq!(render_hinted("tests/text/alignment-baseline/hanging-on-tspan"), 0); } +#[test] fn hinted_text_alignment_baseline_hanging_on_vertical() { assert_eq!(render_hinted("tests/text/alignment-baseline/hanging-on-vertical"), 0); } +#[test] fn hinted_text_alignment_baseline_hanging_with_underline() { assert_eq!(render_hinted("tests/text/alignment-baseline/hanging-with-underline"), 0); } +#[test] fn hinted_text_alignment_baseline_hanging() { assert_eq!(render_hinted("tests/text/alignment-baseline/hanging"), 0); } +#[test] fn hinted_text_alignment_baseline_ideographic() { assert_eq!(render_hinted("tests/text/alignment-baseline/ideographic"), 0); } +#[test] fn hinted_text_alignment_baseline_inherit() { assert_eq!(render_hinted("tests/text/alignment-baseline/inherit"), 0); } +#[test] fn hinted_text_alignment_baseline_mathematical() { assert_eq!(render_hinted("tests/text/alignment-baseline/mathematical"), 0); } +#[test] fn hinted_text_alignment_baseline_middle_on_textPath() { assert_eq!(render_hinted("tests/text/alignment-baseline/middle-on-textPath"), 0); } +#[test] fn hinted_text_alignment_baseline_middle() { assert_eq!(render_hinted("tests/text/alignment-baseline/middle"), 0); } +#[test] fn hinted_text_alignment_baseline_text_after_edge() { assert_eq!(render_hinted("tests/text/alignment-baseline/text-after-edge"), 0); } +#[test] fn hinted_text_alignment_baseline_text_before_edge() { assert_eq!(render_hinted("tests/text/alignment-baseline/text-before-edge"), 0); } +#[test] fn hinted_text_alignment_baseline_two_textPath_with_middle_on_first() { assert_eq!(render_hinted("tests/text/alignment-baseline/two-textPath-with-middle-on-first"), 0); } +#[test] fn hinted_text_baseline_shift__10() { assert_eq!(render_hinted("tests/text/baseline-shift/-10"), 0); } +#[test] fn hinted_text_baseline_shift__50percent() { assert_eq!(render_hinted("tests/text/baseline-shift/-50percent"), 0); } +#[test] fn hinted_text_baseline_shift_0() { assert_eq!(render_hinted("tests/text/baseline-shift/0"), 0); } +#[test] fn hinted_text_baseline_shift_10() { assert_eq!(render_hinted("tests/text/baseline-shift/10"), 0); } +#[test] fn hinted_text_baseline_shift_2mm() { assert_eq!(render_hinted("tests/text/baseline-shift/2mm"), 0); } +#[test] fn hinted_text_baseline_shift_50percent() { assert_eq!(render_hinted("tests/text/baseline-shift/50percent"), 0); } +#[test] fn hinted_text_baseline_shift_baseline() { assert_eq!(render_hinted("tests/text/baseline-shift/baseline"), 0); } +#[test] fn hinted_text_baseline_shift_deeply_nested_super() { assert_eq!(render_hinted("tests/text/baseline-shift/deeply-nested-super"), 0); } +#[test] fn hinted_text_baseline_shift_inheritance_1() { assert_eq!(render_hinted("tests/text/baseline-shift/inheritance-1"), 0); } +#[test] fn hinted_text_baseline_shift_inheritance_2() { assert_eq!(render_hinted("tests/text/baseline-shift/inheritance-2"), 0); } +#[test] fn hinted_text_baseline_shift_inheritance_3() { assert_eq!(render_hinted("tests/text/baseline-shift/inheritance-3"), 0); } +#[test] fn hinted_text_baseline_shift_inheritance_4() { assert_eq!(render_hinted("tests/text/baseline-shift/inheritance-4"), 0); } +#[test] fn hinted_text_baseline_shift_inheritance_5() { assert_eq!(render_hinted("tests/text/baseline-shift/inheritance-5"), 0); } +#[test] fn hinted_text_baseline_shift_invalid_value() { assert_eq!(render_hinted("tests/text/baseline-shift/invalid-value"), 0); } +#[test] fn hinted_text_baseline_shift_mixed_nested() { assert_eq!(render_hinted("tests/text/baseline-shift/mixed-nested"), 0); } +#[test] fn hinted_text_baseline_shift_nested_length() { assert_eq!(render_hinted("tests/text/baseline-shift/nested-length"), 0); } +#[test] fn hinted_text_baseline_shift_nested_super() { assert_eq!(render_hinted("tests/text/baseline-shift/nested-super"), 0); } +#[test] fn hinted_text_baseline_shift_nested_with_baseline_1() { assert_eq!(render_hinted("tests/text/baseline-shift/nested-with-baseline-1"), 0); } +#[test] fn hinted_text_baseline_shift_nested_with_baseline_2() { assert_eq!(render_hinted("tests/text/baseline-shift/nested-with-baseline-2"), 0); } +#[test] fn hinted_text_baseline_shift_sub() { assert_eq!(render_hinted("tests/text/baseline-shift/sub"), 0); } +#[test] fn hinted_text_baseline_shift_super() { assert_eq!(render_hinted("tests/text/baseline-shift/super"), 0); } +#[test] fn hinted_text_baseline_shift_with_rotate() { assert_eq!(render_hinted("tests/text/baseline-shift/with-rotate"), 0); } +#[test] fn hinted_text_color_font_cbdt() { assert_eq!(render_hinted("tests/text/color-font/cbdt"), 0); } +#[test] fn hinted_text_color_font_colrv0() { assert_eq!(render_hinted("tests/text/color-font/colrv0"), 0); } +#[test] fn hinted_text_color_font_colrv1() { assert_eq!(render_hinted("tests/text/color-font/colrv1"), 0); } +#[test] fn hinted_text_color_font_compound_emojis_and_coordinates_list() { assert_eq!(render_hinted("tests/text/color-font/compound-emojis-and-coordinates-list"), 0); } +#[test] fn hinted_text_color_font_compound_emojis() { assert_eq!(render_hinted("tests/text/color-font/compound-emojis"), 0); } +#[test] fn hinted_text_color_font_mixed_text_rtl() { assert_eq!(render_hinted("tests/text/color-font/mixed-text-rtl"), 0); } +#[test] fn hinted_text_color_font_mixed_text() { assert_eq!(render_hinted("tests/text/color-font/mixed-text"), 0); } +#[test] fn hinted_text_color_font_sbix() { assert_eq!(render_hinted("tests/text/color-font/sbix"), 0); } +#[test] fn hinted_text_color_font_svg() { assert_eq!(render_hinted("tests/text/color-font/svg"), 0); } +#[test] fn hinted_text_color_font_writing_mode_eq_tb() { assert_eq!(render_hinted("tests/text/color-font/writing-mode=tb"), 0); } +#[test] fn hinted_text_direction_rtl_with_vertical_writing_mode() { assert_eq!(render_hinted("tests/text/direction/rtl-with-vertical-writing-mode"), 0); } +#[test] fn hinted_text_direction_rtl() { assert_eq!(render_hinted("tests/text/direction/rtl"), 0); } +#[test] fn hinted_text_dominant_baseline_alignment_baseline_and_baseline_shift_on_tspans() { assert_eq!(render_hinted("tests/text/dominant-baseline/alignment-baseline-and-baseline-shift-on-tspans"), 0); } +#[test] fn hinted_text_dominant_baseline_alignment_baseline_eq_baseline_on_tspan() { assert_eq!(render_hinted("tests/text/dominant-baseline/alignment-baseline=baseline-on-tspan"), 0); } +#[test] fn hinted_text_dominant_baseline_alphabetic() { assert_eq!(render_hinted("tests/text/dominant-baseline/alphabetic"), 0); } +#[test] fn hinted_text_dominant_baseline_auto() { assert_eq!(render_hinted("tests/text/dominant-baseline/auto"), 0); } +#[test] fn hinted_text_dominant_baseline_central() { assert_eq!(render_hinted("tests/text/dominant-baseline/central"), 0); } +#[test] fn hinted_text_dominant_baseline_complex() { assert_eq!(render_hinted("tests/text/dominant-baseline/complex"), 0); } +#[test] fn hinted_text_dominant_baseline_different_alignment_baseline_on_tspan() { assert_eq!(render_hinted("tests/text/dominant-baseline/different-alignment-baseline-on-tspan"), 0); } +#[test] fn hinted_text_dominant_baseline_dummy_tspan() { assert_eq!(render_hinted("tests/text/dominant-baseline/dummy-tspan"), 0); } +#[test] fn hinted_text_dominant_baseline_equal_alignment_baseline_on_tspan() { assert_eq!(render_hinted("tests/text/dominant-baseline/equal-alignment-baseline-on-tspan"), 0); } +#[test] fn hinted_text_dominant_baseline_hanging() { assert_eq!(render_hinted("tests/text/dominant-baseline/hanging"), 0); } +#[test] fn hinted_text_dominant_baseline_ideographic() { assert_eq!(render_hinted("tests/text/dominant-baseline/ideographic"), 0); } +#[test] fn hinted_text_dominant_baseline_inherit() { assert_eq!(render_hinted("tests/text/dominant-baseline/inherit"), 0); } +#[test] fn hinted_text_dominant_baseline_mathematical() { assert_eq!(render_hinted("tests/text/dominant-baseline/mathematical"), 0); } +#[test] fn hinted_text_dominant_baseline_middle() { assert_eq!(render_hinted("tests/text/dominant-baseline/middle"), 0); } +#[test] fn hinted_text_dominant_baseline_nested() { assert_eq!(render_hinted("tests/text/dominant-baseline/nested"), 0); } +#[test] fn hinted_text_dominant_baseline_no_change() { assert_eq!(render_hinted("tests/text/dominant-baseline/no-change"), 0); } +#[test] fn hinted_text_dominant_baseline_reset_size() { assert_eq!(render_hinted("tests/text/dominant-baseline/reset-size"), 0); } +#[test] fn hinted_text_dominant_baseline_sequential() { assert_eq!(render_hinted("tests/text/dominant-baseline/sequential"), 0); } +#[test] fn hinted_text_dominant_baseline_text_after_edge() { assert_eq!(render_hinted("tests/text/dominant-baseline/text-after-edge"), 0); } +#[test] fn hinted_text_dominant_baseline_text_before_edge() { assert_eq!(render_hinted("tests/text/dominant-baseline/text-before-edge"), 0); } +#[test] fn hinted_text_dominant_baseline_use_script() { assert_eq!(render_hinted("tests/text/dominant-baseline/use-script"), 0); } +#[test] fn hinted_text_font_font_shorthand() { assert_eq!(render_hinted("tests/text/font/font-shorthand"), 0); } +#[test] fn hinted_text_font_simple_case() { assert_eq!(render_hinted("tests/text/font/simple-case"), 0); } +#[test] fn hinted_text_font_family_bold_sans_serif() { assert_eq!(render_hinted("tests/text/font-family/bold-sans-serif"), 0); } +#[test] fn hinted_text_font_family_cursive() { assert_eq!(render_hinted("tests/text/font-family/cursive"), 0); } +#[test] fn hinted_text_font_family_double_quoted() { assert_eq!(render_hinted("tests/text/font-family/double-quoted"), 0); } +#[test] fn hinted_text_font_family_fallback_1() { assert_eq!(render_hinted("tests/text/font-family/fallback-1"), 0); } +#[test] fn hinted_text_font_family_fallback_2() { assert_eq!(render_hinted("tests/text/font-family/fallback-2"), 0); } +#[test] fn hinted_text_font_family_fantasy() { assert_eq!(render_hinted("tests/text/font-family/fantasy"), 0); } +#[test] fn hinted_text_font_family_font_list() { assert_eq!(render_hinted("tests/text/font-family/font-list"), 0); } +#[test] fn hinted_text_font_family_monospace() { assert_eq!(render_hinted("tests/text/font-family/monospace"), 0); } +#[test] fn hinted_text_font_family_noto_sans() { assert_eq!(render_hinted("tests/text/font-family/noto-sans"), 0); } +#[test] fn hinted_text_font_family_sans_serif() { assert_eq!(render_hinted("tests/text/font-family/sans-serif"), 0); } +#[test] fn hinted_text_font_family_serif() { assert_eq!(render_hinted("tests/text/font-family/serif"), 0); } +#[test] fn hinted_text_font_family_source_sans_pro() { assert_eq!(render_hinted("tests/text/font-family/source-sans-pro"), 0); } +#[test] fn hinted_text_font_kerning_arabic_script() { assert_eq!(render_hinted("tests/text/font-kerning/arabic-script"), 0); } +#[test] fn hinted_text_font_kerning_as_property() { assert_eq!(render_hinted("tests/text/font-kerning/as-property"), 0); } +#[test] fn hinted_text_font_kerning_none() { assert_eq!(render_hinted("tests/text/font-kerning/none"), 0); } +#[test] fn hinted_text_font_size_em_nested_and_mixed() { assert_eq!(render_hinted("tests/text/font-size/em-nested-and-mixed"), 0); } +#[test] fn hinted_text_font_size_em_on_the_root_element() { assert_eq!(render_hinted("tests/text/font-size/em-on-the-root-element"), 0); } +#[test] fn hinted_text_font_size_em() { assert_eq!(render_hinted("tests/text/font-size/em"), 0); } +#[test] fn hinted_text_font_size_ex_nested_and_mixed() { assert_eq!(render_hinted("tests/text/font-size/ex-nested-and-mixed"), 0); } +#[test] fn hinted_text_font_size_ex_on_the_root_element() { assert_eq!(render_hinted("tests/text/font-size/ex-on-the-root-element"), 0); } +#[test] fn hinted_text_font_size_ex() { assert_eq!(render_hinted("tests/text/font-size/ex"), 0); } +#[test] fn hinted_text_font_size_inheritance() { assert_eq!(render_hinted("tests/text/font-size/inheritance"), 0); } +#[test] fn hinted_text_font_size_mixed_values() { assert_eq!(render_hinted("tests/text/font-size/mixed-values"), 0); } +#[test] fn hinted_text_font_size_named_value_without_a_parent() { assert_eq!(render_hinted("tests/text/font-size/named-value-without-a-parent"), 0); } +#[test] fn hinted_text_font_size_named_value() { assert_eq!(render_hinted("tests/text/font-size/named-value"), 0); } +#[test] fn hinted_text_font_size_negative_size() { assert_eq!(render_hinted("tests/text/font-size/negative-size"), 0); } +#[test] fn hinted_text_font_size_nested_percent_values_1() { assert_eq!(render_hinted("tests/text/font-size/nested-percent-values-1"), 0); } +#[test] fn hinted_text_font_size_nested_percent_values_2() { assert_eq!(render_hinted("tests/text/font-size/nested-percent-values-2"), 0); } +#[test] fn hinted_text_font_size_percent_value_without_a_parent() { assert_eq!(render_hinted("tests/text/font-size/percent-value-without-a-parent"), 0); } +#[test] fn hinted_text_font_size_percent_value() { assert_eq!(render_hinted("tests/text/font-size/percent-value"), 0); } +#[test] fn hinted_text_font_size_simple_case() { assert_eq!(render_hinted("tests/text/font-size/simple-case"), 0); } +#[test] fn hinted_text_font_size_zero_size_on_parent_1() { assert_eq!(render_hinted("tests/text/font-size/zero-size-on-parent-1"), 0); } +#[test] fn hinted_text_font_size_zero_size_on_parent_2() { assert_eq!(render_hinted("tests/text/font-size/zero-size-on-parent-2"), 0); } +#[test] fn hinted_text_font_size_zero_size_on_parent_3() { assert_eq!(render_hinted("tests/text/font-size/zero-size-on-parent-3"), 0); } +#[test] fn hinted_text_font_size_zero_size() { assert_eq!(render_hinted("tests/text/font-size/zero-size"), 0); } +#[test] fn hinted_text_font_size_adjust_simple_case() { assert_eq!(render_hinted("tests/text/font-size-adjust/simple-case"), 0); } +#[test] fn hinted_text_font_stretch_extra_condensed() { assert_eq!(render_hinted("tests/text/font-stretch/extra-condensed"), 0); } +#[test] fn hinted_text_font_stretch_inherit() { assert_eq!(render_hinted("tests/text/font-stretch/inherit"), 0); } +#[test] fn hinted_text_font_stretch_narrower() { assert_eq!(render_hinted("tests/text/font-stretch/narrower"), 0); } +#[test] fn hinted_text_font_style_inherit() { assert_eq!(render_hinted("tests/text/font-style/inherit"), 0); } +#[test] fn hinted_text_font_style_italic() { assert_eq!(render_hinted("tests/text/font-style/italic"), 0); } +#[test] fn hinted_text_font_style_oblique() { assert_eq!(render_hinted("tests/text/font-style/oblique"), 0); } +#[test] fn hinted_text_font_variant_inherit() { assert_eq!(render_hinted("tests/text/font-variant/inherit"), 0); } +#[test] fn hinted_text_font_variant_small_caps() { assert_eq!(render_hinted("tests/text/font-variant/small-caps"), 0); } +#[test] fn hinted_text_font_variation_settings_all_axes_combined() { assert_eq!(render_hinted("tests/text/font-variation-settings/all-axes-combined"), 0); } +#[test] fn hinted_text_font_variation_settings_auto_font_stretch_condensed() { assert_eq!(render_hinted("tests/text/font-variation-settings/auto-font-stretch-condensed"), 0); } +#[test] fn hinted_text_font_variation_settings_auto_font_style_oblique() { assert_eq!(render_hinted("tests/text/font-variation-settings/auto-font-style-oblique"), 0); } +#[test] fn hinted_text_font_variation_settings_auto_font_weight_700() { assert_eq!(render_hinted("tests/text/font-variation-settings/auto-font-weight-700"), 0); } +#[test] fn hinted_text_font_variation_settings_explicit_overrides_auto() { assert_eq!(render_hinted("tests/text/font-variation-settings/explicit-overrides-auto"), 0); } +#[test] fn hinted_text_font_variation_settings_grad_negative() { assert_eq!(render_hinted("tests/text/font-variation-settings/grad-negative"), 0); } +#[test] fn hinted_text_font_variation_settings_multiple_axes() { assert_eq!(render_hinted("tests/text/font-variation-settings/multiple-axes"), 0); } +#[test] fn hinted_text_font_variation_settings_opsz_144() { assert_eq!(render_hinted("tests/text/font-variation-settings/opsz-144"), 0); } +#[test] fn hinted_text_font_variation_settings_slnt_negative() { assert_eq!(render_hinted("tests/text/font-variation-settings/slnt-negative"), 0); } +#[test] fn hinted_text_font_variation_settings_wdth_151() { assert_eq!(render_hinted("tests/text/font-variation-settings/wdth-151"), 0); } +#[test] fn hinted_text_font_variation_settings_wdth_25() { assert_eq!(render_hinted("tests/text/font-variation-settings/wdth-25"), 0); } +#[test] fn hinted_text_font_variation_settings_wght_100() { assert_eq!(render_hinted("tests/text/font-variation-settings/wght-100"), 0); } +#[test] fn hinted_text_font_variation_settings_wght_700() { assert_eq!(render_hinted("tests/text/font-variation-settings/wght-700"), 0); } +#[test] fn hinted_text_font_variation_settings_xtra_extreme() { assert_eq!(render_hinted("tests/text/font-variation-settings/xtra-extreme"), 0); } +#[test] fn hinted_text_font_weight_650() { assert_eq!(render_hinted("tests/text/font-weight/650"), 0); } +#[test] fn hinted_text_font_weight_700() { assert_eq!(render_hinted("tests/text/font-weight/700"), 0); } +#[test] fn hinted_text_font_weight_bold() { assert_eq!(render_hinted("tests/text/font-weight/bold"), 0); } +#[test] fn hinted_text_font_weight_bolder_with_clamping() { assert_eq!(render_hinted("tests/text/font-weight/bolder-with-clamping"), 0); } +#[test] fn hinted_text_font_weight_bolder_without_parent() { assert_eq!(render_hinted("tests/text/font-weight/bolder-without-parent"), 0); } +#[test] fn hinted_text_font_weight_bolder() { assert_eq!(render_hinted("tests/text/font-weight/bolder"), 0); } +#[test] fn hinted_text_font_weight_inherit() { assert_eq!(render_hinted("tests/text/font-weight/inherit"), 0); } +#[test] fn hinted_text_font_weight_invalid_number_1() { assert_eq!(render_hinted("tests/text/font-weight/invalid-number-1"), 0); } +#[test] fn hinted_text_font_weight_lighter_with_clamping() { assert_eq!(render_hinted("tests/text/font-weight/lighter-with-clamping"), 0); } +#[test] fn hinted_text_font_weight_lighter_without_parent() { assert_eq!(render_hinted("tests/text/font-weight/lighter-without-parent"), 0); } +#[test] fn hinted_text_font_weight_lighter() { assert_eq!(render_hinted("tests/text/font-weight/lighter"), 0); } +#[test] fn hinted_text_font_weight_normal() { assert_eq!(render_hinted("tests/text/font-weight/normal"), 0); } +#[test] fn hinted_text_glyph_orientation_horizontal_simple_case() { assert_eq!(render_hinted("tests/text/glyph-orientation-horizontal/simple-case"), 0); } +#[test] fn hinted_text_glyph_orientation_vertical_simple_case() { assert_eq!(render_hinted("tests/text/glyph-orientation-vertical/simple-case"), 0); } +#[test] fn hinted_text_kerning_0() { assert_eq!(render_hinted("tests/text/kerning/0"), 0); } +#[test] fn hinted_text_kerning_10percent() { assert_eq!(render_hinted("tests/text/kerning/10percent"), 0); } +#[test] fn hinted_text_lengthAdjust_spacingAndGlyphs() { assert_eq!(render_hinted("tests/text/lengthAdjust/spacingAndGlyphs"), 0); } +#[test] fn hinted_text_lengthAdjust_text_on_path() { assert_eq!(render_hinted("tests/text/lengthAdjust/text-on-path"), 0); } +#[test] fn hinted_text_lengthAdjust_vertical() { assert_eq!(render_hinted("tests/text/lengthAdjust/vertical"), 0); } +#[test] fn hinted_text_lengthAdjust_with_underline() { assert_eq!(render_hinted("tests/text/lengthAdjust/with-underline"), 0); } +#[test] fn hinted_text_letter_spacing__3() { assert_eq!(render_hinted("tests/text/letter-spacing/-3"), 0); } +#[test] fn hinted_text_letter_spacing_0() { assert_eq!(render_hinted("tests/text/letter-spacing/0"), 0); } +#[test] fn hinted_text_letter_spacing_1mm() { assert_eq!(render_hinted("tests/text/letter-spacing/1mm"), 0); } +#[test] fn hinted_text_letter_spacing_3() { assert_eq!(render_hinted("tests/text/letter-spacing/3"), 0); } +#[test] fn hinted_text_letter_spacing_5percent() { assert_eq!(render_hinted("tests/text/letter-spacing/5percent"), 0); } +#[test] fn hinted_text_letter_spacing_filter_bbox() { assert_eq!(render_hinted("tests/text/letter-spacing/filter-bbox"), 0); } +#[test] fn hinted_text_letter_spacing_large_negative() { assert_eq!(render_hinted("tests/text/letter-spacing/large-negative"), 0); } +#[test] fn hinted_text_letter_spacing_mixed_scripts() { assert_eq!(render_hinted("tests/text/letter-spacing/mixed-scripts"), 0); } +#[test] fn hinted_text_letter_spacing_mixed_spacing() { assert_eq!(render_hinted("tests/text/letter-spacing/mixed-spacing"), 0); } +#[test] fn hinted_text_letter_spacing_non_ASCII_character() { assert_eq!(render_hinted("tests/text/letter-spacing/non-ASCII-character"), 0); } +#[test] fn hinted_text_letter_spacing_normal() { assert_eq!(render_hinted("tests/text/letter-spacing/normal"), 0); } +#[test] fn hinted_text_letter_spacing_on_Arabic() { assert_eq!(render_hinted("tests/text/letter-spacing/on-Arabic"), 0); } +#[test] fn hinted_text_text_bidi_reordering() { assert_eq!(render_hinted("tests/text/text/bidi-reordering"), 0); } +#[test] fn hinted_text_text_complex_grapheme_split_by_tspan() { assert_eq!(render_hinted("tests/text/text/complex-grapheme-split-by-tspan"), 0); } +#[test] fn hinted_text_text_complex_graphemes_and_coordinates_list() { assert_eq!(render_hinted("tests/text/text/complex-graphemes-and-coordinates-list"), 0); } +#[test] fn hinted_text_text_complex_graphemes() { assert_eq!(render_hinted("tests/text/text/complex-graphemes"), 0); } +#[test] fn hinted_text_text_dx_and_dy_instead_of_x_and_y() { assert_eq!(render_hinted("tests/text/text/dx-and-dy-instead-of-x-and-y"), 0); } +#[test] fn hinted_text_text_dx_and_dy_with_less_values_than_characters() { assert_eq!(render_hinted("tests/text/text/dx-and-dy-with-less-values-than-characters"), 0); } +#[test] fn hinted_text_text_dx_and_dy_with_more_values_than_characters() { assert_eq!(render_hinted("tests/text/text/dx-and-dy-with-more-values-than-characters"), 0); } +#[test] fn hinted_text_text_dx_and_dy_with_multiple_values() { assert_eq!(render_hinted("tests/text/text/dx-and-dy-with-multiple-values"), 0); } +#[test] fn hinted_text_text_em_and_ex_coordinates() { assert_eq!(render_hinted("tests/text/text/em-and-ex-coordinates"), 0); } +#[test] fn hinted_text_text_escaped_text_1() { assert_eq!(render_hinted("tests/text/text/escaped-text-1"), 0); } +#[test] fn hinted_text_text_escaped_text_2() { assert_eq!(render_hinted("tests/text/text/escaped-text-2"), 0); } +#[test] fn hinted_text_text_escaped_text_3() { assert_eq!(render_hinted("tests/text/text/escaped-text-3"), 0); } +#[test] fn hinted_text_text_escaped_text_4() { assert_eq!(render_hinted("tests/text/text/escaped-text-4"), 0); } +#[test] fn hinted_text_text_fill_rule_eq_evenodd() { assert_eq!(render_hinted("tests/text/text/fill-rule=evenodd"), 0); } +#[test] fn hinted_text_text_filter_bbox() { assert_eq!(render_hinted("tests/text/text/filter-bbox"), 0); } +#[test] fn hinted_text_text_glyph_splitting() { assert_eq!(render_hinted("tests/text/text/glyph-splitting"), 0); } +#[test] fn hinted_text_text_ligatures_handling_in_mixed_fonts_1() { assert_eq!(render_hinted("tests/text/text/ligatures-handling-in-mixed-fonts-1"), 0); } +#[test] fn hinted_text_text_ligatures_handling_in_mixed_fonts_2() { assert_eq!(render_hinted("tests/text/text/ligatures-handling-in-mixed-fonts-2"), 0); } +#[test] fn hinted_text_text_mm_coordinates() { assert_eq!(render_hinted("tests/text/text/mm-coordinates"), 0); } +#[test] fn hinted_text_text_nested() { assert_eq!(render_hinted("tests/text/text/nested"), 0); } +#[test] fn hinted_text_text_no_coordinates() { assert_eq!(render_hinted("tests/text/text/no-coordinates"), 0); } +#[test] fn hinted_text_text_percent_value_on_dx_and_dy() { assert_eq!(render_hinted("tests/text/text/percent-value-on-dx-and-dy"), 0); } +#[test] fn hinted_text_text_percent_value_on_x_and_y() { assert_eq!(render_hinted("tests/text/text/percent-value-on-x-and-y"), 0); } +#[test] fn hinted_text_text_real_text_height() { assert_eq!(render_hinted("tests/text/text/real-text-height"), 0); } +#[test] fn hinted_text_text_rotate_on_Arabic() { assert_eq!(render_hinted("tests/text/text/rotate-on-Arabic"), 0); } +#[test] fn hinted_text_text_rotate_with_an_invalid_angle() { assert_eq!(render_hinted("tests/text/text/rotate-with-an-invalid-angle"), 0); } +#[test] fn hinted_text_text_rotate_with_less_values_than_characters() { assert_eq!(render_hinted("tests/text/text/rotate-with-less-values-than-characters"), 0); } +#[test] fn hinted_text_text_rotate_with_more_values_than_characters() { assert_eq!(render_hinted("tests/text/text/rotate-with-more-values-than-characters"), 0); } +#[test] fn hinted_text_text_rotate_with_multiple_values_and_complex_text() { assert_eq!(render_hinted("tests/text/text/rotate-with-multiple-values-and-complex-text"), 0); } +#[test] fn hinted_text_text_rotate_with_multiple_values_underline_and_pattern() { assert_eq!(render_hinted("tests/text/text/rotate-with-multiple-values-underline-and-pattern"), 0); } +#[test] fn hinted_text_text_rotate_with_multiple_values() { assert_eq!(render_hinted("tests/text/text/rotate-with-multiple-values"), 0); } +#[test] fn hinted_text_text_rotate() { assert_eq!(render_hinted("tests/text/text/rotate"), 0); } +#[test] fn hinted_text_text_simple_case() { assert_eq!(render_hinted("tests/text/text/simple-case"), 0); } +#[test] fn hinted_text_text_transform() { assert_eq!(render_hinted("tests/text/text/transform"), 0); } +#[test] fn hinted_text_text_x_and_y_with_dx_and_dy_lists() { assert_eq!(render_hinted("tests/text/text/x-and-y-with-dx-and-dy-lists"), 0); } +#[test] fn hinted_text_text_x_and_y_with_dx_and_dy() { assert_eq!(render_hinted("tests/text/text/x-and-y-with-dx-and-dy"), 0); } +#[test] fn hinted_text_text_x_and_y_with_less_values_than_characters() { assert_eq!(render_hinted("tests/text/text/x-and-y-with-less-values-than-characters"), 0); } +#[test] fn hinted_text_text_x_and_y_with_more_values_than_characters() { assert_eq!(render_hinted("tests/text/text/x-and-y-with-more-values-than-characters"), 0); } +#[test] fn hinted_text_text_x_and_y_with_multiple_values_and_arabic_text() { assert_eq!(render_hinted("tests/text/text/x-and-y-with-multiple-values-and-arabic-text"), 0); } +#[test] fn hinted_text_text_x_and_y_with_multiple_values_and_tspan() { assert_eq!(render_hinted("tests/text/text/x-and-y-with-multiple-values-and-tspan"), 0); } +#[test] fn hinted_text_text_x_and_y_with_multiple_values() { assert_eq!(render_hinted("tests/text/text/x-and-y-with-multiple-values"), 0); } +#[test] fn hinted_text_text_xml_lang_eq_ja() { assert_eq!(render_hinted("tests/text/text/xml-lang=ja"), 0); } +#[test] fn hinted_text_text_xml_space() { assert_eq!(render_hinted("tests/text/text/xml-space"), 0); } +#[test] fn hinted_text_text_zalgo() { assert_eq!(render_hinted("tests/text/text/zalgo"), 0); } +#[test] fn hinted_text_text_anchor_coordinates_list() { assert_eq!(render_hinted("tests/text/text-anchor/coordinates-list"), 0); } +#[test] fn hinted_text_text_anchor_end_on_text() { assert_eq!(render_hinted("tests/text/text-anchor/end-on-text"), 0); } +#[test] fn hinted_text_text_anchor_end_with_letter_spacing() { assert_eq!(render_hinted("tests/text/text-anchor/end-with-letter-spacing"), 0); } +#[test] fn hinted_text_text_anchor_inheritance_1() { assert_eq!(render_hinted("tests/text/text-anchor/inheritance-1"), 0); } +#[test] fn hinted_text_text_anchor_inheritance_2() { assert_eq!(render_hinted("tests/text/text-anchor/inheritance-2"), 0); } +#[test] fn hinted_text_text_anchor_inheritance_3() { assert_eq!(render_hinted("tests/text/text-anchor/inheritance-3"), 0); } +#[test] fn hinted_text_text_anchor_invalid_value_on_text() { assert_eq!(render_hinted("tests/text/text-anchor/invalid-value-on-text"), 0); } +#[test] fn hinted_text_text_anchor_middle_on_text() { assert_eq!(render_hinted("tests/text/text-anchor/middle-on-text"), 0); } +#[test] fn hinted_text_text_anchor_on_the_first_tspan() { assert_eq!(render_hinted("tests/text/text-anchor/on-the-first-tspan"), 0); } +#[test] fn hinted_text_text_anchor_on_tspan_with_arabic() { assert_eq!(render_hinted("tests/text/text-anchor/on-tspan-with-arabic"), 0); } +#[test] fn hinted_text_text_anchor_on_tspan() { assert_eq!(render_hinted("tests/text/text-anchor/on-tspan"), 0); } +#[test] fn hinted_text_text_anchor_start_on_text() { assert_eq!(render_hinted("tests/text/text-anchor/start-on-text"), 0); } +#[test] fn hinted_text_text_anchor_text_anchor_not_on_text_chunk() { assert_eq!(render_hinted("tests/text/text-anchor/text-anchor-not-on-text-chunk"), 0); } +#[test] fn hinted_text_text_decoration_all_types_inline_comma_separated() { assert_eq!(render_hinted("tests/text/text-decoration/all-types-inline-comma-separated"), 0); } +#[test] fn hinted_text_text_decoration_all_types_inline_no_spaces() { assert_eq!(render_hinted("tests/text/text-decoration/all-types-inline-no-spaces"), 0); } +#[test] fn hinted_text_text_decoration_all_types_inline() { assert_eq!(render_hinted("tests/text/text-decoration/all-types-inline"), 0); } +#[test] fn hinted_text_text_decoration_all_types_nested() { assert_eq!(render_hinted("tests/text/text-decoration/all-types-nested"), 0); } +#[test] fn hinted_text_text_decoration_indirect_with_multiple_colors() { assert_eq!(render_hinted("tests/text/text-decoration/indirect-with-multiple-colors"), 0); } +#[test] fn hinted_text_text_decoration_indirect() { assert_eq!(render_hinted("tests/text/text-decoration/indirect"), 0); } +#[test] fn hinted_text_text_decoration_line_through() { assert_eq!(render_hinted("tests/text/text-decoration/line-through"), 0); } +#[test] fn hinted_text_text_decoration_outside_the_text_element() { assert_eq!(render_hinted("tests/text/text-decoration/outside-the-text-element"), 0); } +#[test] fn hinted_text_text_decoration_overline() { assert_eq!(render_hinted("tests/text/text-decoration/overline"), 0); } +#[test] fn hinted_text_text_decoration_style_resolving_1() { assert_eq!(render_hinted("tests/text/text-decoration/style-resolving-1"), 0); } +#[test] fn hinted_text_text_decoration_style_resolving_2() { assert_eq!(render_hinted("tests/text/text-decoration/style-resolving-2"), 0); } +#[test] fn hinted_text_text_decoration_style_resolving_3() { assert_eq!(render_hinted("tests/text/text-decoration/style-resolving-3"), 0); } +#[test] fn hinted_text_text_decoration_style_resolving_4() { assert_eq!(render_hinted("tests/text/text-decoration/style-resolving-4"), 0); } +#[test] fn hinted_text_text_decoration_tspan_decoration() { assert_eq!(render_hinted("tests/text/text-decoration/tspan-decoration"), 0); } +#[test] fn hinted_text_text_decoration_underline_with_dy_list_1() { assert_eq!(render_hinted("tests/text/text-decoration/underline-with-dy-list-1"), 0); } +#[test] fn hinted_text_text_decoration_underline_with_dy_list_2() { assert_eq!(render_hinted("tests/text/text-decoration/underline-with-dy-list-2"), 0); } +#[test] fn hinted_text_text_decoration_underline_with_rotate_list_3() { assert_eq!(render_hinted("tests/text/text-decoration/underline-with-rotate-list-3"), 0); } +#[test] fn hinted_text_text_decoration_underline_with_rotate_list_4() { assert_eq!(render_hinted("tests/text/text-decoration/underline-with-rotate-list-4"), 0); } +#[test] fn hinted_text_text_decoration_underline_with_y_list() { assert_eq!(render_hinted("tests/text/text-decoration/underline-with-y-list"), 0); } +#[test] fn hinted_text_text_decoration_underline() { assert_eq!(render_hinted("tests/text/text-decoration/underline"), 0); } +#[test] fn hinted_text_text_decoration_with_textLength_on_a_single_character() { assert_eq!(render_hinted("tests/text/text-decoration/with-textLength-on-a-single-character"), 0); } +#[test] fn hinted_text_text_rendering_geometricPrecision() { assert_eq!(render_hinted("tests/text/text-rendering/geometricPrecision"), 0); } +#[test] fn hinted_text_text_rendering_on_tspan() { assert_eq!(render_hinted("tests/text/text-rendering/on-tspan"), 0); } +#[test] fn hinted_text_text_rendering_optimizeLegibility() { assert_eq!(render_hinted("tests/text/text-rendering/optimizeLegibility"), 0); } +#[test] fn hinted_text_text_rendering_optimizeSpeed() { assert_eq!(render_hinted("tests/text/text-rendering/optimizeSpeed"), 0); } +#[test] fn hinted_text_text_rendering_with_underline() { assert_eq!(render_hinted("tests/text/text-rendering/with-underline"), 0); } +#[test] fn hinted_text_textLength_150_on_parent() { assert_eq!(render_hinted("tests/text/textLength/150-on-parent"), 0); } +#[test] fn hinted_text_textLength_150_on_tspan() { assert_eq!(render_hinted("tests/text/textLength/150-on-tspan"), 0); } +#[test] fn hinted_text_textLength_150() { assert_eq!(render_hinted("tests/text/textLength/150"), 0); } +#[test] fn hinted_text_textLength_40mm() { assert_eq!(render_hinted("tests/text/textLength/40mm"), 0); } +#[test] fn hinted_text_textLength_75percent() { assert_eq!(render_hinted("tests/text/textLength/75percent"), 0); } +#[test] fn hinted_text_textLength_arabic_with_lengthAdjust() { assert_eq!(render_hinted("tests/text/textLength/arabic-with-lengthAdjust"), 0); } +#[test] fn hinted_text_textLength_arabic() { assert_eq!(render_hinted("tests/text/textLength/arabic"), 0); } +#[test] fn hinted_text_textLength_inherit() { assert_eq!(render_hinted("tests/text/textLength/inherit"), 0); } +#[test] fn hinted_text_textLength_negative() { assert_eq!(render_hinted("tests/text/textLength/negative"), 0); } +#[test] fn hinted_text_textLength_on_a_single_tspan() { assert_eq!(render_hinted("tests/text/textLength/on-a-single-tspan"), 0); } +#[test] fn hinted_text_textLength_on_text_and_tspan() { assert_eq!(render_hinted("tests/text/textLength/on-text-and-tspan"), 0); } +#[test] fn hinted_text_textLength_zero() { assert_eq!(render_hinted("tests/text/textLength/zero"), 0); } +#[test] fn hinted_text_textPath_closed_path() { assert_eq!(render_hinted("tests/text/textPath/closed-path"), 0); } +#[test] fn hinted_text_textPath_complex() { assert_eq!(render_hinted("tests/text/textPath/complex"), 0); } +#[test] fn hinted_text_textPath_dy_with_tiny_coordinates() { assert_eq!(render_hinted("tests/text/textPath/dy-with-tiny-coordinates"), 0); } +#[test] fn hinted_text_textPath_invalid_link() { assert_eq!(render_hinted("tests/text/textPath/invalid-link"), 0); } +#[test] fn hinted_text_textPath_invalid_textPath_in_the_middle() { assert_eq!(render_hinted("tests/text/textPath/invalid-textPath-in-the-middle"), 0); } +#[test] fn hinted_text_textPath_link_to_rect() { assert_eq!(render_hinted("tests/text/textPath/link-to-rect"), 0); } +#[test] fn hinted_text_textPath_m_A_path() { assert_eq!(render_hinted("tests/text/textPath/m-A-path"), 0); } +#[test] fn hinted_text_textPath_m_L_Z_path() { assert_eq!(render_hinted("tests/text/textPath/m-L-Z-path"), 0); } +#[test] fn hinted_text_textPath_method_eq_stretch() { assert_eq!(render_hinted("tests/text/textPath/method=stretch"), 0); } +#[test] fn hinted_text_textPath_mixed_children_1() { assert_eq!(render_hinted("tests/text/textPath/mixed-children-1"), 0); } +#[test] fn hinted_text_textPath_mixed_children_2() { assert_eq!(render_hinted("tests/text/textPath/mixed-children-2"), 0); } +#[test] fn hinted_text_textPath_nested() { assert_eq!(render_hinted("tests/text/textPath/nested"), 0); } +#[test] fn hinted_text_textPath_no_link() { assert_eq!(render_hinted("tests/text/textPath/no-link"), 0); } +#[test] fn hinted_text_textPath_path_with_ClosePath() { assert_eq!(render_hinted("tests/text/textPath/path-with-ClosePath"), 0); } +#[test] fn hinted_text_textPath_path_with_subpaths_and_startOffset() { assert_eq!(render_hinted("tests/text/textPath/path-with-subpaths-and-startOffset"), 0); } +#[test] fn hinted_text_textPath_path_with_subpaths() { assert_eq!(render_hinted("tests/text/textPath/path-with-subpaths"), 0); } +#[test] fn hinted_text_textPath_side_eq_right() { assert_eq!(render_hinted("tests/text/textPath/side=right"), 0); } +#[test] fn hinted_text_textPath_simple_case() { assert_eq!(render_hinted("tests/text/textPath/simple-case"), 0); } +#[test] fn hinted_text_textPath_spacing_eq_auto() { assert_eq!(render_hinted("tests/text/textPath/spacing=auto"), 0); } +#[test] fn hinted_text_textPath_startOffset_eq__100() { assert_eq!(render_hinted("tests/text/textPath/startOffset=-100"), 0); } +#[test] fn hinted_text_textPath_startOffset_eq_10percent() { assert_eq!(render_hinted("tests/text/textPath/startOffset=10percent"), 0); } +#[test] fn hinted_text_textPath_startOffset_eq_30() { assert_eq!(render_hinted("tests/text/textPath/startOffset=30"), 0); } +#[test] fn hinted_text_textPath_startOffset_eq_5mm() { assert_eq!(render_hinted("tests/text/textPath/startOffset=5mm"), 0); } +#[test] fn hinted_text_textPath_startOffset_eq_9999() { assert_eq!(render_hinted("tests/text/textPath/startOffset=9999"), 0); } +#[test] fn hinted_text_textPath_tspan_with_absolute_position() { assert_eq!(render_hinted("tests/text/textPath/tspan-with-absolute-position"), 0); } +#[test] fn hinted_text_textPath_tspan_with_relative_position() { assert_eq!(render_hinted("tests/text/textPath/tspan-with-relative-position"), 0); } +#[test] fn hinted_text_textPath_two_paths() { assert_eq!(render_hinted("tests/text/textPath/two-paths"), 0); } +#[test] fn hinted_text_textPath_very_long_text() { assert_eq!(render_hinted("tests/text/textPath/very-long-text"), 0); } +#[test] fn hinted_text_textPath_with_baseline_shift_and_rotate() { assert_eq!(render_hinted("tests/text/textPath/with-baseline-shift-and-rotate"), 0); } +#[test] fn hinted_text_textPath_with_baseline_shift() { assert_eq!(render_hinted("tests/text/textPath/with-baseline-shift"), 0); } +#[test] fn hinted_text_textPath_with_big_letter_spacing() { assert_eq!(render_hinted("tests/text/textPath/with-big-letter-spacing"), 0); } +#[test] fn hinted_text_textPath_with_coordinates_on_text() { assert_eq!(render_hinted("tests/text/textPath/with-coordinates-on-text"), 0); } +#[test] fn hinted_text_textPath_with_coordinates_on_textPath() { assert_eq!(render_hinted("tests/text/textPath/with-coordinates-on-textPath"), 0); } +#[test] fn hinted_text_textPath_with_filter() { assert_eq!(render_hinted("tests/text/textPath/with-filter"), 0); } +#[test] fn hinted_text_textPath_with_invalid_path_and_xlink_href() { assert_eq!(render_hinted("tests/text/textPath/with-invalid-path-and-xlink-href"), 0); } +#[test] fn hinted_text_textPath_with_letter_spacing() { assert_eq!(render_hinted("tests/text/textPath/with-letter-spacing"), 0); } +#[test] fn hinted_text_textPath_with_path_and_xlink_href() { assert_eq!(render_hinted("tests/text/textPath/with-path-and-xlink-href"), 0); } +#[test] fn hinted_text_textPath_with_path() { assert_eq!(render_hinted("tests/text/textPath/with-path"), 0); } +#[test] fn hinted_text_textPath_with_rotate() { assert_eq!(render_hinted("tests/text/textPath/with-rotate"), 0); } +#[test] fn hinted_text_textPath_with_text_anchor() { assert_eq!(render_hinted("tests/text/textPath/with-text-anchor"), 0); } +#[test] fn hinted_text_textPath_with_transform_on_a_referenced_path() { assert_eq!(render_hinted("tests/text/textPath/with-transform-on-a-referenced-path"), 0); } +#[test] fn hinted_text_textPath_with_transform_outside_a_referenced_path() { assert_eq!(render_hinted("tests/text/textPath/with-transform-outside-a-referenced-path"), 0); } +#[test] fn hinted_text_textPath_with_underline() { assert_eq!(render_hinted("tests/text/textPath/with-underline"), 0); } +#[test] fn hinted_text_textPath_writing_mode_eq_tb() { assert_eq!(render_hinted("tests/text/textPath/writing-mode=tb"), 0); } +#[test] fn hinted_text_tref_link_to_a_complex_text() { assert_eq!(render_hinted("tests/text/tref/link-to-a-complex-text"), 0); } +#[test] fn hinted_text_tref_link_to_a_non_SVG_element() { assert_eq!(render_hinted("tests/text/tref/link-to-a-non-SVG-element"), 0); } +#[test] fn hinted_text_tref_link_to_a_non_text_element() { assert_eq!(render_hinted("tests/text/tref/link-to-a-non-text-element"), 0); } +#[test] fn hinted_text_tref_link_to_an_external_file_element() { assert_eq!(render_hinted("tests/text/tref/link-to-an-external-file-element"), 0); } +#[test] fn hinted_text_tref_link_to_text() { assert_eq!(render_hinted("tests/text/tref/link-to-text"), 0); } +#[test] fn hinted_text_tref_nested() { assert_eq!(render_hinted("tests/text/tref/nested"), 0); } +#[test] fn hinted_text_tref_position_attributes() { assert_eq!(render_hinted("tests/text/tref/position-attributes"), 0); } +#[test] fn hinted_text_tref_style_attributes() { assert_eq!(render_hinted("tests/text/tref/style-attributes"), 0); } +#[test] fn hinted_text_tref_with_a_title_child() { assert_eq!(render_hinted("tests/text/tref/with-a-title-child"), 0); } +#[test] fn hinted_text_tref_with_text() { assert_eq!(render_hinted("tests/text/tref/with-text"), 0); } +#[test] fn hinted_text_tref_xml_space() { assert_eq!(render_hinted("tests/text/tref/xml-space"), 0); } +#[test] fn hinted_text_tspan_bidi_reordering() { assert_eq!(render_hinted("tests/text/tspan/bidi-reordering"), 0); } +#[test] fn hinted_text_tspan_mixed_font_size() { assert_eq!(render_hinted("tests/text/tspan/mixed-font-size"), 0); } +#[test] fn hinted_text_tspan_mixed_xml_space_1() { assert_eq!(render_hinted("tests/text/tspan/mixed-xml-space-1"), 0); } +#[test] fn hinted_text_tspan_mixed_xml_space_2() { assert_eq!(render_hinted("tests/text/tspan/mixed-xml-space-2"), 0); } +#[test] fn hinted_text_tspan_mixed_xml_space_3() { assert_eq!(render_hinted("tests/text/tspan/mixed-xml-space-3"), 0); } +#[test] fn hinted_text_tspan_mixed() { assert_eq!(render_hinted("tests/text/tspan/mixed"), 0); } +#[test] fn hinted_text_tspan_multiple_coordinates() { assert_eq!(render_hinted("tests/text/tspan/multiple-coordinates"), 0); } +#[test] fn hinted_text_tspan_nested_rotate() { assert_eq!(render_hinted("tests/text/tspan/nested-rotate"), 0); } +#[test] fn hinted_text_tspan_nested_whitespaces() { assert_eq!(render_hinted("tests/text/tspan/nested-whitespaces"), 0); } +#[test] fn hinted_text_tspan_nested() { assert_eq!(render_hinted("tests/text/tspan/nested"), 0); } +#[test] fn hinted_text_tspan_only_with_y() { assert_eq!(render_hinted("tests/text/tspan/only-with-y"), 0); } +#[test] fn hinted_text_tspan_outside_the_text() { assert_eq!(render_hinted("tests/text/tspan/outside-the-text"), 0); } +#[test] fn hinted_text_tspan_pseudo_multi_line() { assert_eq!(render_hinted("tests/text/tspan/pseudo-multi-line"), 0); } +#[test] fn hinted_text_tspan_rotate_and_display_none() { assert_eq!(render_hinted("tests/text/tspan/rotate-and-display-none"), 0); } +#[test] fn hinted_text_tspan_rotate_on_child() { assert_eq!(render_hinted("tests/text/tspan/rotate-on-child"), 0); } +#[test] fn hinted_text_tspan_sequential() { assert_eq!(render_hinted("tests/text/tspan/sequential"), 0); } +#[test] fn hinted_text_tspan_style_override() { assert_eq!(render_hinted("tests/text/tspan/style-override"), 0); } +#[test] fn hinted_text_tspan_text_shaping_across_multiple_tspan_1() { assert_eq!(render_hinted("tests/text/tspan/text-shaping-across-multiple-tspan-1"), 0); } +#[test] fn hinted_text_tspan_text_shaping_across_multiple_tspan_2() { assert_eq!(render_hinted("tests/text/tspan/text-shaping-across-multiple-tspan-2"), 0); } +#[test] fn hinted_text_tspan_transform() { assert_eq!(render_hinted("tests/text/tspan/transform"), 0); } +#[test] fn hinted_text_tspan_tspan_bbox_1() { assert_eq!(render_hinted("tests/text/tspan/tspan-bbox-1"), 0); } +#[test] fn hinted_text_tspan_tspan_bbox_2() { assert_eq!(render_hinted("tests/text/tspan/tspan-bbox-2"), 0); } +#[test] fn hinted_text_tspan_with_clip_path() { assert_eq!(render_hinted("tests/text/tspan/with-clip-path"), 0); } +#[test] fn hinted_text_tspan_with_dy() { assert_eq!(render_hinted("tests/text/tspan/with-dy"), 0); } +#[test] fn hinted_text_tspan_with_filter() { assert_eq!(render_hinted("tests/text/tspan/with-filter"), 0); } +#[test] fn hinted_text_tspan_with_mask() { assert_eq!(render_hinted("tests/text/tspan/with-mask"), 0); } +#[test] fn hinted_text_tspan_with_opacity() { assert_eq!(render_hinted("tests/text/tspan/with-opacity"), 0); } +#[test] fn hinted_text_tspan_with_x_and_y() { assert_eq!(render_hinted("tests/text/tspan/with-x-and-y"), 0); } +#[test] fn hinted_text_tspan_without_attributes() { assert_eq!(render_hinted("tests/text/tspan/without-attributes"), 0); } +#[test] fn hinted_text_tspan_xml_space_1() { assert_eq!(render_hinted("tests/text/tspan/xml-space-1"), 0); } +#[test] fn hinted_text_tspan_xml_space_2() { assert_eq!(render_hinted("tests/text/tspan/xml-space-2"), 0); } +#[test] fn hinted_text_unicode_bidi_bidi_override() { assert_eq!(render_hinted("tests/text/unicode-bidi/bidi-override"), 0); } +#[test] fn hinted_text_word_spacing__5() { assert_eq!(render_hinted("tests/text/word-spacing/-5"), 0); } +#[test] fn hinted_text_word_spacing_0() { assert_eq!(render_hinted("tests/text/word-spacing/0"), 0); } +#[test] fn hinted_text_word_spacing_10() { assert_eq!(render_hinted("tests/text/word-spacing/10"), 0); } +#[test] fn hinted_text_word_spacing_2mm() { assert_eq!(render_hinted("tests/text/word-spacing/2mm"), 0); } +#[test] fn hinted_text_word_spacing_5percent() { assert_eq!(render_hinted("tests/text/word-spacing/5percent"), 0); } +#[test] fn hinted_text_word_spacing_large_negative() { assert_eq!(render_hinted("tests/text/word-spacing/large-negative"), 0); } +#[test] fn hinted_text_word_spacing_normal() { assert_eq!(render_hinted("tests/text/word-spacing/normal"), 0); } +#[test] fn hinted_text_writing_mode_arabic_with_rl() { assert_eq!(render_hinted("tests/text/writing-mode/arabic-with-rl"), 0); } +#[test] fn hinted_text_writing_mode_horizontal_tb() { assert_eq!(render_hinted("tests/text/writing-mode/horizontal-tb"), 0); } +#[test] fn hinted_text_writing_mode_inheritance() { assert_eq!(render_hinted("tests/text/writing-mode/inheritance"), 0); } +#[test] fn hinted_text_writing_mode_invalid_value() { assert_eq!(render_hinted("tests/text/writing-mode/invalid-value"), 0); } +#[test] fn hinted_text_writing_mode_japanese_with_tb() { assert_eq!(render_hinted("tests/text/writing-mode/japanese-with-tb"), 0); } +#[test] fn hinted_text_writing_mode_lr_tb() { assert_eq!(render_hinted("tests/text/writing-mode/lr-tb"), 0); } +#[test] fn hinted_text_writing_mode_lr() { assert_eq!(render_hinted("tests/text/writing-mode/lr"), 0); } +#[test] fn hinted_text_writing_mode_mixed_languages_with_tb_and_underline() { assert_eq!(render_hinted("tests/text/writing-mode/mixed-languages-with-tb-and-underline"), 0); } +#[test] fn hinted_text_writing_mode_mixed_languages_with_tb() { assert_eq!(render_hinted("tests/text/writing-mode/mixed-languages-with-tb"), 0); } +#[test] fn hinted_text_writing_mode_on_tspan() { assert_eq!(render_hinted("tests/text/writing-mode/on-tspan"), 0); } +#[test] fn hinted_text_writing_mode_rl_tb() { assert_eq!(render_hinted("tests/text/writing-mode/rl-tb"), 0); } +#[test] fn hinted_text_writing_mode_rl() { assert_eq!(render_hinted("tests/text/writing-mode/rl"), 0); } +#[test] fn hinted_text_writing_mode_tb_and_punctuation() { assert_eq!(render_hinted("tests/text/writing-mode/tb-and-punctuation"), 0); } +#[test] fn hinted_text_writing_mode_tb_rl() { assert_eq!(render_hinted("tests/text/writing-mode/tb-rl"), 0); } +#[test] fn hinted_text_writing_mode_tb_with_alignment() { assert_eq!(render_hinted("tests/text/writing-mode/tb-with-alignment"), 0); } +#[test] fn hinted_text_writing_mode_tb_with_dx_on_second_tspan() { assert_eq!(render_hinted("tests/text/writing-mode/tb-with-dx-on-second-tspan"), 0); } +#[test] fn hinted_text_writing_mode_tb_with_dx_on_tspan() { assert_eq!(render_hinted("tests/text/writing-mode/tb-with-dx-on-tspan"), 0); } +#[test] fn hinted_text_writing_mode_tb_with_dy_on_second_tspan() { assert_eq!(render_hinted("tests/text/writing-mode/tb-with-dy-on-second-tspan"), 0); } +#[test] fn hinted_text_writing_mode_tb_with_rotate_and_underline() { assert_eq!(render_hinted("tests/text/writing-mode/tb-with-rotate-and-underline"), 0); } +#[test] fn hinted_text_writing_mode_tb_with_rotate() { assert_eq!(render_hinted("tests/text/writing-mode/tb-with-rotate"), 0); } +#[test] fn hinted_text_writing_mode_tb() { assert_eq!(render_hinted("tests/text/writing-mode/tb"), 0); } +#[test] fn hinted_text_writing_mode_vertical_lr() { assert_eq!(render_hinted("tests/text/writing-mode/vertical-lr"), 0); } +#[test] fn hinted_text_writing_mode_vertical_rl() { assert_eq!(render_hinted("tests/text/writing-mode/vertical-rl"), 0); } diff --git a/crates/resvg/tests/tests/text/color-font/colrv0.png b/crates/resvg/tests/tests/text/color-font/colrv0.png index e4b090d24..087615997 100644 Binary files a/crates/resvg/tests/tests/text/color-font/colrv0.png and b/crates/resvg/tests/tests/text/color-font/colrv0.png differ diff --git a/crates/resvg/tests/tests/text/color-font/colrv1.png b/crates/resvg/tests/tests/text/color-font/colrv1.png index 7537da902..23bb63a05 100644 Binary files a/crates/resvg/tests/tests/text/color-font/colrv1.png and b/crates/resvg/tests/tests/text/color-font/colrv1.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/all-axes-combined.png b/crates/resvg/tests/tests/text/font-variation-settings/all-axes-combined.png new file mode 100644 index 000000000..c4222be24 Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/all-axes-combined.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/all-axes-combined.svg b/crates/resvg/tests/tests/text/font-variation-settings/all-axes-combined.svg new file mode 100644 index 000000000..6f3fca97c --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/all-axes-combined.svg @@ -0,0 +1,10 @@ + + Multiple custom axes combined + + Combo + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/auto-font-stretch-condensed.png b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-stretch-condensed.png new file mode 100644 index 000000000..bfa287942 Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-stretch-condensed.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/auto-font-stretch-condensed.svg b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-stretch-condensed.svg new file mode 100644 index 000000000..919945f3a --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-stretch-condensed.svg @@ -0,0 +1,9 @@ + + font-stretch condensed auto-maps to wdth axis + + Narrow + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/auto-font-style-oblique.png b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-style-oblique.png new file mode 100644 index 000000000..36cd4484f Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-style-oblique.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/auto-font-style-oblique.svg b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-style-oblique.svg new file mode 100644 index 000000000..28079ceb6 --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-style-oblique.svg @@ -0,0 +1,9 @@ + + font-style oblique auto-maps to slnt axis + + Slant + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/auto-font-weight-700.png b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-weight-700.png new file mode 100644 index 000000000..90e0d815f Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-weight-700.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/auto-font-weight-700.svg b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-weight-700.svg new file mode 100644 index 000000000..9f718eaea --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-weight-700.svg @@ -0,0 +1,9 @@ + + font-weight auto-maps to wght axis + + Bold + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/explicit-overrides-auto.png b/crates/resvg/tests/tests/text/font-variation-settings/explicit-overrides-auto.png new file mode 100644 index 000000000..650ae9d86 Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/explicit-overrides-auto.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/explicit-overrides-auto.svg b/crates/resvg/tests/tests/text/font-variation-settings/explicit-overrides-auto.svg new file mode 100644 index 000000000..66f953d39 --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/explicit-overrides-auto.svg @@ -0,0 +1,10 @@ + + Explicit settings override font-weight auto-map + + Thin + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/grad-negative.png b/crates/resvg/tests/tests/text/font-variation-settings/grad-negative.png new file mode 100644 index 000000000..133a33f07 Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/grad-negative.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/grad-negative.svg b/crates/resvg/tests/tests/text/font-variation-settings/grad-negative.svg new file mode 100644 index 000000000..b9fc20712 --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/grad-negative.svg @@ -0,0 +1,10 @@ + + `font-variation-settings: "GRAD" -200` + + Grade + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/multiple-axes.png b/crates/resvg/tests/tests/text/font-variation-settings/multiple-axes.png new file mode 100644 index 000000000..bd0a21e00 Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/multiple-axes.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/multiple-axes.svg b/crates/resvg/tests/tests/text/font-variation-settings/multiple-axes.svg new file mode 100644 index 000000000..04e32a7b1 --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/multiple-axes.svg @@ -0,0 +1,10 @@ + + `font-variation-settings: "wght" 700, "wdth" 75` + + Bold + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/opsz-144.png b/crates/resvg/tests/tests/text/font-variation-settings/opsz-144.png new file mode 100644 index 000000000..01f851888 Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/opsz-144.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/opsz-144.svg b/crates/resvg/tests/tests/text/font-variation-settings/opsz-144.svg new file mode 100644 index 000000000..68da8f538 --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/opsz-144.svg @@ -0,0 +1,10 @@ + + `font-variation-settings: "opsz" 144` + + Display + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/slnt-negative.png b/crates/resvg/tests/tests/text/font-variation-settings/slnt-negative.png new file mode 100644 index 000000000..36cd4484f Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/slnt-negative.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/slnt-negative.svg b/crates/resvg/tests/tests/text/font-variation-settings/slnt-negative.svg new file mode 100644 index 000000000..74f60b6fa --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/slnt-negative.svg @@ -0,0 +1,10 @@ + + `font-variation-settings: "slnt" -10` + + Slant + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/wdth-151.png b/crates/resvg/tests/tests/text/font-variation-settings/wdth-151.png new file mode 100644 index 000000000..0065f0758 Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/wdth-151.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/wdth-151.svg b/crates/resvg/tests/tests/text/font-variation-settings/wdth-151.svg new file mode 100644 index 000000000..a73f2128b --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/wdth-151.svg @@ -0,0 +1,10 @@ + + `font-variation-settings: "wdth" 151` + + Wide + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/wdth-25.png b/crates/resvg/tests/tests/text/font-variation-settings/wdth-25.png new file mode 100644 index 000000000..91c571855 Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/wdth-25.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/wdth-25.svg b/crates/resvg/tests/tests/text/font-variation-settings/wdth-25.svg new file mode 100644 index 000000000..6b690ab48 --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/wdth-25.svg @@ -0,0 +1,10 @@ + + `font-variation-settings: "wdth" 25` + + Narrow + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/wght-100.png b/crates/resvg/tests/tests/text/font-variation-settings/wght-100.png new file mode 100644 index 000000000..650ae9d86 Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/wght-100.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/wght-100.svg b/crates/resvg/tests/tests/text/font-variation-settings/wght-100.svg new file mode 100644 index 000000000..43c6c8358 --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/wght-100.svg @@ -0,0 +1,10 @@ + + `font-variation-settings: "wght" 100` + + Thin + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/wght-700.png b/crates/resvg/tests/tests/text/font-variation-settings/wght-700.png new file mode 100644 index 000000000..90e0d815f Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/wght-700.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/wght-700.svg b/crates/resvg/tests/tests/text/font-variation-settings/wght-700.svg new file mode 100644 index 000000000..f5e426f90 --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/wght-700.svg @@ -0,0 +1,10 @@ + + `font-variation-settings: "wght" 700` + + Bold + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/xtra-extreme.png b/crates/resvg/tests/tests/text/font-variation-settings/xtra-extreme.png new file mode 100644 index 000000000..c4e2511c5 Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/xtra-extreme.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/xtra-extreme.svg b/crates/resvg/tests/tests/text/font-variation-settings/xtra-extreme.svg new file mode 100644 index 000000000..a2f20e5c1 --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/xtra-extreme.svg @@ -0,0 +1,10 @@ + + `font-variation-settings: "XTRA" 603` + + Wide + + + + diff --git a/crates/resvg/tests/tests/text/text/zalgo.png b/crates/resvg/tests/tests/text/text/zalgo.png index 3e7ca99db..b0ae2b0fd 100644 Binary files a/crates/resvg/tests/tests/text/text/zalgo.png and b/crates/resvg/tests/tests/text/text/zalgo.png differ diff --git a/crates/usvg/Cargo.toml b/crates/usvg/Cargo.toml index f9c1778e5..844551fff 100644 --- a/crates/usvg/Cargo.toml +++ b/crates/usvg/Cargo.toml @@ -37,20 +37,27 @@ siphasher = "1.0" # perfect hash implementation # text fontdb = { version = "0.23.0", default-features = false, optional = true } -rustybuzz = { version = "0.20.1", optional = true } +harfrust = { version = "0.5", optional = true } unicode-bidi = { version = "0.3", optional = true } unicode-script = { version = "0.5", optional = true } unicode-vo = { version = "0.1", optional = true } +# skrifa for font metrics, outlines, and COLR (via harfrust's read-fonts) +skrifa = { version = "0.40", optional = true } + [dev-dependencies] once_cell = "1.5" [features] default = ["text", "system-fonts", "memmap-fonts"] # Enables text-to-path conversion support. -# Adds around 400KiB to your binary. -text = ["fontdb", "rustybuzz", "unicode-bidi", "unicode-script", "unicode-vo"] +# Uses harfrust (HarfBuzz port) for shaping and skrifa for font access. +text = ["fontdb", "harfrust", "skrifa", "unicode-bidi", "unicode-script", "unicode-vo"] # Enables system fonts loading. system-fonts = ["fontdb/fs", "fontdb/fontconfig"] # Enables font files memmaping for faster loading. memmap-fonts = ["fontdb/memmap"] +# Enables font hinting via skrifa. +# Uses skrifa for outline extraction with grid-fitting support. +# Controlled by text-rendering CSS property. +hinting = ["text"] diff --git a/crates/usvg/codegen/attributes.txt b/crates/usvg/codegen/attributes.txt index 32cac0622..3ed557d77 100644 --- a/crates/usvg/codegen/attributes.txt +++ b/crates/usvg/codegen/attributes.txt @@ -40,6 +40,7 @@ font font-family font-feature-settings font-kerning +font-optical-sizing font-size font-size-adjust font-stretch @@ -51,6 +52,7 @@ font-variant-east-asian font-variant-ligatures font-variant-numeric font-variant-position +font-variation-settings font-weight fr fx diff --git a/crates/usvg/src/main.rs b/crates/usvg/src/main.rs index 84ae50250..a329eff9c 100644 --- a/crates/usvg/src/main.rs +++ b/crates/usvg/src/main.rs @@ -431,6 +431,7 @@ fn process(args: Args) -> Result<(), String> { image_href_resolver: usvg::ImageHrefResolver::default(), font_resolver: usvg::FontResolver::default(), fontdb: Arc::new(fontdb), + hinting: usvg::HintingOptions::default(), style_sheet, }; diff --git a/crates/usvg/src/parser/converter.rs b/crates/usvg/src/parser/converter.rs index 7f5758ca9..479c13786 100644 --- a/crates/usvg/src/parser/converter.rs +++ b/crates/usvg/src/parser/converter.rs @@ -11,7 +11,7 @@ use fontdb::Database; #[cfg(feature = "text")] use fontdb::ID; #[cfg(feature = "text")] -use rustybuzz::ttf_parser::GlyphId; +use skrifa::GlyphId; use svgtypes::{Length, LengthUnit as Unit, PaintOrderKind, TransformOrigin}; use tiny_skia_path::PathBuilder; diff --git a/crates/usvg/src/parser/mod.rs b/crates/usvg/src/parser/mod.rs index b3fbccdd6..ec9d0eac5 100644 --- a/crates/usvg/src/parser/mod.rs +++ b/crates/usvg/src/parser/mod.rs @@ -21,6 +21,8 @@ mod text; #[cfg(feature = "text")] pub(crate) use converter::Cache; pub use image::{ImageHrefDataResolverFn, ImageHrefResolver, ImageHrefStringResolverFn}; +#[cfg(feature = "text")] +pub use options::HintingOptions; pub use options::Options; pub(crate) use svgtree::{AId, EId}; @@ -136,6 +138,8 @@ impl crate::Tree { (opt.font_resolver.select_fallback)(c, used_fonts, db) }), }, + #[cfg(feature = "text")] + hinting: opt.hinting, ..Options::default() }; diff --git a/crates/usvg/src/parser/options.rs b/crates/usvg/src/parser/options.rs index fcf70b114..95217f803 100644 --- a/crates/usvg/src/parser/options.rs +++ b/crates/usvg/src/parser/options.rs @@ -8,6 +8,45 @@ use std::sync::Arc; use crate::FontResolver; use crate::{ImageHrefResolver, ImageRendering, ShapeRendering, Size, TextRendering}; +/// Font hinting configuration. +/// +/// Controls how font outlines are grid-fitted for better rendering at small sizes. +#[cfg(feature = "text")] +#[derive(Debug, Clone, Copy)] +pub struct HintingOptions { + /// Whether to enable font hinting. + /// + /// When enabled, uses skrifa to apply grid-fitting to glyph outlines. + /// The actual hinting behavior is controlled by the `text-rendering` CSS property: + /// - `optimizeLegibility` / `optimizeSpeed`: Full hinting + /// - `geometricPrecision`: No hinting (preserve exact outlines) + /// + /// Default: `true` (matching browser behavior) + pub enabled: bool, + + /// DPI to use for ppem calculation when hinting. + /// + /// If `None`, uses the global `Options::dpi` value. + /// + /// ppem (pixels per em) = font_size * dpi / 72.0 + /// + /// Default: `None` (use Options::dpi) + pub dpi: Option, +} + +#[cfg(feature = "text")] +impl Default for HintingOptions { + fn default() -> Self { + Self { + // When the hinting feature is compiled, enable hinting by default + // (matching browser behavior). CSS text-rendering property controls + // per-element hinting: geometricPrecision disables, optimizeLegibility enables. + enabled: true, + dpi: None, + } + } +} + /// Processing options. #[derive(Debug)] pub struct Options<'a> { @@ -95,6 +134,14 @@ pub struct Options<'a> { /// be the same as this one. #[cfg(feature = "text")] pub fontdb: Arc, + + /// Font hinting configuration. + /// + /// Controls grid-fitting of glyph outlines for better rendering at small sizes. + /// Available when the `text` feature is enabled. + #[cfg(feature = "text")] + pub hinting: HintingOptions, + /// A CSS stylesheet that should be injected into the SVG. Can be used to overwrite /// certain attributes. pub style_sheet: Option, @@ -118,6 +165,8 @@ impl Default for Options<'_> { font_resolver: FontResolver::default(), #[cfg(feature = "text")] fontdb: Arc::new(fontdb::Database::new()), + #[cfg(feature = "text")] + hinting: HintingOptions::default(), style_sheet: None, } } diff --git a/crates/usvg/src/parser/svgtree/mod.rs b/crates/usvg/src/parser/svgtree/mod.rs index 3591fd8f3..5aeadb33c 100644 --- a/crates/usvg/src/parser/svgtree/mod.rs +++ b/crates/usvg/src/parser/svgtree/mod.rs @@ -710,12 +710,14 @@ impl AId { | AId::FloodOpacity | AId::FontFamily | AId::FontKerning // technically not presentation + | AId::FontOpticalSizing // technically not presentation | AId::FontSize | AId::FontSizeAdjust | AId::FontStretch | AId::FontStyle | AId::FontVariant | AId::FontWeight + | AId::FontVariationSettings | AId::GlyphOrientationHorizontal | AId::GlyphOrientationVertical | AId::ImageRendering @@ -786,6 +788,7 @@ impl AId { | AId::FloodOpacity | AId::FontFamily | AId::FontKerning + | AId::FontOpticalSizing | AId::FontSize | AId::FontStretch | AId::FontStyle diff --git a/crates/usvg/src/parser/svgtree/names.rs b/crates/usvg/src/parser/svgtree/names.rs index 1e6e2590c..d2afec152 100644 --- a/crates/usvg/src/parser/svgtree/names.rs +++ b/crates/usvg/src/parser/svgtree/names.rs @@ -205,6 +205,7 @@ pub enum AId { FontFamily, FontFeatureSettings, FontKerning, + FontOpticalSizing, FontSize, FontSizeAdjust, FontStretch, @@ -216,6 +217,7 @@ pub enum AId { FontVariantLigatures, FontVariantNumeric, FontVariantPosition, + FontVariationSettings, FontWeight, Fr, Fx, @@ -375,261 +377,264 @@ pub enum AId { } static ATTRIBUTES: Map = Map { - key: 3347381344252206323, + key: 3213172566270843353, disps: &[ - (0, 111), - (0, 2), - (0, 45), - (0, 5), + (0, 63), + (4, 146), + (0, 0), + (3, 42), + (2, 197), + (0, 0), (0, 1), - (2, 56), - (0, 5), - (2, 99), - (13, 198), - (0, 61), - (0, 52), - (1, 29), - (0, 21), - (0, 70), - (0, 164), - (2, 60), - (3, 52), + (0, 0), + (0, 0), + (0, 18), + (0, 11), + (1, 20), + (0, 8), + (17, 110), + (1, 112), + (1, 108), + (5, 94), + (2, 128), + (4, 95), + (0, 63), + (0, 96), + (0, 0), + (1, 110), (0, 1), - (0, 86), - (0, 10), + (40, 30), + (17, 157), + (0, 61), + (0, 16), + (7, 16), + (0, 80), + (0, 107), + (6, 111), + (0, 153), + (6, 202), + (18, 86), + (0, 194), (0, 0), - (0, 4), - (2, 175), - (6, 59), - (1, 14), - (0, 13), - (3, 175), - (1, 10), - (2, 76), - (0, 53), - (0, 24), - (123, 202), - (0, 14), - (0, 30), - (0, 62), - (0, 98), - (11, 193), - (8, 79), - (0, 17), - (22, 5), - (36, 106), - (1, 1), + (0, 7), + (0, 69), + (0, 5), + (0, 19), + (0, 0), + (4, 65), ], entries: &[ - ("mask-border-source", AId::MaskBorderSource), - ("stop-opacity", AId::StopOpacity), - ("stroke-linejoin", AId::StrokeLinejoin), - ("dominant-baseline", AId::DominantBaseline), - ("spreadMethod", AId::SpreadMethod), - ("order", AId::Order), - ("stroke", AId::Stroke), - ("stitchTiles", AId::StitchTiles), - ("height", AId::Height), - ("font-size", AId::FontSize), - ("background-color", AId::BackgroundColor), - ("tableValues", AId::TableValues), - ("x1", AId::X1), - ("y", AId::Y), - ("width", AId::Width), - ("text-indent", AId::TextIndent), - ("fill-opacity", AId::FillOpacity), - ("word-spacing", AId::WordSpacing), - ("cy", AId::Cy), - ("scale", AId::Scale), - ("x2", AId::X2), + ("alignment-baseline", AId::AlignmentBaseline), + ("fx", AId::Fx), + ("targetY", AId::TargetY), + ("clip-path", AId::ClipPath), ("lengthAdjust", AId::LengthAdjust), - ("glyph-orientation-horizontal", AId::GlyphOrientationHorizontal), - ("opacity", AId::Opacity), - ("mask-border", AId::MaskBorder), - ("font-stretch", AId::FontStretch), - ("stroke-dashoffset", AId::StrokeDashoffset), - ("fill", AId::Fill), - ("space", AId::Space), - ("baseline-shift", AId::BaselineShift), - ("text-align-last", AId::TextAlignLast), - ("font-variant-east-asian", AId::FontVariantEastAsian), - ("mask-border-mode", AId::MaskBorderMode), - ("font-variant-caps", AId::FontVariantCaps), + ("mask-size", AId::MaskSize), + ("unicode-bidi", AId::UnicodeBidi), + ("z", AId::Z), + ("font-variant-numeric", AId::FontVariantNumeric), + ("clip-rule", AId::ClipRule), + ("font", AId::Font), ("gradientUnits", AId::GradientUnits), - ("exponent", AId::Exponent), - ("text-decoration-color", AId::TextDecorationColor), - ("refX", AId::RefX), - ("enable-background", AId::EnableBackground), - ("mask-border-width", AId::MaskBorderWidth), + ("style", AId::Style), + ("font-stretch", AId::FontStretch), + ("intercept", AId::Intercept), + ("mask-border-slice", AId::MaskBorderSlice), + ("y", AId::Y), + ("xChannelSelector", AId::XChannelSelector), ("numOctaves", AId::NumOctaves), - ("kerning", AId::Kerning), + ("x1", AId::X1), + ("fill-rule", AId::FillRule), + ("image-rendering", AId::ImageRendering), + ("surfaceScale", AId::SurfaceScale), + ("seed", AId::Seed), ("mix-blend-mode", AId::MixBlendMode), - ("mask-clip", AId::MaskClip), - ("mask-mode", AId::MaskMode), - ("type", AId::Type), - ("class", AId::Class), - ("font", AId::Font), + ("path", AId::Path), ("mask-border-repeat", AId::MaskBorderRepeat), + ("transform", AId::Transform), + ("stroke", AId::Stroke), + ("refX", AId::RefX), + ("text-orientation", AId::TextOrientation), + ("line-height", AId::LineHeight), + ("display", AId::Display), + ("kerning", AId::Kerning), + ("transform-origin", AId::TransformOrigin), + ("shape-subtract", AId::ShapeSubtract), + ("width", AId::Width), ("stroke-miterlimit", AId::StrokeMiterlimit), + ("dy", AId::Dy), + ("text-decoration-color", AId::TextDecorationColor), + ("white-space", AId::WhiteSpace), + ("diffuseConstant", AId::DiffuseConstant), ("text-decoration-stroke", AId::TextDecorationStroke), - ("z", AId::Z), + ("values", AId::Values), + ("font-size", AId::FontSize), + ("shape-image-threshold", AId::ShapeImageThreshold), + ("href", AId::Href), + ("cy", AId::Cy), + ("mask-image", AId::MaskImage), + ("unicode-range", AId::UnicodeRange), + ("specularConstant", AId::SpecularConstant), + ("baseline-shift", AId::BaselineShift), + ("k3", AId::K3), + ("text-anchor", AId::TextAnchor), + ("mask-border-mode", AId::MaskBorderMode), + ("requiredFeatures", AId::RequiredFeatures), + ("color-rendering", AId::ColorRendering), + ("amplitude", AId::Amplitude), + ("mask-border-width", AId::MaskBorderWidth), + ("stroke-linecap", AId::StrokeLinecap), + ("paint-order", AId::PaintOrder), + ("lighting-color", AId::LightingColor), ("dx", AId::Dx), - ("clip-path", AId::ClipPath), - ("markerHeight", AId::MarkerHeight), - ("text-underline-position", AId::TextUnderlinePosition), - ("stdDeviation", AId::StdDeviation), + ("markerWidth", AId::MarkerWidth), + ("scale", AId::Scale), ("id", AId::Id), - ("paint-order", AId::PaintOrder), - ("elevation", AId::Elevation), - ("specularConstant", AId::SpecularConstant), - ("result", AId::Result), - ("font-size-adjust", AId::FontSizeAdjust), - ("mask-origin", AId::MaskOrigin), + ("color", AId::Color), + ("in2", AId::In2), + ("targetX", AId::TargetX), ("direction", AId::Direction), - ("font-variant-numeric", AId::FontVariantNumeric), - ("startOffset", AId::StartOffset), - ("maskUnits", AId::MaskUnits), - ("font-variant", AId::FontVariant), - ("text-orientation", AId::TextOrientation), - ("amplitude", AId::Amplitude), - ("rx", AId::Rx), - ("mask-type", AId::MaskType), - ("filter", AId::Filter), - ("in", AId::In), - ("display", AId::Display), - ("seed", AId::Seed), - ("unicode-range", AId::UnicodeRange), - ("color-profile", AId::ColorProfile), - ("x", AId::X), - ("href", AId::Href), - ("font-feature-settings", AId::FontFeatureSettings), - ("fill-rule", AId::FillRule), - ("fr", AId::Fr), - ("font-variant-ligatures", AId::FontVariantLigatures), - ("text-decoration-style", AId::TextDecorationStyle), - ("radius", AId::Radius), - ("xChannelSelector", AId::XChannelSelector), - ("orient", AId::Orient), - ("isolation", AId::Isolation), - ("gradientTransform", AId::GradientTransform), - ("transform-box", AId::TransformBox), - ("pointsAtY", AId::PointsAtY), - ("text-decoration-line", AId::TextDecorationLine), - ("requiredFeatures", AId::RequiredFeatures), - ("patternContentUnits", AId::PatternContentUnits), + ("pointsAtX", AId::PointsAtX), + ("stitchTiles", AId::StitchTiles), + ("patternUnits", AId::PatternUnits), ("shape-padding", AId::ShapePadding), - ("text-overflow", AId::TextOverflow), - ("clipPathUnits", AId::ClipPathUnits), - ("azimuth", AId::Azimuth), - ("line-height", AId::LineHeight), - ("viewBox", AId::ViewBox), - ("preserveAspectRatio", AId::PreserveAspectRatio), - ("path", AId::Path), + ("k2", AId::K2), + ("font-optical-sizing", AId::FontOpticalSizing), ("k4", AId::K4), - ("systemLanguage", AId::SystemLanguage), + ("vector-effect", AId::VectorEffect), + ("mask-composite", AId::MaskComposite), ("stroke-width", AId::StrokeWidth), - ("specularExponent", AId::SpecularExponent), - ("writing-mode", AId::WritingMode), - ("transform-origin", AId::TransformOrigin), - ("stroke-linecap", AId::StrokeLinecap), - ("points", AId::Points), - ("style", AId::Style), - ("pointsAtZ", AId::PointsAtZ), - ("targetX", AId::TargetX), - ("font-synthesis", AId::FontSynthesis), - ("maskContentUnits", AId::MaskContentUnits), - ("text-align", AId::TextAlign), - ("cx", AId::Cx), - ("alignment-baseline", AId::AlignmentBaseline), - ("font-kerning", AId::FontKerning), - ("requiredExtensions", AId::RequiredExtensions), - ("clip-rule", AId::ClipRule), + ("font-variation-settings", AId::FontVariationSettings), ("mask-border-outset", AId::MaskBorderOutset), - ("primitiveUnits", AId::PrimitiveUnits), - ("textLength", AId::TextLength), + ("in", AId::In), + ("stroke-linejoin", AId::StrokeLinejoin), + ("stop-opacity", AId::StopOpacity), + ("inline-size", AId::InlineSize), + ("mask-type", AId::MaskType), + ("filterUnits", AId::FilterUnits), + ("color-profile", AId::ColorProfile), + ("space", AId::Space), ("text-decoration-fill", AId::TextDecorationFill), - ("fy", AId::Fy), - ("mask-size", AId::MaskSize), - ("k3", AId::K3), - ("marker-start", AId::MarkerStart), - ("mode", AId::Mode), - ("k1", AId::K1), - ("refY", AId::RefY), - ("y1", AId::Y1), - ("shape-rendering", AId::ShapeRendering), - ("operator", AId::Operator), - ("mask-image", AId::MaskImage), - ("marker-end", AId::MarkerEnd), - ("rotate", AId::Rotate), - ("limitingConeAngle", AId::LimitingConeAngle), - ("surfaceScale", AId::SurfaceScale), - ("intercept", AId::Intercept), - ("font-variant-position", AId::FontVariantPosition), + ("font-kerning", AId::FontKerning), + ("offset", AId::Offset), + ("pointsAtZ", AId::PointsAtZ), + ("text-align", AId::TextAlign), ("clip", AId::Clip), - ("fx", AId::Fx), - ("visibility", AId::Visibility), - ("shape-margin", AId::ShapeMargin), - ("font-style", AId::FontStyle), - ("y2", AId::Y2), - ("dy", AId::Dy), + ("y1", AId::Y1), + ("mask-origin", AId::MaskOrigin), + ("mask-mode", AId::MaskMode), ("yChannelSelector", AId::YChannelSelector), - ("ry", AId::Ry), - ("color-rendering", AId::ColorRendering), - ("white-space", AId::WhiteSpace), - ("patternUnits", AId::PatternUnits), - ("shape-subtract", AId::ShapeSubtract), - ("markerWidth", AId::MarkerWidth), - ("d", AId::D), - ("shape-inside", AId::ShapeInside), - ("preserveAlpha", AId::PreserveAlpha), - ("shape-image-threshold", AId::ShapeImageThreshold), - ("image-rendering", AId::ImageRendering), + ("font-variant-caps", AId::FontVariantCaps), ("marker-mid", AId::MarkerMid), - ("filterUnits", AId::FilterUnits), - ("bias", AId::Bias), - ("mask-border-slice", AId::MaskBorderSlice), - ("pointsAtX", AId::PointsAtX), + ("shape-rendering", AId::ShapeRendering), + ("text-rendering", AId::TextRendering), + ("fill-opacity", AId::FillOpacity), + ("word-spacing", AId::WordSpacing), + ("fill", AId::Fill), + ("mask-clip", AId::MaskClip), + ("font-feature-settings", AId::FontFeatureSettings), + ("radius", AId::Radius), ("kernelMatrix", AId::KernelMatrix), - ("color-interpolation", AId::ColorInterpolation), - ("glyph-orientation-vertical", AId::GlyphOrientationVertical), - ("color", AId::Color), - ("patternTransform", AId::PatternTransform), ("kernelUnitLength", AId::KernelUnitLength), + ("mask-border-source", AId::MaskBorderSource), + ("k1", AId::K1), + ("mask", AId::Mask), + ("opacity", AId::Opacity), ("markerUnits", AId::MarkerUnits), + ("visibility", AId::Visibility), + ("spreadMethod", AId::SpreadMethod), + ("pointsAtY", AId::PointsAtY), + ("d", AId::D), + ("slope", AId::Slope), + ("side", AId::Side), + ("tableValues", AId::TableValues), + ("order", AId::Order), + ("text-align-last", AId::TextAlignLast), + ("font-size-adjust", AId::FontSizeAdjust), + ("rotate", AId::Rotate), + ("shape-margin", AId::ShapeMargin), + ("limitingConeAngle", AId::LimitingConeAngle), ("font-weight", AId::FontWeight), - ("overflow", AId::Overflow), + ("text-decoration-line", AId::TextDecorationLine), ("stop-color", AId::StopColor), + ("requiredExtensions", AId::RequiredExtensions), + ("enable-background", AId::EnableBackground), + ("systemLanguage", AId::SystemLanguage), + ("clipPathUnits", AId::ClipPathUnits), + ("stroke-dashoffset", AId::StrokeDashoffset), + ("ry", AId::Ry), + ("overflow", AId::Overflow), + ("class", AId::Class), + ("mask-border", AId::MaskBorder), + ("specularExponent", AId::SpecularExponent), + ("text-decoration", AId::TextDecoration), + ("startOffset", AId::StartOffset), + ("stroke-dasharray", AId::StrokeDasharray), + ("fr", AId::Fr), + ("mask-position", AId::MaskPosition), + ("writing-mode", AId::WritingMode), + ("font-synthesis", AId::FontSynthesis), + ("isolation", AId::Isolation), + ("rx", AId::Rx), + ("bias", AId::Bias), + ("markerHeight", AId::MarkerHeight), + ("edgeMode", AId::EdgeMode), ("r", AId::R), - ("k2", AId::K2), - ("text-anchor", AId::TextAnchor), - ("inline-size", AId::InlineSize), - ("unicode-bidi", AId::UnicodeBidi), + ("stroke-opacity", AId::StrokeOpacity), + ("maskContentUnits", AId::MaskContentUnits), + ("height", AId::Height), + ("font-variant-position", AId::FontVariantPosition), + ("operator", AId::Operator), ("font-family", AId::FontFamily), - ("color-interpolation-filters", AId::ColorInterpolationFilters), - ("slope", AId::Slope), - ("baseFrequency", AId::BaseFrequency), - ("transform", AId::Transform), - ("text-rendering", AId::TextRendering), - ("divisor", AId::Divisor), - ("edgeMode", AId::EdgeMode), + ("fy", AId::Fy), + ("dominant-baseline", AId::DominantBaseline), + ("y2", AId::Y2), + ("shape-inside", AId::ShapeInside), ("letter-spacing", AId::LetterSpacing), + ("azimuth", AId::Azimuth), + ("stdDeviation", AId::StdDeviation), ("flood-color", AId::FloodColor), - ("in2", AId::In2), - ("side", AId::Side), - ("mask-composite", AId::MaskComposite), - ("offset", AId::Offset), - ("values", AId::Values), - ("vector-effect", AId::VectorEffect), - ("mask", AId::Mask), - ("pathLength", AId::PathLength), - ("lighting-color", AId::LightingColor), - ("mask-position", AId::MaskPosition), - ("stroke-dasharray", AId::StrokeDasharray), - ("text-decoration", AId::TextDecoration), - ("stroke-opacity", AId::StrokeOpacity), - ("targetY", AId::TargetY), ("flood-opacity", AId::FloodOpacity), - ("diffuseConstant", AId::DiffuseConstant), + ("type", AId::Type), + ("font-variant-east-asian", AId::FontVariantEastAsian), + ("points", AId::Points), + ("refY", AId::RefY), + ("text-underline-position", AId::TextUnderlinePosition), + ("patternContentUnits", AId::PatternContentUnits), + ("baseFrequency", AId::BaseFrequency), + ("color-interpolation", AId::ColorInterpolation), + ("font-variant-ligatures", AId::FontVariantLigatures), + ("font-style", AId::FontStyle), + ("filter", AId::Filter), + ("text-decoration-style", AId::TextDecorationStyle), + ("preserveAlpha", AId::PreserveAlpha), + ("mode", AId::Mode), + ("divisor", AId::Divisor), + ("cx", AId::Cx), + ("patternTransform", AId::PatternTransform), + ("background-color", AId::BackgroundColor), + ("preserveAspectRatio", AId::PreserveAspectRatio), + ("gradientTransform", AId::GradientTransform), + ("x2", AId::X2), + ("pathLength", AId::PathLength), + ("marker-start", AId::MarkerStart), + ("glyph-orientation-horizontal", AId::GlyphOrientationHorizontal), + ("maskUnits", AId::MaskUnits), + ("textLength", AId::TextLength), + ("viewBox", AId::ViewBox), + ("text-overflow", AId::TextOverflow), + ("glyph-orientation-vertical", AId::GlyphOrientationVertical), + ("result", AId::Result), + ("primitiveUnits", AId::PrimitiveUnits), + ("exponent", AId::Exponent), + ("x", AId::X), + ("font-variant", AId::FontVariant), + ("elevation", AId::Elevation), + ("color-interpolation-filters", AId::ColorInterpolationFilters), + ("text-indent", AId::TextIndent), + ("marker-end", AId::MarkerEnd), + ("transform-box", AId::TransformBox), + ("orient", AId::Orient), ], }; diff --git a/crates/usvg/src/parser/text.rs b/crates/usvg/src/parser/text.rs index 743756a05..1b74cdcb3 100644 --- a/crates/usvg/src/parser/text.rs +++ b/crates/usvg/src/parser/text.rs @@ -140,7 +140,16 @@ pub(crate) fn convert( layouted: vec![], }; - if text::convert(&mut text, &state.opt.font_resolver, cache).is_none() { + let hinting_ctx = if state.opt.hinting.enabled { + Some(crate::text::flatten::HintingContext { + enabled: true, + dpi: state.opt.hinting.dpi.unwrap_or(state.opt.dpi), + }) + } else { + None + }; + + if text::convert(&mut text, &state.opt.font_resolver, cache, hinting_ctx).is_none() { return; } @@ -263,6 +272,12 @@ fn collect_text_chunks_impl( apply_kerning = false; } + // Parse font-optical-sizing (defaults to auto to match browser behavior) + let font_optical_sizing = match parent.find_attribute::<&str>(AId::FontOpticalSizing) { + Some("none") => crate::FontOpticalSizing::None, + _ => crate::FontOpticalSizing::Auto, // "auto" or missing = Auto (browser default) + }; + let mut text_length = parent.try_convert_length(AId::TextLength, Units::UserSpaceOnUse, state); // Negative values should be ignored. @@ -284,6 +299,7 @@ fn collect_text_chunks_impl( font_size, small_caps: parent.find_attribute::<&str>(AId::FontVariant) == Some("small-caps"), apply_kerning, + font_optical_sizing, decoration: resolve_decoration(parent, state, cache), visible: visibility == Visibility::Visible, dominant_baseline, @@ -392,6 +408,49 @@ fn convert_font(node: SvgNode, state: &converter::State) -> Font { let style: FontStyle = node.find_attribute(AId::FontStyle).unwrap_or_default(); let stretch = conv_font_stretch(node); let weight = resolve_font_weight(node); + let mut variations = parse_font_variation_settings(node); + + // Auto-map standard font properties to variation axes if not explicitly set. + // This allows variable fonts to work with regular font-weight/font-stretch properties. + let has_wght = variations.iter().any(|v| &v.tag == b"wght"); + let has_wdth = variations.iter().any(|v| &v.tag == b"wdth"); + let has_ital = variations.iter().any(|v| &v.tag == b"ital"); + let has_slnt = variations.iter().any(|v| &v.tag == b"slnt"); + + // Map font-weight to wght axis (if not already set) + if !has_wght && weight != 400 { + variations.push(FontVariation::new(*b"wght", weight as f32)); + } + + // Map font-stretch to wdth axis (if not already set) + // CSS font-stretch percentages: ultra-condensed=50%, condensed=75%, normal=100%, expanded=125%, ultra-expanded=200% + if !has_wdth { + let wdth = match stretch { + FontStretch::UltraCondensed => 50.0, + FontStretch::ExtraCondensed => 62.5, + FontStretch::Condensed => 75.0, + FontStretch::SemiCondensed => 87.5, + FontStretch::Normal => 100.0, + FontStretch::SemiExpanded => 112.5, + FontStretch::Expanded => 125.0, + FontStretch::ExtraExpanded => 150.0, + FontStretch::UltraExpanded => 200.0, + }; + if wdth != 100.0 { + variations.push(FontVariation::new(*b"wdth", wdth)); + } + } + + // Map font-style: italic to ital axis (if not already set) + if !has_ital && style == FontStyle::Italic { + variations.push(FontVariation::new(*b"ital", 1.0)); + } + + // Map font-style: oblique to slnt axis (if not already set) + // Default oblique angle is typically 12-14 degrees + if !has_slnt && style == FontStyle::Oblique { + variations.push(FontVariation::new(*b"slnt", -12.0)); + } let font_families = if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::FontFamily)) { @@ -421,9 +480,99 @@ fn convert_font(node: SvgNode, state: &converter::State) -> Font { style, stretch, weight, + variations, } } +/// Parses the `font-variation-settings` CSS property. +/// +/// Syntax: `normal | [ ]#` +/// Example: `"wght" 700, "wdth" 50` +fn parse_font_variation_settings(node: SvgNode) -> Vec { + let value = if let Some(n) = node + .ancestors() + .find(|n| n.has_attribute(AId::FontVariationSettings)) + { + let v = n.attribute(AId::FontVariationSettings).unwrap_or(""); + log::debug!("Found font-variation-settings: '{}'", v); + v + } else { + return Vec::new(); + }; + + // "normal" means no variations + if value.eq_ignore_ascii_case("normal") || value.is_empty() { + return Vec::new(); + } + + let mut variations = Vec::new(); + + // Parse comma-separated list of "tag" value pairs + for part in value.split(',') { + let part = part.trim(); + if part.is_empty() { + continue; + } + + // Find the tag (quoted string) and value + // Format: "wght" 700 or 'wght' 700 + let mut chars = part.chars().peekable(); + + // Skip whitespace + while chars.peek().map_or(false, |c| c.is_whitespace()) { + chars.next(); + } + + // Parse quoted tag + let quote = match chars.next() { + Some('"') => '"', + Some('\'') => '\'', + _ => continue, // Invalid format + }; + + let mut tag_str = String::new(); + for c in chars.by_ref() { + if c == quote { + break; + } + tag_str.push(c); + } + + // Tag must be exactly 4 characters + if tag_str.len() != 4 { + log::warn!( + "Invalid font-variation-settings tag: '{}' (must be 4 characters)", + tag_str + ); + continue; + } + + // Skip whitespace before value + while chars.peek().map_or(false, |c| c.is_whitespace()) { + chars.next(); + } + + // Parse the numeric value + let value_str: String = chars.collect(); + let value_str = value_str.trim(); + + let value = match value_str.parse::() { + Ok(v) => v, + Err(_) => { + log::warn!("Invalid font-variation-settings value: '{}'", value_str); + continue; + } + }; + + let tag_bytes = tag_str.as_bytes(); + let tag = [tag_bytes[0], tag_bytes[1], tag_bytes[2], tag_bytes[3]]; + + variations.push(FontVariation::new(tag, value)); + } + + variations +} + // TODO: properly resolve narrower/wider fn conv_font_stretch(node: SvgNode) -> FontStretch { if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::FontStretch)) { diff --git a/crates/usvg/src/text/colr.rs b/crates/usvg/src/text/colr.rs deleted file mode 100644 index b6d2ddf2b..000000000 --- a/crates/usvg/src/text/colr.rs +++ /dev/null @@ -1,343 +0,0 @@ -// Copyright 2024 the Resvg Authors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use crate::parser::OptionLog; -use rustybuzz::ttf_parser; - -struct Builder<'a>(&'a mut String); - -impl Builder<'_> { - fn finish(&mut self) { - if !self.0.is_empty() { - self.0.pop(); // remove trailing space - } - } -} - -impl ttf_parser::OutlineBuilder for Builder<'_> { - fn move_to(&mut self, x: f32, y: f32) { - use std::fmt::Write; - write!(self.0, "M {} {} ", x, y).unwrap(); - } - - fn line_to(&mut self, x: f32, y: f32) { - use std::fmt::Write; - write!(self.0, "L {} {} ", x, y).unwrap(); - } - - fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { - use std::fmt::Write; - write!(self.0, "Q {} {} {} {} ", x1, y1, x, y).unwrap(); - } - - fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { - use std::fmt::Write; - write!(self.0, "C {} {} {} {} {} {} ", x1, y1, x2, y2, x, y).unwrap(); - } - - fn close(&mut self) { - self.0.push_str("Z "); - } -} - -trait XmlWriterExt { - fn write_color_attribute(&mut self, name: &str, ts: ttf_parser::RgbaColor); - fn write_transform_attribute(&mut self, name: &str, ts: ttf_parser::Transform); - fn write_spread_method_attribute(&mut self, method: ttf_parser::colr::GradientExtend); -} - -impl XmlWriterExt for xmlwriter::XmlWriter { - fn write_color_attribute(&mut self, name: &str, color: ttf_parser::RgbaColor) { - self.write_attribute_fmt( - name, - format_args!("rgb({}, {}, {})", color.red, color.green, color.blue), - ); - } - - fn write_transform_attribute(&mut self, name: &str, ts: ttf_parser::Transform) { - if ts.is_default() { - return; - } - - self.write_attribute_fmt( - name, - format_args!( - "matrix({} {} {} {} {} {})", - ts.a, ts.b, ts.c, ts.d, ts.e, ts.f - ), - ); - } - - fn write_spread_method_attribute(&mut self, extend: ttf_parser::colr::GradientExtend) { - self.write_attribute( - "spreadMethod", - match extend { - ttf_parser::colr::GradientExtend::Pad => &"pad", - ttf_parser::colr::GradientExtend::Repeat => &"repeat", - ttf_parser::colr::GradientExtend::Reflect => &"reflect", - }, - ); - } -} - -// NOTE: This is only a best-effort translation of COLR into SVG. -pub(crate) struct GlyphPainter<'a> { - pub(crate) face: &'a ttf_parser::Face<'a>, - pub(crate) svg: &'a mut xmlwriter::XmlWriter, - pub(crate) path_buf: &'a mut String, - pub(crate) gradient_index: usize, - pub(crate) clip_path_index: usize, - pub(crate) palette_index: u16, - pub(crate) transform: ttf_parser::Transform, - pub(crate) outline_transform: ttf_parser::Transform, - pub(crate) transforms_stack: Vec, -} - -impl<'a> GlyphPainter<'a> { - fn write_gradient_stops(&mut self, stops: ttf_parser::colr::GradientStopsIter) { - for stop in stops { - self.svg.start_element("stop"); - self.svg.write_attribute("offset", &stop.stop_offset); - self.svg.write_color_attribute("stop-color", stop.color); - let opacity = f32::from(stop.color.alpha) / 255.0; - self.svg.write_attribute("stop-opacity", &opacity); - self.svg.end_element(); - } - } - - fn paint_solid(&mut self, color: ttf_parser::RgbaColor) { - self.svg.start_element("path"); - self.svg.write_color_attribute("fill", color); - let opacity = f32::from(color.alpha) / 255.0; - self.svg.write_attribute("fill-opacity", &opacity); - self.svg - .write_transform_attribute("transform", self.outline_transform); - self.svg.write_attribute("d", self.path_buf); - self.svg.end_element(); - } - - fn paint_linear_gradient(&mut self, gradient: ttf_parser::colr::LinearGradient<'a>) { - let gradient_id = format!("lg{}", self.gradient_index); - self.gradient_index += 1; - - let gradient_transform = paint_transform(self.outline_transform, self.transform); - - // TODO: We ignore x2, y2. Have to apply them somehow. - // TODO: The way spreadMode works in ttf and svg is a bit different. In SVG, the spreadMode - // will always be applied based on x1/y1 and x2/y2. However, in TTF the spreadMode will - // be applied from the first/last stop. So if we have a gradient with x1=0 x2=1, and - // a stop at x=0.4 and x=0.6, then in SVG we will always see a padding, while in ttf - // we will see the actual spreadMode. We need to account for that somehow. - self.svg.start_element("linearGradient"); - self.svg.write_attribute("id", &gradient_id); - self.svg.write_attribute("x1", &gradient.x0); - self.svg.write_attribute("y1", &gradient.y0); - self.svg.write_attribute("x2", &gradient.x1); - self.svg.write_attribute("y2", &gradient.y1); - self.svg.write_attribute("gradientUnits", &"userSpaceOnUse"); - self.svg.write_spread_method_attribute(gradient.extend); - self.svg - .write_transform_attribute("gradientTransform", gradient_transform); - self.write_gradient_stops( - gradient.stops(self.palette_index, self.face.variation_coordinates()), - ); - self.svg.end_element(); - - self.svg.start_element("path"); - self.svg - .write_attribute_fmt("fill", format_args!("url(#{})", gradient_id)); - self.svg - .write_transform_attribute("transform", self.outline_transform); - self.svg.write_attribute("d", self.path_buf); - self.svg.end_element(); - } - - fn paint_radial_gradient(&mut self, gradient: ttf_parser::colr::RadialGradient<'a>) { - let gradient_id = format!("rg{}", self.gradient_index); - self.gradient_index += 1; - - let gradient_transform = paint_transform(self.outline_transform, self.transform); - - self.svg.start_element("radialGradient"); - self.svg.write_attribute("id", &gradient_id); - self.svg.write_attribute("cx", &gradient.x1); - self.svg.write_attribute("cy", &gradient.y1); - self.svg.write_attribute("r", &gradient.r1); - self.svg.write_attribute("fr", &gradient.r0); - self.svg.write_attribute("fx", &gradient.x0); - self.svg.write_attribute("fy", &gradient.y0); - self.svg.write_attribute("gradientUnits", &"userSpaceOnUse"); - self.svg.write_spread_method_attribute(gradient.extend); - self.svg - .write_transform_attribute("gradientTransform", gradient_transform); - self.write_gradient_stops( - gradient.stops(self.palette_index, self.face.variation_coordinates()), - ); - self.svg.end_element(); - - self.svg.start_element("path"); - self.svg - .write_attribute_fmt("fill", format_args!("url(#{})", gradient_id)); - self.svg - .write_transform_attribute("transform", self.outline_transform); - self.svg.write_attribute("d", self.path_buf); - self.svg.end_element(); - } - - fn paint_sweep_gradient(&mut self, _: ttf_parser::colr::SweepGradient<'a>) { - println!("Warning: sweep gradients are not supported."); - } -} - -fn paint_transform( - outline_transform: ttf_parser::Transform, - transform: ttf_parser::Transform, -) -> ttf_parser::Transform { - let outline_transform = tiny_skia_path::Transform::from_row( - outline_transform.a, - outline_transform.b, - outline_transform.c, - outline_transform.d, - outline_transform.e, - outline_transform.f, - ); - - let gradient_transform = tiny_skia_path::Transform::from_row( - transform.a, - transform.b, - transform.c, - transform.d, - transform.e, - transform.f, - ); - - let gradient_transform = outline_transform - .invert() - .log_none(|| log::warn!("Failed to calculate transform for gradient in glyph.")) - .unwrap_or_default() - .pre_concat(gradient_transform); - - ttf_parser::Transform { - a: gradient_transform.sx, - b: gradient_transform.ky, - c: gradient_transform.kx, - d: gradient_transform.sy, - e: gradient_transform.tx, - f: gradient_transform.ty, - } -} - -impl GlyphPainter<'_> { - fn clip_with_path(&mut self, path: &str) { - let clip_id = format!("cp{}", self.clip_path_index); - self.clip_path_index += 1; - - self.svg.start_element("clipPath"); - self.svg.write_attribute("id", &clip_id); - self.svg.start_element("path"); - self.svg - .write_transform_attribute("transform", self.outline_transform); - self.svg.write_attribute("d", &path); - self.svg.end_element(); - self.svg.end_element(); - - self.svg.start_element("g"); - self.svg - .write_attribute_fmt("clip-path", format_args!("url(#{})", clip_id)); - } -} - -impl<'a> ttf_parser::colr::Painter<'a> for GlyphPainter<'a> { - fn outline_glyph(&mut self, glyph_id: ttf_parser::GlyphId) { - self.path_buf.clear(); - let mut builder = Builder(self.path_buf); - match self.face.outline_glyph(glyph_id, &mut builder) { - Some(v) => v, - None => return, - }; - builder.finish(); - - // We have to write outline using the current transform. - self.outline_transform = self.transform; - } - - fn push_layer(&mut self, mode: ttf_parser::colr::CompositeMode) { - self.svg.start_element("g"); - - use ttf_parser::colr::CompositeMode; - // TODO: Need to figure out how to represent the other blend modes - // in SVG. - let mode = match mode { - CompositeMode::SourceOver => "normal", - CompositeMode::Screen => "screen", - CompositeMode::Overlay => "overlay", - CompositeMode::Darken => "darken", - CompositeMode::Lighten => "lighten", - CompositeMode::ColorDodge => "color-dodge", - CompositeMode::ColorBurn => "color-burn", - CompositeMode::HardLight => "hard-light", - CompositeMode::SoftLight => "soft-light", - CompositeMode::Difference => "difference", - CompositeMode::Exclusion => "exclusion", - CompositeMode::Multiply => "multiply", - CompositeMode::Hue => "hue", - CompositeMode::Saturation => "saturation", - CompositeMode::Color => "color", - CompositeMode::Luminosity => "luminosity", - _ => { - println!("Warning: unsupported blend mode: {:?}", mode); - "normal" - } - }; - self.svg.write_attribute_fmt( - "style", - format_args!("mix-blend-mode: {}; isolation: isolate", mode), - ); - } - - fn pop_layer(&mut self) { - self.svg.end_element(); // g - } - - fn push_transform(&mut self, transform: ttf_parser::Transform) { - self.transforms_stack.push(self.transform); - self.transform = ttf_parser::Transform::combine(self.transform, transform); - } - - fn paint(&mut self, paint: ttf_parser::colr::Paint<'a>) { - match paint { - ttf_parser::colr::Paint::Solid(color) => self.paint_solid(color), - ttf_parser::colr::Paint::LinearGradient(lg) => self.paint_linear_gradient(lg), - ttf_parser::colr::Paint::RadialGradient(rg) => self.paint_radial_gradient(rg), - ttf_parser::colr::Paint::SweepGradient(sg) => self.paint_sweep_gradient(sg), - } - } - - fn pop_transform(&mut self) { - if let Some(ts) = self.transforms_stack.pop() { - self.transform = ts; - } - } - - fn push_clip(&mut self) { - self.clip_with_path(&self.path_buf.clone()); - } - - fn pop_clip(&mut self) { - self.svg.end_element(); - } - - fn push_clip_box(&mut self, clipbox: ttf_parser::colr::ClipBox) { - let x_min = clipbox.x_min; - let x_max = clipbox.x_max; - let y_min = clipbox.y_min; - let y_max = clipbox.y_max; - - let clip_path = format!( - "M {} {} L {} {} L {} {} L {} {} Z", - x_min, y_min, x_max, y_min, x_max, y_max, x_min, y_max - ); - - self.clip_with_path(&clip_path); - } -} diff --git a/crates/usvg/src/text/flatten.rs b/crates/usvg/src/text/flatten.rs index 89929a08e..b2111db84 100644 --- a/crates/usvg/src/text/flatten.rs +++ b/crates/usvg/src/text/flatten.rs @@ -5,14 +5,22 @@ use std::mem; use std::sync::Arc; use fontdb::{Database, ID}; -use rustybuzz::ttf_parser; -use rustybuzz::ttf_parser::{GlyphId, RasterImageFormat, RgbaColor}; +use harfrust::Tag; +use skrifa::{ + bitmap::BitmapData, + instance::{LocationRef, Size as SkrifaSize}, + outline::{pen::ControlBoundsPen, DrawSettings, HintingInstance, OutlinePen, Target}, + raw::TableProvider, + setting::VariationSetting, + FontRef, GlyphId, MetadataProvider, +}; use tiny_skia_path::{NonZeroRect, Size, Transform}; -use xmlwriter::XmlWriter; -use crate::text::colr::GlyphPainter; use crate::*; +/// Points per inch - standard typographic conversion factor for ppem calculation. +const POINTS_PER_INCH: f32 = 72.0; + fn resolve_rendering_mode(text: &Text) -> ShapeRendering { match text.rendering_mode { TextRendering::OptimizeSpeed => ShapeRendering::CrispEdges, @@ -45,10 +53,46 @@ fn push_outline_paths( } } -pub(crate) fn flatten(text: &mut Text, cache: &mut Cache) -> Option<(Group, NonZeroRect)> { +/// Hinting context for controlling font hinting behavior. +#[derive(Clone, Copy, Debug)] +pub struct HintingContext { + /// Whether hinting is enabled globally. + pub enabled: bool, + /// DPI for ppem calculation. + pub dpi: f32, +} + +impl HintingContext { + /// Calculate pixels per em from font size. + pub fn ppem(&self, font_size: f32) -> f32 { + // ppem = font_size * dpi / 72 (converting points to pixels) + font_size * self.dpi / POINTS_PER_INCH + } +} + +/// Convert positioned glyphs to path outlines. +pub(crate) fn flatten( + text: &mut Text, + cache: &mut Cache, + hinting_ctx: Option, +) -> Option<(Group, NonZeroRect)> { + flatten_impl(text, cache, hinting_ctx) +} + +fn flatten_impl( + text: &mut Text, + cache: &mut Cache, + hinting_ctx: Option, +) -> Option<(Group, NonZeroRect)> { let mut new_children = vec![]; let rendering_mode = resolve_rendering_mode(text); + let hinting_mode = HintingMode::from_text_rendering(text.rendering_mode); + + // Determine if we should use hinting + let use_hinting = hinting_ctx + .map(|ctx| ctx.enabled && hinting_mode == HintingMode::Full) + .unwrap_or(false); for span in &text.layouted { if let Some(path) = span.overline.as_ref() { @@ -104,20 +148,15 @@ pub(crate) fn flatten(text: &mut Text, cache: &mut Cache) -> Option<(Group, NonZ let transform = if img.is_sbix { glyph.sbix_transform( - img.x as f32, - img.y as f32, - img.glyph_bbox.map(|bbox| bbox.x_min).unwrap_or(0) as f32, - img.glyph_bbox.map(|bbox| bbox.y_min).unwrap_or(0) as f32, - img.pixels_per_em as f32, + img.x, + img.y, + img.glyph_bbox.map(|bbox| bbox.x_min as f32).unwrap_or(0.0), + img.glyph_bbox.map(|bbox| bbox.y_min as f32).unwrap_or(0.0), + img.pixels_per_em, img.image.size.height(), ) } else { - glyph.cbdt_transform( - img.x as f32, - img.y as f32, - img.pixels_per_em as f32, - img.image.size.height(), - ) + glyph.cbdt_transform(img.x, img.y, img.pixels_per_em) }; let mut group = Group { @@ -128,11 +167,41 @@ pub(crate) fn flatten(text: &mut Text, cache: &mut Cache) -> Option<(Group, NonZ group.calculate_bounding_boxes(); new_children.push(Node::Group(Box::new(group))); - } else if let Some(outline) = cache - .fontdb_outline(glyph.font, glyph.id) - .and_then(|p| p.transform(glyph.outline_transform())) - { - span_builder.push_path(&outline); + } else { + // For variable fonts, we need to extract the outline with variations applied. + // We can't use the cache here since the outline depends on variation values. + // Also handle auto-opsz for variable fonts. + let needs_variations = !glyph.variations.is_empty() + || glyph.font_optical_sizing() == crate::FontOpticalSizing::Auto; + + let outline = if use_hinting { + // Use skrifa for hinted outline extraction + let ppem = hinting_ctx.map(|ctx| ctx.ppem(glyph.font_size())); + extract_outline_skrifa( + &cache.fontdb, + glyph.font, + glyph.id, + &glyph.variations, + glyph.font_size(), + glyph.font_optical_sizing(), + ppem, + hinting_mode, + ) + } else if needs_variations { + cache.fontdb.outline_with_variations( + glyph.font, + glyph.id, + &glyph.variations, + glyph.font_size(), + glyph.font_optical_sizing(), + ) + } else { + cache.fontdb_outline(glyph.font, glyph.id) + }; + + if let Some(outline) = outline.and_then(|p| p.transform(glyph.outline_transform())) { + span_builder.push_path(&outline); + } } } @@ -159,11 +228,171 @@ pub(crate) fn flatten(text: &mut Text, cache: &mut Cache) -> Option<(Group, NonZ Some((group, stroke_bbox)) } -struct PathBuilder { +/// Extract glyph outline using skrifa with optional hinting. +fn extract_outline_skrifa( + fontdb: &fontdb::Database, + font_id: fontdb::ID, + glyph_id: GlyphId, + variations: &[crate::FontVariation], + font_size: f32, + font_optical_sizing: crate::FontOpticalSizing, + ppem: Option, + hinting_mode: HintingMode, +) -> Option { + fontdb.with_face_data(font_id, |data, face_index| -> Option { + let font = FontRef::from_index(data, face_index).ok()?; + let outlines = font.outline_glyphs(); + let glyph = outlines.get(glyph_id)?; + + // Build variation coordinates if needed, using avar-aware normalization + let needs_variations = !variations.is_empty() + || font_optical_sizing == crate::FontOpticalSizing::Auto; + + let location = if needs_variations { + let axes = font.axes(); + let mut coords: Vec = + vec![Default::default(); axes.len()]; + + // Build variation settings including auto-opsz + let mut settings: Vec = variations + .iter() + .map(|v| VariationSetting::new(Tag::new(&v.tag), v.value)) + .collect(); + + // Auto-set opsz if font-optical-sizing is auto and not explicitly set + if font_optical_sizing == crate::FontOpticalSizing::Auto { + let has_explicit_opsz = variations.iter().any(|v| v.tag == *b"opsz"); + if !has_explicit_opsz { + // Check if font has opsz axis + let has_opsz_axis = axes.iter().any(|a| a.tag() == Tag::new(b"opsz")); + if has_opsz_axis { + settings.push(VariationSetting::new(Tag::new(b"opsz"), font_size)); + } + } + } + + // Use location_to_slice which applies avar (axis variations) table remapping. + // This differs from ttf-parser's set_variation() which used raw user-space values. + // Avar remapping transforms user-space axis values to design-space coordinates, + // which is required for correct variable font rendering (especially for fonts + // like Roboto Flex that rely heavily on avar for intermediate axis values). + axes.location_to_slice(&settings, &mut coords); + + Some(coords) + } else { + None + }; + + let location_ref = location + .as_ref() + .map(|c| LocationRef::new(c)) + .unwrap_or_default(); + + // Choose drawing settings based on hinting + // Hinted output is in pixel units (scaled by ppem), while unhinted is in font units. + // We scale hinted output back to font units so outline_transform() can apply consistent scaling. + if let (Some(ppem_val), HintingMode::Full) = (ppem, hinting_mode) { + let size = SkrifaSize::new(ppem_val); + // Create hinting instance for smooth rendering. + // Note: HintingInstance is created per-glyph. For performance optimization, + // consider caching instances keyed by (font_id, ppem, location) if profiling + // shows this is a bottleneck. + let hinting_options = Target::Smooth { + mode: skrifa::outline::SmoothMode::Normal, + symmetric_rendering: true, + preserve_linear_metrics: false, + }; + + if let Ok(hinting_instance) = HintingInstance::new( + &outlines, + size, + location_ref, + hinting_options, + ) { + // Use hinted drawing with the hinting instance + // Output is in pixel units at ppem scale, so we need to scale back to font units + let scale_back = font.head().unwrap().units_per_em() as f32 / ppem_val; + let mut pen = ScalingPen::new(scale_back); + let settings = DrawSettings::hinted(&hinting_instance, false); + glyph.draw(settings, &mut pen).ok()?; + return pen.finish(); + } + } + + // Fallback to unhinted drawing (font units) + let mut pen = SkrifaPen::new(); + let settings = DrawSettings::unhinted(SkrifaSize::unscaled(), location_ref); + glyph.draw(settings, &mut pen).ok()?; + pen.finish() + })? +} + +/// Pen adapter for skrifa's OutlinePen trait -> tiny_skia_path::PathBuilder +struct SkrifaPen { builder: tiny_skia_path::PathBuilder, } -impl ttf_parser::OutlineBuilder for PathBuilder { +impl SkrifaPen { + fn new() -> Self { + Self { + builder: tiny_skia_path::PathBuilder::new(), + } + } + + fn finish(self) -> Option { + self.builder.finish() + } +} + +/// Pen that scales coordinates by a factor (used to convert hinted pixel coords back to font units) +struct ScalingPen { + builder: tiny_skia_path::PathBuilder, + scale: f32, +} + +impl ScalingPen { + fn new(scale: f32) -> Self { + Self { + builder: tiny_skia_path::PathBuilder::new(), + scale, + } + } + + fn finish(self) -> Option { + self.builder.finish() + } +} + +impl OutlinePen for ScalingPen { + fn move_to(&mut self, x: f32, y: f32) { + self.builder.move_to(x * self.scale, y * self.scale); + } + + fn line_to(&mut self, x: f32, y: f32) { + self.builder.line_to(x * self.scale, y * self.scale); + } + + fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) { + self.builder.quad_to( + cx * self.scale, cy * self.scale, + x * self.scale, y * self.scale, + ); + } + + fn curve_to(&mut self, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32) { + self.builder.cubic_to( + cx1 * self.scale, cy1 * self.scale, + cx2 * self.scale, cy2 * self.scale, + x * self.scale, y * self.scale, + ); + } + + fn close(&mut self) { + self.builder.close(); + } +} + +impl OutlinePen for SkrifaPen { fn move_to(&mut self, x: f32, y: f32) { self.builder.move_to(x, y); } @@ -172,12 +401,12 @@ impl ttf_parser::OutlineBuilder for PathBuilder { self.builder.line_to(x, y); } - fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { - self.builder.quad_to(x1, y1, x, y); + fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) { + self.builder.quad_to(cx0, cy0, x, y); } - fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { - self.builder.cubic_to(x1, y1, x2, y2, x, y); + fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { + self.builder.cubic_to(cx0, cy0, cx1, cy1, x, y); } fn close(&mut self) { @@ -185,20 +414,58 @@ impl ttf_parser::OutlineBuilder for PathBuilder { } } +/// Hinting mode derived from CSS text-rendering property +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum HintingMode { + /// No hinting (text-rendering: geometricPrecision) + None, + /// Full hinting (text-rendering: optimizeLegibility) + Full, +} + +impl HintingMode { + /// Convert CSS TextRendering to HintingMode + pub fn from_text_rendering(text_rendering: TextRendering) -> Self { + match text_rendering { + TextRendering::OptimizeSpeed => HintingMode::Full, + TextRendering::OptimizeLegibility => HintingMode::Full, + TextRendering::GeometricPrecision => HintingMode::None, + } + } +} + pub(crate) trait DatabaseExt { fn outline(&self, id: ID, glyph_id: GlyphId) -> Option; + fn outline_with_variations( + &self, + id: ID, + glyph_id: GlyphId, + variations: &[crate::FontVariation], + font_size: f32, + font_optical_sizing: crate::FontOpticalSizing, + ) -> Option; fn raster(&self, id: ID, glyph_id: GlyphId) -> Option; fn svg(&self, id: ID, glyph_id: GlyphId) -> Option; fn colr(&self, id: ID, glyph_id: GlyphId) -> Option; } +/// Bounding box for a glyph (x_min, y_min, x_max, y_max) +#[derive(Clone, Copy, Debug)] +#[allow(dead_code)] +pub(crate) struct GlyphBbox { + pub x_min: i16, + pub y_min: i16, + pub x_max: i16, + pub y_max: i16, +} + #[derive(Clone)] pub(crate) struct BitmapImage { image: Image, - x: i16, - y: i16, - pixels_per_em: u16, - glyph_bbox: Option, + x: f32, + y: f32, + pixels_per_em: f32, + glyph_bbox: Option, is_sbix: bool, } @@ -206,44 +473,139 @@ impl DatabaseExt for Database { #[inline(never)] fn outline(&self, id: ID, glyph_id: GlyphId) -> Option { self.with_face_data(id, |data, face_index| -> Option { - let font = ttf_parser::Face::parse(data, face_index).ok()?; + let font = FontRef::from_index(data, face_index).ok()?; + let outlines = font.outline_glyphs(); + let glyph = outlines.get(glyph_id)?; + + let mut pen = SkrifaPen::new(); + let settings = DrawSettings::unhinted(SkrifaSize::unscaled(), LocationRef::default()); + glyph.draw(settings, &mut pen).ok()?; + pen.finish() + })? + } - let mut builder = PathBuilder { - builder: tiny_skia_path::PathBuilder::new(), - }; + #[inline(never)] + fn outline_with_variations( + &self, + id: ID, + glyph_id: GlyphId, + variations: &[crate::FontVariation], + font_size: f32, + font_optical_sizing: crate::FontOpticalSizing, + ) -> Option { + self.with_face_data(id, |data, face_index| -> Option { + let font = FontRef::from_index(data, face_index).ok()?; + let outlines = font.outline_glyphs(); + let glyph = outlines.get(glyph_id)?; + + // Build variation coordinates using avar-aware normalization + let axes = font.axes(); + let mut coords: Vec = vec![Default::default(); axes.len()]; + + // Build variation settings including auto-opsz + let mut settings: Vec = variations + .iter() + .map(|v| VariationSetting::new(Tag::new(&v.tag), v.value)) + .collect(); + + // Auto-set opsz if font-optical-sizing is auto and not explicitly set + if font_optical_sizing == crate::FontOpticalSizing::Auto { + let has_explicit_opsz = variations.iter().any(|v| v.tag == *b"opsz"); + if !has_explicit_opsz { + let has_opsz_axis = axes.iter().any(|a| a.tag() == Tag::new(b"opsz")); + if has_opsz_axis { + settings.push(VariationSetting::new(Tag::new(b"opsz"), font_size)); + } + } + } - font.outline_glyph(glyph_id, &mut builder)?; - builder.builder.finish() + // Use location_to_slice which applies avar (axis variations) table remapping. + // This differs from ttf-parser's set_variation() which used raw user-space values. + // Avar remapping transforms user-space axis values to design-space coordinates, + // which is required for correct variable font rendering (especially for fonts + // like Roboto Flex that rely heavily on avar for intermediate axis values). + axes.location_to_slice(&settings, &mut coords); + + let location = LocationRef::new(&coords); + let mut pen = SkrifaPen::new(); + let settings = DrawSettings::unhinted(SkrifaSize::unscaled(), location); + glyph.draw(settings, &mut pen).ok()?; + pen.finish() })? } fn raster(&self, id: ID, glyph_id: GlyphId) -> Option { self.with_face_data(id, |data, face_index| -> Option { - let font = ttf_parser::Face::parse(data, face_index).ok()?; - let image = font.glyph_raster_image(glyph_id, u16::MAX)?; + let font = FontRef::from_index(data, face_index).ok()?; + + // Try to get bitmap strikes + let strikes = font.bitmap_strikes(); + // Get the largest available strike (use partial_cmp for f32) + let strike = strikes + .iter() + .max_by(|a, b| a.ppem().partial_cmp(&b.ppem()).unwrap_or(std::cmp::Ordering::Equal))?; + + let bitmap_glyph = strike.get(glyph_id)?; + let bitmap_data = bitmap_glyph.data; + + // Handle PNG data + if let BitmapData::Png(png_data) = bitmap_data { + // Get PNG dimensions using imagesize + let (width, height) = if let Ok(size) = imagesize::blob_size(png_data) { + (size.width as u32, size.height as u32) + } else { + // Fallback: estimate from strike ppem + let ppem = strike.ppem(); + (ppem as u32, ppem as u32) + }; + + // Get the glyph outline bounding box for SBIX positioning. + // SBIX requires the outline bbox for proper vertical alignment. + let glyph_bbox = { + let outlines = font.outline_glyphs(); + outlines.get(glyph_id).and_then(|glyph| { + let mut bounds_pen = ControlBoundsPen::new(); + let settings = DrawSettings::unhinted(SkrifaSize::unscaled(), LocationRef::default()); + glyph.draw(settings, &mut bounds_pen).ok()?; + bounds_pen.bounding_box().map(|bb| GlyphBbox { + x_min: bb.x_min as i16, + y_min: bb.y_min as i16, + x_max: bb.x_max as i16, + y_max: bb.y_max as i16, + }) + }) + }; + + // Detect SBIX format by checking if the font has an sbix table. + // The previous heuristic using inner_bearing was unreliable. + let is_sbix = font.table_data(Tag::new(b"sbix")).is_some(); + + log::warn!( + "Bitmap glyph: bearing=({}, {}), inner_bearing=({}, {}), ppem={}, bbox={:?}, is_sbix={}, height={}", + bitmap_glyph.bearing_x, bitmap_glyph.bearing_y, + bitmap_glyph.inner_bearing_x, bitmap_glyph.inner_bearing_y, + strike.ppem(), glyph_bbox, is_sbix, height + ); + + // Use skrifa's inner_bearing values directly for both SBIX and CBDT. + // inner_bearing_x/y contain the glyph positioning offsets we need. + let (x, y) = (bitmap_glyph.inner_bearing_x, bitmap_glyph.inner_bearing_y); - if image.format == RasterImageFormat::PNG { let bitmap_image = BitmapImage { image: Image { id: String::new(), visible: true, - size: Size::from_wh(image.width as f32, image.height as f32)?, + size: Size::from_wh(width as f32, height as f32)?, rendering_mode: ImageRendering::OptimizeQuality, - kind: ImageKind::PNG(Arc::new(image.data.into())), + kind: ImageKind::PNG(Arc::new(png_data.to_vec())), abs_transform: Transform::default(), - abs_bounding_box: NonZeroRect::from_xywh( - 0.0, - 0.0, - image.width as f32, - image.height as f32, - )?, + abs_bounding_box: NonZeroRect::from_xywh(0.0, 0.0, width as f32, height as f32)?, }, - x: image.x, - y: image.y, - pixels_per_em: image.pixels_per_em, - glyph_bbox: font.glyph_bounding_box(glyph_id), - // ttf-parser always checks sbix first, so if this table exists, it was used. - is_sbix: font.tables().sbix.is_some(), + x, + y, + pixels_per_em: strike.ppem(), + glyph_bbox, + is_sbix, }; return Some(bitmap_image); @@ -254,71 +616,158 @@ impl DatabaseExt for Database { } fn svg(&self, id: ID, glyph_id: GlyphId) -> Option { - // TODO: Technically not 100% accurate because the SVG format in a OTF font - // is actually a subset/superset of a normal SVG, but it seems to work fine - // for Twitter Color Emoji, so might as well use what we already have. - - // TODO: Glyph records can contain the data for multiple glyphs. We should - // add a cache so we don't need to reparse the data every time. + // Parse SVG table manually since skrifa doesn't expose SVG table access yet. + // SVG table format (OpenType spec): + // - Header: version (u16), svgDocListOffset (u32), reserved (u32) + // - Document list at offset: numEntries (u16), entries[] + // - Each entry: startGlyphID (u16), endGlyphID (u16), svgDocOffset (u32), svgDocLength (u32) self.with_face_data(id, |data, face_index| -> Option { - let font = ttf_parser::Face::parse(data, face_index).ok()?; - let image = font.glyph_svg_image(glyph_id)?; - let tree = Tree::from_data(image.data, &Options::default()).ok()?; - - // Twitter Color Emoji seems to always have one SVG record per glyph, - // while Noto Color Emoji sometimes contains multiple ones. It's kind of hacky, - // but the best we have for now. - let node = if image.start_glyph_id == image.end_glyph_id { - Node::Group(Box::new(tree.root)) - } else { - tree.node_by_id(&format!("glyph{}", glyph_id.0)) - .log_none(|| { - log::warn!("Failed to find SVG glyph node for glyph {}", glyph_id.0); - }) - .cloned()? - }; + let font = FontRef::from_index(data, face_index).ok()?; + + let svg_table = font.table_data(Tag::new(b"SVG "))?; + let svg_data = svg_table.as_ref(); + + // Need at least header (10 bytes) + if svg_data.len() < 10 { + return None; + } - Some(node) + // Parse header + let _version = u16::from_be_bytes([svg_data[0], svg_data[1]]); + let doc_list_offset = u32::from_be_bytes([svg_data[2], svg_data[3], svg_data[4], svg_data[5]]) as usize; + + // Navigate to document list + if doc_list_offset + 2 > svg_data.len() { + return None; + } + + let doc_list = &svg_data[doc_list_offset..]; + let num_entries = u16::from_be_bytes([doc_list[0], doc_list[1]]) as usize; + + // Each entry is 12 bytes + let entries_start = 2; + let glyph_id_val = glyph_id.to_u32() as u16; + + // Find the entry for this glyph + for i in 0..num_entries { + let entry_offset = entries_start + i * 12; + if entry_offset + 12 > doc_list.len() { + break; + } + + let entry = &doc_list[entry_offset..entry_offset + 12]; + let start_glyph = u16::from_be_bytes([entry[0], entry[1]]); + let end_glyph = u16::from_be_bytes([entry[2], entry[3]]); + let svg_doc_offset = u32::from_be_bytes([entry[4], entry[5], entry[6], entry[7]]) as usize; + let svg_doc_length = u32::from_be_bytes([entry[8], entry[9], entry[10], entry[11]]) as usize; + + if glyph_id_val >= start_glyph && glyph_id_val <= end_glyph { + // Found the entry - extract SVG document + // Offset is relative to start of SVG table + let abs_offset = doc_list_offset + svg_doc_offset; + if abs_offset + svg_doc_length > svg_data.len() { + return None; + } + + let svg_doc_data = &svg_data[abs_offset..abs_offset + svg_doc_length]; + + // Handle gzip compression (SVG documents may be gzip compressed) + let svg_bytes: std::borrow::Cow<[u8]> = if svg_doc_data.starts_with(&[0x1f, 0x8b]) { + // Gzip compressed + use std::io::Read; + let mut decoder = flate2::read::GzDecoder::new(svg_doc_data); + let mut decompressed = Vec::new(); + if decoder.read_to_end(&mut decompressed).is_err() { + return None; + } + std::borrow::Cow::Owned(decompressed) + } else { + std::borrow::Cow::Borrowed(svg_doc_data) + }; + + // Parse the SVG document + let tree = crate::Tree::from_data(&svg_bytes, &crate::Options::default()).ok()?; + + // If this record covers a single glyph, return the whole tree + // Otherwise, look for the specific glyph by ID + let node = if start_glyph == end_glyph { + Node::Group(Box::new(tree.root)) + } else { + // Multi-glyph record - find the specific glyph by ID + let glyph_node_id = format!("glyph{}", glyph_id_val); + tree.node_by_id(&glyph_node_id).cloned()? + }; + + return Some(node); + } + } + + None })? } fn colr(&self, id: ID, glyph_id: GlyphId) -> Option { - self.with_face_data(id, |data, face_index| -> Option { - let face = ttf_parser::Face::parse(data, face_index).ok()?; - - let mut svg = XmlWriter::new(xmlwriter::Options::default()); - - svg.start_element("svg"); - svg.write_attribute("xmlns", "http://www.w3.org/2000/svg"); - svg.write_attribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); - - let mut path_buf = String::with_capacity(256); - let gradient_index = 1; - let clip_path_index = 1; - - svg.start_element("g"); - - let mut glyph_painter = GlyphPainter { - face: &face, - svg: &mut svg, - path_buf: &mut path_buf, - gradient_index, - clip_path_index, - palette_index: 0, - transform: ttf_parser::Transform::default(), - outline_transform: ttf_parser::Transform::default(), - transforms_stack: vec![ttf_parser::Transform::default()], - }; - - face.paint_color_glyph( - glyph_id, - 0, - RgbaColor::new(0, 0, 0, 255), - &mut glyph_painter, - )?; - svg.end_element(); + // Use skrifa-based COLR painting + // This provides COLRv1 support (sweep gradients, advanced blend modes) + let result = self.with_face_data(id, |data, face_index| { + super::skrifa_colr::paint_colr_glyph(data, face_index, glyph_id) + })?; + result + } +} - Tree::from_data(svg.end_document().as_bytes(), &Options::default()).ok() - })? +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_skrifa_variable_font() { + // Test that skrifa properly applies variable font axes + let font_path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../crates/resvg/tests/fonts/RobotoFlex.subset.ttf" + ); + let font_data = std::fs::read(font_path).expect("Font not found"); + + let font = FontRef::new(&font_data).expect("Failed to parse font"); + let outlines = font.outline_glyphs(); + + // Get glyph for 'N' + let charmap = font.charmap(); + let glyph_id = charmap.map('N').expect("Glyph not found"); + let glyph = outlines.get(glyph_id).expect("Outline not found"); + + // Get axes + let axes = font.axes(); + + // Find wdth axis + let wdth_idx = axes.iter().position(|a| a.tag() == Tag::new(b"wdth")).expect("wdth axis not found"); + + // Draw with default location + let mut pen1 = SkrifaPen::new(); + let settings1 = DrawSettings::unhinted(SkrifaSize::unscaled(), LocationRef::default()); + glyph.draw(settings1, &mut pen1).expect("Draw failed"); + let path1 = pen1.finish().expect("Path failed"); + let bounds1 = path1.bounds(); + + // Draw with wdth=25 (narrow) + let mut coords = vec![skrifa::instance::NormalizedCoord::default(); axes.len()]; + coords[wdth_idx] = axes.get(wdth_idx).unwrap().normalize(25.0); + + let location = LocationRef::new(&coords); + let mut pen2 = SkrifaPen::new(); + let settings2 = DrawSettings::unhinted(SkrifaSize::unscaled(), location); + glyph.draw(settings2, &mut pen2).expect("Draw failed"); + let path2 = pen2.finish().expect("Path failed"); + let bounds2 = path2.bounds(); + + // The narrow version should have a smaller width + assert!( + bounds2.width() < bounds1.width(), + "wdth=25 should be narrower than default! default width: {}, wdth=25 width: {}", + bounds1.width(), + bounds2.width() + ); } } + diff --git a/crates/usvg/src/text/layout.rs b/crates/usvg/src/text/layout.rs index 2261f66bb..4c0572ab0 100644 --- a/crates/usvg/src/text/layout.rs +++ b/crates/usvg/src/text/layout.rs @@ -6,9 +6,9 @@ use std::num::NonZeroU16; use std::sync::Arc; use fontdb::{Database, ID}; +use harfrust::Tag; use kurbo::{ParamCurve, ParamCurveArclen, ParamCurveDeriv}; -use rustybuzz::ttf_parser; -use rustybuzz::ttf_parser::{GlyphId, Tag}; +use skrifa::{GlyphId, MetadataProvider}; use strict_num::NonZeroPositiveF32; use tiny_skia_path::{NonZeroRect, Transform}; use unicode_script::UnicodeScript; @@ -45,9 +45,23 @@ pub struct PositionedGlyph { /// The ID of the font the glyph should be taken from. Can be used with the /// [font database of the tree](crate::Tree::fontdb) this glyph is part of. pub font: ID, + /// Font variation settings for variable fonts. + pub variations: Vec, + /// Font optical sizing mode for auto-opsz computation. + pub font_optical_sizing: crate::FontOpticalSizing, } impl PositionedGlyph { + /// Returns the font size for this glyph. + pub fn font_size(&self) -> f32 { + self.font_size + } + + /// Returns the font optical sizing mode. + pub fn font_optical_sizing(&self) -> crate::FontOpticalSizing { + self.font_optical_sizing + } + /// Returns the transform of glyph. pub fn transform(&self) -> Transform { let sx = self.font_size / self.units_per_em as f32; @@ -66,18 +80,17 @@ impl PositionedGlyph { .pre_concat(Transform::from_scale(1.0, -1.0)) } - /// Returns the transform for the glyph, assuming that a CBTD-based raster glyph + /// Returns the transform for the glyph, assuming that a CBDT-based raster glyph /// is being used. - pub fn cbdt_transform(&self, x: f32, y: f32, pixels_per_em: f32, height: f32) -> Transform { + pub fn cbdt_transform(&self, x: f32, y: f32, pixels_per_em: f32) -> Transform { self.transform() .pre_concat(Transform::from_scale( self.units_per_em as f32 / pixels_per_em, self.units_per_em as f32 / pixels_per_em, )) - // Right now, the top-left corner of the image would be placed in - // on the "text cursor", but we want the bottom-left corner to be there, - // so we need to shift it up and also apply the x/y offset. - .pre_translate(x, -height - y) + // The y value from skrifa's inner_bearing_y points to the top of the glyph. + // We negate it to convert from font coordinates (y-up) to image coordinates (y-down). + .pre_translate(x, -y) } /// Returns the transform for the glyph, assuming that a sbix-based raster glyph @@ -899,6 +912,9 @@ fn process_chunk( font, span.small_caps, span.apply_kerning, + &span.font.variations, + span.font_size.get(), + span.font_optical_sizing, resolver, fontdb, ); @@ -953,10 +969,25 @@ fn process_chunk( let mut clusters = Vec::new(); for (range, byte_idx) in GlyphClusters::new(&glyphs) { if let Some(span) = chunk_span_at(chunk, byte_idx) { + // Compute effective variations including auto-opsz to match what was used during shaping. + // This ensures the glyph outlines use the same variations as the advance/position calculations. + let font_id = fonts_cache + .get(&span.font) + .map(|f| f.id) + .unwrap_or_else(|| glyphs[range.start].font.id); + let effective_variations = compute_effective_variations( + &span.font.variations, + span.font_size.get(), + span.font_optical_sizing, + font_id, + fontdb, + ); clusters.push(form_glyph_clusters( &glyphs[range], &chunk.text, span.font_size.get(), + &effective_variations, + span.font_optical_sizing, )); } } @@ -1127,7 +1158,13 @@ fn apply_word_spacing(chunk: &TextChunk, clusters: &mut [GlyphCluster]) { } } -fn form_glyph_clusters(glyphs: &[Glyph], text: &str, font_size: f32) -> GlyphCluster { +fn form_glyph_clusters( + glyphs: &[Glyph], + text: &str, + font_size: f32, + variations: &[crate::FontVariation], + font_optical_sizing: crate::FontOpticalSizing, +) -> GlyphCluster { debug_assert!(!glyphs.is_empty()); let mut width = 0.0; @@ -1157,6 +1194,8 @@ fn form_glyph_clusters(glyphs: &[Glyph], text: &str, font_size: f32) -> GlyphClu font: glyph.font.id, text: glyph.text.clone(), id: glyph.id, + variations: variations.to_vec(), + font_optical_sizing, }); x += glyph.width as f32; @@ -1189,89 +1228,21 @@ pub(crate) trait DatabaseExt { fn has_char(&self, id: ID, c: char) -> bool; } +// Skrifa-based implementation for font metrics impl DatabaseExt for Database { #[inline(never)] fn load_font(&self, id: ID) -> Option { - self.with_face_data(id, |data, face_index| -> Option { - let font = ttf_parser::Face::parse(data, face_index).ok()?; - - let units_per_em = NonZeroU16::new(font.units_per_em())?; - - let ascent = font.ascender(); - let descent = font.descender(); - - let x_height = font - .x_height() - .and_then(|x| u16::try_from(x).ok()) - .and_then(NonZeroU16::new); - let x_height = match x_height { - Some(height) => height, - None => { - // If not set - fallback to height * 45%. - // 45% is what Firefox uses. - u16::try_from((f32::from(ascent - descent) * 0.45) as i32) - .ok() - .and_then(NonZeroU16::new)? - } - }; - - let line_through = font.strikeout_metrics(); - let line_through_position = match line_through { - Some(metrics) => metrics.position, - None => x_height.get() as i16 / 2, - }; - - let (underline_position, underline_thickness) = match font.underline_metrics() { - Some(metrics) => { - let thickness = u16::try_from(metrics.thickness) - .ok() - .and_then(NonZeroU16::new) - // `ttf_parser` guarantees that units_per_em is >= 16 - .unwrap_or_else(|| NonZeroU16::new(units_per_em.get() / 12).unwrap()); - - (metrics.position, thickness) - } - None => ( - -(units_per_em.get() as i16) / 9, - NonZeroU16::new(units_per_em.get() / 12).unwrap(), - ), - }; - - // 0.2 and 0.4 are generic offsets used by some applications (Inkscape/librsvg). - let mut subscript_offset = (units_per_em.get() as f32 / 0.2).round() as i16; - let mut superscript_offset = (units_per_em.get() as f32 / 0.4).round() as i16; - if let Some(metrics) = font.subscript_metrics() { - subscript_offset = metrics.y_offset; - } - - if let Some(metrics) = font.superscript_metrics() { - superscript_offset = metrics.y_offset; - } - - Some(ResolvedFont { - id, - units_per_em, - ascent, - descent, - x_height, - underline_position, - underline_thickness, - line_through_position, - subscript_offset, - superscript_offset, - }) + self.with_face_data(id, |data, face_index| { + super::skrifa_metrics::load_font_metrics(data, face_index, id) })? } #[inline(never)] fn has_char(&self, id: ID, c: char) -> bool { - let res = self.with_face_data(id, |font_data, face_index| -> Option { - let font = ttf_parser::Face::parse(font_data, face_index).ok()?; - font.glyph_index(c)?; - Some(true) - }); - - res == Some(Some(true)) + self.with_face_data(id, |font_data, face_index| { + super::skrifa_metrics::has_char(font_data, face_index, c) + }) + .unwrap_or(false) } } @@ -1281,11 +1252,23 @@ pub(crate) fn shape_text( font: Arc, small_caps: bool, apply_kerning: bool, + variations: &[crate::FontVariation], + font_size: f32, + font_optical_sizing: crate::FontOpticalSizing, resolver: &FontResolver, fontdb: &mut Arc, ) -> Vec { - let mut glyphs = shape_text_with_font(text, font.clone(), small_caps, apply_kerning, fontdb) - .unwrap_or_default(); + let mut glyphs = shape_text_with_font( + text, + font.clone(), + small_caps, + apply_kerning, + variations, + font_size, + font_optical_sizing, + fontdb, + ) + .unwrap_or_default(); // Remember all fonts used for shaping. let mut used_fonts = vec![font.id]; @@ -1314,6 +1297,9 @@ pub(crate) fn shape_text( fallback_font.clone(), small_caps, apply_kerning, + variations, + font_size, + font_optical_sizing, fontdb, ) .unwrap_or_default(); @@ -1371,10 +1357,75 @@ fn shape_text_with_font( font: Arc, small_caps: bool, apply_kerning: bool, + variations: &[crate::FontVariation], + font_size: f32, + font_optical_sizing: crate::FontOpticalSizing, fontdb: &fontdb::Database, ) -> Option> { fontdb.with_face_data(font.id, |font_data, face_index| -> Option> { - let rb_font = rustybuzz::Face::from_slice(font_data, face_index)?; + let hr_font = harfrust::FontRef::from_index(font_data, face_index).ok()?; + + // Build the list of variations to apply + let mut final_variations: Vec = variations + .iter() + .map(|v| harfrust::Variation { + tag: Tag::new(&v.tag), + value: v.value, + }) + .collect(); + + // Automatic optical sizing: if font-optical-sizing is auto and the font has + // an 'opsz' axis that isn't explicitly set, auto-set it to match font size. + // This matches browser behavior (CSS font-optical-sizing: auto). + if font_optical_sizing == crate::FontOpticalSizing::Auto { + let has_explicit_opsz = variations.iter().any(|v| v.tag == *b"opsz"); + if !has_explicit_opsz { + // Check if font has opsz axis using skrifa + if let Ok(skrifa_font) = skrifa::FontRef::from_index(font_data, face_index) { + let axes = skrifa_font.axes(); + let has_opsz_axis = axes.iter().any(|axis| axis.tag() == Tag::new(b"opsz")); + if has_opsz_axis { + log::debug!( + "Auto-setting opsz={} (font-optical-sizing: auto)", + font_size + ); + final_variations.push(harfrust::Variation { + tag: Tag::new(b"opsz"), + value: font_size, + }); + } + } + } + } + + // Log variations if any + if !final_variations.is_empty() { + log::debug!( + "Applying {} font variations for shaping", + final_variations.len() + ); + for v in &final_variations { + log::debug!( + " Setting variation {:?} = {}", + std::str::from_utf8(&v.tag.into_bytes()).unwrap_or("????"), + v.value + ); + } + } + + // Create shaper data and instance + let shaper_data = harfrust::ShaperData::new(&hr_font); + let shaper_instance = if !final_variations.is_empty() { + Some(harfrust::ShaperInstance::from_variations(&hr_font, final_variations)) + } else { + None + }; + + // Build shaper with optional instance + let shaper = shaper_data + .shaper(&hr_font) + .instance(shaper_instance.as_ref()) + .build(); let bidi_info = unicode_bidi::BidiInfo::new(text, Some(unicode_bidi::Level::ltr())); let paragraph = &bidi_info.paragraphs[0]; @@ -1390,31 +1441,37 @@ fn shape_text_with_font( } let ltr = levels[run.start].is_ltr(); - let hb_direction = if ltr { - rustybuzz::Direction::LeftToRight + let hr_direction = if ltr { + harfrust::Direction::LeftToRight } else { - rustybuzz::Direction::RightToLeft + harfrust::Direction::RightToLeft }; - let mut buffer = rustybuzz::UnicodeBuffer::new(); + let mut buffer = harfrust::UnicodeBuffer::new(); buffer.push_str(sub_text); - buffer.set_direction(hb_direction); + buffer.set_direction(hr_direction); + // Set script based on the first character's script for proper shaping + // This is critical for Arabic and other complex scripts + if let Some(first_char) = sub_text.chars().next() { + let script = unicode_script_to_harfrust(first_char.script()); + buffer.set_script(script); + } let mut features = Vec::new(); if small_caps { - features.push(rustybuzz::Feature::new(Tag::from_bytes(b"smcp"), 1, ..)); + features.push(harfrust::Feature::new(Tag::new(b"smcp"), 1, ..)); } if !apply_kerning { - features.push(rustybuzz::Feature::new(Tag::from_bytes(b"kern"), 0, ..)); + features.push(harfrust::Feature::new(Tag::new(b"kern"), 0, ..)); } - let output = rustybuzz::shape(&rb_font, &features, buffer); + let output = shaper.shape(buffer, &features); let positions = output.glyph_positions(); let infos = output.glyph_infos(); - for i in 0..output.len() { + for i in 0usize..output.len() { let pos = positions[i]; let info = infos[i]; let idx = run.start + info.cluster as usize; @@ -1433,7 +1490,7 @@ fn shape_text_with_font( byte_idx: ByteIndex::new(idx), cluster_len: end.checked_sub(start).unwrap_or(0), // TODO: can fail? text: sub_text[start..end].to_string(), - id: GlyphId(info.glyph_id as u16), + id: GlyphId::new(info.glyph_id as u32), dx: pos.x_offset, dy: pos.y_offset, width: pos.x_advance, @@ -1548,7 +1605,7 @@ pub(crate) struct Glyph { impl Glyph { fn is_missing(&self) -> bool { - self.id.0 == 0 + self.id.to_u32() == 0 } } @@ -1597,6 +1654,33 @@ pub(crate) fn is_word_separator_characters(c: char) -> bool { } impl ResolvedFont { + /// Creates a new ResolvedFont with all required metrics. + pub(crate) fn new( + id: ID, + units_per_em: NonZeroU16, + ascent: i16, + descent: i16, + x_height: NonZeroU16, + underline_position: i16, + underline_thickness: NonZeroU16, + line_through_position: i16, + subscript_offset: i16, + superscript_offset: i16, + ) -> Self { + Self { + id, + units_per_em, + ascent, + descent, + x_height, + underline_position, + underline_thickness, + line_through_position, + subscript_offset, + superscript_offset, + } + } + #[inline] pub(crate) fn scale(&self, font_size: f32) -> f32 { font_size / self.units_per_em.get() as f32 @@ -1744,3 +1828,81 @@ impl ByteIndex { text[self.0..].chars().next().unwrap() } } + +/// Converts unicode_script::Script to harfrust::Script +fn unicode_script_to_harfrust(script: unicode_script::Script) -> harfrust::Script { + use unicode_script::Script::*; + match script { + Arabic => harfrust::script::ARABIC, + Armenian => harfrust::script::ARMENIAN, + Bengali => harfrust::script::BENGALI, + Bopomofo => harfrust::script::BOPOMOFO, + Cyrillic => harfrust::script::CYRILLIC, + Devanagari => harfrust::script::DEVANAGARI, + Georgian => harfrust::script::GEORGIAN, + Greek => harfrust::script::GREEK, + Gujarati => harfrust::script::GUJARATI, + Gurmukhi => harfrust::script::GURMUKHI, + Han => harfrust::script::HAN, + Hangul => harfrust::script::HANGUL, + Hebrew => harfrust::script::HEBREW, + Hiragana => harfrust::script::HIRAGANA, + Kannada => harfrust::script::KANNADA, + Katakana => harfrust::script::KATAKANA, + Khmer => harfrust::script::KHMER, + Lao => harfrust::script::LAO, + Latin => harfrust::script::LATIN, + Malayalam => harfrust::script::MALAYALAM, + Myanmar => harfrust::script::MYANMAR, + Oriya => harfrust::script::ORIYA, + Sinhala => harfrust::script::SINHALA, + Syriac => harfrust::script::SYRIAC, + Tamil => harfrust::script::TAMIL, + Telugu => harfrust::script::TELUGU, + Thai => harfrust::script::THAI, + Tibetan => harfrust::script::TIBETAN, + _ => harfrust::script::COMMON, + } +} + +/// Computes effective font variations including automatic optical sizing. +/// +/// If `font_optical_sizing` is `Auto` and the font has an `opsz` axis that isn't +/// explicitly set in `variations`, this function adds `opsz=font_size` to match +/// browser behavior (CSS font-optical-sizing: auto). +fn compute_effective_variations( + variations: &[crate::FontVariation], + font_size: f32, + font_optical_sizing: crate::FontOpticalSizing, + font_id: ID, + fontdb: &fontdb::Database, +) -> Vec { + let mut effective = variations.to_vec(); + + // Automatic optical sizing: if font-optical-sizing is auto and the font has + // an 'opsz' axis that isn't explicitly set, auto-set it to match font size. + if font_optical_sizing == crate::FontOpticalSizing::Auto { + let has_explicit_opsz = variations.iter().any(|v| v.tag == *b"opsz"); + if !has_explicit_opsz { + // Check if font has opsz axis + let has_opsz_axis = fontdb + .with_face_data(font_id, |font_data, face_index| { + if let Ok(font) = skrifa::FontRef::from_index(font_data, face_index) { + font.axes().iter().any(|axis| axis.tag() == Tag::new(b"opsz")) + } else { + false + } + }) + .unwrap_or(false); + + if has_opsz_axis { + effective.push(crate::FontVariation { + tag: *b"opsz", + value: font_size, + }); + } + } + } + + effective +} diff --git a/crates/usvg/src/text/mod.rs b/crates/usvg/src/text/mod.rs index 4b48274e1..9e844885c 100644 --- a/crates/usvg/src/text/mod.rs +++ b/crates/usvg/src/text/mod.rs @@ -11,10 +11,13 @@ use crate::{Cache, Font, FontStretch, FontStyle, Text}; pub(crate) mod flatten; -mod colr; /// Provides access to the layout of a text node. pub mod layout; +// Skrifa-based implementations for font metrics and COLR +mod skrifa_metrics; +mod skrifa_colr; + /// A shorthand for [FontResolver]'s font selection function. /// /// This function receives a font specification (families + a style, weight, @@ -201,13 +204,18 @@ impl std::fmt::Debug for FontResolver<'_> { /// is not based on the outlines of a glyph, but instead the glyph metrics as well /// as decoration spans). /// 2. We convert all of the positioned glyphs into outlines. -pub(crate) fn convert(text: &mut Text, resolver: &FontResolver, cache: &mut Cache) -> Option<()> { +pub(crate) fn convert( + text: &mut Text, + resolver: &FontResolver, + cache: &mut Cache, + hinting_ctx: Option, +) -> Option<()> { let (text_fragments, bbox) = layout::layout_text(text, resolver, &mut cache.fontdb)?; text.layouted = text_fragments; text.bounding_box = bbox.to_rect(); text.abs_bounding_box = bbox.transform(text.abs_transform)?.to_rect(); - let (group, stroke_bbox) = flatten::flatten(text, cache)?; + let (group, stroke_bbox) = flatten::flatten(text, cache, hinting_ctx)?; text.flattened = Box::new(group); text.stroke_bounding_box = stroke_bbox.to_rect(); text.abs_stroke_bounding_box = stroke_bbox.transform(text.abs_transform)?.to_rect(); diff --git a/crates/usvg/src/text/skrifa_colr.rs b/crates/usvg/src/text/skrifa_colr.rs new file mode 100644 index 000000000..4ee87bde8 --- /dev/null +++ b/crates/usvg/src/text/skrifa_colr.rs @@ -0,0 +1,396 @@ +// Copyright 2024 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! COLRv1 color glyph painting using skrifa's ColorPainter. +//! +//! This module provides an alternative to ttf-parser for rendering COLR glyphs, +//! using skrifa's ColorPainter trait. This enables full COLRv1 support including +//! sweep/conic gradients. + +use skrifa::{ + color::{Brush, ColorGlyphFormat, ColorPainter, CompositeMode}, + instance::LocationRef, + outline::OutlinePen, + raw::types::BoundingBox, + FontRef, GlyphId, MetadataProvider, +}; +use xmlwriter::XmlWriter; + +use crate::{Options, Tree}; + +/// Skrifa-based pen for building SVG path data. +struct SvgPathPen<'a> { + path: &'a mut String, +} + +impl<'a> SvgPathPen<'a> { + fn new(path: &'a mut String) -> Self { + Self { path } + } + + fn finish(&mut self) { + if !self.path.is_empty() { + self.path.pop(); // remove trailing space + } + } +} + +impl OutlinePen for SvgPathPen<'_> { + fn move_to(&mut self, x: f32, y: f32) { + use std::fmt::Write; + write!(self.path, "M {} {} ", x, y).unwrap(); + } + + fn line_to(&mut self, x: f32, y: f32) { + use std::fmt::Write; + write!(self.path, "L {} {} ", x, y).unwrap(); + } + + fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) { + use std::fmt::Write; + write!(self.path, "Q {} {} {} {} ", cx0, cy0, x, y).unwrap(); + } + + fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { + use std::fmt::Write; + write!(self.path, "C {} {} {} {} {} {} ", cx0, cy0, cx1, cy1, x, y).unwrap(); + } + + fn close(&mut self) { + self.path.push_str("Z "); + } +} + +/// COLR glyph painter that outputs SVG using skrifa's ColorPainter. +pub(crate) struct SkrifaGlyphPainter<'a> { + font: FontRef<'a>, + svg: &'a mut XmlWriter, + path_buf: &'a mut String, + gradient_index: usize, + clip_path_index: usize, + transform_stack: Vec, + current_transform: skrifa::color::Transform, +} + +impl<'a> SkrifaGlyphPainter<'a> { + pub fn new( + font: FontRef<'a>, + svg: &'a mut XmlWriter, + path_buf: &'a mut String, + ) -> Self { + Self { + font, + svg, + path_buf, + gradient_index: 1, + clip_path_index: 1, + transform_stack: Vec::new(), + current_transform: skrifa::color::Transform::default(), + } + } + + fn get_color(&self, palette_index: u16) -> Option { + // TODO: SVG 2 allows specifying color palette via CSS font-palette property. + // Currently we always use palette 0 (the default). Supporting font-palette + // would require passing the palette index through the rendering pipeline. + self.font + .color_palettes() + .get(0)? + .colors() + .get(palette_index as usize) + .copied() + } + + fn write_color(&mut self, name: &str, palette_index: u16, alpha: f32) { + if let Some(color) = self.get_color(palette_index) { + self.svg.write_attribute_fmt( + name, + format_args!("rgb({}, {}, {})", color.red, color.green, color.blue), + ); + let opacity = (color.alpha as f32 / 255.0) * alpha; + if opacity < 1.0 { + let opacity_name = if name == "fill" { + "fill-opacity" + } else { + "stop-opacity" + }; + self.svg.write_attribute(opacity_name, &opacity); + } + } + } + + fn write_transform(&mut self, name: &str, ts: skrifa::color::Transform) { + // Check if it's an identity transform (no transformation) + if ts.xx == 1.0 && ts.yx == 0.0 && ts.xy == 0.0 && ts.yy == 1.0 && ts.dx == 0.0 && ts.dy == 0.0 { + return; + } + + self.svg.write_attribute_fmt( + name, + format_args!( + "matrix({} {} {} {} {} {})", + ts.xx, ts.yx, ts.xy, ts.yy, ts.dx, ts.dy + ), + ); + } + + fn paint_solid(&mut self, palette_index: u16, alpha: f32) { + self.svg.start_element("path"); + self.write_color("fill", palette_index, alpha); + self.write_transform("transform", self.current_transform); + self.svg.write_attribute("d", self.path_buf); + self.svg.end_element(); + } + + fn paint_linear_gradient( + &mut self, + p0: skrifa::raw::types::Point, + p1: skrifa::raw::types::Point, + stops: &[skrifa::color::ColorStop], + extend: skrifa::color::Extend, + ) { + let gradient_id = format!("lg{}", self.gradient_index); + self.gradient_index += 1; + + self.svg.start_element("linearGradient"); + self.svg.write_attribute("id", &gradient_id); + self.svg.write_attribute("x1", &p0.x); + self.svg.write_attribute("y1", &p0.y); + self.svg.write_attribute("x2", &p1.x); + self.svg.write_attribute("y2", &p1.y); + self.svg.write_attribute("gradientUnits", &"userSpaceOnUse"); + self.write_spread_method(extend); + self.write_transform("gradientTransform", self.current_transform); + self.write_gradient_stops(stops); + self.svg.end_element(); + + self.svg.start_element("path"); + self.svg + .write_attribute_fmt("fill", format_args!("url(#{})", gradient_id)); + self.svg.write_attribute("d", self.path_buf); + self.svg.end_element(); + } + + fn paint_radial_gradient( + &mut self, + c0: skrifa::raw::types::Point, + r0: f32, + c1: skrifa::raw::types::Point, + r1: f32, + stops: &[skrifa::color::ColorStop], + extend: skrifa::color::Extend, + ) { + let gradient_id = format!("rg{}", self.gradient_index); + self.gradient_index += 1; + + self.svg.start_element("radialGradient"); + self.svg.write_attribute("id", &gradient_id); + self.svg.write_attribute("cx", &c1.x); + self.svg.write_attribute("cy", &c1.y); + self.svg.write_attribute("r", &r1); + self.svg.write_attribute("fr", &r0); + self.svg.write_attribute("fx", &c0.x); + self.svg.write_attribute("fy", &c0.y); + self.svg.write_attribute("gradientUnits", &"userSpaceOnUse"); + self.write_spread_method(extend); + self.write_transform("gradientTransform", self.current_transform); + self.write_gradient_stops(stops); + self.svg.end_element(); + + self.svg.start_element("path"); + self.svg + .write_attribute_fmt("fill", format_args!("url(#{})", gradient_id)); + self.svg.write_attribute("d", self.path_buf); + self.svg.end_element(); + } + + fn paint_sweep_gradient( + &mut self, + c0: skrifa::raw::types::Point, + start_angle: f32, + end_angle: f32, + stops: &[skrifa::color::ColorStop], + extend: skrifa::color::Extend, + ) { + // SVG doesn't have native sweep gradient support. + // We approximate with a conic gradient in CSS or fall back to first stop color. + // For now, use the first stop color as a fallback. + log::warn!( + "Sweep gradient at ({}, {}) from {}° to {}° - using fallback", + c0.x, c0.y, start_angle, end_angle + ); + + if let Some(first_stop) = stops.first() { + self.paint_solid(first_stop.palette_index, first_stop.alpha); + } + + // Consume extend to suppress unused warning + let _ = extend; + } + + fn write_spread_method(&mut self, extend: skrifa::color::Extend) { + let method = match extend { + skrifa::color::Extend::Pad => "pad", + skrifa::color::Extend::Repeat => "repeat", + skrifa::color::Extend::Reflect => "reflect", + _ => "pad", // Default to pad for unknown values + }; + self.svg.write_attribute("spreadMethod", &method); + } + + fn write_gradient_stops(&mut self, stops: &[skrifa::color::ColorStop]) { + for stop in stops { + self.svg.start_element("stop"); + self.svg.write_attribute("offset", &stop.offset); + self.write_color("stop-color", stop.palette_index, stop.alpha); + self.svg.end_element(); + } + } + + fn clip_with_path(&mut self, path: &str) { + let clip_id = format!("cp{}", self.clip_path_index); + self.clip_path_index += 1; + + self.svg.start_element("clipPath"); + self.svg.write_attribute("id", &clip_id); + self.svg.start_element("path"); + self.write_transform("transform", self.current_transform); + self.svg.write_attribute("d", &path); + self.svg.end_element(); + self.svg.end_element(); + + self.svg.start_element("g"); + self.svg + .write_attribute_fmt("clip-path", format_args!("url(#{})", clip_id)); + } +} + +impl<'a> ColorPainter for SkrifaGlyphPainter<'a> { + fn push_transform(&mut self, transform: skrifa::color::Transform) { + self.transform_stack.push(self.current_transform); + self.current_transform = self.current_transform * transform; + } + + fn pop_transform(&mut self) { + if let Some(ts) = self.transform_stack.pop() { + self.current_transform = ts; + } + } + + fn push_clip_glyph(&mut self, glyph_id: GlyphId) { + self.path_buf.clear(); + let outlines = self.font.outline_glyphs(); + if let Some(glyph) = outlines.get(glyph_id) { + let mut pen = SvgPathPen::new(self.path_buf); + let settings = skrifa::outline::DrawSettings::unhinted( + skrifa::instance::Size::unscaled(), + LocationRef::default(), + ); + let _ = glyph.draw(settings, &mut pen); + pen.finish(); + } + self.clip_with_path(&self.path_buf.clone()); + } + + fn push_clip_box(&mut self, clip_box: BoundingBox) { + let x_min = clip_box.x_min; + let x_max = clip_box.x_max; + let y_min = clip_box.y_min; + let y_max = clip_box.y_max; + + let clip_path = format!( + "M {} {} L {} {} L {} {} L {} {} Z", + x_min, y_min, x_max, y_min, x_max, y_max, x_min, y_max + ); + + self.clip_with_path(&clip_path); + } + + fn pop_clip(&mut self) { + self.svg.end_element(); // g with clip-path + } + + fn fill(&mut self, brush: Brush<'_>) { + match brush { + Brush::Solid { palette_index, alpha } => { + self.paint_solid(palette_index, alpha); + } + Brush::LinearGradient { p0, p1, color_stops, extend } => { + self.paint_linear_gradient(p0, p1, color_stops, extend); + } + Brush::RadialGradient { c0, r0, c1, r1, color_stops, extend } => { + self.paint_radial_gradient(c0, r0, c1, r1, color_stops, extend); + } + Brush::SweepGradient { c0, start_angle, end_angle, color_stops, extend } => { + self.paint_sweep_gradient(c0, start_angle, end_angle, color_stops, extend); + } + } + } + + fn push_layer(&mut self, mode: CompositeMode) { + self.svg.start_element("g"); + + let mode_str = match mode { + CompositeMode::SrcOver => "normal", + CompositeMode::Screen => "screen", + CompositeMode::Overlay => "overlay", + CompositeMode::Darken => "darken", + CompositeMode::Lighten => "lighten", + CompositeMode::ColorDodge => "color-dodge", + CompositeMode::ColorBurn => "color-burn", + CompositeMode::HardLight => "hard-light", + CompositeMode::SoftLight => "soft-light", + CompositeMode::Difference => "difference", + CompositeMode::Exclusion => "exclusion", + CompositeMode::Multiply => "multiply", + CompositeMode::HslHue => "hue", + CompositeMode::HslSaturation => "saturation", + CompositeMode::HslColor => "color", + CompositeMode::HslLuminosity => "luminosity", + _ => { + log::warn!("Unsupported blend mode: {:?}", mode); + "normal" + } + }; + self.svg.write_attribute_fmt( + "style", + format_args!("mix-blend-mode: {}; isolation: isolate", mode_str), + ); + } + + fn pop_layer(&mut self) { + self.svg.end_element(); // g + } +} + +/// Paint a COLR glyph using skrifa's ColorPainter and return the resulting SVG tree. +pub fn paint_colr_glyph(data: &[u8], face_index: u32, glyph_id: GlyphId) -> Option { + let font = FontRef::from_index(data, face_index).ok()?; + + let mut svg = XmlWriter::new(xmlwriter::Options::default()); + + svg.start_element("svg"); + svg.write_attribute("xmlns", "http://www.w3.org/2000/svg"); + svg.write_attribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); + + let mut path_buf = String::with_capacity(256); + + svg.start_element("g"); + + let skrifa_glyph_id = GlyphId::new(glyph_id.to_u32()); + let color_glyphs = font.color_glyphs(); + + // Try COLRv1 first, then fall back to COLRv0 + let color_glyph = color_glyphs + .get_with_format(skrifa_glyph_id, ColorGlyphFormat::ColrV1) + .or_else(|| color_glyphs.get_with_format(skrifa_glyph_id, ColorGlyphFormat::ColrV0))?; + + let mut painter = SkrifaGlyphPainter::new(font, &mut svg, &mut path_buf); + + // Paint the glyph - this calls our ColorPainter implementation + let _ = color_glyph.paint(LocationRef::default(), &mut painter); + + svg.end_element(); // g + + Tree::from_data(svg.end_document().as_bytes(), &Options::default()).ok() +} diff --git a/crates/usvg/src/text/skrifa_metrics.rs b/crates/usvg/src/text/skrifa_metrics.rs new file mode 100644 index 000000000..091637dd8 --- /dev/null +++ b/crates/usvg/src/text/skrifa_metrics.rs @@ -0,0 +1,105 @@ +// Copyright 2024 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Font metrics extraction using skrifa. +//! +//! This module provides an alternative to ttf-parser for extracting font metrics, +//! using skrifa's MetadataProvider trait. This is used when the `hinting` feature +//! is enabled to reduce double font parsing. + +use std::num::NonZeroU16; + +use fontdb::ID; +use skrifa::{ + instance::LocationRef, instance::Size as SkrifaSize, raw::TableProvider, FontRef, + MetadataProvider, +}; + +use super::layout::ResolvedFont; + +/// Load font metrics using skrifa's MetadataProvider. +/// +/// Returns a ResolvedFont containing all necessary metrics for text layout. +pub fn load_font_metrics(data: &[u8], face_index: u32, id: ID) -> Option { + let font = FontRef::from_index(data, face_index).ok()?; + let metrics = font.metrics(SkrifaSize::unscaled(), LocationRef::default()); + + let units_per_em = NonZeroU16::new(metrics.units_per_em)?; + + // skrifa provides ascent/descent as f32 in font units (when using unscaled size) + let ascent = metrics.ascent as i16; + let descent = metrics.descent as i16; + + // x_height is optional in skrifa + let x_height = metrics + .x_height + .and_then(|x| u16::try_from(x as i32).ok()) + .and_then(NonZeroU16::new); + let x_height = match x_height { + Some(height) => height, + None => { + // If not set - fallback to height * 45%. + // 45% is what Firefox uses. + u16::try_from((f32::from(ascent - descent) * 0.45) as i32) + .ok() + .and_then(NonZeroU16::new)? + } + }; + + // Get strikeout/line-through position from skrifa's strikeout decoration + let line_through_position = match metrics.strikeout { + Some(decoration) => decoration.offset as i16, + None => x_height.get() as i16 / 2, + }; + + // Get underline metrics from skrifa + let (underline_position, underline_thickness) = match metrics.underline { + Some(decoration) => { + let thickness = u16::try_from(decoration.thickness as i32) + .ok() + .and_then(NonZeroU16::new) + // skrifa guarantees that units_per_em is >= 16 + .unwrap_or_else(|| NonZeroU16::new(units_per_em.get() / 12).unwrap()); + + (decoration.offset as i16, thickness) + } + None => ( + -(units_per_em.get() as i16) / 9, + NonZeroU16::new(units_per_em.get() / 12).unwrap(), + ), + }; + + // Get subscript/superscript metrics from OS/2 table, fall back to calculation + // 0.2 and 0.4 are generic offsets used by some applications (Inkscape/librsvg). + let mut subscript_offset = (units_per_em.get() as f32 / 0.2).round() as i16; + let mut superscript_offset = (units_per_em.get() as f32 / 0.4).round() as i16; + + // Try to get actual values from OS/2 table + if let Ok(os2) = font.os2() { + subscript_offset = os2.y_subscript_y_offset(); + superscript_offset = os2.y_superscript_y_offset(); + } + + Some(ResolvedFont::new( + id, + units_per_em, + ascent, + descent, + x_height, + underline_position, + underline_thickness, + line_through_position, + subscript_offset, + superscript_offset, + )) +} + +/// Check if a font contains a glyph for the given character using skrifa's charmap. +pub fn has_char(data: &[u8], face_index: u32, c: char) -> bool { + let font = match FontRef::from_index(data, face_index) { + Ok(f) => f, + Err(_) => return false, + }; + + font.charmap().map(c).is_some() +} diff --git a/crates/usvg/src/tree/text.rs b/crates/usvg/src/tree/text.rs index 10898bb3c..c1739b901 100644 --- a/crates/usvg/src/tree/text.rs +++ b/crates/usvg/src/tree/text.rs @@ -66,6 +66,39 @@ impl From for fontdb::Stretch { } } +/// A font variation axis setting. +/// +/// Used for variable fonts to specify axis values like weight, width, etc. +#[derive(Clone, Copy, Debug)] +pub struct FontVariation { + /// The 4-byte axis tag (e.g., b"wght" for weight). + pub tag: [u8; 4], + /// The axis value. + pub value: f32, +} + +impl FontVariation { + /// Creates a new font variation. + pub fn new(tag: [u8; 4], value: f32) -> Self { + Self { tag, value } + } +} + +impl PartialEq for FontVariation { + fn eq(&self, other: &Self) -> bool { + self.tag == other.tag && self.value.to_bits() == other.value.to_bits() + } +} + +impl Eq for FontVariation {} + +impl std::hash::Hash for FontVariation { + fn hash(&self, state: &mut H) { + self.tag.hash(state); + self.value.to_bits().hash(state); + } +} + /// A font style property. #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] pub enum FontStyle { @@ -113,6 +146,7 @@ pub struct Font { pub(crate) style: FontStyle, pub(crate) stretch: FontStretch, pub(crate) weight: u16, + pub(crate) variations: Vec, } impl Font { @@ -137,6 +171,11 @@ impl Font { pub fn weight(&self) -> u16 { self.weight } + + /// Font variation settings for variable fonts. + pub fn variations(&self) -> &[FontVariation] { + &self.variations + } } /// A dominant baseline property. @@ -218,6 +257,24 @@ impl Default for LengthAdjust { } } +/// A font optical sizing property. +/// +/// Controls automatic adjustment of the `opsz` axis in variable fonts +/// based on font size. Matches CSS `font-optical-sizing`. +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum FontOpticalSizing { + /// Automatically set `opsz` to match font size (browser default). + Auto, + /// Do not automatically adjust `opsz`. + None, +} + +impl Default for FontOpticalSizing { + fn default() -> Self { + Self::Auto + } +} + /// A text span decoration style. /// /// In SVG, text decoration and text it's applied to can have different styles. @@ -281,6 +338,7 @@ pub struct TextSpan { pub(crate) font_size: NonZeroPositiveF32, pub(crate) small_caps: bool, pub(crate) apply_kerning: bool, + pub(crate) font_optical_sizing: FontOpticalSizing, pub(crate) decoration: TextDecoration, pub(crate) dominant_baseline: DominantBaseline, pub(crate) alignment_baseline: AlignmentBaseline, @@ -346,6 +404,15 @@ impl TextSpan { self.apply_kerning } + /// Font optical sizing mode. + /// + /// When `Auto` (default), the `opsz` axis will be automatically set + /// to match the font size for variable fonts that support it. + /// This matches the CSS `font-optical-sizing: auto` behavior. + pub fn font_optical_sizing(&self) -> FontOpticalSizing { + self.font_optical_sizing + } + /// A span decorations. pub fn decoration(&self) -> &TextDecoration { &self.decoration