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#"
+
+ "#;
+
+ // 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#"
+
+ "#;
+
+ 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#"
+
+ "#;
+
+ // 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#"
+
+ "#;
+
+ 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, 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#"
+
+ "#;
+
+ 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#"
+
+ "#;
+
+ 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#"
+
+ "#;
+
+ // SVG with explicit optimizeLegibility
+ let svg_legibility = br#"
+
+ "#;
+
+ 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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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