diff --git a/.github/copyright.sh b/.github/copyright.sh index b2d899024..7bfc8d7cf 100644 --- a/.github/copyright.sh +++ b/.github/copyright.sh @@ -7,7 +7,8 @@ # -g "!src/special_directory" # Check all the standard Rust source files -output=$(rg "^// Copyright (19|20)[\d]{2} (.+ and )?the Resvg Authors( and .+)?$\n^// SPDX-License-Identifier: Apache-2\.0 OR MIT$\n\n" --files-without-match --multiline -g "*.{rs,c,cpp,h}" .) +# Exclude vendored fontdb (has its own copyright from original author) +output=$(rg "^// Copyright (19|20)[\d]{2} (.+ and )?the Resvg Authors( and .+)?$\n^// SPDX-License-Identifier: Apache-2\.0 OR MIT$\n\n" --files-without-match --multiline -g "*.{rs,c,cpp,h}" -g "!crates/fontdb/*" .) if [ -n "$output" ]; then echo -e "The following files lack the correct copyright header:\n" diff --git a/.gitignore b/.gitignore index 97e0980f0..c23714a2f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ target .vscode tools/build-* **/diffs +**/diffs-hinted 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/CHANGELOG.md b/CHANGELOG.md index 8d259dc62..5c332a74d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,35 @@ This changelog also contains important changes in dependencies. This release has an MSRV of 1.87.0 for `usvg` and `resvg` and the C API. +### Added + +- Hinting support for text rendering with configurable settings via CSS custom properties + (`-resvg-hinting-target`, `-resvg-hinting-mode`, `-resvg-hinting-engine`, + `-resvg-hinting-symmetric`, `-resvg-hinting-preserve-linear-metrics`). +- Variable font support with avar-aware axis normalization for correct rendering. +- LRU cache for glyph outlines with full parameter support (hinting settings, variations, ppem). + Dramatically improves performance for text-heavy documents with repeated glyphs. +- Cache statistics tracking (`CacheStats`) for monitoring outline cache hits/misses/evictions. +- Cache management methods: `outline_cache_stats()`, `clear_outline_cache()`, + `resize_outline_cache()`, `outline_cache_len()`, `outline_cache_capacity()`. + +### Changed + +- Port text rendering from `ttf-parser`/`rustybuzz` to `skrifa`/`harfrust`. +- Vendor `fontdb` 0.23.0 as a workspace crate, ported from `ttf-parser` to `skrifa`. + This eliminates the `ttf-parser` dependency entirely. +- Move span-uniform fields (`variations`, `font_optical_sizing`, `hinting`) from + `PositionedGlyph` to `Span` for reduced memory usage and better cache efficiency. +- Unify three outline extraction code paths (simple/variable/hinted) into single cached lookup. + +### Fixed + +- Remove empty `--drop-tables` flag from README. +- Fix potential panics on empty slices in font fallback. +- Add empty path check in `path_length()`. +- Add ASCII validation for font variation tags. +- Fix incorrect safety comment on mmap in fontdb. + ## [0.46.0] This release has an MSRV of 1.87.0 for `usvg` and `resvg` and the C API. diff --git a/Cargo.lock b/Cargo.lock index 5e4fd69b8..e9f42746b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "arrayref" version = "0.3.9" @@ -58,6 +64,20 @@ name = "bytemuck" version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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" @@ -124,6 +144,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "euclid" version = "0.22.11" @@ -158,6 +184,21 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[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.8" @@ -170,15 +211,13 @@ dependencies = [ [[package]] name = "fontdb" version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" dependencies = [ "fontconfig-parser", "log", "memmap2", + "skrifa", "slotmap", "tinyvec", - "ttf-parser", ] [[package]] @@ -197,6 +236,30 @@ 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 = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "image-webp" version = "0.2.4" @@ -251,6 +314,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown", +] + [[package]] name = "memchr" version = "2.7.6" @@ -352,12 +424,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" @@ -473,6 +563,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.46.0" @@ -522,24 +623,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "rustybuzz" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" -dependencies = [ - "bitflags 2.10.0", - "bytemuck", - "core_maths", - "log", - "smallvec", - "ttf-parser", - "unicode-bidi-mirroring", - "unicode-ccc", - "unicode-properties", - "unicode-script", -] - [[package]] name = "simd-adler32" version = "0.3.8" @@ -567,6 +650,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.1.1" @@ -601,6 +694,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" @@ -642,15 +746,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "ttf-parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" -dependencies = [ - "core_maths", -] - [[package]] name = "unicode-bidi" version = "0.3.18" @@ -658,22 +753,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.4" +name = "unicode-ident" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-script" @@ -695,15 +778,18 @@ dependencies = [ "data-url", "flate2", "fontdb", + "harfrust", "imagesize", "kurbo", "log", + "lru", "once_cell", "pico-args", + "png 0.18.0", "roxmltree 0.21.1", - "rustybuzz", "simplecss", "siphasher 1.0.1", + "skrifa", "strict-num", "svgtypes", "tiny-skia-path", diff --git a/Cargo.toml b/Cargo.toml index 1bfecbc73..17c7baeb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "crates/c-api", + "crates/fontdb", "crates/resvg", "crates/usvg", "crates/usvg/codegen", diff --git a/crates/fontdb/Cargo.toml b/crates/fontdb/Cargo.toml new file mode 100644 index 000000000..cbe2ba786 --- /dev/null +++ b/crates/fontdb/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "fontdb" +version = "0.23.0" +authors = ["Yevhenii Reizner "] +edition = "2024" +rust-version = "1.87.0" +license = "MIT" +description = "A simple, in-memory font database with CSS-like queries. Vendored for resvg with skrifa backend." +repository = "https://github.com/linebender/resvg" +documentation = "https://docs.rs/fontdb/" +keywords = ["font", "db", "css", "truetype", "opentype"] +categories = ["text-processing"] + +[dependencies] +slotmap = "1.0" +tinyvec = { version = "1.8", features = ["alloc"] } +skrifa = "0.40" +log = "0.4" + +memmap2 = { version = "0.9", optional = true } +fontconfig-parser = { version = "0.5.8", optional = true } + +[features] +default = ["std", "fs", "system-fonts", "memmap"] +# Enables standard library support (always on for this vendored version). +std = [] +# Enables file system operations (loading fonts from files/directories). +fs = ["std"] +# Enables system fonts loading. +system-fonts = ["fs", "fontconfig"] +# Enables fontconfig support on Linux. +fontconfig = ["fontconfig-parser"] +# Enables font files memory mapping for faster loading. +memmap = ["fs", "memmap2"] diff --git a/crates/fontdb/src/lib.rs b/crates/fontdb/src/lib.rs new file mode 100644 index 000000000..837c99d74 --- /dev/null +++ b/crates/fontdb/src/lib.rs @@ -0,0 +1,1361 @@ +// Copyright 2020 Yevhenii Reizner (original fontdb, MIT licensed) +// Copyright 2026 the Resvg Authors (modifications) +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Vendored and modified version of fontdb 0.23.0 that uses skrifa instead of ttf-parser. +//! +//! Original: (MIT licensed) +//! +//! # Features +//! +//! - The database can load fonts from files, directories and raw data (`Vec`). +//! - The database can match a font using CSS-like queries. See `Database::query`. +//! - The database can try to load system fonts. +//! Currently, this is implemented by scanning predefined directories. +//! The library does not interact with the system API. +//! - Provides a unique ID for each font face. +//! +//! # Font vs Face +//! +//! A font is a collection of font faces. Therefore, a font face is a subset of a font. +//! A simple font (*.ttf/*.otf) usually contains a single font face, +//! but a font collection (*.ttc) can contain multiple font faces. +//! +//! `fontdb` stores and matches font faces, not fonts. +//! Therefore, after loading a font collection with 5 faces (for example), the database will be populated +//! with 5 `FaceInfo` objects, all of which will be pointing to the same file or binary data. +//! +//! # Performance +//! +//! The database performance is largely limited by the storage itself. +//! Font parsing is handled by skrifa. +//! +//! # Safety +//! +//! The library relies on memory-mapped files, which is inherently unsafe. +//! But since we do not keep the files open it should be perfectly safe. +//! +//! If you would like to use a persistent memory mapping of the font files, +//! then you can use the unsafe [`Database::make_shared_face_data`] function. + +// Allow unsafe code for mmap operations (from original fontdb) +#![allow(unsafe_code)] +#![deny(missing_docs)] + +use slotmap::SlotMap; +use tinyvec::TinyVec; + +use skrifa::{FontRef, MetadataProvider, raw::FileRef, raw::TableProvider, string::StringId}; + +/// A font face language. +/// +/// Simplified version - we only need to distinguish English US for family name prioritization. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Default)] +pub enum Language { + /// English (United States) + EnglishUnitedStates, + /// Any other language + #[default] + Unknown, +} + +impl Language { + /// Returns the primary language tag. + pub fn primary_language(&self) -> &'static str { + match self { + Language::EnglishUnitedStates => "en", + Language::Unknown => "und", + } + } + + /// Returns the region tag. + pub fn region(&self) -> &'static str { + match self { + Language::EnglishUnitedStates => "US", + Language::Unknown => "", + } + } +} + +/// Convert from BCP-47 language tag to our Language enum +fn language_from_bcp47(tag: Option<&str>) -> Language { + match tag { + Some(t) if t.starts_with("en-US") || t == "en" => Language::EnglishUnitedStates, + _ => Language::Unknown, + } +} + +/// Selects a normal, condensed, or expanded face from a font family. +#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Debug, Hash, Default)] +pub enum Stretch { + /// 50% + UltraCondensed, + /// 62.5% + ExtraCondensed, + /// 75% + Condensed, + /// 87.5% + SemiCondensed, + /// 100% + #[default] + Normal, + /// 112.5% + SemiExpanded, + /// 125% + Expanded, + /// 150% + ExtraExpanded, + /// 200% + UltraExpanded, +} + +impl Stretch { + /// Convert to a numeric value for CSS matching calculations. + fn to_number(self) -> i32 { + match self { + Stretch::UltraCondensed => 1, + Stretch::ExtraCondensed => 2, + Stretch::Condensed => 3, + Stretch::SemiCondensed => 4, + Stretch::Normal => 5, + Stretch::SemiExpanded => 6, + Stretch::Expanded => 7, + Stretch::ExtraExpanded => 8, + Stretch::UltraExpanded => 9, + } + } +} + +/// Convert from skrifa's Stretch percentage to our Stretch enum +fn stretch_from_skrifa(s: skrifa::attribute::Stretch) -> Stretch { + let pct = s.percentage(); + if pct <= 56.25 { + Stretch::UltraCondensed + } else if pct <= 68.75 { + Stretch::ExtraCondensed + } else if pct <= 81.25 { + Stretch::Condensed + } else if pct <= 93.75 { + Stretch::SemiCondensed + } else if pct <= 106.25 { + Stretch::Normal + } else if pct <= 118.75 { + Stretch::SemiExpanded + } else if pct <= 137.5 { + Stretch::Expanded + } else if pct <= 175.0 { + Stretch::ExtraExpanded + } else { + Stretch::UltraExpanded + } +} + +/// Get the number of fonts in a font collection (TTC), or 1 for single fonts +fn fonts_in_collection(data: &[u8]) -> u32 { + match FileRef::new(data) { + Ok(FileRef::Collection(c)) => c.len(), + Ok(FileRef::Font(_)) => 1, + Err(_) => 1, + } +} + +/// A unique per database face ID. +/// +/// Since `Database` is not global/unique, we cannot guarantee that a specific ID +/// is actually from the same db instance. This is up to the caller. +/// +/// ID overflow will cause a panic, but it's highly unlikely that someone would +/// load more than 4 billion font faces. +/// +/// Because the internal representation of ID is private, The `Display` trait +/// implementation for this type only promise that unequal IDs will be displayed +/// as different strings, but does not make any guarantees about format or +/// content of the strings. +/// +/// [`KeyData`]: https://docs.rs/slotmap/latest/slotmap/struct.KeyData.html +#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Debug, Default)] +pub struct ID(InnerId); + +slotmap::new_key_type! { + /// Internal ID type. + struct InnerId; +} + +impl ID { + /// Creates a dummy ID. + /// + /// Should be used in tandem with [`Database::push_face_info`]. + #[inline] + pub fn dummy() -> Self { + Self(InnerId::from(slotmap::KeyData::from_ffi(u64::MAX))) + } +} + +impl core::fmt::Display for ID { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", (self.0).0.as_ffi()) + } +} + +/// A list of possible font loading errors. +#[derive(Debug)] +enum LoadError { + /// A malformed font. + /// + /// Typically means that skrifa wasn't able to parse it. + MalformedFont, + /// A valid TrueType font without a valid *Family Name*. + UnnamedFont, + /// A file IO related error. + #[cfg(feature = "std")] + IoError(std::io::Error), +} + +#[cfg(feature = "std")] +impl From for LoadError { + #[inline] + fn from(e: std::io::Error) -> Self { + LoadError::IoError(e) + } +} + +impl core::fmt::Display for LoadError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + LoadError::MalformedFont => write!(f, "malformed font"), + LoadError::UnnamedFont => write!(f, "font doesn't have a family name"), + #[cfg(feature = "std")] + LoadError::IoError(e) => write!(f, "{}", e), + } + } +} + +/// A font database. +#[derive(Clone, Debug)] +pub struct Database { + faces: SlotMap, + family_serif: String, + family_sans_serif: String, + family_cursive: String, + family_fantasy: String, + family_monospace: String, +} + +impl Default for Database { + fn default() -> Self { + Self::new() + } +} + +impl Database { + /// Create a new, empty `Database`. + /// + /// Generic font families would be set to: + /// + /// - `serif` - Times New Roman + /// - `sans-serif` - Arial + /// - `cursive` - Comic Sans MS + /// - `fantasy` - Impact (Papyrus on macOS) + /// - `monospace` - Courier New + #[inline] + pub fn new() -> Self { + Database { + faces: SlotMap::with_key(), + family_serif: "Times New Roman".to_string(), + family_sans_serif: "Arial".to_string(), + family_cursive: "Comic Sans MS".to_string(), + #[cfg(not(target_os = "macos"))] + family_fantasy: "Impact".to_string(), + #[cfg(target_os = "macos")] + family_fantasy: "Papyrus".to_string(), + family_monospace: "Courier New".to_string(), + } + } + + /// Loads a font data into the `Database`. + /// + /// Will load all font faces in case of a font collection. + pub fn load_font_data(&mut self, data: Vec) { + self.load_font_source(Source::Binary(std::sync::Arc::new(data))); + } + + /// Loads a font from the given source into the `Database` and returns + /// the ID of the loaded font. + /// + /// Will load all font faces in case of a font collection. + pub fn load_font_source(&mut self, source: Source) -> TinyVec<[ID; 8]> { + let ids = source.with_data(|data| { + let n = fonts_in_collection(data); + let mut ids = TinyVec::with_capacity(n as usize); + + for index in 0..n { + match parse_face_info(source.clone(), data, index) { + Ok(mut info) => { + let id = self.faces.insert_with_key(|k| { + info.id = ID(k); + info + }); + ids.push(ID(id)); + } + Err(e) => log::warn!( + "Failed to load a font face {} from source cause {}.", + index, + e + ), + } + } + + ids + }); + + ids.unwrap_or_default() + } + + /// Backend function used by load_font_file to load font files. + #[cfg(feature = "fs")] + fn load_fonts_from_file(&mut self, path: &std::path::Path, data: &[u8]) { + let source = Source::File(path.into()); + + let n = fonts_in_collection(data); + for index in 0..n { + match parse_face_info(source.clone(), data, index) { + Ok(info) => { + self.push_face_info(info); + } + Err(e) => { + log::warn!( + "Failed to load a font face {} from '{}' cause {}.", + index, + path.display(), + e + ) + } + } + } + } + + /// Loads a font file into the `Database`. + /// + /// Will load all font faces in case of a font collection. + #[cfg(all(feature = "fs", feature = "memmap"))] + pub fn load_font_file>( + &mut self, + path: P, + ) -> Result<(), std::io::Error> { + self.load_font_file_impl(path.as_ref()) + } + + // A non-generic version. + #[cfg(all(feature = "fs", feature = "memmap"))] + fn load_font_file_impl(&mut self, path: &std::path::Path) -> Result<(), std::io::Error> { + let file = std::fs::File::open(path)?; + let data: &[u8] = unsafe { &memmap2::MmapOptions::new().map(&file)? }; + + self.load_fonts_from_file(path, data); + Ok(()) + } + + /// Loads a font file into the `Database`. + /// + /// Will load all font faces in case of a font collection. + #[cfg(all(feature = "fs", not(feature = "memmap")))] + pub fn load_font_file>( + &mut self, + path: P, + ) -> Result<(), std::io::Error> { + self.load_font_file_impl(path.as_ref()) + } + + // A non-generic version. + #[cfg(all(feature = "fs", not(feature = "memmap")))] + fn load_font_file_impl(&mut self, path: &std::path::Path) -> Result<(), std::io::Error> { + let data = std::fs::read(path)?; + + self.load_fonts_from_file(path, &data); + Ok(()) + } + + /// Loads font files from the selected directory into the `Database`. + /// + /// This method will scan directories recursively. + /// + /// Will load `ttf`, `otf`, `ttc` and `otc` fonts. + /// + /// Unlike other `load_*` methods, this one doesn't return an error. + /// It will simply skip malformed fonts and will print a warning into the log for each of them. + #[cfg(feature = "fs")] + pub fn load_fonts_dir>(&mut self, dir: P) { + self.load_fonts_dir_impl(dir.as_ref(), &mut Default::default()) + } + + #[cfg(feature = "fs")] + fn canonicalize( + &self, + path: std::path::PathBuf, + entry: std::fs::DirEntry, + seen: &mut std::collections::HashSet, + ) -> Option<(std::path::PathBuf, std::fs::FileType)> { + let file_type = entry.file_type().ok()?; + if !file_type.is_symlink() { + if !seen.is_empty() { + if seen.contains(&path) { + return None; + } + seen.insert(path.clone()); + } + + return Some((path, file_type)); + } + + if seen.is_empty() && file_type.is_dir() { + seen.reserve(8192 / std::mem::size_of::()); + + for (_, info) in self.faces.iter() { + let path = match &info.source { + Source::Binary(_) => continue, + Source::File(path) => path.to_path_buf(), + #[cfg(feature = "memmap")] + Source::SharedFile(path, _) => path.to_path_buf(), + }; + seen.insert(path); + } + } + + let stat = std::fs::metadata(&path).ok()?; + if stat.is_symlink() { + return None; + } + + let canon = std::fs::canonicalize(path).ok()?; + if seen.contains(&canon) { + return None; + } + seen.insert(canon.clone()); + Some((canon, stat.file_type())) + } + + // A non-generic version. + #[cfg(feature = "fs")] + fn load_fonts_dir_impl( + &mut self, + dir: &std::path::Path, + seen: &mut std::collections::HashSet, + ) { + let fonts_dir = match std::fs::read_dir(dir) { + Ok(dir) => dir, + Err(_) => return, + }; + + for entry in fonts_dir.flatten() { + let (path, file_type) = match self.canonicalize(entry.path(), entry, seen) { + Some(v) => v, + None => continue, + }; + + if file_type.is_file() { + match path.extension().and_then(|e| e.to_str()) { + #[rustfmt::skip] // keep extensions match as is + Some("ttf") | Some("ttc") | Some("TTF") | Some("TTC") | + Some("otf") | Some("otc") | Some("OTF") | Some("OTC") => { + if let Err(e) = self.load_font_file(&path) { + log::warn!("Failed to load '{}' cause {}.", path.display(), e); + } + }, + _ => {} + } + } else if file_type.is_dir() { + self.load_fonts_dir_impl(&path, seen); + } + } + } + + /// Attempts to load system fonts. + /// + /// Supports Windows, Linux and macOS. + /// + /// System fonts loading is a surprisingly complicated task, + /// mostly unsolvable without interacting with system libraries. + /// And since `fontdb` tries to be small and portable, this method + /// will simply scan some predefined directories. + /// Which means that fonts that are not in those directories must + /// be added manually. + #[cfg(feature = "fs")] + pub fn load_system_fonts(&mut self) { + #[cfg(target_os = "windows")] + { + let mut seen = Default::default(); + if let Some(ref system_root) = std::env::var_os("SYSTEMROOT") { + let system_root_path = std::path::Path::new(system_root); + self.load_fonts_dir_impl(&system_root_path.join("Fonts"), &mut seen); + } else { + self.load_fonts_dir_impl("C:\\Windows\\Fonts\\".as_ref(), &mut seen); + } + + if let Ok(ref home) = std::env::var("USERPROFILE") { + let home_path = std::path::Path::new(home); + self.load_fonts_dir_impl( + &home_path.join("AppData\\Local\\Microsoft\\Windows\\Fonts"), + &mut seen, + ); + self.load_fonts_dir_impl( + &home_path.join("AppData\\Roaming\\Microsoft\\Windows\\Fonts"), + &mut seen, + ); + } + } + + #[cfg(target_os = "macos")] + { + let mut seen = Default::default(); + self.load_fonts_dir_impl("/Library/Fonts".as_ref(), &mut seen); + self.load_fonts_dir_impl("/System/Library/Fonts".as_ref(), &mut seen); + // Downloadable fonts, location varies on major macOS releases + if let Ok(dir) = std::fs::read_dir("/System/Library/AssetsV2") { + for entry in dir { + let entry = match entry { + Ok(entry) => entry, + Err(_) => continue, + }; + if entry + .file_name() + .to_string_lossy() + .starts_with("com_apple_MobileAsset_Font") + { + self.load_fonts_dir_impl(&entry.path(), &mut seen); + } + } + } + self.load_fonts_dir_impl("/Network/Library/Fonts".as_ref(), &mut seen); + + if let Ok(ref home) = std::env::var("HOME") { + let home_path = std::path::Path::new(home); + self.load_fonts_dir_impl(&home_path.join("Library/Fonts"), &mut seen); + } + } + + // Redox OS. + #[cfg(target_os = "redox")] + { + let mut seen = Default::default(); + self.load_fonts_dir_impl("/ui/fonts".as_ref(), &mut seen); + } + + // Linux. + #[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))] + { + #[cfg(feature = "fontconfig")] + { + if !self.load_fontconfig() { + log::warn!("Fallback to loading from known font dir paths."); + self.load_no_fontconfig(); + } + } + + #[cfg(not(feature = "fontconfig"))] + { + self.load_no_fontconfig(); + } + } + } + + // Linux. + #[cfg(all( + unix, + feature = "fs", + not(any(target_os = "macos", target_os = "android")) + ))] + fn load_no_fontconfig(&mut self) { + let mut seen = Default::default(); + self.load_fonts_dir_impl("/usr/share/fonts/".as_ref(), &mut seen); + self.load_fonts_dir_impl("/usr/local/share/fonts/".as_ref(), &mut seen); + + if let Ok(ref home) = std::env::var("HOME") { + let home_path = std::path::Path::new(home); + self.load_fonts_dir_impl(&home_path.join(".fonts"), &mut seen); + self.load_fonts_dir_impl(&home_path.join(".local/share/fonts"), &mut seen); + } + } + + // Linux. + #[cfg(all( + unix, + feature = "fontconfig", + not(any(target_os = "macos", target_os = "android")) + ))] + fn load_fontconfig(&mut self) -> bool { + use std::path::Path; + + let mut fontconfig = fontconfig_parser::FontConfig::default(); + let home = std::env::var("HOME"); + + if let Ok(ref config_file) = std::env::var("FONTCONFIG_FILE") { + let _ = fontconfig.merge_config(Path::new(config_file)); + } else { + let xdg_config_home = if let Ok(val) = std::env::var("XDG_CONFIG_HOME") { + Some(val.into()) + } else if let Ok(ref home) = home { + // according to https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + // $XDG_CONFIG_HOME should default to $HOME/.config if not set + Some(Path::new(home).join(".config")) + } else { + None + }; + + let read_global = match xdg_config_home { + Some(p) => fontconfig + .merge_config(&p.join("fontconfig/fonts.conf")) + .is_err(), + None => true, + }; + + if read_global { + let _ = fontconfig.merge_config(Path::new("/etc/fonts/local.conf")); + } + let _ = fontconfig.merge_config(Path::new("/etc/fonts/fonts.conf")); + } + + for fontconfig_parser::Alias { + alias, + default, + prefer, + accept, + } in fontconfig.aliases + { + let name = prefer + .first() + .or_else(|| accept.first()) + .or_else(|| default.first()); + + if let Some(name) = name { + match alias.to_lowercase().as_str() { + "serif" => self.set_serif_family(name), + "sans-serif" => self.set_sans_serif_family(name), + "sans serif" => self.set_sans_serif_family(name), + "monospace" => self.set_monospace_family(name), + "cursive" => self.set_cursive_family(name), + "fantasy" => self.set_fantasy_family(name), + _ => {} + } + } + } + + if fontconfig.dirs.is_empty() { + return false; + } + + let mut seen = Default::default(); + for dir in fontconfig.dirs { + let path = if dir.path.starts_with("~") { + if let Ok(ref home) = home { + Path::new(home).join(dir.path.strip_prefix("~").unwrap()) + } else { + continue; + } + } else { + dir.path + }; + self.load_fonts_dir_impl(&path, &mut seen); + } + + true + } + + /// Pushes a user-provided `FaceInfo` to the database. + /// + /// In some cases, a caller might want to ignore the font's metadata and provide their own. + /// This method doesn't parse the `source` font. + /// + /// The `id` field should be set to [`ID::dummy()`] and will be then overwritten by this method. + pub fn push_face_info(&mut self, mut info: FaceInfo) -> ID { + ID(self.faces.insert_with_key(|k| { + info.id = ID(k); + info + })) + } + + /// Removes a font face by `id` from the database. + /// + /// Returns `false` while attempting to remove a non-existing font face. + /// + /// Useful when you want to ignore some specific font face(s) + /// after loading a large directory with fonts. + /// Or a specific face from a font. + pub fn remove_face(&mut self, id: ID) { + self.faces.remove(id.0); + } + + /// Returns `true` if the `Database` contains no font faces. + #[inline] + pub fn is_empty(&self) -> bool { + self.faces.is_empty() + } + + /// Returns the number of font faces in the `Database`. + /// + /// Note that `Database` stores font faces, not fonts. + /// For example, if a caller will try to load a font collection (`*.ttc`) that contains 5 faces, + /// then the `Database` will load 5 font faces and this method will return 5, not 1. + #[inline] + pub fn len(&self) -> usize { + self.faces.len() + } + + /// Sets the family that will be used by `Family::Serif`. + pub fn set_serif_family>(&mut self, family: S) { + self.family_serif = family.into(); + } + + /// Sets the family that will be used by `Family::SansSerif`. + pub fn set_sans_serif_family>(&mut self, family: S) { + self.family_sans_serif = family.into(); + } + + /// Sets the family that will be used by `Family::Cursive`. + pub fn set_cursive_family>(&mut self, family: S) { + self.family_cursive = family.into(); + } + + /// Sets the family that will be used by `Family::Fantasy`. + pub fn set_fantasy_family>(&mut self, family: S) { + self.family_fantasy = family.into(); + } + + /// Sets the family that will be used by `Family::Monospace`. + pub fn set_monospace_family>(&mut self, family: S) { + self.family_monospace = family.into(); + } + + /// Returns the generic family name or the `Family::Name` itself. + /// + /// Generic family names should be set via `Database::set_*_family` methods. + pub fn family_name<'a>(&'a self, family: &'a Family) -> &'a str { + match family { + Family::Name(name) => name, + Family::Serif => self.family_serif.as_str(), + Family::SansSerif => self.family_sans_serif.as_str(), + Family::Cursive => self.family_cursive.as_str(), + Family::Fantasy => self.family_fantasy.as_str(), + Family::Monospace => self.family_monospace.as_str(), + } + } + + /// Performs a CSS-like query and returns the best matched font face. + pub fn query(&self, query: &Query) -> Option { + for family in query.families { + let name = self.family_name(family); + let candidates: Vec<_> = self + .faces + .iter() + .filter(|(_, face)| face.families.iter().any(|family| family.0 == name)) + .map(|(_, info)| info) + .collect(); + + if !candidates.is_empty() { + if let Some(index) = find_best_match(&candidates, query) { + return Some(candidates[index].id); + } + } + } + + None + } + + /// Returns an iterator over the internal storage. + /// + /// This can be used for manual font matching. + #[inline] + pub fn faces(&self) -> impl Iterator + '_ { + self.faces.iter().map(|(_, info)| info) + } + + /// Selects a `FaceInfo` by `id`. + /// + /// Returns `None` if a face with such ID was already removed, + /// or this ID belong to the other `Database`. + pub fn face(&self, id: ID) -> Option<&FaceInfo> { + self.faces.get(id.0) + } + + /// Returns font face storage and the face index by `ID`. + pub fn face_source(&self, id: ID) -> Option<(Source, u32)> { + self.face(id).map(|info| (info.source.clone(), info.index)) + } + + /// Executes a closure with a font's data. + /// + /// We can't return a reference to a font binary data because of lifetimes. + /// So instead, you can use this method to process font's data. + /// + /// The closure accepts raw font data and font face index. + /// + /// In case of `Source::File`, the font file will be memory mapped. + /// + /// Returns `None` when font file loading failed. + /// + /// # Example + /// + /// ```ignore + /// let is_variable = db.with_face_data(id, |font_data, face_index| { + /// let font = ttf_parser::Face::from_slice(font_data, face_index).unwrap(); + /// font.is_variable() + /// })?; + /// ``` + pub fn with_face_data(&self, id: ID, p: P) -> Option + where + P: FnOnce(&[u8], u32) -> T, + { + let (src, face_index) = self.face_source(id)?; + src.with_data(|data| p(data, face_index)) + } + + /// Makes the font data that backs the specified face id shared so that the application can + /// hold a reference to it. + /// + /// # Safety + /// + /// If the face originates from a file from disk, then the file is mapped from disk. This is unsafe as + /// another process may make changes to the file on disk, which may become visible in this process' + /// mapping and possibly cause crashes. + /// + /// If the underlying font provides multiple faces, then all faces are updated to participate in + /// the data sharing. If the face was previously marked for data sharing, then this function will + /// return a clone of the existing reference. + #[cfg(all(feature = "fs", feature = "memmap"))] + pub unsafe fn make_shared_face_data( + &mut self, + id: ID, + ) -> Option<(std::sync::Arc + Send + Sync>, u32)> { + let face_info = self.faces.get(id.0)?; + let face_index = face_info.index; + + let old_source = face_info.source.clone(); + + let (path, shared_data) = match &old_source { + Source::Binary(data) => { + return Some((data.clone(), face_index)); + } + Source::File(path) => { + let file = std::fs::File::open(path).ok()?; + // SAFETY: The mmap is kept alive through Arc sharing. While another process + // could modify the underlying file (which is why this function is marked unsafe), + // the mapping remains valid for the lifetime of the Arc. + let shared_data = + std::sync::Arc::new(unsafe { memmap2::MmapOptions::new().map(&file).ok()? }) + as std::sync::Arc + Send + Sync>; + (path.clone(), shared_data) + } + Source::SharedFile(_, data) => { + return Some((data.clone(), face_index)); + } + }; + + let shared_source = Source::SharedFile(path.clone(), shared_data.clone()); + + self.faces.iter_mut().for_each(|(_, face)| { + if matches!(&face.source, Source::File(old_path) if old_path == &path) { + face.source = shared_source.clone(); + } + }); + + Some((shared_data, face_index)) + } + + /// Transfers ownership of shared font data back to the font database. This is the reverse operation + /// of [`Self::make_shared_face_data`]. If the font data belonging to the specified face is mapped + /// from a file on disk, then that mapping is closed and the data becomes private to the process again. + #[cfg(all(feature = "fs", feature = "memmap"))] + pub fn make_face_data_unshared(&mut self, id: ID) { + let face_info = match self.faces.get(id.0) { + Some(face_info) => face_info, + None => return, + }; + + let old_source = face_info.source.clone(); + + let shared_path = match old_source { + #[cfg(all(feature = "fs", feature = "memmap"))] + Source::SharedFile(path, _) => path, + _ => return, + }; + + let new_source = Source::File(shared_path.clone()); + + self.faces.iter_mut().for_each(|(_, face)| { + if matches!(&face.source, Source::SharedFile(path, ..) if path == &shared_path) { + face.source = new_source.clone(); + } + }); + } +} + +/// A single font face info. +/// +/// A font can have multiple faces. +/// +/// A single item of the `Database`. +#[derive(Clone, Debug)] +pub struct FaceInfo { + /// An unique ID. + pub id: ID, + + /// A font source. + /// + /// Note that multiple `FaceInfo` objects can reference the same data in case of + /// font collections, which means that they'll use the same Source. + pub source: Source, + + /// A face index in the `source`. + pub index: u32, + + /// A list of family names. + /// + /// Contains pairs of Name + Language. Where the first family is always English US, + /// unless it's missing from the font. + /// + /// Corresponds to a *Typographic Family* (ID 16) or a *Font Family* (ID 1) [name ID] + /// in a TrueType font. + /// + /// This is not an *Extended Typographic Family* or a *Full Name*. + /// Meaning it will contain _Arial_ and not _Arial Bold_. + /// + /// [name ID]: https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids + pub families: Vec<(String, Language)>, + + /// A PostScript name. + /// + /// Corresponds to a *PostScript name* (6) [name ID] in a TrueType font. + /// + /// [name ID]: https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids + pub post_script_name: String, + + /// A font face style. + pub style: Style, + + /// A font face weight. + pub weight: Weight, + + /// A font face stretch. + pub stretch: Stretch, + + /// Indicates that the font face is monospaced. + pub monospaced: bool, +} + +/// A font source. +/// +/// Either a raw binary data or a file path. +/// +/// Stores the whole font and not just a single face. +#[derive(Clone)] +pub enum Source { + /// A font's raw data, typically backed by a Vec. + Binary(std::sync::Arc + Sync + Send>), + + /// A font's path. + #[cfg(feature = "fs")] + File(std::path::PathBuf), + + /// A font's raw data originating from a shared file mapping. + #[cfg(all(feature = "fs", feature = "memmap"))] + SharedFile( + std::path::PathBuf, + std::sync::Arc + Sync + Send>, + ), +} + +impl core::fmt::Debug for Source { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Binary(arg0) => f + .debug_tuple("SharedBinary") + .field(&arg0.as_ref().as_ref()) + .finish(), + #[cfg(feature = "fs")] + Self::File(arg0) => f.debug_tuple("File").field(arg0).finish(), + #[cfg(all(feature = "fs", feature = "memmap"))] + Self::SharedFile(arg0, arg1) => f + .debug_tuple("SharedFile") + .field(arg0) + .field(&arg1.as_ref().as_ref()) + .finish(), + } + } +} + +impl Source { + fn with_data(&self, p: P) -> Option + where + P: FnOnce(&[u8]) -> T, + { + match &self { + #[cfg(all(feature = "fs", not(feature = "memmap")))] + Source::File(path) => { + let data = std::fs::read(path).ok()?; + + Some(p(&data)) + } + #[cfg(all(feature = "fs", feature = "memmap"))] + Source::File(path) => { + let file = std::fs::File::open(path).ok()?; + // SAFETY: Memory mapping is valid for the duration of this function call + let data = unsafe { memmap2::MmapOptions::new().map(&file).ok()? }; + + Some(p(&data)) + } + Source::Binary(data) => Some(p(data.as_ref().as_ref())), + #[cfg(all(feature = "fs", feature = "memmap"))] + Source::SharedFile(_, data) => Some(p(data.as_ref().as_ref())), + } + } +} + +/// A database query. +/// +/// Mainly used by `Database::query()`. +#[derive(Clone, Copy, Default, Debug, Eq, PartialEq, Hash)] +pub struct Query<'a> { + /// A prioritized list of font family names or generic family names. + /// + /// [font-family](https://www.w3.org/TR/2018/REC-css-fonts-3-20180920/#propdef-font-family) in CSS. + pub families: &'a [Family<'a>], + + /// Specifies the weight of glyphs in the font, their degree of blackness or stroke thickness. + /// + /// [font-weight](https://www.w3.org/TR/2018/REC-css-fonts-3-20180920/#font-weight-prop) in CSS. + pub weight: Weight, + + /// Selects a normal, condensed, or expanded face from a font family. + /// + /// [font-stretch](https://www.w3.org/TR/2018/REC-css-fonts-3-20180920/#font-stretch-prop) in CSS. + pub stretch: Stretch, + + /// Allows italic or oblique faces to be selected. + /// + /// [font-style](https://www.w3.org/TR/2018/REC-css-fonts-3-20180920/#font-style-prop) in CSS. + pub style: Style, +} + +// Enum value descriptions are from the CSS spec. +/// A [font family](https://www.w3.org/TR/2018/REC-css-fonts-3-20180920/#propdef-font-family). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Family<'a> { + /// The name of a font family of choice. + /// + /// This must be a *Typographic Family* (ID 16) or a *Family Name* (ID 1) in terms of TrueType. + /// Meaning you have to pass a family without any additional suffixes like _Bold_, _Italic_, + /// _Regular_, etc. + /// + /// Localized names are allowed. + Name(&'a str), + + /// Serif fonts represent the formal text style for a script. + Serif, + + /// Glyphs in sans-serif fonts, as the term is used in CSS, are generally low contrast + /// and have stroke endings that are plain — without any flaring, cross stroke, + /// or other ornamentation. + SansSerif, + + /// Glyphs in cursive fonts generally use a more informal script style, + /// and the result looks more like handwritten pen or brush writing than printed letterwork. + Cursive, + + /// Fantasy fonts are primarily decorative or expressive fonts that + /// contain decorative or expressive representations of characters. + Fantasy, + + /// The sole criterion of a monospace font is that all glyphs have the same fixed width. + Monospace, +} + +/// Specifies the weight of glyphs in the font, their degree of blackness or stroke thickness. +#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Debug, Hash)] +pub struct Weight(pub u16); + +impl Default for Weight { + #[inline] + fn default() -> Weight { + Weight::NORMAL + } +} + +impl Weight { + /// Thin weight (100), the thinnest value. + pub const THIN: Weight = Weight(100); + /// Extra light weight (200). + pub const EXTRA_LIGHT: Weight = Weight(200); + /// Light weight (300). + pub const LIGHT: Weight = Weight(300); + /// Normal (400). + pub const NORMAL: Weight = Weight(400); + /// Medium weight (500, higher than normal). + pub const MEDIUM: Weight = Weight(500); + /// Semibold weight (600). + pub const SEMIBOLD: Weight = Weight(600); + /// Bold weight (700). + pub const BOLD: Weight = Weight(700); + /// Extra-bold weight (800). + pub const EXTRA_BOLD: Weight = Weight(800); + /// Black weight (900), the thickest value. + pub const BLACK: Weight = Weight(900); +} + +/// Allows italic or oblique faces to be selected. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] +pub enum Style { + /// A face that is neither italic not obliqued. + Normal, + /// A form that is generally cursive in nature. + Italic, + /// A typically-sloped version of the regular face. + Oblique, +} + +impl Default for Style { + #[inline] + fn default() -> Style { + Style::Normal + } +} + +fn parse_face_info(source: Source, data: &[u8], index: u32) -> Result { + let font = FontRef::from_index(data, index).map_err(|_| LoadError::MalformedFont)?; + let (families, post_script_name) = parse_names(&font).ok_or(LoadError::UnnamedFont)?; + let (mut style, weight, stretch) = parse_os2(&font); + let (monospaced, italic) = parse_post(&font); + + if style == Style::Normal && italic { + style = Style::Italic; + } + + Ok(FaceInfo { + id: ID::dummy(), + source, + index, + families, + post_script_name, + style, + weight, + stretch, + monospaced, + }) +} + +fn parse_names(font: &FontRef) -> Option<(Vec<(String, Language)>, String)> { + let mut families = Vec::new(); + + // Try Typographic Family (ID 16) first + for s in font.localized_strings(StringId::TYPOGRAPHIC_FAMILY_NAME) { + let lang = language_from_bcp47(s.language()); + let name: String = s.chars().collect(); + if !name.is_empty() { + families.push((name, lang)); + } + } + + // Fallback to Family Name (ID 1) + if families.is_empty() { + for s in font.localized_strings(StringId::FAMILY_NAME) { + let lang = language_from_bcp47(s.language()); + let name: String = s.chars().collect(); + if !name.is_empty() { + families.push((name, lang)); + } + } + } + + // Make English US the first one + if families.len() > 1 { + if let Some(index) = families + .iter() + .position(|f| f.1 == Language::EnglishUnitedStates) + { + if index != 0 { + families.swap(0, index); + } + } + } + + if families.is_empty() { + return None; + } + + // Get PostScript name + let post_script_name = font + .localized_strings(StringId::POSTSCRIPT_NAME) + .next() + .map(|s| s.chars().collect::()) + .unwrap_or_default(); + + Some((families, post_script_name)) +} + +fn parse_os2(font: &FontRef) -> (Style, Weight, Stretch) { + let attrs = font.attributes(); + + let style = match attrs.style { + skrifa::attribute::Style::Normal => Style::Normal, + skrifa::attribute::Style::Italic => Style::Italic, + skrifa::attribute::Style::Oblique(_) => Style::Oblique, + }; + + let weight = Weight(attrs.weight.value() as u16); + let stretch = stretch_from_skrifa(attrs.stretch); + + (style, weight, stretch) +} + +fn parse_post(font: &FontRef) -> (bool, bool) { + // Check if monospaced using skrifa's metrics + let monospaced = font + .metrics( + skrifa::instance::Size::unscaled(), + skrifa::instance::LocationRef::default(), + ) + .is_monospace; + + // Check italic angle from post table + let italic = font + .post() + .map(|post| post.italic_angle().to_f64() != 0.0) + .unwrap_or(false); + + (monospaced, italic) +} + +// https://www.w3.org/TR/2018/REC-css-fonts-3-20180920/#font-style-matching +// Based on https://github.com/servo/font-kit +#[inline(never)] +fn find_best_match(candidates: &[&FaceInfo], query: &Query) -> Option { + debug_assert!(!candidates.is_empty()); + + // Step 4. + let mut matching_set: Vec = (0..candidates.len()).collect(); + + // Step 4a (`font-stretch`). + let matches = matching_set + .iter() + .any(|&index| candidates[index].stretch == query.stretch); + let matching_stretch = if matches { + // Exact match. + query.stretch + } else if query.stretch <= Stretch::Normal { + // Closest stretch, first checking narrower values and then wider values. + let stretch = matching_set + .iter() + .filter(|&&index| candidates[index].stretch < query.stretch) + .min_by_key(|&&index| { + query.stretch.to_number() - candidates[index].stretch.to_number() + }); + + match stretch { + Some(&matching_index) => candidates[matching_index].stretch, + None => { + let matching_index = *matching_set.iter().min_by_key(|&&index| { + candidates[index].stretch.to_number() - query.stretch.to_number() + })?; + + candidates[matching_index].stretch + } + } + } else { + // Closest stretch, first checking wider values and then narrower values. + let stretch = matching_set + .iter() + .filter(|&&index| candidates[index].stretch > query.stretch) + .min_by_key(|&&index| { + candidates[index].stretch.to_number() - query.stretch.to_number() + }); + + match stretch { + Some(&matching_index) => candidates[matching_index].stretch, + None => { + let matching_index = *matching_set.iter().min_by_key(|&&index| { + query.stretch.to_number() - candidates[index].stretch.to_number() + })?; + + candidates[matching_index].stretch + } + } + }; + matching_set.retain(|&index| candidates[index].stretch == matching_stretch); + + // Step 4b (`font-style`). + let style_preference = match query.style { + Style::Italic => [Style::Italic, Style::Oblique, Style::Normal], + Style::Oblique => [Style::Oblique, Style::Italic, Style::Normal], + Style::Normal => [Style::Normal, Style::Oblique, Style::Italic], + }; + let matching_style = *style_preference.iter().find(|&query_style| { + matching_set + .iter() + .any(|&index| candidates[index].style == *query_style) + })?; + + matching_set.retain(|&index| candidates[index].style == matching_style); + + // Step 4c (`font-weight`). + // + // The spec doesn't say what to do if the weight is between 400 and 500 exclusive, so we + // just use 450 as the cutoff. + let weight = query.weight.0; + + let matching_weight = if matching_set + .iter() + .any(|&index| candidates[index].weight.0 == weight) + { + Weight(weight) + } else if (400..450).contains(&weight) + && matching_set + .iter() + .any(|&index| candidates[index].weight.0 == 500) + { + // Check 500 first. + Weight::MEDIUM + } else if (450..=500).contains(&weight) + && matching_set + .iter() + .any(|&index| candidates[index].weight.0 == 400) + { + // Check 400 first. + Weight::NORMAL + } else if weight <= 500 { + // Closest weight, first checking thinner values and then fatter ones. + let idx = matching_set + .iter() + .filter(|&&index| candidates[index].weight.0 <= weight) + .min_by_key(|&&index| weight - candidates[index].weight.0); + + match idx { + Some(&matching_index) => candidates[matching_index].weight, + None => { + let matching_index = *matching_set + .iter() + .min_by_key(|&&index| candidates[index].weight.0 - weight)?; + candidates[matching_index].weight + } + } + } else { + // Closest weight, first checking fatter values and then thinner ones. + let idx = matching_set + .iter() + .filter(|&&index| candidates[index].weight.0 >= weight) + .min_by_key(|&&index| candidates[index].weight.0 - weight); + + match idx { + Some(&matching_index) => candidates[matching_index].weight, + None => { + let matching_index = *matching_set + .iter() + .min_by_key(|&&index| weight - candidates[index].weight.0)?; + candidates[matching_index].weight + } + } + }; + matching_set.retain(|&index| candidates[index].weight == matching_weight); + + // Ignore step 4d (`font-size`). + + // Return the result. + matching_set.into_iter().next() +} diff --git a/crates/fontdb/tests/add_fonts.rs b/crates/fontdb/tests/add_fonts.rs new file mode 100644 index 000000000..0373f856a --- /dev/null +++ b/crates/fontdb/tests/add_fonts.rs @@ -0,0 +1,18 @@ +// Copyright 2020 Yevhenii Reizner (original fontdb, MIT licensed) +// Copyright 2026 the Resvg Authors (modifications) +// SPDX-License-Identifier: Apache-2.0 OR MIT + +const DEMO_TTF: &[u8] = include_bytes!("./fonts/Tuffy.ttf"); +use std::sync::Arc; + +#[test] +fn add_fonts_and_get_ids_back() { + let mut font_db = fontdb::Database::new(); + let ids = font_db.load_font_source(fontdb::Source::Binary(Arc::new(DEMO_TTF))); + + assert_eq!(ids.len(), 1); + let id = ids[0]; + + let font = font_db.face(id).unwrap(); + assert!(font.families.iter().any(|(name, _)| name == "Tuffy")); +} diff --git a/crates/fontdb/tests/fonts/LICENSE.txt b/crates/fontdb/tests/fonts/LICENSE.txt new file mode 100644 index 000000000..defced0c0 --- /dev/null +++ b/crates/fontdb/tests/fonts/LICENSE.txt @@ -0,0 +1,11 @@ +We, the copyright holders of this work, hereby release it into the +public domain. This applies worldwide. + +In case this is not legally possible, + +We grant any entity the right to use this work for any purpose, without +any conditions, unless such conditions are required by law. + +Thatcher Ulrich http://tulrich.com +Karoly Barta bartakarcsi@gmail.com +Michael Evans http://www.evertype.com diff --git a/crates/fontdb/tests/fonts/Tuffy.ttf b/crates/fontdb/tests/fonts/Tuffy.ttf new file mode 100644 index 000000000..ade553a60 Binary files /dev/null and b/crates/fontdb/tests/fonts/Tuffy.ttf differ diff --git a/crates/resvg/Cargo.toml b/crates/resvg/Cargo.toml index 36d149f87..9defadf5a 100644 --- a/crates/resvg/Cargo.toml +++ b/crates/resvg/Cargo.toml @@ -14,11 +14,16 @@ workspace = "../.." name = "resvg" required-features = ["text", "system-fonts", "memmap-fonts"] +[[example]] +name = "generate_hinting_comparison" +required-features = ["text"] + [dependencies] gif = { version = "0.14.1", optional = true } image-webp = { version = "0.2.4", optional = true } log = "0.4" pico-args = { version = "0.5", features = ["eq-separator"] } +png = "0.18" rgb = "0.8" svgtypes = "0.16.1" tiny-skia = "0.11.4" @@ -27,7 +32,6 @@ zune-jpeg = { version = "0.5.8", optional = true } [dev-dependencies] once_cell = "1.21" -png = "0.18.0" [features] default = ["text", "system-fonts", "memmap-fonts", "raster-images"] diff --git a/crates/resvg/examples/generate_hinting_comparison.rs b/crates/resvg/examples/generate_hinting_comparison.rs new file mode 100644 index 000000000..191bb4233 --- /dev/null +++ b/crates/resvg/examples/generate_hinting_comparison.rs @@ -0,0 +1,90 @@ +// Copyright 2026 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Generates comparison images showing the effect of font hinting. +//! +//! This example renders the same SVG text twice: once with hinting enabled +//! and once without. The resulting images can be compared to see how hinting +//! affects text rendering, especially at small font sizes. +//! +//! Hinting improves text clarity by aligning glyph outlines to the pixel grid, +//! which is most noticeable at sizes below 20px. +//! +//! Run with: `cargo run --example generate_hinting_comparison --features text` + +use std::sync::Arc; +use usvg::fontdb; + +fn main() { + // Load fonts + let mut fontdb = fontdb::Database::new(); + fontdb.load_fonts_dir("crates/resvg/tests/fonts"); + fontdb.set_sans_serif_family("Noto Sans"); + let fontdb = Arc::new(fontdb); + + // SVG with small text where hinting is most visible + let svg = br#" + + + The quick brown fox jumps over the lazy dog. (12px) + + + The quick brown fox jumps over the lazy dog. (14px) + + + The quick brown fox jumps over the lazy dog. (16px) + + + The quick brown fox jumps over. (20px) + + + The quick brown fox. (24px) + + + "#; + + // Render with hinting + let opt_hinted = usvg::Options { + fontdb: fontdb.clone(), + hinting: usvg::HintingOptions { + enabled: true, + dpi: Some(96.0), + }, + ..usvg::Options::default() + }; + + let tree = usvg::Tree::from_data(svg, &opt_hinted).unwrap(); + let size = tree.size().to_int_size(); + let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height()).unwrap(); + pixmap.fill(tiny_skia::Color::WHITE); + resvg::render( + &tree, + tiny_skia::Transform::identity(), + &mut pixmap.as_mut(), + ); + pixmap.save_png("hinted.png").unwrap(); + println!("Saved hinted.png"); + + // Render without hinting + let opt_unhinted = usvg::Options { + fontdb: fontdb.clone(), + hinting: usvg::HintingOptions { + enabled: false, + dpi: Some(96.0), + }, + ..usvg::Options::default() + }; + + let tree = usvg::Tree::from_data(svg, &opt_unhinted).unwrap(); + let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height()).unwrap(); + pixmap.fill(tiny_skia::Color::WHITE); + resvg::render( + &tree, + tiny_skia::Transform::identity(), + &mut pixmap.as_mut(), + ); + pixmap.save_png("unhinted.png").unwrap(); + println!("Saved unhinted.png"); + + println!("Done! Compare hinted.png and unhinted.png"); +} diff --git a/crates/resvg/src/lib.rs b/crates/resvg/src/lib.rs index 45a4d9d6c..6ed57159a 100644 --- a/crates/resvg/src/lib.rs +++ b/crates/resvg/src/lib.rs @@ -3,6 +3,17 @@ /*! [resvg](https://github.com/linebender/resvg) is an SVG rendering library. + +## Main functions + +- [`render`] - Renders an SVG tree onto a pixmap +- [`render_node`] - Renders a single node onto a pixmap +- [`encode_png_with_dpi`] - Encodes a pixmap as PNG with DPI metadata +- [`save_png_with_dpi`] - Saves a pixmap as PNG with DPI metadata + +## Re-exports + +This crate re-exports [`tiny_skia`] for pixmap handling and [`usvg`] for SVG parsing. */ #![forbid(unsafe_code)] @@ -83,6 +94,74 @@ pub fn render_node( Some(()) } +/// Encodes a pixmap as PNG with DPI metadata in the pHYs chunk. +/// +/// This is useful when you need the output PNG to have specific resolution metadata, +/// for example when targeting print or e-ink displays. +/// +/// The DPI value is converted to pixels per meter for the PNG pHYs chunk. +/// +/// # Example +/// +/// ```no_run +/// let svg_data = r#""#; +/// let tree = usvg::Tree::from_str(&svg_data, &usvg::Options::default()).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::default(), &mut pixmap.as_mut()); +/// let png_data = resvg::encode_png_with_dpi(&pixmap, 96).unwrap(); +/// ``` +pub fn encode_png_with_dpi( + pixmap: &tiny_skia::Pixmap, + dpi: u32, +) -> Result, png::EncodingError> { + // Convert DPI to pixels per meter for PNG pHYs chunk + // 1 inch = 0.0254 meters, so pixels_per_meter = dpi / 0.0254 + let pixels_per_meter = (dpi as f64 / 0.0254).round() as u32; + + // Demultiply alpha (same as tiny-skia's encode_png) + let mut tmp_data: Vec = pixmap.data().to_vec(); + for chunk in tmp_data.chunks_exact_mut(4) { + let a = chunk[3]; + if a != 0 && a != 255 { + let a_f = a as f32 / 255.0; + chunk[0] = (chunk[0] as f32 / a_f).min(255.0) as u8; + chunk[1] = (chunk[1] as f32 / a_f).min(255.0) as u8; + chunk[2] = (chunk[2] as f32 / a_f).min(255.0) as u8; + } + } + + let mut data = Vec::new(); + { + let mut encoder = png::Encoder::new(&mut data, pixmap.width(), pixmap.height()); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + // Set physical pixel dimensions (pHYs chunk) with unit = meters + encoder.set_pixel_dims(Some(png::PixelDimensions { + xppu: pixels_per_meter, + yppu: pixels_per_meter, + unit: png::Unit::Meter, + })); + let mut writer = encoder.write_header()?; + writer.write_image_data(&tmp_data)?; + } + + Ok(data) +} + +/// Saves a pixmap as a PNG file with DPI metadata. +/// +/// This is a convenience wrapper around [`encode_png_with_dpi`] that writes directly to a file. +pub fn save_png_with_dpi( + pixmap: &tiny_skia::Pixmap, + path: &std::path::Path, + dpi: u32, +) -> Result<(), png::EncodingError> { + let data = encode_png_with_dpi(pixmap, dpi)?; + std::fs::write(path, data)?; + Ok(()) +} + pub(crate) trait OptionLog { fn log_none(self, f: F) -> Self; } diff --git a/crates/resvg/src/main.rs b/crates/resvg/src/main.rs index 9abac77fc..1412c7b9b 100644 --- a/crates/resvg/src/main.rs +++ b/crates/resvg/src/main.rs @@ -47,9 +47,9 @@ fn process() -> Result<(), String> { } } - let mut svg_data = timed(args.perf, "Reading", || -> Result, &str> { + let mut svg_data = timed(args.perf, "Reading", || -> Result, String> { if let InputFrom::File(ref file) = args.in_svg { - std::fs::read(file).map_err(|_| "failed to open the provided file") + std::fs::read(file).map_err(|e| format!("failed to open '{}': {}", file.display(), e)) } else { use std::io::Read; let mut buf = Vec::new(); @@ -57,7 +57,7 @@ fn process() -> Result<(), String> { let mut handle = stdin.lock(); handle .read_to_end(&mut buf) - .map_err(|_| "failed to read stdin")?; + .map_err(|e| format!("failed to read stdin: {}", e))?; Ok(buf) } })?; @@ -102,15 +102,16 @@ fn process() -> Result<(), String> { // Render. let img = render_svg(&args, &tree)?; + let dpi = args.raw_args.dpi; match args.out_png.unwrap() { OutputTo::Stdout => { use std::io::Write; - let buf = img.encode_png().map_err(|e| e.to_string())?; + let buf = resvg::encode_png_with_dpi(&img, dpi).map_err(|e| e.to_string())?; std::io::stdout().write_all(&buf).unwrap(); } OutputTo::File(ref file) => { timed(args.perf, "Saving", || { - img.save_png(file).map_err(|e| e.to_string()) + resvg::save_png_with_dpi(&img, file, dpi).map_err(|e| e.to_string()) })?; } }; @@ -267,7 +268,7 @@ fn collect_args() -> Result { std::process::exit(0); } - Ok(CliArgs { + let result = CliArgs { width: input.opt_value_from_fn(["-w", "--width"], parse_length)?, height: input.opt_value_from_fn(["-h", "--height"], parse_length)?, zoom: input.opt_value_from_fn(["-z", "--zoom"], parse_zoom)?, @@ -316,7 +317,17 @@ fn collect_args() -> Result { input: input.opt_free_from_str()?, output: input.opt_free_from_str()?, - }) + }; + + // Check for any remaining/unknown arguments + let remaining = input.finish(); + if !remaining.is_empty() { + let args: Vec<_> = remaining.iter().map(|s| s.to_string_lossy()).collect(); + eprintln!("Error: unknown arguments: {}", args.join(", ")); + std::process::exit(1); + } + + Ok(result) } fn parse_dpi(s: &str) -> Result { @@ -577,6 +588,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..7d8c9c0e6 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..c373ea3af 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..c373ea3af 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..c373ea3af 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..2ef2a4b31 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..d4738e238 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..315961b2e 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..08f21c508 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..1f9c332da 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..c70ee0edb 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..26a51e43e 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..7d8c9c0e6 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..26a51e43e 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..c051f52f8 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..6aaf5abfb 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..62f85992f 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..7d8c9c0e6 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..2ef2a4b31 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..50c98e6c5 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..5f87dc610 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..3ce37d105 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..8c1320ce9 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..6ce01fb82 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..cec1b5774 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..47ba520d5 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..8c1320ce9 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..c75e1e8d1 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..9d3532cb6 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..dd4089f72 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..dd4089f72 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..dd4089f72 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..dd4089f72 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..8c1320ce9 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..879aa1729 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..19aafd452 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..d4b8f374c 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..91ba93618 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..91ba93618 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..cff14d13a 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..4fb63c2f0 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..35d985a24 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..19a1ea79b 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..bf0d5e065 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..e0e450972 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..e24d936ad 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..490e5b4c3 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..84acbeda0 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..0c2f2dd47 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..030f07899 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..030f07899 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..9c2cb70ea 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..cfeddc290 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..07f159c76 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..0c2f2dd47 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..aeae8fd67 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..d9256d324 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..aa3bc1b63 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..9d5c0afcb 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..2a967a431 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..9d5c0afcb 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..7e13795d0 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..752954dc4 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..030f07899 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..d94ef3eae 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..aa3bc1b63 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..992ccb57b 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..22f07d493 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..729445382 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..92f6c2211 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..95553622e 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..88de74637 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..95553622e 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..5b781865c 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..365870ead 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..ba5383f1a 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..95553622e 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..95553622e 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..bff82e3a4 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..365870ead 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..091994a38 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..3fc891f94 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..1e5d4d620 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..486b985c6 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..532facb74 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..532facb74 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..532facb74 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..94b459a5e 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..97e54384f 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..97e54384f 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..95553622e 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..b4cf7054c 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..13d787405 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..59333a085 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..95999f1d2 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..95999f1d2 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..d80056f3e 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..95999f1d2 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..95553622e 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..95553622e 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..95553622e 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..95553622e 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..95553622e 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..95553622e 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..ca0f3b6d6 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..ca0f3b6d6 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..ca0f3b6d6 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..5afd7d670 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..5afd7d670 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..56da12fe6 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..8be4da434 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..eedce335d 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..9289cc987 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..77923d4b1 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..0bc27a1ef 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..549b7ad5d 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..648e6ffd0 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..eedce335d 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..9fc0ecd1b 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..88b25ca5a 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..77923d4b1 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..9289cc987 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..00a48affa 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..7b50a960c 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..729445382 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..6e802955c 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..22d105dc4 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..6e802955c 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..6e802955c 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..7b50a960c 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..7b50a960c 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..e6893b82d 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..e6893b82d 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..6e802955c 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..7b50a960c 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..7d9c2b903 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..be5848e7c 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..5511daee4 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..9ce5af612 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/hinting-options/all-options-comparison.png b/crates/resvg/tests-hinted/tests/text/hinting-options/all-options-comparison.png new file mode 100644 index 000000000..6f5535c67 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/hinting-options/all-options-comparison.png differ diff --git a/crates/resvg/tests-hinted/tests/text/hinting-options/css-style-syntax.png b/crates/resvg/tests-hinted/tests/text/hinting-options/css-style-syntax.png new file mode 100644 index 000000000..4279ccc50 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/hinting-options/css-style-syntax.png differ diff --git a/crates/resvg/tests-hinted/tests/text/hinting-options/eink-mono-clarity.png b/crates/resvg/tests-hinted/tests/text/hinting-options/eink-mono-clarity.png new file mode 100644 index 000000000..208a2e3b8 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/hinting-options/eink-mono-clarity.png differ diff --git a/crates/resvg/tests-hinted/tests/text/hinting-options/eink-mono-engine.png b/crates/resvg/tests-hinted/tests/text/hinting-options/eink-mono-engine.png new file mode 100644 index 000000000..746cbb5f1 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/hinting-options/eink-mono-engine.png differ diff --git a/crates/resvg/tests-hinted/tests/text/hinting-options/eink-mono-target.png b/crates/resvg/tests-hinted/tests/text/hinting-options/eink-mono-target.png new file mode 100644 index 000000000..0e7c77341 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/hinting-options/eink-mono-target.png differ diff --git a/crates/resvg/tests-hinted/tests/text/hinting-options/engine-auto.png b/crates/resvg/tests-hinted/tests/text/hinting-options/engine-auto.png new file mode 100644 index 000000000..ea6946322 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/hinting-options/engine-auto.png differ diff --git a/crates/resvg/tests-hinted/tests/text/hinting-options/engine-native.png b/crates/resvg/tests-hinted/tests/text/hinting-options/engine-native.png new file mode 100644 index 000000000..8c7c260b3 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/hinting-options/engine-native.png differ diff --git a/crates/resvg/tests-hinted/tests/text/hinting-options/mode-lcd.png b/crates/resvg/tests-hinted/tests/text/hinting-options/mode-lcd.png new file mode 100644 index 000000000..ad15c760f Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/hinting-options/mode-lcd.png differ diff --git a/crates/resvg/tests-hinted/tests/text/hinting-options/mode-light.png b/crates/resvg/tests-hinted/tests/text/hinting-options/mode-light.png new file mode 100644 index 000000000..fe9a5c8a2 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/hinting-options/mode-light.png differ diff --git a/crates/resvg/tests-hinted/tests/text/hinting-options/mode-vertical-lcd.png b/crates/resvg/tests-hinted/tests/text/hinting-options/mode-vertical-lcd.png new file mode 100644 index 000000000..330a438df Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/hinting-options/mode-vertical-lcd.png differ diff --git a/crates/resvg/tests-hinted/tests/text/hinting-options/mono-hinted-vs-unhinted.png b/crates/resvg/tests-hinted/tests/text/hinting-options/mono-hinted-vs-unhinted.png new file mode 100644 index 000000000..fa1006cb2 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/hinting-options/mono-hinted-vs-unhinted.png differ diff --git a/crates/resvg/tests-hinted/tests/text/hinting-options/preserve-linear-metrics.png b/crates/resvg/tests-hinted/tests/text/hinting-options/preserve-linear-metrics.png new file mode 100644 index 000000000..e63f0f868 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/hinting-options/preserve-linear-metrics.png differ diff --git a/crates/resvg/tests-hinted/tests/text/hinting-options/symmetric-false.png b/crates/resvg/tests-hinted/tests/text/hinting-options/symmetric-false.png new file mode 100644 index 000000000..688f292dc Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/hinting-options/symmetric-false.png differ diff --git a/crates/resvg/tests-hinted/tests/text/hinting-options/target-mono-1x.png b/crates/resvg/tests-hinted/tests/text/hinting-options/target-mono-1x.png new file mode 100644 index 000000000..96d6ee6d5 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/hinting-options/target-mono-1x.png differ diff --git a/crates/resvg/tests-hinted/tests/text/hinting-options/target-mono.png b/crates/resvg/tests-hinted/tests/text/hinting-options/target-mono.png new file mode 100644 index 000000000..484b9c291 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/hinting-options/target-mono.png differ diff --git a/crates/resvg/tests-hinted/tests/text/hinting-options/terminus-mono-clarity.png b/crates/resvg/tests-hinted/tests/text/hinting-options/terminus-mono-clarity.png new file mode 100644 index 000000000..7d1d0f56f Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/hinting-options/terminus-mono-clarity.png differ diff --git a/crates/resvg/tests-hinted/tests/text/hinting-options/terminus-mono-target.png b/crates/resvg/tests-hinted/tests/text/hinting-options/terminus-mono-target.png new file mode 100644 index 000000000..51ddc5e30 Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/hinting-options/terminus-mono-target.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..cd6cd37cb 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..fbbcf7c23 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..57978f33f 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..0fc3bf564 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..bbd5829a8 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..fcaf4af76 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..6e5c11e92 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..b98539db4 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..22e9910c5 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..e75b40163 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..03519873c 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..bd13d448f 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..1385919c9 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..9297d7a7e 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..cb7433d1a 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..8cd33c7bb 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..b98539db4 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..139deff35 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..7edb411fe 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..167600529 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..449a0a35f 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..71ee9c508 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..d64f0952a 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..3fc400128 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..a960578fc 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..fbbcf7c23 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..a25e38ce0 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..ef91276b6 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..4de5a06f1 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..a960578fc 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..6940ad224 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..0ee313392 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..0ee313392 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..ee37fc81d 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..ee37fc81d 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..456973b3c 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..603eb3025 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..8717688c2 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..603eb3025 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..2fcfff11e 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..611f02802 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..611f02802 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..db696abcc 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..be82a6956 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..fc3ca4184 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..417be1a2b 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..a7a85ac20 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..177303e22 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..3fdac8876 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..396d2f0b0 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..22f07ffd3 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..6debe9390 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..a9f5361a3 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/hinting-comparison.png b/crates/resvg/tests-hinted/tests/text/text-rendering/hinting-comparison.png new file mode 100644 index 000000000..4c9bcb1bf Binary files /dev/null and b/crates/resvg/tests-hinted/tests/text/text-rendering/hinting-comparison.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..fbbcf7c23 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..4a2f4032d 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..a850c97d0 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..071639282 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..6d3f033b3 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..d3252db07 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..e1318dc1a 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..d6d499fe5 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..b8263e281 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..b13af91a1 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..952fbb1ee 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..952fbb1ee 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..40abce241 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..c8cbe9479 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..3f6361b17 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..f07634954 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..ca74cfb3e 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..08438e1a9 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..eb4be4f1d 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..1b0b25625 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..2bb73c9ae 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..6317518d9 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..4923a32d2 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..67859112b 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..ffbed26b6 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..ffbed26b6 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..6faf9d8b1 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..4d7463570 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..72f6e1caa 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..87e9c970e 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..6b518b34b 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..137ec2426 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..a12b90d1b 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..6b518b34b 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..77f3a9470 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..67859112b 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..52c8d75c2 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..45d075eef 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..b8263e281 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..d45a405f6 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..d213cd15c 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..7e7a11c33 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..d39be11ee 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..d213cd15c 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..7d838eb69 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..e1591c377 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..376c256d3 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..6676ff8f1 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..6e2e7d4c3 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..6e2e7d4c3 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..93ccd5ff9 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..6e2e7d4c3 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..69b7f0f81 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..1ee10e5ba 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..6676ff8f1 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..6676ff8f1 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..77effeaa0 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..70cec7827 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..075b3a8cf 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..3fa824045 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..3f077d418 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..5af315279 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..50af9f8a6 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..0c3fb25eb 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..7030482b0 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..74b46c232 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..91a621132 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..43a60a95d 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..9f19a3c94 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..7cacc06e5 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..a467e219f 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..7de61a9c9 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..7bf6ac6a6 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..91a621132 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..91a621132 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..91a621132 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..2e451f303 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..3fdd05cc5 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..7eaec517f 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..08fb289ac 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..f36d05fae 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..3c4f44a64 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..a5bdf4979 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..8427034e9 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..4a02b42f0 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..dd33c2535 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..e8db2236b 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..bee45b6f3 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..43c9c03a4 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..a5bdf4979 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..14c2c9990 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..47916929c 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..0df818ef1 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..53d5dab02 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..8ebf154cd 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..e4800431c 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..0e7fb7b5d 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..af62719f3 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..c8cbe9479 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..5404704b2 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..c8cbe9479 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..c8cbe9479 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..c8cbe9479 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..494403eb6 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..494403eb6 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..34e406548 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..84b451acb 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..afec95d0c 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..1449b4e12 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..a855dac4d 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..38a2dcd58 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..b4ebe0cd5 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..3efa66a7b 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..8da17fb04 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..67859112b 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..8089e577a 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..b8263e281 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..63c2283be 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..3143cfea5 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..76a4839f6 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..b4ebe0cd5 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..138919905 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..621763ecc 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..14c91928d 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..b8263e281 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..fe98f5ec3 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..147aa370d 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..b8263e281 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..b5ebae597 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..b8263e281 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..b8263e281 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..732312903 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..b8263e281 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..b8263e281 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..9699c7609 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..9699c7609 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..ced894d21 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..ce94fc28a 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..b0970e86a 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..8e0af36dc 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..0c3fe986b 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..8e0af36dc 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..b0970e86a 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..000ca6f9f 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..9d3532cb6 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..7919401b6 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..9d3532cb6 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..b0c289dd8 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..9d3532cb6 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..9d3532cb6 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..58d6e8561 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..a6abe8ecb 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..9d3532cb6 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..9d3532cb6 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..9d3532cb6 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..9509f1a8d 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..7919401b6 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..c0f2a3d36 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..d858b8309 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..4b8b605f6 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..712c22a4b 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..5556ba4f6 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..fb4f4bca8 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..7919401b6 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..7919401b6 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..7919401b6 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/Inter-LICENSE-OFL.txt b/crates/resvg/tests/fonts/Inter-LICENSE-OFL.txt new file mode 100644 index 000000000..9b2ca37b3 --- /dev/null +++ b/crates/resvg/tests/fonts/Inter-LICENSE-OFL.txt @@ -0,0 +1,92 @@ +Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) + +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 AND 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/Inter-Regular.ttf b/crates/resvg/tests/fonts/Inter-Regular.ttf new file mode 100644 index 000000000..c544be478 Binary files /dev/null and b/crates/resvg/tests/fonts/Inter-Regular.ttf differ diff --git a/crates/resvg/tests/fonts/README.md b/crates/resvg/tests/fonts/README.md index 7108843b6..d100d9164 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='*' --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/fonts/TerminusTTF-LICENSE-OFL.txt b/crates/resvg/tests/fonts/TerminusTTF-LICENSE-OFL.txt new file mode 100644 index 000000000..33929007e --- /dev/null +++ b/crates/resvg/tests/fonts/TerminusTTF-LICENSE-OFL.txt @@ -0,0 +1,97 @@ +Copyright (c) 2010 Dimitar Toshkov Zhekov, +with Reserved Font Name "Terminus Font". + +Copyright (c) 2011-2023 Tilman Blumenbach, +with Reserved Font Name "Terminus (TTF)". + +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/TerminusTTF-Regular.ttf b/crates/resvg/tests/fonts/TerminusTTF-Regular.ttf new file mode 100644 index 000000000..d125e6347 Binary files /dev/null and b/crates/resvg/tests/fonts/TerminusTTF-Regular.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..1b9024c9f --- /dev/null +++ b/crates/resvg/tests/integration/hinting.rs @@ -0,0 +1,293 @@ +// Copyright 2025 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Tests for font hinting functionality. +//! +//! These tests verify that: +//! 1. Hinting produces visibly different output than non-hinted rendering +//! 2. The `text-rendering` CSS property correctly controls hinting behavior +//! 3. Hinting works correctly at various font sizes + +use crate::GLOBAL_FONTDB; + +/// Renders an SVG with the specified hinting settings and returns the pixel data. +fn render_with_hinting(svg_data: &[u8], hinting_enabled: bool) -> Vec { + let opt = usvg::Options { + fontdb: GLOBAL_FONTDB.clone(), + hinting: usvg::HintingOptions { + enabled: hinting_enabled, + dpi: Some(96.0), + }, + ..usvg::Options::default() + }; + + let tree = usvg::Tree::from_data(svg_data, &opt).unwrap(); + let size = tree.size().to_int_size(); + let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height()).unwrap(); + resvg::render( + &tree, + tiny_skia::Transform::identity(), + &mut pixmap.as_mut(), + ); + + pixmap.take() +} + +/// Count the number of pixels that differ between two images. +fn count_different_pixels(img1: &[u8], img2: &[u8]) -> usize { + assert_eq!(img1.len(), img2.len()); + img1.chunks(4) + .zip(img2.chunks(4)) + .filter(|(p1, p2)| p1 != p2) + .count() +} + +/// Test that hinting produces different output than non-hinted rendering. +/// This demonstrates that hinting is actually being applied. +#[test] +fn hinting_produces_different_output() { + // Small text at 12px where hinting effects are most visible + let svg = br#" + + + Hinting Test + + + "#; + + let hinted = render_with_hinting(svg, true); + let unhinted = render_with_hinting(svg, false); + + let diff_count = count_different_pixels(&hinted, &unhinted); + + // Hinted and unhinted output should differ + // The exact number of different pixels depends on the font and size, + // but there should be a noticeable difference + assert!( + diff_count > 0, + "Hinted and unhinted output should differ, but they are identical" + ); + + // Log the difference for debugging + eprintln!( + "hinting_produces_different_output: {} pixels differ", + diff_count + ); +} + +/// Test that geometric-precision disables hinting even when hinting is enabled. +#[test] +fn geometric_precision_disables_hinting() { + let svg_geometric = br#" + + + Geometric Precision + + + "#; + + // With geometricPrecision, hinting should be disabled regardless of the option + let with_hinting_option = render_with_hinting(svg_geometric, true); + let without_hinting_option = render_with_hinting(svg_geometric, false); + + let diff_count = count_different_pixels(&with_hinting_option, &without_hinting_option); + + // Both should produce the same output since geometricPrecision disables hinting + assert_eq!( + diff_count, 0, + "geometricPrecision should produce identical output regardless of hinting option" + ); +} + +/// Test that optimizeLegibility enables hinting when the option is set. +#[test] +fn optimize_legibility_enables_hinting() { + let svg = br#" + + + Optimize Legibility + + + "#; + + let hinted = render_with_hinting(svg, true); + let unhinted = render_with_hinting(svg, false); + + let diff_count = count_different_pixels(&hinted, &unhinted); + + // optimizeLegibility with hinting enabled should differ from unhinted + assert!( + diff_count > 0, + "optimizeLegibility should produce different output when hinting is enabled" + ); +} + +/// Test hinting at various font sizes to demonstrate size-dependent effects. +#[test] +fn hinting_at_various_sizes() { + let sizes = [8, 10, 12, 14, 16, 20, 24, 32, 48]; + let mut results = Vec::new(); + + for size in sizes { + let svg = format!( + r#" + + + Size {} pixels + + + "#, + size, size + ); + + let hinted = render_with_hinting(svg.as_bytes(), true); + let unhinted = render_with_hinting(svg.as_bytes(), false); + + let diff_count = count_different_pixels(&hinted, &unhinted); + results.push((size, diff_count)); + + eprintln!("Size {}px: {} pixels differ", size, diff_count); + } + + // Verify that at least some sizes show hinting differences + let sizes_with_differences = results.iter().filter(|(_, diff)| *diff > 0).count(); + assert!( + sizes_with_differences > 0, + "Hinting should produce differences at various sizes" + ); +} + +/// Test that hinting DPI option is accepted and doesn't cause errors. +/// Note: Different DPI values affect the ppem used for hinting calculations, +/// but when rendering to the same canvas size at the same nominal font size, +/// the pixel output may be identical because hinting aligns glyphs to the +/// same target pixel grid. +#[test] +fn hinting_with_different_dpi() { + let svg = br#" + + + DPI Test + + + "#; + + let render_at_dpi = |dpi: f32| -> Vec { + let opt = usvg::Options { + fontdb: GLOBAL_FONTDB.clone(), + dpi, + hinting: usvg::HintingOptions { + enabled: true, + dpi: Some(dpi), + }, + ..usvg::Options::default() + }; + + let tree = usvg::Tree::from_data(svg, &opt).unwrap(); + let size = tree.size().to_int_size(); + let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height()).unwrap(); + resvg::render( + &tree, + tiny_skia::Transform::identity(), + &mut pixmap.as_mut(), + ); + pixmap.take() + }; + + let at_72dpi = render_at_dpi(72.0); + let at_96dpi = render_at_dpi(96.0); + let at_144dpi = render_at_dpi(144.0); + + // Different DPI values affect ppem calculation for 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 + ); + + // Verify that rendering at different DPIs doesn't crash and produces valid output. + // The actual pixel differences depend on the font's hinting instructions and + // may be zero when rendering to the same canvas size. + assert!( + !at_72dpi.is_empty(), + "72 DPI rendering should produce output" + ); + assert!( + !at_96dpi.is_empty(), + "96 DPI rendering should produce output" + ); + assert!( + !at_144dpi.is_empty(), + "144 DPI rendering should produce output" + ); +} + +/// Test hinting with variable fonts (Roboto Flex). +#[test] +fn hinting_with_variable_font() { + let svg = br#" + + + Variable Font Hinting + + + "#; + + let hinted = render_with_hinting(svg, true); + let unhinted = render_with_hinting(svg, false); + + let diff_count = count_different_pixels(&hinted, &unhinted); + + eprintln!("Variable font hinting: {} pixels differ", diff_count); + + // Variable fonts should also show hinting differences + // (though the exact behavior depends on the font's hinting data) +} + +/// Test that auto text-rendering defaults to optimizeLegibility behavior. +#[test] +fn auto_text_rendering_uses_hinting() { + // SVG with auto (default) text-rendering + let svg_auto = br#" + + + Auto Text Rendering + + + "#; + + // SVG with explicit optimizeLegibility + let svg_legibility = br#" + + + Auto Text Rendering + + + "#; + + let auto_hinted = render_with_hinting(svg_auto, true); + let legibility_hinted = render_with_hinting(svg_legibility, true); + + let diff_count = count_different_pixels(&auto_hinted, &legibility_hinted); + + // Both should produce the same output since auto defaults to optimizeLegibility + assert_eq!( + diff_count, 0, + "auto and optimizeLegibility should produce identical output" + ); +} diff --git a/crates/resvg/tests/integration/main.rs b/crates/resvg/tests/integration/main.rs index 7590c60cf..799607dc7 100644 --- a/crates/resvg/tests/integration/main.rs +++ b/crates/resvg/tests/integration/main.rs @@ -11,11 +11,24 @@ use std::process::Command; use std::sync::Arc; use usvg::fontdb; +/// Save a tiny_skia::Pixmap as PNG with 96 DPI metadata. +fn save_pixmap_png_with_dpi( + pixmap: &tiny_skia::Pixmap, + path: &str, +) -> Result<(), png::EncodingError> { + resvg::save_png_with_dpi(pixmap, std::path::Path::new(path), 96) +} + #[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 +47,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 +63,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 +106,8 @@ pub fn render_inner(name: &str, test_mode: TestMode) -> usize { .unwrap() .to_owned(), ), + #[cfg(feature = "text")] + hinting: hinting_options, ..usvg::Options::default() }; @@ -110,7 +156,13 @@ pub fn render_inner(name: &str, test_mode: TestMode) -> usize { }; let make_ref_fn = || -> ! { - pixmap.save_png(&png_path).unwrap(); + // 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); + } + } + save_pixmap_png_with_dpi(&pixmap, &png_path).unwrap(); Command::new("oxipng") .args([ "-o".to_owned(), @@ -129,7 +181,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 +189,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 +201,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..4a597e10a 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); } @@ -1471,6 +1485,23 @@ use crate::render; #[test] fn text_font_weight_normal() { assert_eq!(render("tests/text/font-weight/normal"), 0); } #[test] fn text_glyph_orientation_horizontal_simple_case() { assert_eq!(render("tests/text/glyph-orientation-horizontal/simple-case"), 0); } #[test] fn text_glyph_orientation_vertical_simple_case() { assert_eq!(render("tests/text/glyph-orientation-vertical/simple-case"), 0); } +#[test] fn text_hinting_options_all_options_comparison() { assert_eq!(render("tests/text/hinting-options/all-options-comparison"), 0); } +#[test] fn text_hinting_options_css_style_syntax() { assert_eq!(render("tests/text/hinting-options/css-style-syntax"), 0); } +#[test] fn text_hinting_options_eink_mono_clarity() { assert_eq!(render("tests/text/hinting-options/eink-mono-clarity"), 0); } +#[test] fn text_hinting_options_eink_mono_engine() { assert_eq!(render("tests/text/hinting-options/eink-mono-engine"), 0); } +#[test] fn text_hinting_options_eink_mono_target() { assert_eq!(render("tests/text/hinting-options/eink-mono-target"), 0); } +#[test] fn text_hinting_options_engine_auto() { assert_eq!(render("tests/text/hinting-options/engine-auto"), 0); } +#[test] fn text_hinting_options_engine_native() { assert_eq!(render("tests/text/hinting-options/engine-native"), 0); } +#[test] fn text_hinting_options_mode_lcd() { assert_eq!(render("tests/text/hinting-options/mode-lcd"), 0); } +#[test] fn text_hinting_options_mode_light() { assert_eq!(render("tests/text/hinting-options/mode-light"), 0); } +#[test] fn text_hinting_options_mode_vertical_lcd() { assert_eq!(render("tests/text/hinting-options/mode-vertical-lcd"), 0); } +#[test] fn text_hinting_options_mono_hinted_vs_unhinted() { assert_eq!(render("tests/text/hinting-options/mono-hinted-vs-unhinted"), 0); } +#[test] fn text_hinting_options_preserve_linear_metrics() { assert_eq!(render("tests/text/hinting-options/preserve-linear-metrics"), 0); } +#[test] fn text_hinting_options_symmetric_false() { assert_eq!(render("tests/text/hinting-options/symmetric-false"), 0); } +#[test] fn text_hinting_options_target_mono_1x() { assert_eq!(render("tests/text/hinting-options/target-mono-1x"), 0); } +#[test] fn text_hinting_options_target_mono() { assert_eq!(render("tests/text/hinting-options/target-mono"), 0); } +#[test] fn text_hinting_options_terminus_mono_clarity() { assert_eq!(render("tests/text/hinting-options/terminus-mono-clarity"), 0); } +#[test] fn text_hinting_options_terminus_mono_target() { assert_eq!(render("tests/text/hinting-options/terminus-mono-target"), 0); } #[test] fn text_kerning_0() { assert_eq!(render("tests/text/kerning/0"), 0); } #[test] fn text_kerning_10percent() { assert_eq!(render("tests/text/kerning/10percent"), 0); } #[test] fn text_lengthAdjust_spacingAndGlyphs() { assert_eq!(render("tests/text/lengthAdjust/spacingAndGlyphs"), 0); } @@ -1568,6 +1599,7 @@ use crate::render; #[test] fn text_text_decoration_underline() { assert_eq!(render("tests/text/text-decoration/underline"), 0); } #[test] fn text_text_decoration_with_textLength_on_a_single_character() { assert_eq!(render("tests/text/text-decoration/with-textLength-on-a-single-character"), 0); } #[test] fn text_text_rendering_geometricPrecision() { assert_eq!(render("tests/text/text-rendering/geometricPrecision"), 0); } +#[test] fn text_text_rendering_hinting_comparison() { assert_eq!(render("tests/text/text-rendering/hinting-comparison"), 0); } #[test] fn text_text_rendering_on_tspan() { assert_eq!(render("tests/text/text-rendering/on-tspan"), 0); } #[test] fn text_text_rendering_optimizeLegibility() { assert_eq!(render("tests/text/text-rendering/optimizeLegibility"), 0); } #[test] fn text_text_rendering_optimizeSpeed() { assert_eq!(render("tests/text/text-rendering/optimizeSpeed"), 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..0340b9c81 --- /dev/null +++ b/crates/resvg/tests/integration/render_hinted.rs @@ -0,0 +1,405 @@ +// 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_hinting_options_all_options_comparison() { assert_eq!(render_hinted("tests/text/hinting-options/all-options-comparison"), 0); } +#[test] fn hinted_text_hinting_options_css_style_syntax() { assert_eq!(render_hinted("tests/text/hinting-options/css-style-syntax"), 0); } +#[test] fn hinted_text_hinting_options_eink_mono_clarity() { assert_eq!(render_hinted("tests/text/hinting-options/eink-mono-clarity"), 0); } +#[test] fn hinted_text_hinting_options_eink_mono_engine() { assert_eq!(render_hinted("tests/text/hinting-options/eink-mono-engine"), 0); } +#[test] fn hinted_text_hinting_options_eink_mono_target() { assert_eq!(render_hinted("tests/text/hinting-options/eink-mono-target"), 0); } +#[test] fn hinted_text_hinting_options_engine_auto() { assert_eq!(render_hinted("tests/text/hinting-options/engine-auto"), 0); } +#[test] fn hinted_text_hinting_options_engine_native() { assert_eq!(render_hinted("tests/text/hinting-options/engine-native"), 0); } +#[test] fn hinted_text_hinting_options_mode_lcd() { assert_eq!(render_hinted("tests/text/hinting-options/mode-lcd"), 0); } +#[test] fn hinted_text_hinting_options_mode_light() { assert_eq!(render_hinted("tests/text/hinting-options/mode-light"), 0); } +#[test] fn hinted_text_hinting_options_mode_vertical_lcd() { assert_eq!(render_hinted("tests/text/hinting-options/mode-vertical-lcd"), 0); } +#[test] fn hinted_text_hinting_options_mono_hinted_vs_unhinted() { assert_eq!(render_hinted("tests/text/hinting-options/mono-hinted-vs-unhinted"), 0); } +#[test] fn hinted_text_hinting_options_preserve_linear_metrics() { assert_eq!(render_hinted("tests/text/hinting-options/preserve-linear-metrics"), 0); } +#[test] fn hinted_text_hinting_options_symmetric_false() { assert_eq!(render_hinted("tests/text/hinting-options/symmetric-false"), 0); } +#[test] fn hinted_text_hinting_options_target_mono_1x() { assert_eq!(render_hinted("tests/text/hinting-options/target-mono-1x"), 0); } +#[test] fn hinted_text_hinting_options_target_mono() { assert_eq!(render_hinted("tests/text/hinting-options/target-mono"), 0); } +#[test] fn hinted_text_hinting_options_terminus_mono_clarity() { assert_eq!(render_hinted("tests/text/hinting-options/terminus-mono-clarity"), 0); } +#[test] fn hinted_text_hinting_options_terminus_mono_target() { assert_eq!(render_hinted("tests/text/hinting-options/terminus-mono-target"), 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_hinting_comparison() { assert_eq!(render_hinted("tests/text/text-rendering/hinting-comparison"), 0); } +#[test] fn hinted_text_text_rendering_on_tspan() { assert_eq!(render_hinted("tests/text/text-rendering/on-tspan"), 0); } +#[test] fn hinted_text_text_rendering_optimizeLegibility() { assert_eq!(render_hinted("tests/text/text-rendering/optimizeLegibility"), 0); } +#[test] fn hinted_text_text_rendering_optimizeSpeed() { assert_eq!(render_hinted("tests/text/text-rendering/optimizeSpeed"), 0); } +#[test] fn hinted_text_text_rendering_with_underline() { assert_eq!(render_hinted("tests/text/text-rendering/with-underline"), 0); } +#[test] fn hinted_text_textLength_150_on_parent() { assert_eq!(render_hinted("tests/text/textLength/150-on-parent"), 0); } +#[test] fn hinted_text_textLength_150_on_tspan() { assert_eq!(render_hinted("tests/text/textLength/150-on-tspan"), 0); } +#[test] fn hinted_text_textLength_150() { assert_eq!(render_hinted("tests/text/textLength/150"), 0); } +#[test] fn hinted_text_textLength_40mm() { assert_eq!(render_hinted("tests/text/textLength/40mm"), 0); } +#[test] fn hinted_text_textLength_75percent() { assert_eq!(render_hinted("tests/text/textLength/75percent"), 0); } +#[test] fn hinted_text_textLength_arabic_with_lengthAdjust() { assert_eq!(render_hinted("tests/text/textLength/arabic-with-lengthAdjust"), 0); } +#[test] fn hinted_text_textLength_arabic() { assert_eq!(render_hinted("tests/text/textLength/arabic"), 0); } +#[test] fn hinted_text_textLength_inherit() { assert_eq!(render_hinted("tests/text/textLength/inherit"), 0); } +#[test] fn hinted_text_textLength_negative() { assert_eq!(render_hinted("tests/text/textLength/negative"), 0); } +#[test] fn hinted_text_textLength_on_a_single_tspan() { assert_eq!(render_hinted("tests/text/textLength/on-a-single-tspan"), 0); } +#[test] fn hinted_text_textLength_on_text_and_tspan() { assert_eq!(render_hinted("tests/text/textLength/on-text-and-tspan"), 0); } +#[test] fn hinted_text_textLength_zero() { assert_eq!(render_hinted("tests/text/textLength/zero"), 0); } +#[test] fn hinted_text_textPath_closed_path() { assert_eq!(render_hinted("tests/text/textPath/closed-path"), 0); } +#[test] fn hinted_text_textPath_complex() { assert_eq!(render_hinted("tests/text/textPath/complex"), 0); } +#[test] fn hinted_text_textPath_dy_with_tiny_coordinates() { assert_eq!(render_hinted("tests/text/textPath/dy-with-tiny-coordinates"), 0); } +#[test] fn hinted_text_textPath_invalid_link() { assert_eq!(render_hinted("tests/text/textPath/invalid-link"), 0); } +#[test] fn hinted_text_textPath_invalid_textPath_in_the_middle() { assert_eq!(render_hinted("tests/text/textPath/invalid-textPath-in-the-middle"), 0); } +#[test] fn hinted_text_textPath_link_to_rect() { assert_eq!(render_hinted("tests/text/textPath/link-to-rect"), 0); } +#[test] fn hinted_text_textPath_m_A_path() { assert_eq!(render_hinted("tests/text/textPath/m-A-path"), 0); } +#[test] fn hinted_text_textPath_m_L_Z_path() { assert_eq!(render_hinted("tests/text/textPath/m-L-Z-path"), 0); } +#[test] fn hinted_text_textPath_method_eq_stretch() { assert_eq!(render_hinted("tests/text/textPath/method=stretch"), 0); } +#[test] fn hinted_text_textPath_mixed_children_1() { assert_eq!(render_hinted("tests/text/textPath/mixed-children-1"), 0); } +#[test] fn hinted_text_textPath_mixed_children_2() { assert_eq!(render_hinted("tests/text/textPath/mixed-children-2"), 0); } +#[test] fn hinted_text_textPath_nested() { assert_eq!(render_hinted("tests/text/textPath/nested"), 0); } +#[test] fn hinted_text_textPath_no_link() { assert_eq!(render_hinted("tests/text/textPath/no-link"), 0); } +#[test] fn hinted_text_textPath_path_with_ClosePath() { assert_eq!(render_hinted("tests/text/textPath/path-with-ClosePath"), 0); } +#[test] fn hinted_text_textPath_path_with_subpaths_and_startOffset() { assert_eq!(render_hinted("tests/text/textPath/path-with-subpaths-and-startOffset"), 0); } +#[test] fn hinted_text_textPath_path_with_subpaths() { assert_eq!(render_hinted("tests/text/textPath/path-with-subpaths"), 0); } +#[test] fn hinted_text_textPath_side_eq_right() { assert_eq!(render_hinted("tests/text/textPath/side=right"), 0); } +#[test] fn hinted_text_textPath_simple_case() { assert_eq!(render_hinted("tests/text/textPath/simple-case"), 0); } +#[test] fn hinted_text_textPath_spacing_eq_auto() { assert_eq!(render_hinted("tests/text/textPath/spacing=auto"), 0); } +#[test] fn hinted_text_textPath_startOffset_eq__100() { assert_eq!(render_hinted("tests/text/textPath/startOffset=-100"), 0); } +#[test] fn hinted_text_textPath_startOffset_eq_10percent() { assert_eq!(render_hinted("tests/text/textPath/startOffset=10percent"), 0); } +#[test] fn hinted_text_textPath_startOffset_eq_30() { assert_eq!(render_hinted("tests/text/textPath/startOffset=30"), 0); } +#[test] fn hinted_text_textPath_startOffset_eq_5mm() { assert_eq!(render_hinted("tests/text/textPath/startOffset=5mm"), 0); } +#[test] fn hinted_text_textPath_startOffset_eq_9999() { assert_eq!(render_hinted("tests/text/textPath/startOffset=9999"), 0); } +#[test] fn hinted_text_textPath_tspan_with_absolute_position() { assert_eq!(render_hinted("tests/text/textPath/tspan-with-absolute-position"), 0); } +#[test] fn hinted_text_textPath_tspan_with_relative_position() { assert_eq!(render_hinted("tests/text/textPath/tspan-with-relative-position"), 0); } +#[test] fn hinted_text_textPath_two_paths() { assert_eq!(render_hinted("tests/text/textPath/two-paths"), 0); } +#[test] fn hinted_text_textPath_very_long_text() { assert_eq!(render_hinted("tests/text/textPath/very-long-text"), 0); } +#[test] fn hinted_text_textPath_with_baseline_shift_and_rotate() { assert_eq!(render_hinted("tests/text/textPath/with-baseline-shift-and-rotate"), 0); } +#[test] fn hinted_text_textPath_with_baseline_shift() { assert_eq!(render_hinted("tests/text/textPath/with-baseline-shift"), 0); } +#[test] fn hinted_text_textPath_with_big_letter_spacing() { assert_eq!(render_hinted("tests/text/textPath/with-big-letter-spacing"), 0); } +#[test] fn hinted_text_textPath_with_coordinates_on_text() { assert_eq!(render_hinted("tests/text/textPath/with-coordinates-on-text"), 0); } +#[test] fn hinted_text_textPath_with_coordinates_on_textPath() { assert_eq!(render_hinted("tests/text/textPath/with-coordinates-on-textPath"), 0); } +#[test] fn hinted_text_textPath_with_filter() { assert_eq!(render_hinted("tests/text/textPath/with-filter"), 0); } +#[test] fn hinted_text_textPath_with_invalid_path_and_xlink_href() { assert_eq!(render_hinted("tests/text/textPath/with-invalid-path-and-xlink-href"), 0); } +#[test] fn hinted_text_textPath_with_letter_spacing() { assert_eq!(render_hinted("tests/text/textPath/with-letter-spacing"), 0); } +#[test] fn hinted_text_textPath_with_path_and_xlink_href() { assert_eq!(render_hinted("tests/text/textPath/with-path-and-xlink-href"), 0); } +#[test] fn hinted_text_textPath_with_path() { assert_eq!(render_hinted("tests/text/textPath/with-path"), 0); } +#[test] fn hinted_text_textPath_with_rotate() { assert_eq!(render_hinted("tests/text/textPath/with-rotate"), 0); } +#[test] fn hinted_text_textPath_with_text_anchor() { assert_eq!(render_hinted("tests/text/textPath/with-text-anchor"), 0); } +#[test] fn hinted_text_textPath_with_transform_on_a_referenced_path() { assert_eq!(render_hinted("tests/text/textPath/with-transform-on-a-referenced-path"), 0); } +#[test] fn hinted_text_textPath_with_transform_outside_a_referenced_path() { assert_eq!(render_hinted("tests/text/textPath/with-transform-outside-a-referenced-path"), 0); } +#[test] fn hinted_text_textPath_with_underline() { assert_eq!(render_hinted("tests/text/textPath/with-underline"), 0); } +#[test] fn hinted_text_textPath_writing_mode_eq_tb() { assert_eq!(render_hinted("tests/text/textPath/writing-mode=tb"), 0); } +#[test] fn hinted_text_tref_link_to_a_complex_text() { assert_eq!(render_hinted("tests/text/tref/link-to-a-complex-text"), 0); } +#[test] fn hinted_text_tref_link_to_a_non_SVG_element() { assert_eq!(render_hinted("tests/text/tref/link-to-a-non-SVG-element"), 0); } +#[test] fn hinted_text_tref_link_to_a_non_text_element() { assert_eq!(render_hinted("tests/text/tref/link-to-a-non-text-element"), 0); } +#[test] fn hinted_text_tref_link_to_an_external_file_element() { assert_eq!(render_hinted("tests/text/tref/link-to-an-external-file-element"), 0); } +#[test] fn hinted_text_tref_link_to_text() { assert_eq!(render_hinted("tests/text/tref/link-to-text"), 0); } +#[test] fn hinted_text_tref_nested() { assert_eq!(render_hinted("tests/text/tref/nested"), 0); } +#[test] fn hinted_text_tref_position_attributes() { assert_eq!(render_hinted("tests/text/tref/position-attributes"), 0); } +#[test] fn hinted_text_tref_style_attributes() { assert_eq!(render_hinted("tests/text/tref/style-attributes"), 0); } +#[test] fn hinted_text_tref_with_a_title_child() { assert_eq!(render_hinted("tests/text/tref/with-a-title-child"), 0); } +#[test] fn hinted_text_tref_with_text() { assert_eq!(render_hinted("tests/text/tref/with-text"), 0); } +#[test] fn hinted_text_tref_xml_space() { assert_eq!(render_hinted("tests/text/tref/xml-space"), 0); } +#[test] fn hinted_text_tspan_bidi_reordering() { assert_eq!(render_hinted("tests/text/tspan/bidi-reordering"), 0); } +#[test] fn hinted_text_tspan_mixed_font_size() { assert_eq!(render_hinted("tests/text/tspan/mixed-font-size"), 0); } +#[test] fn hinted_text_tspan_mixed_xml_space_1() { assert_eq!(render_hinted("tests/text/tspan/mixed-xml-space-1"), 0); } +#[test] fn hinted_text_tspan_mixed_xml_space_2() { assert_eq!(render_hinted("tests/text/tspan/mixed-xml-space-2"), 0); } +#[test] fn hinted_text_tspan_mixed_xml_space_3() { assert_eq!(render_hinted("tests/text/tspan/mixed-xml-space-3"), 0); } +#[test] fn hinted_text_tspan_mixed() { assert_eq!(render_hinted("tests/text/tspan/mixed"), 0); } +#[test] fn hinted_text_tspan_multiple_coordinates() { assert_eq!(render_hinted("tests/text/tspan/multiple-coordinates"), 0); } +#[test] fn hinted_text_tspan_nested_rotate() { assert_eq!(render_hinted("tests/text/tspan/nested-rotate"), 0); } +#[test] fn hinted_text_tspan_nested_whitespaces() { assert_eq!(render_hinted("tests/text/tspan/nested-whitespaces"), 0); } +#[test] fn hinted_text_tspan_nested() { assert_eq!(render_hinted("tests/text/tspan/nested"), 0); } +#[test] fn hinted_text_tspan_only_with_y() { assert_eq!(render_hinted("tests/text/tspan/only-with-y"), 0); } +#[test] fn hinted_text_tspan_outside_the_text() { assert_eq!(render_hinted("tests/text/tspan/outside-the-text"), 0); } +#[test] fn hinted_text_tspan_pseudo_multi_line() { assert_eq!(render_hinted("tests/text/tspan/pseudo-multi-line"), 0); } +#[test] fn hinted_text_tspan_rotate_and_display_none() { assert_eq!(render_hinted("tests/text/tspan/rotate-and-display-none"), 0); } +#[test] fn hinted_text_tspan_rotate_on_child() { assert_eq!(render_hinted("tests/text/tspan/rotate-on-child"), 0); } +#[test] fn hinted_text_tspan_sequential() { assert_eq!(render_hinted("tests/text/tspan/sequential"), 0); } +#[test] fn hinted_text_tspan_style_override() { assert_eq!(render_hinted("tests/text/tspan/style-override"), 0); } +#[test] fn hinted_text_tspan_text_shaping_across_multiple_tspan_1() { assert_eq!(render_hinted("tests/text/tspan/text-shaping-across-multiple-tspan-1"), 0); } +#[test] fn hinted_text_tspan_text_shaping_across_multiple_tspan_2() { assert_eq!(render_hinted("tests/text/tspan/text-shaping-across-multiple-tspan-2"), 0); } +#[test] fn hinted_text_tspan_transform() { assert_eq!(render_hinted("tests/text/tspan/transform"), 0); } +#[test] fn hinted_text_tspan_tspan_bbox_1() { assert_eq!(render_hinted("tests/text/tspan/tspan-bbox-1"), 0); } +#[test] fn hinted_text_tspan_tspan_bbox_2() { assert_eq!(render_hinted("tests/text/tspan/tspan-bbox-2"), 0); } +#[test] fn hinted_text_tspan_with_clip_path() { assert_eq!(render_hinted("tests/text/tspan/with-clip-path"), 0); } +#[test] fn hinted_text_tspan_with_dy() { assert_eq!(render_hinted("tests/text/tspan/with-dy"), 0); } +#[test] fn hinted_text_tspan_with_filter() { assert_eq!(render_hinted("tests/text/tspan/with-filter"), 0); } +#[test] fn hinted_text_tspan_with_mask() { assert_eq!(render_hinted("tests/text/tspan/with-mask"), 0); } +#[test] fn hinted_text_tspan_with_opacity() { assert_eq!(render_hinted("tests/text/tspan/with-opacity"), 0); } +#[test] fn hinted_text_tspan_with_x_and_y() { assert_eq!(render_hinted("tests/text/tspan/with-x-and-y"), 0); } +#[test] fn hinted_text_tspan_without_attributes() { assert_eq!(render_hinted("tests/text/tspan/without-attributes"), 0); } +#[test] fn hinted_text_tspan_xml_space_1() { assert_eq!(render_hinted("tests/text/tspan/xml-space-1"), 0); } +#[test] fn hinted_text_tspan_xml_space_2() { assert_eq!(render_hinted("tests/text/tspan/xml-space-2"), 0); } +#[test] fn hinted_text_unicode_bidi_bidi_override() { assert_eq!(render_hinted("tests/text/unicode-bidi/bidi-override"), 0); } +#[test] fn hinted_text_word_spacing__5() { assert_eq!(render_hinted("tests/text/word-spacing/-5"), 0); } +#[test] fn hinted_text_word_spacing_0() { assert_eq!(render_hinted("tests/text/word-spacing/0"), 0); } +#[test] fn hinted_text_word_spacing_10() { assert_eq!(render_hinted("tests/text/word-spacing/10"), 0); } +#[test] fn hinted_text_word_spacing_2mm() { assert_eq!(render_hinted("tests/text/word-spacing/2mm"), 0); } +#[test] fn hinted_text_word_spacing_5percent() { assert_eq!(render_hinted("tests/text/word-spacing/5percent"), 0); } +#[test] fn hinted_text_word_spacing_large_negative() { assert_eq!(render_hinted("tests/text/word-spacing/large-negative"), 0); } +#[test] fn hinted_text_word_spacing_normal() { assert_eq!(render_hinted("tests/text/word-spacing/normal"), 0); } +#[test] fn hinted_text_writing_mode_arabic_with_rl() { assert_eq!(render_hinted("tests/text/writing-mode/arabic-with-rl"), 0); } +#[test] fn hinted_text_writing_mode_horizontal_tb() { assert_eq!(render_hinted("tests/text/writing-mode/horizontal-tb"), 0); } +#[test] fn hinted_text_writing_mode_inheritance() { assert_eq!(render_hinted("tests/text/writing-mode/inheritance"), 0); } +#[test] fn hinted_text_writing_mode_invalid_value() { assert_eq!(render_hinted("tests/text/writing-mode/invalid-value"), 0); } +#[test] fn hinted_text_writing_mode_japanese_with_tb() { assert_eq!(render_hinted("tests/text/writing-mode/japanese-with-tb"), 0); } +#[test] fn hinted_text_writing_mode_lr_tb() { assert_eq!(render_hinted("tests/text/writing-mode/lr-tb"), 0); } +#[test] fn hinted_text_writing_mode_lr() { assert_eq!(render_hinted("tests/text/writing-mode/lr"), 0); } +#[test] fn hinted_text_writing_mode_mixed_languages_with_tb_and_underline() { assert_eq!(render_hinted("tests/text/writing-mode/mixed-languages-with-tb-and-underline"), 0); } +#[test] fn hinted_text_writing_mode_mixed_languages_with_tb() { assert_eq!(render_hinted("tests/text/writing-mode/mixed-languages-with-tb"), 0); } +#[test] fn hinted_text_writing_mode_on_tspan() { assert_eq!(render_hinted("tests/text/writing-mode/on-tspan"), 0); } +#[test] fn hinted_text_writing_mode_rl_tb() { assert_eq!(render_hinted("tests/text/writing-mode/rl-tb"), 0); } +#[test] fn hinted_text_writing_mode_rl() { assert_eq!(render_hinted("tests/text/writing-mode/rl"), 0); } +#[test] fn hinted_text_writing_mode_tb_and_punctuation() { assert_eq!(render_hinted("tests/text/writing-mode/tb-and-punctuation"), 0); } +#[test] fn hinted_text_writing_mode_tb_rl() { assert_eq!(render_hinted("tests/text/writing-mode/tb-rl"), 0); } +#[test] fn hinted_text_writing_mode_tb_with_alignment() { assert_eq!(render_hinted("tests/text/writing-mode/tb-with-alignment"), 0); } +#[test] fn hinted_text_writing_mode_tb_with_dx_on_second_tspan() { assert_eq!(render_hinted("tests/text/writing-mode/tb-with-dx-on-second-tspan"), 0); } +#[test] fn hinted_text_writing_mode_tb_with_dx_on_tspan() { assert_eq!(render_hinted("tests/text/writing-mode/tb-with-dx-on-tspan"), 0); } +#[test] fn hinted_text_writing_mode_tb_with_dy_on_second_tspan() { assert_eq!(render_hinted("tests/text/writing-mode/tb-with-dy-on-second-tspan"), 0); } +#[test] fn hinted_text_writing_mode_tb_with_rotate_and_underline() { assert_eq!(render_hinted("tests/text/writing-mode/tb-with-rotate-and-underline"), 0); } +#[test] fn hinted_text_writing_mode_tb_with_rotate() { assert_eq!(render_hinted("tests/text/writing-mode/tb-with-rotate"), 0); } +#[test] fn hinted_text_writing_mode_tb() { assert_eq!(render_hinted("tests/text/writing-mode/tb"), 0); } +#[test] fn hinted_text_writing_mode_vertical_lr() { assert_eq!(render_hinted("tests/text/writing-mode/vertical-lr"), 0); } +#[test] fn hinted_text_writing_mode_vertical_rl() { assert_eq!(render_hinted("tests/text/writing-mode/vertical-rl"), 0); } diff --git a/crates/resvg/tests/tests/text/color-font/colrv0.png b/crates/resvg/tests/tests/text/color-font/colrv0.png index e4b090d24..087615997 100644 Binary files a/crates/resvg/tests/tests/text/color-font/colrv0.png and b/crates/resvg/tests/tests/text/color-font/colrv0.png differ diff --git a/crates/resvg/tests/tests/text/color-font/colrv1.png b/crates/resvg/tests/tests/text/color-font/colrv1.png index 7537da902..23bb63a05 100644 Binary files a/crates/resvg/tests/tests/text/color-font/colrv1.png and b/crates/resvg/tests/tests/text/color-font/colrv1.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/all-axes-combined.png b/crates/resvg/tests/tests/text/font-variation-settings/all-axes-combined.png new file mode 100644 index 000000000..c4222be24 Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/all-axes-combined.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/all-axes-combined.svg b/crates/resvg/tests/tests/text/font-variation-settings/all-axes-combined.svg new file mode 100644 index 000000000..6f3fca97c --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/all-axes-combined.svg @@ -0,0 +1,10 @@ + + Multiple custom axes combined + + Combo + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/auto-font-stretch-condensed.png b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-stretch-condensed.png new file mode 100644 index 000000000..bfa287942 Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-stretch-condensed.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/auto-font-stretch-condensed.svg b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-stretch-condensed.svg new file mode 100644 index 000000000..919945f3a --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-stretch-condensed.svg @@ -0,0 +1,9 @@ + + font-stretch condensed auto-maps to wdth axis + + Narrow + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/auto-font-style-oblique.png b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-style-oblique.png new file mode 100644 index 000000000..36cd4484f Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-style-oblique.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/auto-font-style-oblique.svg b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-style-oblique.svg new file mode 100644 index 000000000..28079ceb6 --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-style-oblique.svg @@ -0,0 +1,9 @@ + + font-style oblique auto-maps to slnt axis + + Slant + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/auto-font-weight-700.png b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-weight-700.png new file mode 100644 index 000000000..90e0d815f Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-weight-700.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/auto-font-weight-700.svg b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-weight-700.svg new file mode 100644 index 000000000..9f718eaea --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/auto-font-weight-700.svg @@ -0,0 +1,9 @@ + + font-weight auto-maps to wght axis + + Bold + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/explicit-overrides-auto.png b/crates/resvg/tests/tests/text/font-variation-settings/explicit-overrides-auto.png new file mode 100644 index 000000000..650ae9d86 Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/explicit-overrides-auto.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/explicit-overrides-auto.svg b/crates/resvg/tests/tests/text/font-variation-settings/explicit-overrides-auto.svg new file mode 100644 index 000000000..66f953d39 --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/explicit-overrides-auto.svg @@ -0,0 +1,10 @@ + + Explicit settings override font-weight auto-map + + Thin + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/grad-negative.png b/crates/resvg/tests/tests/text/font-variation-settings/grad-negative.png new file mode 100644 index 000000000..133a33f07 Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/grad-negative.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/grad-negative.svg b/crates/resvg/tests/tests/text/font-variation-settings/grad-negative.svg new file mode 100644 index 000000000..b9fc20712 --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/grad-negative.svg @@ -0,0 +1,10 @@ + + `font-variation-settings: "GRAD" -200` + + Grade + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/multiple-axes.png b/crates/resvg/tests/tests/text/font-variation-settings/multiple-axes.png new file mode 100644 index 000000000..bd0a21e00 Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/multiple-axes.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/multiple-axes.svg b/crates/resvg/tests/tests/text/font-variation-settings/multiple-axes.svg new file mode 100644 index 000000000..04e32a7b1 --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/multiple-axes.svg @@ -0,0 +1,10 @@ + + `font-variation-settings: "wght" 700, "wdth" 75` + + Bold + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/opsz-144.png b/crates/resvg/tests/tests/text/font-variation-settings/opsz-144.png new file mode 100644 index 000000000..01f851888 Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/opsz-144.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/opsz-144.svg b/crates/resvg/tests/tests/text/font-variation-settings/opsz-144.svg new file mode 100644 index 000000000..68da8f538 --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/opsz-144.svg @@ -0,0 +1,10 @@ + + `font-variation-settings: "opsz" 144` + + Display + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/slnt-negative.png b/crates/resvg/tests/tests/text/font-variation-settings/slnt-negative.png new file mode 100644 index 000000000..36cd4484f Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/slnt-negative.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/slnt-negative.svg b/crates/resvg/tests/tests/text/font-variation-settings/slnt-negative.svg new file mode 100644 index 000000000..74f60b6fa --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/slnt-negative.svg @@ -0,0 +1,10 @@ + + `font-variation-settings: "slnt" -10` + + Slant + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/wdth-151.png b/crates/resvg/tests/tests/text/font-variation-settings/wdth-151.png new file mode 100644 index 000000000..0065f0758 Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/wdth-151.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/wdth-151.svg b/crates/resvg/tests/tests/text/font-variation-settings/wdth-151.svg new file mode 100644 index 000000000..a73f2128b --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/wdth-151.svg @@ -0,0 +1,10 @@ + + `font-variation-settings: "wdth" 151` + + Wide + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/wdth-25.png b/crates/resvg/tests/tests/text/font-variation-settings/wdth-25.png new file mode 100644 index 000000000..91c571855 Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/wdth-25.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/wdth-25.svg b/crates/resvg/tests/tests/text/font-variation-settings/wdth-25.svg new file mode 100644 index 000000000..6b690ab48 --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/wdth-25.svg @@ -0,0 +1,10 @@ + + `font-variation-settings: "wdth" 25` + + Narrow + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/wght-100.png b/crates/resvg/tests/tests/text/font-variation-settings/wght-100.png new file mode 100644 index 000000000..650ae9d86 Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/wght-100.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/wght-100.svg b/crates/resvg/tests/tests/text/font-variation-settings/wght-100.svg new file mode 100644 index 000000000..43c6c8358 --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/wght-100.svg @@ -0,0 +1,10 @@ + + `font-variation-settings: "wght" 100` + + Thin + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/wght-700.png b/crates/resvg/tests/tests/text/font-variation-settings/wght-700.png new file mode 100644 index 000000000..90e0d815f Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/wght-700.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/wght-700.svg b/crates/resvg/tests/tests/text/font-variation-settings/wght-700.svg new file mode 100644 index 000000000..f5e426f90 --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/wght-700.svg @@ -0,0 +1,10 @@ + + `font-variation-settings: "wght" 700` + + Bold + + + + diff --git a/crates/resvg/tests/tests/text/font-variation-settings/xtra-extreme.png b/crates/resvg/tests/tests/text/font-variation-settings/xtra-extreme.png new file mode 100644 index 000000000..c4e2511c5 Binary files /dev/null and b/crates/resvg/tests/tests/text/font-variation-settings/xtra-extreme.png differ diff --git a/crates/resvg/tests/tests/text/font-variation-settings/xtra-extreme.svg b/crates/resvg/tests/tests/text/font-variation-settings/xtra-extreme.svg new file mode 100644 index 000000000..a2f20e5c1 --- /dev/null +++ b/crates/resvg/tests/tests/text/font-variation-settings/xtra-extreme.svg @@ -0,0 +1,10 @@ + + `font-variation-settings: "XTRA" 603` + + Wide + + + + diff --git a/crates/resvg/tests/tests/text/hinting-options/all-options-comparison.png b/crates/resvg/tests/tests/text/hinting-options/all-options-comparison.png new file mode 100644 index 000000000..2dc89f481 Binary files /dev/null and b/crates/resvg/tests/tests/text/hinting-options/all-options-comparison.png differ diff --git a/crates/resvg/tests/tests/text/hinting-options/all-options-comparison.svg b/crates/resvg/tests/tests/text/hinting-options/all-options-comparison.svg new file mode 100644 index 000000000..aa1aedd46 --- /dev/null +++ b/crates/resvg/tests/tests/text/hinting-options/all-options-comparison.svg @@ -0,0 +1,26 @@ + + + target:smooth + target:mono + unhinted + + mode:normal + mode:light + mode:lcd + + engine:auto + engine:native + engine:fallback + + symmetric:true + symmetric:false + + linear:false + linear:true + + mode:vertical-lcd + + Combined: mono + native engine + All hinting options demonstration + + diff --git a/crates/resvg/tests/tests/text/hinting-options/css-style-syntax.png b/crates/resvg/tests/tests/text/hinting-options/css-style-syntax.png new file mode 100644 index 000000000..fe553ecaf Binary files /dev/null and b/crates/resvg/tests/tests/text/hinting-options/css-style-syntax.png differ diff --git a/crates/resvg/tests/tests/text/hinting-options/css-style-syntax.svg b/crates/resvg/tests/tests/text/hinting-options/css-style-syntax.svg new file mode 100644 index 000000000..f25344c55 --- /dev/null +++ b/crates/resvg/tests/tests/text/hinting-options/css-style-syntax.svg @@ -0,0 +1,17 @@ + + Hinting options via CSS style attribute + + + XML attr: Hamburgefonstiv 0O Il1 + + + CSS style: Hamburgefonstiv 0O Il1 + + + Unhinted: Hamburgefonstiv 0O Il1 S5 Z2 + + XML vs CSS syntax (should match) + diff --git a/crates/resvg/tests/tests/text/hinting-options/eink-mono-clarity.png b/crates/resvg/tests/tests/text/hinting-options/eink-mono-clarity.png new file mode 100644 index 000000000..3663c503c Binary files /dev/null and b/crates/resvg/tests/tests/text/hinting-options/eink-mono-clarity.png differ diff --git a/crates/resvg/tests/tests/text/hinting-options/eink-mono-clarity.svg b/crates/resvg/tests/tests/text/hinting-options/eink-mono-clarity.svg new file mode 100644 index 000000000..68f663293 --- /dev/null +++ b/crates/resvg/tests/tests/text/hinting-options/eink-mono-clarity.svg @@ -0,0 +1,9 @@ + + 12px: The quick brown fox jumps 0123456789 + 14px: The quick brown fox jumps 012345 + 16px: Quick brown fox 012345 + diff --git a/crates/resvg/tests/tests/text/hinting-options/eink-mono-engine.png b/crates/resvg/tests/tests/text/hinting-options/eink-mono-engine.png new file mode 100644 index 000000000..bb928bcec Binary files /dev/null and b/crates/resvg/tests/tests/text/hinting-options/eink-mono-engine.png differ diff --git a/crates/resvg/tests/tests/text/hinting-options/eink-mono-engine.svg b/crates/resvg/tests/tests/text/hinting-options/eink-mono-engine.svg new file mode 100644 index 000000000..146f80f87 --- /dev/null +++ b/crates/resvg/tests/tests/text/hinting-options/eink-mono-engine.svg @@ -0,0 +1,10 @@ + + Auto engine: Hamburgefonstiv 0O Il1 + Native engine: Hamburgefonstiv 0O Il1 + Auto-fallback: Hamburgefonstiv 0O Il1 + Engine comparison at 14px mono + diff --git a/crates/resvg/tests/tests/text/hinting-options/eink-mono-target.png b/crates/resvg/tests/tests/text/hinting-options/eink-mono-target.png new file mode 100644 index 000000000..30016ed98 Binary files /dev/null and b/crates/resvg/tests/tests/text/hinting-options/eink-mono-target.png differ diff --git a/crates/resvg/tests/tests/text/hinting-options/eink-mono-target.svg b/crates/resvg/tests/tests/text/hinting-options/eink-mono-target.svg new file mode 100644 index 000000000..10d601238 --- /dev/null +++ b/crates/resvg/tests/tests/text/hinting-options/eink-mono-target.svg @@ -0,0 +1,10 @@ + + Mono: Hamburgefonstiv 0O Il1 + Smooth: Hamburgefonstiv 0O Il1 + Unhinted: Hamburgefonstiv 0O Il1 + Target comparison at 14px + diff --git a/crates/resvg/tests/tests/text/hinting-options/engine-auto.png b/crates/resvg/tests/tests/text/hinting-options/engine-auto.png new file mode 100644 index 000000000..3c38b738f Binary files /dev/null and b/crates/resvg/tests/tests/text/hinting-options/engine-auto.png differ diff --git a/crates/resvg/tests/tests/text/hinting-options/engine-auto.svg b/crates/resvg/tests/tests/text/hinting-options/engine-auto.svg new file mode 100644 index 000000000..67409efd1 --- /dev/null +++ b/crates/resvg/tests/tests/text/hinting-options/engine-auto.svg @@ -0,0 +1,17 @@ + + Hinting engine: auto (force autohinter) + + + Auto: Hamburgefonstiv 0O Il1 S5 Z2 + + + Native: Hamburgefonstiv 0O Il1 S5 Z2 + + + Fallback: Hamburgefonstiv 0O Il1 S5 Z2 + + Engine comparison at 12px + diff --git a/crates/resvg/tests/tests/text/hinting-options/engine-native.png b/crates/resvg/tests/tests/text/hinting-options/engine-native.png new file mode 100644 index 000000000..69c448b30 Binary files /dev/null and b/crates/resvg/tests/tests/text/hinting-options/engine-native.png differ diff --git a/crates/resvg/tests/tests/text/hinting-options/engine-native.svg b/crates/resvg/tests/tests/text/hinting-options/engine-native.svg new file mode 100644 index 000000000..afba80c39 --- /dev/null +++ b/crates/resvg/tests/tests/text/hinting-options/engine-native.svg @@ -0,0 +1,17 @@ + + Hinting engine: native interpreter + + + Native: Hamburgefonstiv 0O Il1 S5 Z2 + + + Auto-fallback: Hamburgefonstiv 0O Il1 + + + Unhinted: Hamburgefonstiv 0O Il1 S5 Z2 + + Native vs auto-fallback at 12px + diff --git a/crates/resvg/tests/tests/text/hinting-options/mode-lcd.png b/crates/resvg/tests/tests/text/hinting-options/mode-lcd.png new file mode 100644 index 000000000..8955d29aa Binary files /dev/null and b/crates/resvg/tests/tests/text/hinting-options/mode-lcd.png differ diff --git a/crates/resvg/tests/tests/text/hinting-options/mode-lcd.svg b/crates/resvg/tests/tests/text/hinting-options/mode-lcd.svg new file mode 100644 index 000000000..47b7c9872 --- /dev/null +++ b/crates/resvg/tests/tests/text/hinting-options/mode-lcd.svg @@ -0,0 +1,17 @@ + + Hinting mode: lcd subpixel optimization + + + Normal: Hamburgefonstiv 0O Il1 S5 Z2 + + + LCD: Hamburgefonstiv 0O Il1 S5 Z2 + + + Unhinted: Hamburgefonstiv 0O Il1 S5 Z2 + + LCD subpixel mode at 12px + diff --git a/crates/resvg/tests/tests/text/hinting-options/mode-light.png b/crates/resvg/tests/tests/text/hinting-options/mode-light.png new file mode 100644 index 000000000..bb7f6269a Binary files /dev/null and b/crates/resvg/tests/tests/text/hinting-options/mode-light.png differ diff --git a/crates/resvg/tests/tests/text/hinting-options/mode-light.svg b/crates/resvg/tests/tests/text/hinting-options/mode-light.svg new file mode 100644 index 000000000..82b267efb --- /dev/null +++ b/crates/resvg/tests/tests/text/hinting-options/mode-light.svg @@ -0,0 +1,17 @@ + + Hinting mode: light vs normal (default) + + + Normal: Hamburgefonstiv 0O Il1 S5 Z2 + + + Light: Hamburgefonstiv 0O Il1 S5 Z2 + + + Unhinted: Hamburgefonstiv 0O Il1 S5 Z2 + + Mode comparison at 12px + diff --git a/crates/resvg/tests/tests/text/hinting-options/mode-vertical-lcd.png b/crates/resvg/tests/tests/text/hinting-options/mode-vertical-lcd.png new file mode 100644 index 000000000..20d5202d2 Binary files /dev/null and b/crates/resvg/tests/tests/text/hinting-options/mode-vertical-lcd.png differ diff --git a/crates/resvg/tests/tests/text/hinting-options/mode-vertical-lcd.svg b/crates/resvg/tests/tests/text/hinting-options/mode-vertical-lcd.svg new file mode 100644 index 000000000..974dbc3f9 --- /dev/null +++ b/crates/resvg/tests/tests/text/hinting-options/mode-vertical-lcd.svg @@ -0,0 +1,17 @@ + + Hinting mode: vertical-lcd (for rotated displays) + + + Normal: Hamburgefonstiv 0O Il1 S5 Z2 + + + Vert-LCD: Hamburgefonstiv 0O Il1 S5 Z2 + + + Unhinted: Hamburgefonstiv 0O Il1 S5 Z2 + + Vertical LCD mode at 12px + diff --git a/crates/resvg/tests/tests/text/hinting-options/mono-hinted-vs-unhinted.png b/crates/resvg/tests/tests/text/hinting-options/mono-hinted-vs-unhinted.png new file mode 100644 index 000000000..4a7e8a52a Binary files /dev/null and b/crates/resvg/tests/tests/text/hinting-options/mono-hinted-vs-unhinted.png differ diff --git a/crates/resvg/tests/tests/text/hinting-options/mono-hinted-vs-unhinted.svg b/crates/resvg/tests/tests/text/hinting-options/mono-hinted-vs-unhinted.svg new file mode 100644 index 000000000..55f368df4 --- /dev/null +++ b/crates/resvg/tests/tests/text/hinting-options/mono-hinted-vs-unhinted.svg @@ -0,0 +1,6 @@ + + Mono hinted: 0O Il1 S5 Z2 + Mono unhinted: 0O Il1 S5 Z2 + Smooth hinted: 0O Il1 S5 Z2 + Unhinted: 0O Il1 S5 Z2 + diff --git a/crates/resvg/tests/tests/text/hinting-options/preserve-linear-metrics.png b/crates/resvg/tests/tests/text/hinting-options/preserve-linear-metrics.png new file mode 100644 index 000000000..28bb513a8 Binary files /dev/null and b/crates/resvg/tests/tests/text/hinting-options/preserve-linear-metrics.png differ diff --git a/crates/resvg/tests/tests/text/hinting-options/preserve-linear-metrics.svg b/crates/resvg/tests/tests/text/hinting-options/preserve-linear-metrics.svg new file mode 100644 index 000000000..2b71be22c --- /dev/null +++ b/crates/resvg/tests/tests/text/hinting-options/preserve-linear-metrics.svg @@ -0,0 +1,17 @@ + + Preserve linear metrics: prevents horizontal adjustment + + + Linear=false: Hamburgefonstiv 0O Il1 + + + Linear=true: Hamburgefonstiv 0O Il1 + + + Unhinted: Hamburgefonstiv 0O Il1 S5 Z2 + + Linear metrics preservation at 12px + diff --git a/crates/resvg/tests/tests/text/hinting-options/symmetric-false.png b/crates/resvg/tests/tests/text/hinting-options/symmetric-false.png new file mode 100644 index 000000000..d2a0f8de4 Binary files /dev/null and b/crates/resvg/tests/tests/text/hinting-options/symmetric-false.png differ diff --git a/crates/resvg/tests/tests/text/hinting-options/symmetric-false.svg b/crates/resvg/tests/tests/text/hinting-options/symmetric-false.svg new file mode 100644 index 000000000..347281b37 --- /dev/null +++ b/crates/resvg/tests/tests/text/hinting-options/symmetric-false.svg @@ -0,0 +1,17 @@ + + Symmetric rendering: false (ClearType-style narrower stems) + + + Symmetric: Hamburgefonstiv 0O Il1 + + + Asymmetric: Hamburgefonstiv 0O Il1 + + + Unhinted: Hamburgefonstiv 0O Il1 S5 Z2 + + Symmetric stem rendering at 12px + diff --git a/crates/resvg/tests/tests/text/hinting-options/target-mono-1x.png b/crates/resvg/tests/tests/text/hinting-options/target-mono-1x.png new file mode 100644 index 000000000..91824b263 Binary files /dev/null and b/crates/resvg/tests/tests/text/hinting-options/target-mono-1x.png differ diff --git a/crates/resvg/tests/tests/text/hinting-options/target-mono-1x.svg b/crates/resvg/tests/tests/text/hinting-options/target-mono-1x.svg new file mode 100644 index 000000000..522004205 --- /dev/null +++ b/crates/resvg/tests/tests/text/hinting-options/target-mono-1x.svg @@ -0,0 +1,6 @@ + + Mono 16px: Hamburgefonstiv 0O Il1 + Smooth 16px: Hamburgefonstiv 0O Il1 + Unhinted 16px: Hamburgefonstiv 0O Il1 + Target comparison at 16px + diff --git a/crates/resvg/tests/tests/text/hinting-options/target-mono.png b/crates/resvg/tests/tests/text/hinting-options/target-mono.png new file mode 100644 index 000000000..2b190e78e Binary files /dev/null and b/crates/resvg/tests/tests/text/hinting-options/target-mono.png differ diff --git a/crates/resvg/tests/tests/text/hinting-options/target-mono.svg b/crates/resvg/tests/tests/text/hinting-options/target-mono.svg new file mode 100644 index 000000000..890c8c007 --- /dev/null +++ b/crates/resvg/tests/tests/text/hinting-options/target-mono.svg @@ -0,0 +1,17 @@ + + Hinting target: mono (aliased) vs smooth (default) + + + Mono: Hamburgefonstiv 0O Il1 S5 Z2 + + + Smooth: Hamburgefonstiv 0O Il1 S5 Z2 + + + Unhinted: Hamburgefonstiv 0O Il1 S5 Z2 + + Target comparison at 12px + diff --git a/crates/resvg/tests/tests/text/hinting-options/terminus-mono-clarity.png b/crates/resvg/tests/tests/text/hinting-options/terminus-mono-clarity.png new file mode 100644 index 000000000..79d45972d Binary files /dev/null and b/crates/resvg/tests/tests/text/hinting-options/terminus-mono-clarity.png differ diff --git a/crates/resvg/tests/tests/text/hinting-options/terminus-mono-clarity.svg b/crates/resvg/tests/tests/text/hinting-options/terminus-mono-clarity.svg new file mode 100644 index 000000000..c8a3b48bf --- /dev/null +++ b/crates/resvg/tests/tests/text/hinting-options/terminus-mono-clarity.svg @@ -0,0 +1,18 @@ + + + + 12: The quick brown fox 0123456789 + 14: The quick brown fox 0123456789 + + 13: The quick brown fox 0123456789 + 15: The quick brown fox 0123456789 + + Clarity: 0O Q9 Il1 S5 Z2 B8 rn/m + 12,14=bitmap 13,15=outline + diff --git a/crates/resvg/tests/tests/text/hinting-options/terminus-mono-target.png b/crates/resvg/tests/tests/text/hinting-options/terminus-mono-target.png new file mode 100644 index 000000000..5c748d337 Binary files /dev/null and b/crates/resvg/tests/tests/text/hinting-options/terminus-mono-target.png differ diff --git a/crates/resvg/tests/tests/text/hinting-options/terminus-mono-target.svg b/crates/resvg/tests/tests/text/hinting-options/terminus-mono-target.svg new file mode 100644 index 000000000..3e21a0d86 --- /dev/null +++ b/crates/resvg/tests/tests/text/hinting-options/terminus-mono-target.svg @@ -0,0 +1,17 @@ + + + + 12px bitmap: 0O Il1 S5 Z2 + 14px bitmap: 0O Il1 S5 Z2 + 16px bitmap: 0O Il1 S5 Z2 + + 13px outline: 0O Il1 S5 Z2 + 15px outline: 0O Il1 S5 Z2 + bitmap vs outline + diff --git a/crates/resvg/tests/tests/text/text-rendering/hinting-comparison.png b/crates/resvg/tests/tests/text/text-rendering/hinting-comparison.png new file mode 100644 index 000000000..383268545 Binary files /dev/null and b/crates/resvg/tests/tests/text/text-rendering/hinting-comparison.png differ diff --git a/crates/resvg/tests/tests/text/text-rendering/hinting-comparison.svg b/crates/resvg/tests/tests/text/text-rendering/hinting-comparison.svg new file mode 100644 index 000000000..29ddc3e93 --- /dev/null +++ b/crates/resvg/tests/tests/text/text-rendering/hinting-comparison.svg @@ -0,0 +1,39 @@ + + Hinting comparison at small sizes + + + HINTED + 8px: Hinting + 10px: Hinting + 12px: Hinting + 14px: Hint + + + UNHINTED + 8px: Hinting + 10px: Hinting + 12px: Hinting + 14px: Hint + + + + + + Confusables: + 0O Il1 rnm + 0O Il1 rnm + + + Quick fox + Quick fox + + Bold 10px + Bold 10px + + Italic 9px + Italic 9px + + + + 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 26f42924f..a4d3634e7 100644 --- a/crates/usvg/Cargo.toml +++ b/crates/usvg/Cargo.toml @@ -36,20 +36,27 @@ simplecss = "0.2" siphasher = "1.0" # perfect hash implementation # text -fontdb = { version = "0.23.0", default-features = false, optional = true } -rustybuzz = { version = "0.20.1", optional = true } +fontdb = { path = "../fontdb", default-features = false, optional = true } +harfrust = { version = "0.5", optional = true } +lru = { version = "0.12", 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 } + +# PNG encoding for embedded bitmap fonts +png = { version = "0.18", optional = true } + [dev-dependencies] once_cell = "1.21" [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", "lru", "png", "skrifa", "unicode-bidi", "unicode-script", "unicode-vo"] # Enables system fonts loading. system-fonts = ["fontdb/fs", "fontdb/fontconfig"] # Enables font files memmaping for faster loading. diff --git a/crates/usvg/codegen/attributes.txt b/crates/usvg/codegen/attributes.txt index 32cac0622..4667ca88a 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 @@ -133,6 +135,11 @@ refY requiredExtensions requiredFeatures result +resvg:hinting-engine +resvg:hinting-mode +resvg:hinting-preserve-linear-metrics +resvg:hinting-symmetric +resvg:hinting-target rotate rx ry diff --git a/crates/usvg/codegen/main.rs b/crates/usvg/codegen/main.rs index 60d657e8b..2bc901e07 100644 --- a/crates/usvg/codegen/main.rs +++ b/crates/usvg/codegen/main.rs @@ -106,10 +106,25 @@ fn gen_map( let joined_names = names.iter().map(|n| to_enum_name(n)).join(",\n "); + // Collect CSS-style aliases for resvg: namespaced attributes + // resvg:hinting-target -> -resvg-hinting-target (for CSS style attribute) + let css_aliases: Vec<(String, String)> = names + .iter() + .filter(|n| n.starts_with("resvg:")) + .map(|n| { + let css_name = format!("-resvg-{}", &n[6..]); + let enum_variant = format!("{}::{}", enum_name, to_enum_name(n)); + (css_name, enum_variant) + }) + .collect(); + let mut map = phf_codegen::Map::new(); for name in &names { map.entry(*name, &format!("{}::{}", enum_name, to_enum_name(name))); } + for (css_name, enum_variant) in &css_aliases { + map.entry(css_name, enum_variant); + } let mut map_data = Vec::new(); map.build(&mut map_data)?; @@ -172,19 +187,26 @@ fn gen_map( // some-string -> SomeString // some_string -> SomeString // some:string -> SomeString +// -resvg-foo -> ResvgFoo (leading dash is skipped) // 100 -> N100 fn to_enum_name(name: &str) -> String { let mut change_case = false; + let mut is_first_alpha = true; let mut s = String::with_capacity(name.len()); - for (idx, c) in name.chars().enumerate() { - if idx == 0 { + for c in name.chars() { + // Skip leading dashes/underscores/colons + if is_first_alpha && (c == '-' || c == '_' || c == ':') { + continue; + } + + if is_first_alpha { + is_first_alpha = false; if c.is_digit(10) { s.push('N'); s.push(c); } else { s.push(c.to_uppercase().next().unwrap()); } - continue; } diff --git a/crates/usvg/src/lib.rs b/crates/usvg/src/lib.rs index b60831980..12312472c 100644 --- a/crates/usvg/src/lib.rs +++ b/crates/usvg/src/lib.rs @@ -35,6 +35,42 @@ and can focus just on the rendering part. - All filters are supported. Including filter functions, like `filter="contrast(50%)"` - Recursive elements will be detected and removed - `objectBoundingBox` will be replaced with `userSpaceOnUse` +- Variable fonts are supported via `font-variation-settings` CSS property and automatic + mapping of `font-weight`, `font-stretch`, and `font-style` to variation axes +- Font hinting is supported and controlled by the `text-rendering` property: + `geometricPrecision` disables hinting, `optimizeLegibility`/`optimizeSpeed` enable it +- `font-optical-sizing` is supported for automatic optical size axis adjustment + +## Resvg Extensions + +usvg supports custom properties for fine-grained control over font hinting. +These can be specified as SVG attributes (using the `resvg:` namespace) or as +CSS properties (using the `-resvg-` vendor prefix). + +### Hinting Properties + +| Property | Values | Default | Description | +|----------|--------|---------|-------------| +| `resvg:hinting-target` | `smooth`, `mono` | `smooth` | Target rasterization type | +| `resvg:hinting-mode` | `normal`, `light`, `lcd`, `vertical-lcd` | `normal` | Hinting strength | +| `resvg:hinting-engine` | `auto`, `native`, `auto-fallback` | `auto-fallback` | Hinting engine | +| `resvg:hinting-symmetric` | `true`, `false` | `true` | Symmetric rendering | +| `resvg:hinting-preserve-linear-metrics` | `true`, `false` | `false` | Preserve glyph spacing | + +### Example + +```xml + + + Crisp text + + + Crisp text + +``` + +See [`HintingSettings`] for detailed documentation of each property. ## Limitations diff --git a/crates/usvg/src/main.rs b/crates/usvg/src/main.rs index 8dcaca08f..5b2bbc106 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 c2d13256c..60902c75a 100644 --- a/crates/usvg/src/parser/converter.rs +++ b/crates/usvg/src/parser/converter.rs @@ -3,6 +3,8 @@ use std::collections::{HashMap, HashSet}; use std::hash::{Hash, Hasher}; +#[cfg(feature = "text")] +use std::num::NonZeroUsize; use std::str::FromStr; use std::sync::Arc; @@ -11,7 +13,9 @@ use fontdb::Database; #[cfg(feature = "text")] use fontdb::ID; #[cfg(feature = "text")] -use rustybuzz::ttf_parser::GlyphId; +use lru::LruCache; +#[cfg(feature = "text")] +use skrifa::GlyphId; use svgtypes::{Length, LengthUnit as Unit, PaintOrderKind, TransformOrigin}; use tiny_skia_path::PathBuilder; @@ -42,21 +46,97 @@ pub struct State<'a> { pub(crate) opt: &'a Options<'a>, } -#[derive(Clone)] +/// Cache key for glyph outlines that captures all parameters affecting the outline shape. +#[cfg(feature = "text")] +#[derive(Clone, Hash, PartialEq, Eq, Debug)] +pub struct OutlineCacheKey { + /// Font database ID + pub font_id: ID, + /// Glyph ID within the font + pub glyph_id: GlyphId, + /// PPEM for hinting (None = unhinted), stored as f32::to_bits() for exact matching + pub ppem_bits: Option, + /// Hinting target (None = unhinted) + pub hinting_target: Option, + /// Hinting mode (None = unhinted) + pub hinting_mode: Option, + /// Hinting engine (None = unhinted) + pub hinting_engine: Option, + /// Symmetric rendering flag + pub symmetric_rendering: bool, + /// Preserve linear metrics flag + pub preserve_linear_metrics: bool, + /// Hash of variation coordinates (for consistent lookup) + pub variation_hash: u64, +} + +/// Statistics for cache usage tracking. +#[cfg(feature = "text")] +#[derive(Clone, Default, Debug)] +pub struct CacheStats { + /// Number of cache hits + pub outline_hits: usize, + /// Number of cache misses + pub outline_misses: usize, + /// Number of entries evicted due to LRU + pub outline_evictions: usize, +} + +/// Default outline cache capacity (number of entries). +#[cfg(feature = "text")] +pub const DEFAULT_OUTLINE_CACHE_CAPACITY: usize = 10_000; + +/// Computes a hash of variation coordinates for cache key generation. +/// +/// This creates a stable hash from font variations and auto-opsz settings +/// that can be used as part of a cache key. +#[cfg(feature = "text")] +pub fn compute_variation_hash( + variations: &[FontVariation], + font_optical_sizing: FontOpticalSizing, + font_size: f32, + has_opsz_axis: bool, +) -> u64 { + use std::collections::hash_map::DefaultHasher; + + let mut hasher = DefaultHasher::new(); + + // Sort variations by tag for consistent ordering + let mut sorted: Vec<_> = variations.iter().collect(); + sorted.sort_by_key(|v| v.tag); + + for v in sorted { + v.tag.hash(&mut hasher); + v.value.to_bits().hash(&mut hasher); + } + + // Include auto-opsz if applicable + if font_optical_sizing == FontOpticalSizing::Auto && has_opsz_axis { + b"opsz".hash(&mut hasher); + font_size.to_bits().hash(&mut hasher); + } + + hasher.finish() +} + pub struct Cache { /// This fontdb is initialized from [`Options::fontdb`] and then populated /// over the course of conversion. #[cfg(feature = "text")] pub fontdb: Arc, + /// LRU cache for glyph outlines, keyed by all parameters affecting outline shape. + #[cfg(feature = "text")] + cache_outline: LruCache>, + /// Statistics for cache usage tracking. #[cfg(feature = "text")] - cache_outline: HashMap<(ID, GlyphId), Option>, + cache_stats: CacheStats, #[cfg(feature = "text")] cache_colr: HashMap<(ID, GlyphId), Option>, #[cfg(feature = "text")] cache_svg: HashMap<(ID, GlyphId), Option>, #[cfg(feature = "text")] - cache_raster: HashMap<(ID, GlyphId), Option>, + cache_raster: HashMap<(ID, GlyphId, u16), Option>, pub clip_paths: HashMap>, pub masks: HashMap>, @@ -92,13 +172,20 @@ macro_rules! font_lookup { } impl Cache { - pub(crate) fn new(#[cfg(feature = "text")] fontdb: Arc) -> Self { + pub(crate) fn new( + #[cfg(feature = "text")] fontdb: Arc, + #[cfg(feature = "text")] outline_cache_capacity: usize, + ) -> Self { Self { #[cfg(feature = "text")] fontdb, #[cfg(feature = "text")] - cache_outline: HashMap::new(), + cache_outline: LruCache::new( + NonZeroUsize::new(outline_cache_capacity).unwrap_or(NonZeroUsize::MIN), + ), + #[cfg(feature = "text")] + cache_stats: CacheStats::default(), #[cfg(feature = "text")] cache_colr: HashMap::new(), #[cfg(feature = "text")] @@ -200,10 +287,96 @@ impl Cache { } } - font_lookup!(fontdb_outline, cache_outline, outline, tiny_skia_path::Path); + /// Get or compute a glyph outline with full caching support. + /// + /// This method handles all outline types (simple, variable, hinted) through a unified + /// cache keyed by all parameters that affect the outline shape. The `compute` closure + /// is only called on cache miss. + #[cfg(feature = "text")] + pub fn get_or_compute_outline( + &mut self, + key: OutlineCacheKey, + compute: F, + ) -> Option + where + F: FnOnce() -> Option, + { + // Check cache first (get() also updates LRU order) + if let Some(cached) = self.cache_outline.get(&key) { + self.cache_stats.outline_hits += 1; + return cached.clone(); + } + + // Compute on miss + self.cache_stats.outline_misses += 1; + let result = compute(); + + // Track evictions (if cache is at capacity, next put will evict) + if self.cache_outline.len() == self.cache_outline.cap().get() { + self.cache_stats.outline_evictions += 1; + } + + self.cache_outline.put(key, result.clone()); + result + } + font_lookup!(fontdb_colr, cache_colr, colr, Tree); font_lookup!(fontdb_svg, cache_svg, svg, Node); - font_lookup!(fontdb_raster, cache_raster, raster, BitmapImage); + + #[cfg(feature = "text")] + pub(crate) fn fontdb_raster( + &mut self, + font: ID, + glyph: GlyphId, + font_size: f32, + ) -> Option { + // Round font_size to u16 for cache key (sub-pixel precision not needed for strike selection) + let size_key = font_size.round() as u16; + let key = (font, glyph, size_key); + match self.cache_raster.get(&key) { + Some(cache_hit) => cache_hit.clone(), + None => { + let lookup = self.fontdb.raster(font, glyph, font_size); + self.cache_raster.insert(key, lookup.clone()); + lookup + } + } + } + + /// Returns the current cache statistics. + #[cfg(feature = "text")] + pub fn outline_cache_stats(&self) -> &CacheStats { + &self.cache_stats + } + + /// Clears all cached outlines and resets statistics. + #[cfg(feature = "text")] + pub fn clear_outline_cache(&mut self) { + self.cache_outline.clear(); + self.cache_stats = CacheStats::default(); + } + + /// Resizes the outline cache capacity. + /// + /// If the new capacity is smaller than the current number of entries, + /// the least recently used entries will be evicted. + #[cfg(feature = "text")] + pub fn resize_outline_cache(&mut self, new_capacity: usize) { + self.cache_outline + .resize(NonZeroUsize::new(new_capacity).unwrap_or(NonZeroUsize::MIN)); + } + + /// Returns the current number of cached outline entries. + #[cfg(feature = "text")] + pub fn outline_cache_len(&self) -> usize { + self.cache_outline.len() + } + + /// Returns the current outline cache capacity. + #[cfg(feature = "text")] + pub fn outline_cache_capacity(&self) -> usize { + self.cache_outline.cap().get() + } } // TODO: is there a simpler way? @@ -380,6 +553,8 @@ pub(crate) fn convert_doc(svg_doc: &svgtree::Document, opt: &Options) -> Result< let mut cache = Cache::new( #[cfg(feature = "text")] opt.fontdb.clone(), + #[cfg(feature = "text")] + DEFAULT_OUTLINE_CACHE_CAPACITY, ); for node in svg_doc.descendants() { diff --git a/crates/usvg/src/parser/mod.rs b/crates/usvg/src/parser/mod.rs index b3fbccdd6..a2098930c 100644 --- a/crates/usvg/src/parser/mod.rs +++ b/crates/usvg/src/parser/mod.rs @@ -19,8 +19,12 @@ mod use_node; #[cfg(feature = "text")] mod text; #[cfg(feature = "text")] -pub(crate) use converter::Cache; +pub(crate) use converter::{ + Cache, CacheStats, DEFAULT_OUTLINE_CACHE_CAPACITY, OutlineCacheKey, compute_variation_hash, +}; 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 +140,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..fc9d47aeb 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, + + /// Deprecated: This field is no longer used. + /// + /// In SVG, font-size is specified in user units (pixels), so ppem equals + /// font_size directly. Hinting is applied at the source coordinate scale. + /// For pixel-perfect output, render at 1:1 scale or integer zoom factors. + /// + /// This field is kept for API compatibility but has no effect. + pub dpi: Option, +} + +#[cfg(feature = "text")] +impl Default for HintingOptions { + fn default() -> Self { + Self { + // 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 a48b732d5..970f04e12 100644 --- a/crates/usvg/src/parser/svgtree/mod.rs +++ b/crates/usvg/src/parser/svgtree/mod.rs @@ -706,12 +706,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 @@ -727,6 +729,11 @@ impl AId { | AId::Opacity | AId::Overflow | AId::PaintOrder + | AId::ResvgHintingEngine // resvg extension + | AId::ResvgHintingMode // resvg extension + | AId::ResvgHintingPreserveLinearMetrics // resvg extension + | AId::ResvgHintingSymmetric // resvg extension + | AId::ResvgHintingTarget // resvg extension | AId::ShapeRendering | AId::StopColor | AId::StopOpacity @@ -782,6 +789,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..6b1118799 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, @@ -298,6 +300,11 @@ pub enum AId { RequiredExtensions, RequiredFeatures, Result, + ResvgHintingEngine, + ResvgHintingMode, + ResvgHintingPreserveLinearMetrics, + ResvgHintingSymmetric, + ResvgHintingTarget, Rotate, Rx, Ry, @@ -375,261 +382,276 @@ pub enum AId { } static ATTRIBUTES: Map = Map { - key: 3347381344252206323, + key: 3213172566270843353, disps: &[ - (0, 111), (0, 2), - (0, 45), - (0, 5), - (0, 1), - (2, 56), - (0, 5), - (2, 99), - (13, 198), - (0, 61), - (0, 52), - (1, 29), + (0, 50), + (0, 112), + (4, 61), + (0, 12), + (0, 199), (0, 21), - (0, 70), - (0, 164), - (2, 60), - (3, 52), - (0, 1), - (0, 86), - (0, 10), + (0, 2), + (1, 23), + (0, 76), (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, 216), + (0, 37), + (1, 138), + (0, 55), (1, 1), + (0, 5), + (0, 90), + (3, 104), + (0, 104), + (1, 115), + (1, 194), + (2, 129), + (4, 53), + (0, 112), + (0, 30), + (1, 3), + (1, 46), + (0, 1), + (0, 20), + (0, 54), + (6, 7), + (1, 129), + (0, 2), + (1, 3), + (1, 167), + (7, 106), + (0, 59), + (0, 15), + (1, 5), + (12, 201), + (16, 157), + (0, 47), + (0, 6), + (4, 38), ], 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), + ("href", AId::Href), + ("in", AId::In), + ("font-feature-settings", AId::FontFeatureSettings), + ("stroke-miterlimit", AId::StrokeMiterlimit), + ("clipPathUnits", AId::ClipPathUnits), + ("targetY", AId::TargetY), + ("-resvg-hinting-mode", AId::ResvgHintingMode), ("scale", AId::Scale), - ("x2", AId::X2), - ("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), - ("gradientUnits", AId::GradientUnits), - ("exponent", AId::Exponent), - ("text-decoration-color", AId::TextDecorationColor), - ("refX", AId::RefX), - ("enable-background", AId::EnableBackground), + ("patternContentUnits", AId::PatternContentUnits), + ("clip-path", AId::ClipPath), + ("white-space", AId::WhiteSpace), + ("pointsAtY", AId::PointsAtY), + ("in2", AId::In2), + ("fy", AId::Fy), + ("color-interpolation", AId::ColorInterpolation), + ("color", AId::Color), ("mask-border-width", AId::MaskBorderWidth), - ("numOctaves", AId::NumOctaves), - ("kerning", AId::Kerning), + ("mask-border", AId::MaskBorder), + ("inline-size", AId::InlineSize), + ("mask-border-outset", AId::MaskBorderOutset), + ("textLength", AId::TextLength), + ("x1", AId::X1), + ("mask-composite", AId::MaskComposite), + ("width", AId::Width), + ("line-height", AId::LineHeight), + ("text-decoration-line", AId::TextDecorationLine), + ("ry", AId::Ry), ("mix-blend-mode", AId::MixBlendMode), - ("mask-clip", AId::MaskClip), - ("mask-mode", AId::MaskMode), - ("type", AId::Type), - ("class", AId::Class), - ("font", AId::Font), - ("mask-border-repeat", AId::MaskBorderRepeat), - ("stroke-miterlimit", AId::StrokeMiterlimit), - ("text-decoration-stroke", AId::TextDecorationStroke), - ("z", AId::Z), - ("dx", AId::Dx), - ("clip-path", AId::ClipPath), - ("markerHeight", AId::MarkerHeight), - ("text-underline-position", AId::TextUnderlinePosition), - ("stdDeviation", AId::StdDeviation), - ("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), - ("direction", AId::Direction), - ("font-variant-numeric", AId::FontVariantNumeric), - ("startOffset", AId::StartOffset), + ("x2", AId::X2), + ("stitchTiles", AId::StitchTiles), + ("fr", AId::Fr), ("maskUnits", AId::MaskUnits), - ("font-variant", AId::FontVariant), - ("text-orientation", AId::TextOrientation), - ("amplitude", AId::Amplitude), + ("k2", AId::K2), + ("visibility", AId::Visibility), + ("preserveAspectRatio", AId::PreserveAspectRatio), ("rx", AId::Rx), - ("mask-type", AId::MaskType), - ("filter", AId::Filter), - ("in", AId::In), - ("display", AId::Display), - ("seed", AId::Seed), + ("surfaceScale", AId::SurfaceScale), + ("text-decoration", AId::TextDecoration), + ("maskContentUnits", AId::MaskContentUnits), + ("letter-spacing", AId::LetterSpacing), + ("flood-opacity", AId::FloodOpacity), + ("background-color", AId::BackgroundColor), ("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), + ("requiredExtensions", AId::RequiredExtensions), + ("font-weight", AId::FontWeight), + ("stdDeviation", AId::StdDeviation), + ("intercept", AId::Intercept), + ("direction", AId::Direction), + ("azimuth", AId::Azimuth), ("isolation", AId::Isolation), - ("gradientTransform", AId::GradientTransform), - ("transform-box", AId::TransformBox), - ("pointsAtY", AId::PointsAtY), - ("text-decoration-line", AId::TextDecorationLine), - ("requiredFeatures", AId::RequiredFeatures), - ("patternContentUnits", AId::PatternContentUnits), + ("baseFrequency", AId::BaseFrequency), ("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), - ("k4", AId::K4), - ("systemLanguage", AId::SystemLanguage), - ("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), + ("writing-mode", AId::WritingMode), + ("id", AId::Id), + ("font-variant-numeric", AId::FontVariantNumeric), + ("marker-end", AId::MarkerEnd), + ("font-optical-sizing", AId::FontOpticalSizing), ("cx", AId::Cx), - ("alignment-baseline", AId::AlignmentBaseline), - ("font-kerning", AId::FontKerning), - ("requiredExtensions", AId::RequiredExtensions), - ("clip-rule", AId::ClipRule), - ("mask-border-outset", AId::MaskBorderOutset), - ("primitiveUnits", AId::PrimitiveUnits), - ("textLength", AId::TextLength), - ("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), + ("amplitude", AId::Amplitude), + ("fill", AId::Fill), + ("stroke-dasharray", AId::StrokeDasharray), + ("result", AId::Result), ("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), - ("clip", AId::Clip), - ("fx", AId::Fx), - ("visibility", AId::Visibility), - ("shape-margin", AId::ShapeMargin), - ("font-style", AId::FontStyle), - ("y2", AId::Y2), - ("dy", AId::Dy), - ("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), + ("mask-border-mode", AId::MaskBorderMode), + ("mask-type", AId::MaskType), + ("stroke-linecap", AId::StrokeLinecap), + ("baseline-shift", AId::BaselineShift), + ("gradientUnits", AId::GradientUnits), + ("clip-rule", AId::ClipRule), + ("font-variant-east-asian", AId::FontVariantEastAsian), + ("mask-border-source", AId::MaskBorderSource), + ("text-align", AId::TextAlign), ("shape-image-threshold", AId::ShapeImageThreshold), - ("image-rendering", AId::ImageRendering), - ("marker-mid", AId::MarkerMid), - ("filterUnits", AId::FilterUnits), - ("bias", AId::Bias), + ("stroke-linejoin", AId::StrokeLinejoin), + ("transform", AId::Transform), + ("type", AId::Type), + ("mask-position", AId::MaskPosition), ("mask-border-slice", AId::MaskBorderSlice), - ("pointsAtX", AId::PointsAtX), + ("resvg:hinting-symmetric", AId::ResvgHintingSymmetric), + ("font", AId::Font), + ("color-interpolation-filters", AId::ColorInterpolationFilters), ("kernelMatrix", AId::KernelMatrix), - ("color-interpolation", AId::ColorInterpolation), + ("resvg:hinting-engine", AId::ResvgHintingEngine), + ("text-decoration-color", AId::TextDecorationColor), + ("unicode-bidi", AId::UnicodeBidi), + ("preserveAlpha", AId::PreserveAlpha), + ("slope", AId::Slope), + ("font-size-adjust", AId::FontSizeAdjust), + ("radius", AId::Radius), + ("d", AId::D), + ("dx", AId::Dx), + ("mask-mode", AId::MaskMode), + ("viewBox", AId::ViewBox), + ("mode", AId::Mode), + ("font-variation-settings", AId::FontVariationSettings), + ("y2", AId::Y2), + ("pointsAtZ", AId::PointsAtZ), + ("mask-clip", AId::MaskClip), + ("markerWidth", AId::MarkerWidth), + ("font-variant-ligatures", AId::FontVariantLigatures), + ("exponent", AId::Exponent), + ("fx", AId::Fx), + ("enable-background", AId::EnableBackground), + ("font-variant", AId::FontVariant), + ("startOffset", AId::StartOffset), + ("markerUnits", AId::MarkerUnits), + ("systemLanguage", AId::SystemLanguage), + ("opacity", AId::Opacity), + ("text-decoration-style", AId::TextDecorationStyle), + ("font-size", AId::FontSize), + ("stop-opacity", AId::StopOpacity), ("glyph-orientation-vertical", AId::GlyphOrientationVertical), - ("color", AId::Color), + ("mask-origin", AId::MaskOrigin), + ("x", AId::X), + ("shape-margin", AId::ShapeMargin), + ("shape-rendering", AId::ShapeRendering), + ("height", AId::Height), ("patternTransform", AId::PatternTransform), - ("kernelUnitLength", AId::KernelUnitLength), - ("markerUnits", AId::MarkerUnits), - ("font-weight", AId::FontWeight), - ("overflow", AId::Overflow), - ("stop-color", AId::StopColor), - ("r", AId::R), - ("k2", AId::K2), + ("-resvg-hinting-symmetric", AId::ResvgHintingSymmetric), + ("vector-effect", AId::VectorEffect), + ("offset", AId::Offset), ("text-anchor", AId::TextAnchor), - ("inline-size", AId::InlineSize), - ("unicode-bidi", AId::UnicodeBidi), - ("font-family", AId::FontFamily), - ("color-interpolation-filters", AId::ColorInterpolationFilters), - ("slope", AId::Slope), - ("baseFrequency", AId::BaseFrequency), - ("transform", AId::Transform), + ("text-indent", AId::TextIndent), + ("font-synthesis", AId::FontSynthesis), + ("r", AId::R), + ("font-variant-caps", AId::FontVariantCaps), + ("kerning", AId::Kerning), + ("paint-order", AId::PaintOrder), + ("stroke-width", AId::StrokeWidth), + ("mask", AId::Mask), + ("text-decoration-fill", AId::TextDecorationFill), + ("-resvg-hinting-target", AId::ResvgHintingTarget), + ("-resvg-hinting-engine", AId::ResvgHintingEngine), + ("dominant-baseline", AId::DominantBaseline), + ("primitiveUnits", AId::PrimitiveUnits), + ("word-spacing", AId::WordSpacing), + ("kernelUnitLength", AId::KernelUnitLength), + ("style", AId::Style), + ("stroke", AId::Stroke), ("text-rendering", AId::TextRendering), - ("divisor", AId::Divisor), + ("color-rendering", AId::ColorRendering), ("edgeMode", AId::EdgeMode), - ("letter-spacing", AId::LetterSpacing), - ("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), + ("clip", AId::Clip), + ("elevation", AId::Elevation), + ("mask-size", AId::MaskSize), + ("resvg:hinting-target", AId::ResvgHintingTarget), + ("refX", AId::RefX), + ("y1", AId::Y1), + ("dy", AId::Dy), ("pathLength", AId::PathLength), - ("lighting-color", AId::LightingColor), - ("mask-position", AId::MaskPosition), - ("stroke-dasharray", AId::StrokeDasharray), - ("text-decoration", AId::TextDecoration), + ("spreadMethod", AId::SpreadMethod), + ("z", AId::Z), + ("font-stretch", AId::FontStretch), + ("transform-box", AId::TransformBox), + ("text-align-last", AId::TextAlignLast), + ("class", AId::Class), + ("requiredFeatures", AId::RequiredFeatures), + ("divisor", AId::Divisor), + ("tableValues", AId::TableValues), + ("k3", AId::K3), + ("path", AId::Path), + ("text-orientation", AId::TextOrientation), + ("transform-origin", AId::TransformOrigin), + ("color-profile", AId::ColorProfile), + ("rotate", AId::Rotate), + ("alignment-baseline", AId::AlignmentBaseline), + ("marker-mid", AId::MarkerMid), + ("glyph-orientation-horizontal", AId::GlyphOrientationHorizontal), ("stroke-opacity", AId::StrokeOpacity), - ("targetY", AId::TargetY), - ("flood-opacity", AId::FloodOpacity), + ("shape-inside", AId::ShapeInside), + ("filter", AId::Filter), + ("resvg:hinting-mode", AId::ResvgHintingMode), + ("pointsAtX", AId::PointsAtX), + ("stop-color", AId::StopColor), + ("targetX", AId::TargetX), + ("markerHeight", AId::MarkerHeight), + ("filterUnits", AId::FilterUnits), + ("refY", AId::RefY), + ("order", AId::Order), + ("fill-rule", AId::FillRule), ("diffuseConstant", AId::DiffuseConstant), + ("mask-border-repeat", AId::MaskBorderRepeat), + ("display", AId::Display), + ("specularExponent", AId::SpecularExponent), + ("orient", AId::Orient), + ("font-kerning", AId::FontKerning), + ("k4", AId::K4), + ("bias", AId::Bias), + ("text-underline-position", AId::TextUnderlinePosition), + ("marker-start", AId::MarkerStart), + ("text-overflow", AId::TextOverflow), + ("image-rendering", AId::ImageRendering), + ("font-variant-position", AId::FontVariantPosition), + ("specularConstant", AId::SpecularConstant), + ("values", AId::Values), + ("font-family", AId::FontFamily), + ("-resvg-hinting-preserve-linear-metrics", AId::ResvgHintingPreserveLinearMetrics), + ("limitingConeAngle", AId::LimitingConeAngle), + ("overflow", AId::Overflow), + ("flood-color", AId::FloodColor), + ("operator", AId::Operator), + ("seed", AId::Seed), + ("resvg:hinting-preserve-linear-metrics", AId::ResvgHintingPreserveLinearMetrics), + ("stroke-dashoffset", AId::StrokeDashoffset), + ("shape-subtract", AId::ShapeSubtract), + ("xChannelSelector", AId::XChannelSelector), + ("font-style", AId::FontStyle), + ("yChannelSelector", AId::YChannelSelector), + ("lighting-color", AId::LightingColor), + ("gradientTransform", AId::GradientTransform), + ("lengthAdjust", AId::LengthAdjust), + ("numOctaves", AId::NumOctaves), + ("fill-opacity", AId::FillOpacity), + ("k1", AId::K1), + ("text-decoration-stroke", AId::TextDecorationStroke), + ("side", AId::Side), + ("cy", AId::Cy), + ("y", AId::Y), + ("patternUnits", AId::PatternUnits), ], }; diff --git a/crates/usvg/src/parser/svgtree/parse.rs b/crates/usvg/src/parser/svgtree/parse.rs index 2eae321b6..4510c004c 100644 --- a/crates/usvg/src/parser/svgtree/parse.rs +++ b/crates/usvg/src/parser/svgtree/parse.rs @@ -12,6 +12,7 @@ use super::{AId, Attribute, Document, EId, NodeData, NodeId, NodeKind, ShortRang const SVG_NS: &str = "http://www.w3.org/2000/svg"; const XLINK_NS: &str = "http://www.w3.org/1999/xlink"; const XML_NAMESPACE_NS: &str = "http://www.w3.org/XML/1998/namespace"; +const RESVG_NS: &str = "https://resvg.io/ns"; impl<'input> Document<'input> { /// Parses a [`Document`] from a [`roxmltree::Document`]. @@ -232,11 +233,19 @@ pub(crate) fn parse_svg_element<'input>( // Copy presentational attributes first. for attr in xml_node.attributes() { match attr.namespace() { - None | Some(SVG_NS) | Some(XLINK_NS) | Some(XML_NAMESPACE_NS) => {} + None | Some(SVG_NS) | Some(XLINK_NS) | Some(XML_NAMESPACE_NS) | Some(RESVG_NS) => {} _ => continue, } - let aid = match AId::from_str(attr.name()) { + // For resvg namespace attributes, we need to use the prefixed name for lookup + // because the AId enum uses names like "resvg:hinting-target" + let attr_name = if attr.namespace() == Some(RESVG_NS) { + format!("resvg:{}", attr.name()) + } else { + attr.name().to_string() + }; + + let aid = match AId::from_str(&attr_name) { Some(v) => v, None => continue, }; diff --git a/crates/usvg/src/parser/text.rs b/crates/usvg/src/parser/text.rs index eed3aab40..233491dd8 100644 --- a/crates/usvg/src/parser/text.rs +++ b/crates/usvg/src/parser/text.rs @@ -140,7 +140,13 @@ 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 }) + } else { + None + }; + + if text::convert(&mut text, &state.opt.font_resolver, cache, hinting_ctx).is_none() { return; } @@ -263,6 +269,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 +296,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 +405,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)) { @@ -416,12 +472,142 @@ fn convert_font(node: SvgNode, state: &converter::State) -> Font { families.push(FontFamily::Named(state.opt.font_family.clone())); } + let hinting = parse_hinting_settings(node); + Font { families, style, stretch, weight, + variations, + hinting, + } +} + +/// Parses all `resvg:hinting-*` CSS properties (also available as `-resvg-hinting-*` in CSS style). +fn parse_hinting_settings(node: SvgNode) -> HintingSettings { + let target = node + .find_attribute::<&str>(AId::ResvgHintingTarget) + .and_then(|s| s.parse().ok()) + .unwrap_or_default(); + + let mode = node + .find_attribute::<&str>(AId::ResvgHintingMode) + .and_then(|s| s.parse().ok()) + .unwrap_or_default(); + + let engine = node + .find_attribute::<&str>(AId::ResvgHintingEngine) + .and_then(|s| s.parse().ok()) + .unwrap_or_default(); + + let symmetric_rendering = node + .find_attribute::<&str>(AId::ResvgHintingSymmetric) + .map(|s| s == "true") + .unwrap_or(true); // default is true + + let preserve_linear_metrics = node + .find_attribute::<&str>(AId::ResvgHintingPreserveLinearMetrics) + .map(|s| s == "true") + .unwrap_or(false); // default is false + + HintingSettings { + target, + mode, + engine, + symmetric_rendering, + preserve_linear_metrics, + } +} + +/// 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 ASCII characters + if tag_str.len() != 4 || !tag_str.is_ascii() { + log::warn!( + "Invalid font-variation-settings tag: '{}' (must be 4 ASCII 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(); + // SAFETY: We verified above that tag_str is exactly 4 ASCII 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 @@ -668,8 +854,8 @@ fn resolve_decoration( for node in tspan.ancestors() { if find_decoration(node, text_decoration) || node.tag_name() == Some(EId::Text) { - fill_node = fill_node.map_or(Some(node), Some); - stroke_node = stroke_node.map_or(Some(node), Some); + fill_node = fill_node.or(Some(node)); + stroke_node = stroke_node.or(Some(node)); break; } } @@ -771,8 +957,13 @@ fn convert_writing_mode(text_node: SvgNode) -> WritingMode { } fn path_length(path: &tiny_skia_path::Path) -> f64 { - let mut prev_mx = path.points()[0].x; - let mut prev_my = path.points()[0].y; + let points = path.points(); + if points.is_empty() { + return 0.0; + } + + let mut prev_mx = points[0].x; + let mut prev_my = points[0].y; let mut prev_x = prev_mx; let mut prev_y = prev_my; 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..d50850fdc 100644 --- a/crates/usvg/src/text/flatten.rs +++ b/crates/usvg/src/text/flatten.rs @@ -5,14 +5,153 @@ 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::{ + FontRef, GlyphId, MetadataProvider, + bitmap::{BitmapData, MaskData}, + instance::{LocationRef, Size as SkrifaSize}, + outline::{ + DrawSettings, Engine, HintingInstance, HintingOptions, OutlinePen, SmoothMode, Target, + pen::ControlBoundsPen, + }, + raw::TableProvider, + setting::VariationSetting, +}; use tiny_skia_path::{NonZeroRect, Size, Transform}; -use xmlwriter::XmlWriter; -use crate::text::colr::GlyphPainter; use crate::*; +/// Encode raw image data as PNG. +fn encode_png(data: &[u8], width: u32, height: u32, color_type: png::ColorType) -> Option> { + let mut png_data = Vec::new(); + { + let mut encoder = png::Encoder::new(&mut png_data, width, height); + encoder.set_color(color_type); + encoder.set_depth(png::BitDepth::Eight); + let mut writer = encoder.write_header().ok()?; + writer.write_image_data(data).ok()?; + } + Some(png_data) +} + +/// Extract a single pixel from bitmap mask data. +/// Returns grayscale value (0x00 = black, 0xFF = white). +fn extract_mask_pixel(data: &[u8], x: u32, y: u32, width: u32, bpp: u8, is_packed: bool) -> u8 { + match bpp { + 1 => { + let (byte_idx, bit_idx) = if is_packed { + // Packed: bits flow continuously across row boundaries + let bit_pos = (y as usize) * (width as usize) + (x as usize); + (bit_pos / 8, 7 - (bit_pos % 8)) + } else { + // Not packed: each row is byte-aligned + let row_bytes = ((width + 7) / 8) as usize; + let idx = (y as usize) * row_bytes + (x as usize) / 8; + (idx, 7 - ((x as usize) % 8)) + }; + if byte_idx < data.len() { + let bit = (data[byte_idx] >> bit_idx) & 1; + // 1 = black (0x00), 0 = white (0xFF) + if bit == 1 { 0x00 } else { 0xFF } + } else { + 0xFF + } + } + 2 => { + // 2 bpp: each byte contains 4 pixels + let row_bytes = ((width + 3) / 4) as usize; + let byte_idx = (y as usize) * row_bytes + (x as usize) / 4; + let shift = 6 - (((x as usize) % 4) * 2); + if byte_idx < data.len() { + let val = (data[byte_idx] >> shift) & 0x03; + // Scale 0-3 to 0-255 (inverted: 3 = black) + 255 - (val * 85) + } else { + 0xFF + } + } + 4 => { + // 4 bpp: each byte contains 2 pixels + let row_bytes = ((width + 1) / 2) as usize; + let byte_idx = (y as usize) * row_bytes + (x as usize) / 2; + let shift = if x % 2 == 0 { 4 } else { 0 }; + if byte_idx < data.len() { + let val = (data[byte_idx] >> shift) & 0x0F; + // Scale 0-15 to 0-255 (inverted: 15 = black) + 255 - (val * 17) + } else { + 0xFF + } + } + 8 => { + // 8 bpp: one byte per pixel + let idx = (y as usize) * (width as usize) + (x as usize); + if idx < data.len() { + // Invert: 255 = black, 0 = white + 255 - data[idx] + } else { + 0xFF + } + } + _ => 0xFF, + } +} + +/// Convert a monochrome/grayscale bitmap mask to PNG format. +/// Supports 1, 2, 4, and 8 bits per pixel. +fn mask_to_png(mask: &MaskData, width: u32, height: u32) -> Option> { + if width == 0 || height == 0 { + return None; + } + + // Check for overflow: width * height must fit in usize + let capacity = (width as usize).checked_mul(height as usize)?; + + // Decode mask data to 8-bit grayscale + let mut grayscale = Vec::with_capacity(capacity); + + for y in 0..height { + for x in 0..width { + grayscale.push(extract_mask_pixel( + mask.data, + x, + y, + width, + mask.bpp, + mask.is_packed, + )); + } + } + + encode_png(&grayscale, width, height, png::ColorType::Grayscale) +} + +/// Convert BGRA bitmap data to PNG format. +fn bgra_to_png(data: &[u8], width: u32, height: u32) -> Option> { + if width == 0 || height == 0 { + return None; + } + + // Check for overflow: width * height * 4 must fit in usize + let expected_len = (width as usize) + .checked_mul(height as usize)? + .checked_mul(4)?; + if data.len() < expected_len { + return None; + } + + // Convert BGRA to RGBA + let mut rgba = Vec::with_capacity(expected_len); + for chunk in data[..expected_len].chunks(4) { + rgba.push(chunk[2]); // R + rgba.push(chunk[1]); // G + rgba.push(chunk[0]); // B + rgba.push(chunk[3]); // A + } + + encode_png(&rgba, width, height, png::ColorType::Rgba) +} + fn resolve_rendering_mode(text: &Text) -> ShapeRendering { match text.rendering_mode { TextRendering::OptimizeSpeed => ShapeRendering::CrispEdges, @@ -45,10 +184,51 @@ fn push_outline_paths( } } -pub(crate) fn flatten(text: &mut Text, cache: &mut Cache) -> Option<(Group, NonZeroRect)> { +/// Hinting context for controlling font hinting behavior. +/// +/// Note: Hinting is applied at the SVG's source coordinate scale, not the final +/// output scale. For pixel-perfect mono rendering, the output should be rendered +/// at 1:1 scale (no zoom/fit), or use a zoom factor that's an integer multiple +/// (2x, 3x, etc.) to maintain pixel alignment. +#[derive(Clone, Copy, Debug)] +pub(crate) struct HintingContext { + /// Whether hinting is enabled globally. + pub(crate) enabled: bool, +} + +impl HintingContext { + /// Calculate pixels per em from font size. + /// + /// In SVG, font-size is specified in user units (pixels), not points. + /// So ppem equals font_size directly when rendering at 1:1 scale. + pub(crate) fn ppem(&self, font_size: f32) -> f32 { + font_size + } +} + +/// 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() { @@ -69,9 +249,42 @@ pub(crate) fn flatten(text: &mut Text, cache: &mut Cache) -> Option<(Group, NonZ // span to we push the actual outline paths into new_children. This way, we don't need // to create a new path for every glyph if we have many consecutive glyphs // with just outlines (which is the most common case). + // + // We also track rendering mode per-glyph: if a glyph uses Mono hinting target, + // it needs CrispEdges rendering (no anti-aliasing), so we flush the builder + // when the rendering mode changes. let mut span_builder = tiny_skia_path::PathBuilder::new(); + // Determine rendering mode for this span based on hinting target. + // Mono target always disables anti-aliasing (CrispEdges), regardless of whether + // hinting is enabled. This allows comparing hinted vs unhinted mono rendering. + let span_rendering_mode = if span.hinting.target == crate::HintingTarget::Mono { + ShapeRendering::CrispEdges // No anti-aliasing for mono target + } else { + rendering_mode + }; + let mut current_glyph_rendering_mode = span_rendering_mode; + + // Check if we need variations for this span (uniform for all glyphs). + // 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. + let needs_variations = !span.variations.is_empty() + || span.font_optical_sizing == crate::FontOpticalSizing::Auto; + for glyph in &span.positioned_glyphs { + // For mono hinting, all glyphs in the span use the same rendering mode + let glyph_rendering_mode = span_rendering_mode; + + // If rendering mode changed, flush the current path segment + if glyph_rendering_mode != current_glyph_rendering_mode { + push_outline_paths( + span, + &mut span_builder, + &mut new_children, + current_glyph_rendering_mode, + ); + current_glyph_rendering_mode = glyph_rendering_mode; + } // A (best-effort conversion of a) COLR glyph. if let Some(tree) = cache.fontdb_colr(glyph.font, glyph.id) { let mut group = Group { @@ -99,25 +312,20 @@ pub(crate) fn flatten(text: &mut Text, cache: &mut Cache) -> Option<(Group, NonZ new_children.push(Node::Group(Box::new(group))); } // A bitmap glyph. - else if let Some(img) = cache.fontdb_raster(glyph.font, glyph.id) { + else if let Some(img) = cache.fontdb_raster(glyph.font, glyph.id, glyph.font_size()) { push_outline_paths(span, &mut span_builder, &mut new_children, rendering_mode); 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,15 +336,103 @@ 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 { + // Use span-level variation settings (uniform for all glyphs in span). + // Clone Arc before mutable borrow for the closure + let fontdb = cache.fontdb.clone(); + + // Compute ppem for hinting (if applicable) + let ppem = if use_hinting { + hinting_ctx.map(|ctx| ctx.ppem(glyph.font_size())) + } else { + None + }; + + // For cache key: when auto-opsz is enabled, assume font has opsz axis + // This is a safe approximation that may create slightly more cache entries + // for fonts without opsz, but avoids expensive axis lookup + let has_opsz_axis = span.font_optical_sizing == crate::FontOpticalSizing::Auto; + + // Compute variation hash for cache key + let variation_hash = crate::parser::compute_variation_hash( + &span.variations, + span.font_optical_sizing, + glyph.font_size(), + has_opsz_axis, + ); + + // Build cache key with all parameters affecting outline shape + let cache_key = crate::parser::OutlineCacheKey { + font_id: glyph.font, + glyph_id: glyph.id, + ppem_bits: ppem.map(|p| p.to_bits()), + hinting_target: if use_hinting { + Some(span.hinting.target) + } else { + None + }, + hinting_mode: if use_hinting { + Some(span.hinting.mode) + } else { + None + }, + hinting_engine: if use_hinting { + Some(span.hinting.engine) + } else { + None + }, + symmetric_rendering: span.hinting.symmetric_rendering, + preserve_linear_metrics: span.hinting.preserve_linear_metrics, + variation_hash, + }; + + // Capture values for closure + let variations = &span.variations; + let font_optical_sizing = span.font_optical_sizing; + let hinting_settings = span.hinting; + let font_id = glyph.font; + let glyph_id = glyph.id; + let font_size = glyph.font_size(); + + // Get from cache or compute (unified for all outline types) + let outline = cache.get_or_compute_outline(cache_key, || { + if use_hinting { + extract_outline_skrifa( + &fontdb, + font_id, + glyph_id, + variations, + font_size, + font_optical_sizing, + ppem, + hinting_settings, + ) + } else if needs_variations { + fontdb.outline_with_variations( + font_id, + glyph_id, + variations, + font_size, + font_optical_sizing, + ) + } else { + fontdb.outline(font_id, glyph_id) + } + }); + + if let Some(outline) = outline.and_then(|p| p.transform(glyph.outline_transform())) + { + span_builder.push_path(&outline); + } } } - push_outline_paths(span, &mut span_builder, &mut new_children, rendering_mode); + push_outline_paths( + span, + &mut span_builder, + &mut new_children, + current_glyph_rendering_mode, + ); if let Some(path) = span.line_through.as_ref() { let mut path = path.clone(); @@ -159,11 +455,203 @@ 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_settings: crate::HintingSettings, +) -> 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) = ppem { + let size = SkrifaSize::new(ppem_val); + + // Convert HintingSettings to skrifa's Target + let hinting_target = match hinting_settings.target { + crate::HintingTarget::Mono => Target::Mono, + crate::HintingTarget::Smooth => { + // Convert HintingMode to skrifa's SmoothMode + let smooth_mode = match hinting_settings.mode { + crate::HintingMode::Normal => SmoothMode::Normal, + crate::HintingMode::Light => SmoothMode::Light, + crate::HintingMode::Lcd => SmoothMode::Lcd, + crate::HintingMode::VerticalLcd => SmoothMode::VerticalLcd, + }; + Target::Smooth { + mode: smooth_mode, + symmetric_rendering: hinting_settings.symmetric_rendering, + preserve_linear_metrics: hinting_settings.preserve_linear_metrics, + } + } + }; + + // Convert HintingEngine to skrifa's Engine + let engine = match hinting_settings.engine { + crate::HintingEngine::Auto => Engine::Auto(None), + crate::HintingEngine::Native => Engine::Interpreter, + crate::HintingEngine::AutoFallback => Engine::AutoFallback, + }; + + // Build HintingOptions with both engine and target + let hinting_options = HintingOptions { + engine, + target: hinting_target, + }; + + // Create hinting instance with the configured options. + // Note: HintingInstance is created per-glyph. For performance optimization, + // consider caching instances keyed by (font_id, ppem, location, settings) if profiling + // shows this is a bottleneck. + 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 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 ttf_parser::OutlineBuilder for PathBuilder { +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 +660,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 +673,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 raster(&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, font_size: f32) -> 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,119 +732,373 @@ 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 builder = PathBuilder { - builder: tiny_skia_path::PathBuilder::new(), - }; - - font.outline_glyph(glyph_id, &mut builder)?; - builder.builder.finish() + let mut pen = SkrifaPen::new(); + let settings = DrawSettings::unhinted(SkrifaSize::unscaled(), LocationRef::default()); + 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)?; - - 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)?, - rendering_mode: ImageRendering::OptimizeQuality, - kind: ImageKind::PNG(Arc::new(image.data.into())), - abs_transform: Transform::default(), - abs_bounding_box: NonZeroRect::from_xywh( - 0.0, - 0.0, - image.width as f32, - image.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(), - }; + #[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(); - return Some(bitmap_image); + // 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)); + } + } } - None + // 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 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. + fn raster(&self, id: ID, glyph_id: GlyphId, font_size: f32) -> Option { + self.with_face_data(id, |data, face_index| -> Option { + let font = FontRef::from_index(data, face_index).ok()?; - // 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. - 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)) + // Try to get bitmap strikes + let strikes = font.bitmap_strikes(); + + // Get the largest available strike first to check bitmap type + let largest_strike = strikes + .iter() + .max_by(|a, b| a.ppem().partial_cmp(&b.ppem()).unwrap_or(std::cmp::Ordering::Equal))?; + + let bitmap_glyph = largest_strike.get(glyph_id)?; + let bitmap_data = bitmap_glyph.data; + + // Check if this is a color bitmap (PNG/BGRA) or monochrome (Mask) + let is_color_bitmap = matches!(bitmap_data, BitmapData::Png(_) | BitmapData::Bgra(_)); + + // Strike selection strategy: + // - Color bitmaps (PNG/BGRA): use largest strike (original behavior) + // - Monochrome bitmaps (Mask): only use if exact size match OR no outline exists + let strike = if is_color_bitmap { + // Color bitmap: use largest strike (original behavior) + largest_strike } 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); + // Monochrome bitmap: prefer exact match for pixel-perfect rendering + let has_outline = font.outline_glyphs().get(glyph_id).is_some(); + let exact_match = strikes + .iter() + .find(|s| (s.ppem() - font_size).abs() < 0.01); + + if let Some(strike) = exact_match { + strike + } else if !has_outline { + // No outline fallback, use best available strike + strikes + .iter() + .filter(|s| s.ppem() >= font_size) + .min_by(|a, b| a.ppem().partial_cmp(&b.ppem()).unwrap_or(std::cmp::Ordering::Equal)) + .unwrap_or(largest_strike) + } else { + // Has outline and no exact match - caller will use outline + return None; + } + }; + + // Re-get bitmap_glyph for the selected strike (may differ for monochrome) + let bitmap_glyph = strike.get(glyph_id)?; + let bitmap_data = bitmap_glyph.data; + + // Handle different bitmap formats + let (png_data, width, height): (Vec, u32, u32) = match bitmap_data { + BitmapData::Png(data) => { + // Get PNG dimensions using imagesize + let (w, h) = if let Ok(size) = imagesize::blob_size(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) + }; + (data.to_vec(), w, h) + } + BitmapData::Mask(mask) => { + // Convert monochrome/grayscale mask to PNG + let w = bitmap_glyph.width; + let h = bitmap_glyph.height; + match mask_to_png(&mask, w, h) { + Some(png) => (png, w, h), + None => return None, + } + } + BitmapData::Bgra(data) => { + // Convert BGRA to PNG + let w = bitmap_glyph.width; + let h = bitmap_glyph.height; + match bgra_to_png(data, w, h) { + Some(png) => (png, w, h), + None => return None, + } + } + }; + + // 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 { + // Clamp to i16 range to prevent truncation issues with large glyph bounds + x_min: bb.x_min.clamp(i16::MIN as f32, i16::MAX as f32) as i16, + y_min: bb.y_min.clamp(i16::MIN as f32, i16::MAX as f32) as i16, + x_max: bb.x_max.clamp(i16::MIN as f32, i16::MAX as f32) as i16, + y_max: bb.y_max.clamp(i16::MIN as f32, i16::MAX as f32) as i16, }) - .cloned()? + }) + }; + + // Detect SBIX format by checking if the font has an sbix table. + let is_sbix = font.table_data(Tag::new(b"sbix")).is_some(); + + log::trace!( + "Bitmap glyph: bearing=({}, {}), inner_bearing=({}, {}), ppem={}, bbox={:?}, is_sbix={}, size={}x{}", + bitmap_glyph.bearing_x, bitmap_glyph.bearing_y, + bitmap_glyph.inner_bearing_x, bitmap_glyph.inner_bearing_y, + strike.ppem(), glyph_bbox, is_sbix, width, 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); + + // Choose rendering mode based on bitmap type: + // - Color bitmaps: smooth scaling for better quality when resized + // - Monochrome bitmaps: nearest-neighbor for pixel-perfect rendering + let rendering_mode = if is_color_bitmap { + ImageRendering::OptimizeQuality + } else { + ImageRendering::OptimizeSpeed + }; + + let bitmap_image = BitmapImage { + image: Image { + id: String::new(), + visible: true, + size: Size::from_wh(width as f32, height as f32)?, + rendering_mode, + kind: ImageKind::PNG(Arc::new(png_data)), + abs_transform: Transform::default(), + abs_bounding_box: NonZeroRect::from_xywh(0.0, 0.0, width as f32, height as f32)?, + }, + x, + y, + pixels_per_em: strike.ppem(), + glyph_bbox, + is_sbix, }; - Some(node) + Some(bitmap_image) })? } - 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()], - }; + fn svg(&self, id: ID, glyph_id: GlyphId) -> Option { + // 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 = 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; + } + + // 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; - face.paint_color_glyph( - glyph_id, - 0, - RgbaColor::new(0, 0, 0, 255), - &mut glyph_painter, - )?; - svg.end_element(); + // 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; + } - Tree::from_data(svg.end_document().as_bytes(), &Options::default()).ok() + 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 { + // 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 + } +} + +#[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..86f7e835e 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; @@ -48,6 +48,11 @@ pub struct PositionedGlyph { } impl PositionedGlyph { + /// Returns the font size for this glyph. + pub fn font_size(&self) -> f32 { + self.font_size + } + /// Returns the transform of glyph. pub fn transform(&self) -> Transform { let sx = self.font_size / self.units_per_em as f32; @@ -66,18 +71,32 @@ 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 { + // Bitmap glyphs are in pixel units, not font units. + // self.transform() includes scale = font_size / units_per_em (for outline glyphs). + // We need to counteract that and apply the correct bitmap scaling. + // Total scale should be: font_size / pixels_per_em + // So we need: (font_size / units_per_em) * scale = font_size / pixels_per_em + // Therefore: scale = units_per_em / pixels_per_em + // + // When font_size == pixels_per_em, total scale = 1.0 (pixel-perfect rendering). + // Use a tolerance to snap to exactly 1.0 and avoid floating point precision issues. + let bitmap_scale = self.units_per_em as f32 / pixels_per_em; + let total_scale = (self.font_size / self.units_per_em as f32) * bitmap_scale; + let scale = if (total_scale - 1.0).abs() < 0.00001 { + // Snap to exactly 1.0 for pixel-perfect rendering + self.units_per_em as f32 / self.font_size + } else { + bitmap_scale + }; + 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) + .pre_concat(Transform::from_scale(scale, scale)) + // 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 @@ -147,6 +166,12 @@ pub struct Span { pub paint_order: PaintOrder, /// The font size of the span. pub font_size: NonZeroPositiveF32, + /// Font variation settings for variable fonts (uniform for all glyphs in span). + pub variations: Vec, + /// Font optical sizing mode for auto-opsz computation. + pub font_optical_sizing: crate::FontOpticalSizing, + /// Font hinting settings. + pub hinting: crate::HintingSettings, /// The visibility of the span. pub visible: bool, /// The glyphs that make up the span. @@ -338,11 +363,23 @@ pub(crate) fn layout_text( }) .collect(); + // Compute effective variations for this span (including auto-opsz). + let effective_variations = compute_effective_variations( + &span.font.variations, + span.font_size.get(), + span.font_optical_sizing, + font.id, + fontdb, + ); + spans.push(Span { fill, stroke: span.stroke.clone(), paint_order: span.paint_order, font_size: span.font_size, + variations: effective_variations, + font_optical_sizing: span.font_optical_sizing, + hinting: span.font.hinting, visible: span.visible, positioned_glyphs, underline, @@ -899,6 +936,9 @@ fn process_chunk( font, span.small_caps, span.apply_kerning, + &span.font.variations, + span.font_size.get(), + span.font_optical_sizing, resolver, fontdb, ); @@ -1189,89 +1229,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 +1253,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 +1298,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 +1358,59 @@ 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 { + final_variations.push(harfrust::Variation { + tag: Tag::new(b"opsz"), + value: font_size, + }); + } + } + } + } + + // 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 +1426,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; @@ -1431,9 +1473,12 @@ fn shape_text_with_font( glyphs.push(Glyph { byte_idx: ByteIndex::new(idx), - cluster_len: end.checked_sub(start).unwrap_or(0), // TODO: can fail? + cluster_len: end.checked_sub(start).unwrap_or_else(|| { + log::warn!("Invalid cluster bounds: end={} < start={}", end, start); + 0 + }), 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 +1593,7 @@ pub(crate) struct Glyph { impl Glyph { fn is_missing(&self) -> bool { - self.id.0 == 0 + self.id.to_u32() == 0 } } @@ -1597,6 +1642,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 +1816,83 @@ 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..151920575 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_colr; +mod skrifa_metrics; + /// A shorthand for [FontResolver]'s font selection function. /// /// This function receives a font specification (families + a style, weight, @@ -146,7 +149,7 @@ impl FontResolver<'_> { /// to find a font that has the correct style and supports the character. pub fn default_fallback_selector() -> FallbackSelectionFn<'static> { Box::new(|c, exclude_fonts, fontdb| { - let base_font_id = exclude_fonts[0]; + let base_font_id = *exclude_fonts.first()?; // Iterate over fonts and check if any of them support the specified char. for face in fontdb.faces() { @@ -171,14 +174,14 @@ impl FontResolver<'_> { let base_family = base_face .families .iter() - .find(|f| f.1 == fontdb::Language::English_UnitedStates) - .unwrap_or(&base_face.families[0]); + .find(|f| f.1 == fontdb::Language::EnglishUnitedStates) + .or_else(|| base_face.families.first())?; let new_family = face .families .iter() - .find(|f| f.1 == fontdb::Language::English_UnitedStates) - .unwrap_or(&base_face.families[0]); + .find(|f| f.1 == fontdb::Language::EnglishUnitedStates) + .or_else(|| face.families.first())?; log::warn!("Fallback from {} to {}.", base_family.0, new_family.0); return Some(face.id); @@ -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..a7e46ce0e --- /dev/null +++ b/crates/usvg/src/text/skrifa_colr.rs @@ -0,0 +1,422 @@ +// 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::{ + FontRef, GlyphId, MetadataProvider, + color::{Brush, ColorGlyphFormat, ColorPainter, CompositeMode}, + instance::LocationRef, + outline::OutlinePen, + raw::types::BoundingBox, +}; +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..61ed90026 --- /dev/null +++ b/crates/usvg/src/text/skrifa_metrics.rs @@ -0,0 +1,104 @@ +// Copyright 2024 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Font metrics extraction using skrifa. +//! +//! This module provides font metrics extraction using skrifa's MetadataProvider trait, +//! replacing the previous ttf-parser based implementation. + +use std::num::NonZeroU16; + +use fontdb::ID; +use skrifa::{ + FontRef, MetadataProvider, instance::LocationRef, instance::Size as SkrifaSize, + raw::TableProvider, +}; + +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..03287ed4c 100644 --- a/crates/usvg/src/tree/text.rs +++ b/crates/usvg/src/tree/text.rs @@ -66,6 +66,221 @@ 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); + } +} + +/// Hinting target specifies the type of rasterization output. +/// +/// Corresponds to `-resvg-hinting-target` CSS property. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Default)] +pub enum HintingTarget { + /// Strong hinting for aliased, monochromatic rasterization. + /// Best for pixel-perfect rendering without anti-aliasing. + Mono, + /// Hinting suitable for anti-aliased rasterization. + /// This is the default and most common choice. + #[default] + Smooth, +} + +impl std::str::FromStr for HintingTarget { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "mono" => Ok(HintingTarget::Mono), + "smooth" => Ok(HintingTarget::Smooth), + _ => Err("invalid"), + } + } +} + +/// Hinting mode for smooth rendering. +/// +/// Corresponds to `-resvg-hinting-mode` CSS property. +/// Only applies when `HintingTarget::Smooth` is used. +/// +/// **Note:** The `Lcd` and `VerticalLcd` modes only affect how glyph outlines are +/// hinted (optimized for subpixel geometry). They do **not** produce actual LCD +/// subpixel rendering with separate RGB channels, as tiny-skia (the rasterizer) +/// does not support subpixel anti-aliasing. For these modes to produce visible +/// differences from `Normal`, a renderer with LCD subpixel support would be needed. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Default)] +pub enum HintingMode { + /// Standard smooth hinting. Equivalent to FreeType's `FT_LOAD_TARGET_NORMAL`. + #[default] + Normal, + /// Lighter hinting with fewer horizontal adjustments. + /// Produces results closer to unhinted outlines while still aligning to the pixel grid. + /// Equivalent to FreeType's `FT_LOAD_TARGET_LIGHT`. + Light, + /// Optimized for horizontal RGB subpixel rendering (standard LCD displays). + /// Equivalent to FreeType's `FT_LOAD_TARGET_LCD`. + /// + /// **Note:** This only affects outline hinting. Actual subpixel rendering + /// (RGB channel separation) is not implemented in resvg. + Lcd, + /// Optimized for vertical RGB subpixel rendering (rotated LCD displays). + /// Equivalent to FreeType's `FT_LOAD_TARGET_LCD_V`. + /// + /// **Note:** This only affects outline hinting. Actual subpixel rendering + /// (RGB channel separation) is not implemented in resvg. + VerticalLcd, +} + +impl std::str::FromStr for HintingMode { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "normal" => Ok(HintingMode::Normal), + "light" => Ok(HintingMode::Light), + "lcd" => Ok(HintingMode::Lcd), + "vertical-lcd" => Ok(HintingMode::VerticalLcd), + _ => Err("invalid"), + } + } +} + +/// Hinting engine selection. +/// +/// Corresponds to `-resvg-hinting-engine` CSS property. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Default)] +pub enum HintingEngine { + /// Use the automatic hinter (autohinting). + /// Generates hinting on-the-fly for fonts without native hinting. + Auto, + /// Use only the native TrueType/PostScript interpreter. + /// Falls back to unhinted if the font has no hinting instructions. + Native, + /// Automatic selection based on font capabilities (default). + /// Uses native interpreter if `fpgm`/`prep` tables exist, otherwise autohints. + /// This matches FreeType's default behavior. + #[default] + AutoFallback, +} + +impl std::str::FromStr for HintingEngine { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "auto" => Ok(HintingEngine::Auto), + "native" => Ok(HintingEngine::Native), + "auto-fallback" => Ok(HintingEngine::AutoFallback), + _ => Err("invalid"), + } + } +} + +/// Complete hinting configuration for text rendering. +/// +/// These options control how font outlines are adjusted for pixel grid alignment. +/// All options can be set via resvg-specific CSS properties or SVG attributes. +/// +/// # Custom Properties +/// +/// These properties are resvg extensions and not part of the SVG or CSS standards. +/// They can be used in two ways: +/// +/// ## As SVG attributes (resvg namespace) +/// +/// ```xml +/// +/// +/// Pixel-perfect text +/// +/// +/// ``` +/// +/// ## As CSS properties (vendor-prefixed) +/// +/// ```xml +/// +/// +/// Pixel-perfect text +/// +/// +/// ``` +/// +/// # Available Properties +/// +/// | Property | Values | Default | Description | +/// |----------|--------|---------|-------------| +/// | `-resvg-hinting-target` | `smooth`, `mono` | `smooth` | Rasterization target | +/// | `-resvg-hinting-mode` | `normal`, `light`, `lcd`, `vertical-lcd` | `normal` | Smooth hinting mode | +/// | `-resvg-hinting-engine` | `auto`, `native`, `auto-fallback` | `auto-fallback` | Hinting engine | +/// | `-resvg-hinting-symmetric` | `true`, `false` | `true` | Symmetric rendering | +/// | `-resvg-hinting-preserve-linear-metrics` | `true`, `false` | `false` | Preserve spacing | +/// +/// These properties inherit and can be set on any text element or ancestor. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] +pub struct HintingSettings { + /// The hinting target (mono or smooth rasterization). + /// CSS: `-resvg-hinting-target` + pub target: HintingTarget, + /// The smooth hinting mode (only used when target is Smooth). + /// CSS: `-resvg-hinting-mode` + pub mode: HintingMode, + /// The hinting engine to use. + /// CSS: `-resvg-hinting-engine` + pub engine: HintingEngine, + /// Allow TrueType bytecode to assume symmetric rendering (vertical supersampling). + /// When true, ClearType-optimized fonts may produce wider horizontal stems. + /// CSS: `-resvg-hinting-symmetric` + pub symmetric_rendering: bool, + /// Preserve linear advance metrics (no horizontal glyph adjustments). + /// When true, inter-glyph spacing remains constant regardless of hinting. + /// Useful for layout consistency without re-evaluating glyph outlines. + /// CSS: `-resvg-hinting-preserve-linear-metrics` + pub preserve_linear_metrics: bool, +} + +impl Default for HintingSettings { + fn default() -> Self { + Self { + target: HintingTarget::default(), + mode: HintingMode::default(), + engine: HintingEngine::default(), + symmetric_rendering: true, + preserve_linear_metrics: false, + } + } +} + /// A font style property. #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] pub enum FontStyle { @@ -113,6 +328,8 @@ pub struct Font { pub(crate) style: FontStyle, pub(crate) stretch: FontStretch, pub(crate) weight: u16, + pub(crate) variations: Vec, + pub(crate) hinting: HintingSettings, } impl Font { @@ -137,6 +354,18 @@ impl Font { pub fn weight(&self) -> u16 { self.weight } + + /// Font variation settings for variable fonts. + pub fn variations(&self) -> &[FontVariation] { + &self.variations + } + + /// Hinting settings for text rendering. + /// + /// Controlled by `-resvg-hinting-*` CSS properties. + pub fn hinting(&self) -> HintingSettings { + self.hinting + } } /// A dominant baseline property. @@ -218,6 +447,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 +528,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 +594,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 @@ -484,8 +741,8 @@ impl TextChunk { } /// A text chunk flow. - pub fn text_flow(&self) -> TextFlow { - self.text_flow.clone() + pub fn text_flow(&self) -> &TextFlow { + &self.text_flow } /// A text chunk actual text. diff --git a/crates/usvg/src/writer.rs b/crates/usvg/src/writer.rs index 93b131052..d97183e7e 100644 --- a/crates/usvg/src/writer.rs +++ b/crates/usvg/src/writer.rs @@ -1529,6 +1529,38 @@ fn write_span( xml.write_attribute("style", "font-kerning:none"); } + // Write hinting settings if they differ from defaults + if span.font.hinting.target != HintingTarget::Smooth { + xml.write_svg_attribute(AId::ResvgHintingTarget, "mono"); + } + + if span.font.hinting.mode != HintingMode::Normal { + let mode = match span.font.hinting.mode { + HintingMode::Normal => unreachable!(), + HintingMode::Light => "light", + HintingMode::Lcd => "lcd", + HintingMode::VerticalLcd => "vertical-lcd", + }; + xml.write_svg_attribute(AId::ResvgHintingMode, mode); + } + + if span.font.hinting.engine != HintingEngine::AutoFallback { + let engine = match span.font.hinting.engine { + HintingEngine::Auto => "auto", + HintingEngine::Native => "native", + HintingEngine::AutoFallback => unreachable!(), + }; + xml.write_svg_attribute(AId::ResvgHintingEngine, engine); + } + + if !span.font.hinting.symmetric_rendering { + xml.write_svg_attribute(AId::ResvgHintingSymmetric, "false"); + } + + if span.font.hinting.preserve_linear_metrics { + xml.write_svg_attribute(AId::ResvgHintingPreserveLinearMetrics, "true"); + } + if span.dominant_baseline != DominantBaseline::Auto { let name = match span.dominant_baseline { DominantBaseline::UseScript => "use-script", diff --git a/crates/usvg/tests/files/clip-path-with-complex-text-expected.svg b/crates/usvg/tests/files/clip-path-with-complex-text-expected.svg index 49f2ea86e..0000eca71 100644 --- a/crates/usvg/tests/files/clip-path-with-complex-text-expected.svg +++ b/crates/usvg/tests/files/clip-path-with-complex-text-expected.svg @@ -2,7 +2,7 @@ - + diff --git a/crates/usvg/tests/files/clip-path-with-text-expected.svg b/crates/usvg/tests/files/clip-path-with-text-expected.svg index 01f09f81c..eb4733c30 100644 --- a/crates/usvg/tests/files/clip-path-with-text-expected.svg +++ b/crates/usvg/tests/files/clip-path-with-text-expected.svg @@ -1,7 +1,7 @@ - + diff --git a/crates/usvg/tests/files/preserve-text-simple-case-expected.svg b/crates/usvg/tests/files/preserve-text-simple-case-expected.svg index 2a97df767..fd3592f52 100644 --- a/crates/usvg/tests/files/preserve-text-simple-case-expected.svg +++ b/crates/usvg/tests/files/preserve-text-simple-case-expected.svg @@ -1,5 +1,5 @@ - + diff --git a/crates/usvg/tests/files/text-simple-case-expected.svg b/crates/usvg/tests/files/text-simple-case-expected.svg index 2a97df767..fd3592f52 100644 --- a/crates/usvg/tests/files/text-simple-case-expected.svg +++ b/crates/usvg/tests/files/text-simple-case-expected.svg @@ -1,5 +1,5 @@ - + diff --git a/crates/usvg/tests/files/text-with-generated-gradients-expected.svg b/crates/usvg/tests/files/text-with-generated-gradients-expected.svg index 8af171f67..5f0ec9eea 100644 --- a/crates/usvg/tests/files/text-with-generated-gradients-expected.svg +++ b/crates/usvg/tests/files/text-with-generated-gradients-expected.svg @@ -6,8 +6,8 @@ - - - + + +