From fe95143c628e8829639c91cddcef63b99a30e691 Mon Sep 17 00:00:00 2001 From: Tobias Oetiker Date: Tue, 13 Jan 2026 23:33:25 +0100 Subject: [PATCH] Port text rendering from ttf-parser to skrifa This replaces ttf-parser with skrifa for all font parsing and text rendering. fontdb changes: - Vendor fontdb 0.23.0 as a workspace crate - Replace ttf-parser with skrifa for font parsing - Use skrifa's MetadataProvider for font attributes and metrics usvg changes: - Add skrifa_colr.rs: COLR glyph painting using skrifa's ColorPainter - Add skrifa_metrics.rs: Font metrics extraction using skrifa - Update flatten.rs: Use skrifa for glyph outlines and bitmap extraction - Update layout.rs: Use skrifa for font metrics and harfrust for shaping - Replace rustybuzz with harfrust for text shaping - Update test reference images for new skrifa-based rendering Co-Authored-By: Claude Opus 4.5 --- .github/copyright.sh | 3 +- Cargo.lock | 139 +- Cargo.toml | 1 + crates/fontdb/Cargo.toml | 34 + crates/fontdb/src/lib.rs | 1359 +++++++++++++++++ crates/fontdb/tests/add_fonts.rs | 18 + crates/fontdb/tests/fonts/LICENSE.txt | 11 + crates/fontdb/tests/fonts/Tuffy.ttf | Bin 0 -> 172324 bytes .../tests/tests/text/color-font/colrv0.png | Bin 1591 -> 1561 bytes .../tests/tests/text/color-font/colrv1.png | Bin 6152 -> 6295 bytes .../all-axes-combined.png | Bin 1265 -> 1262 bytes .../auto-font-stretch-condensed.png | Bin 1594 -> 1589 bytes .../auto-font-style-oblique.png | Bin 1531 -> 1537 bytes .../auto-font-weight-700.png | Bin 1191 -> 1187 bytes .../explicit-overrides-auto.png | Bin 795 -> 790 bytes .../font-variation-settings/grad-negative.png | Bin 1488 -> 1484 bytes .../font-variation-settings/multiple-axes.png | Bin 1145 -> 1143 bytes .../text/font-variation-settings/opsz-144.png | Bin 1618 -> 1630 bytes .../font-variation-settings/slnt-negative.png | Bin 1531 -> 1537 bytes .../text/font-variation-settings/wdth-151.png | Bin 1583 -> 1582 bytes .../text/font-variation-settings/wdth-25.png | Bin 1401 -> 1407 bytes .../text/font-variation-settings/wght-100.png | Bin 795 -> 790 bytes .../text/font-variation-settings/wght-700.png | Bin 1191 -> 1187 bytes .../font-variation-settings/xtra-extreme.png | Bin 1659 -> 1672 bytes crates/resvg/tests/tests/text/text/zalgo.png | Bin 3123 -> 3104 bytes crates/usvg/Cargo.toml | 10 +- crates/usvg/src/parser/converter.rs | 6 +- crates/usvg/src/text/flatten.rs | 483 +++--- crates/usvg/src/text/layout.rs | 304 ++-- crates/usvg/src/text/mod.rs | 8 +- crates/usvg/src/text/skrifa_colr.rs | 421 +++++ crates/usvg/src/text/skrifa_metrics.rs | 104 ++ .../clip-path-with-complex-text-expected.svg | 2 +- .../files/clip-path-with-text-expected.svg | 2 +- .../preserve-text-simple-case-expected.svg | 2 +- .../tests/files/text-simple-case-expected.svg | 2 +- ...text-with-generated-gradients-expected.svg | 6 +- 37 files changed, 2534 insertions(+), 381 deletions(-) create mode 100644 crates/fontdb/Cargo.toml create mode 100644 crates/fontdb/src/lib.rs create mode 100644 crates/fontdb/tests/add_fonts.rs create mode 100644 crates/fontdb/tests/fonts/LICENSE.txt create mode 100644 crates/fontdb/tests/fonts/Tuffy.ttf create mode 100644 crates/usvg/src/text/skrifa_colr.rs create mode 100644 crates/usvg/src/text/skrifa_metrics.rs 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/Cargo.lock b/Cargo.lock index d6d44e797..42ba3da97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,6 +58,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" @@ -158,6 +172,15 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +[[package]] +name = "font-types" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4d2d0cf79d38430cc9dc9aadec84774bff2e1ba30ae2bf6c16cfce9385a23" +dependencies = [ + "bytemuck", +] + [[package]] name = "fontconfig-parser" version = "0.5.8" @@ -170,15 +193,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 +218,19 @@ dependencies = [ "weezl", ] +[[package]] +name = "harfrust" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9f40651a03bc0f7316bd75267ff5767e93017ef3cfffe76c6aa7252cc5a31c" +dependencies = [ + "bitflags 2.10.0", + "bytemuck", + "core_maths", + "read-fonts", + "smallvec", +] + [[package]] name = "image-webp" version = "0.2.4" @@ -352,12 +386,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 +525,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 +585,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 +612,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 +656,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 +708,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 +715,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,19 +740,19 @@ dependencies = [ "data-url", "flate2", "fontdb", + "harfrust", "imagesize", "kurbo", "log", "once_cell", "pico-args", "roxmltree 0.21.1", - "rustybuzz", "simplecss", "siphasher 1.0.1", + "skrifa", "strict-num", "svgtypes", "tiny-skia-path", - "ttf-parser", "unicode-bidi", "unicode-script", "unicode-vo", 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..f88b764ab --- /dev/null +++ b/crates/fontdb/src/lib.rs @@ -0,0 +1,1359 @@ +// 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(core::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 + .get(0) + .or_else(|| accept.get(0)) + .or_else(|| default.get(0)); + + 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: We immediately copy data out, not keeping the mmap alive + 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 0000000000000000000000000000000000000000..ade553a60fd1b01e16a00a8ef5ea396541245d37 GIT binary patch literal 172324 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

-!d7L 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 Mp!?<=~~)z#T^vVXvA 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 literal 1591 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4rZW;-{LxVAax_aC&U#<{|92?$i~J-h~kEZ z1_&cJHy2rZY;0^yOiVyP0J0!5+X@VT+6@g2>+9>`_QDymU;yVzNJt0^3nQC=%;w_a zVrOSZ7GY*)hH(4R6Mg|raVrV(3uX`yP*5;1u(x+`@b?c0h>wp?NJz-c&o3w_E~%}r zZ)oW6pDyC;@{RsFU0aqr46&0 zV-~+|RS#dg&{$`BWplA_-TVKty)$NNZ2J}SF)vpI`HIqFwH~7xPl%uSEQN zl~;X7Zu;Roskgo&ua}3Z{=f97Z=cM&XxkXpu!~+>Z)uOWM0UmzJEk)I2+3=H8H{k>YQP&bykI8$|7!IF~o<9p9wxC|RBvI%!WCXZvl4 zQ(=G7SetTuVJPDhhVB%;h`rvYcdXgowSHRve%{k39=rE8R{T!*`R(KDBDn~Qrw>kv zwB`Kf4-*YsQ?#U`a+8c&`kf;g;`i;8?HG{&;)l(dgdGg8U-SUimVsPK4KF&Z~C!9Iu-=Aqlzh`+ad%4!^ zm0QH&BTrqI2^g&{Ug2YKXs+?|@V84U6Doh`?Pw7Xxg{v#b0_yzNPH5n@b<~KPY9>D ze-BFMT$9%I)WO%RqhiI=N8c9(t$hEoUSogM-_C$} za!`3`K5=Ziut6QrT01j(#RIW4`MOf769k0>?txZL}}<14~H;Pgg&e IbxsLQ0LwzHF8}}l diff --git a/crates/resvg/tests/tests/text/color-font/colrv1.png b/crates/resvg/tests/tests/text/color-font/colrv1.png index 7537da902e2bd7880bf088772829e4e1d9c63d8e..23bb63a05b00f896290c1a5baaf584cead684658 100644 GIT binary patch delta 5872 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 literal 6152 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 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 index 203656bc746d8d09566543923bd780524f53fcdb..c4222be24253023b7a1de08bd344465db5c9296e 100644 GIT binary patch literal 1262 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4rZW;-{LxVAQc|q6XFV_X#x1APtFCJD^n8W z7wo_wpkQF{9}u6AUr^uBKVklY^&9pdIDg^(gZCfiSKNBcz`&yA>EaktaqG>6M%Duk z0xSp4etZzUI4M6gr_5!Wek!pPKiz zIn!f5U+5EMVbU<3vBNI)E${RH`#&G$b$2`X<(S+%O|f5FgLo?HH$7RpqbxSirY5$O z_tCwSShia$Ql@-mI_YBFrSW=7$mK@0Q&x@>yhT$h4|dvgXq6?0tShCe?V-?2}K|a%*z4+$(Pjh}Pl{i0U<;#v~3r{=w>OIok^kwFn zmut=kwCicB9}b#sEZH`9%|>-^`@G%Nk+tXZbu-UvODix>zq-ks>syM$v=z_Xguk`V zHi+y_*(mbb_{p9>It7t`cv&Q~rzSmW=-lkrGI48WL{6Npf3M9?+Y>>%^=Bk?_HAtO zpY~wys(bJHe#cK`NJ%|@O013NNco9N49xlyqj`QT|5BYIsGle=ka~Bb&Oh1zhLPRj z*JK)WW}Iv|?!Vv>%`-2Q{As= zX=_xj*Eu@>+oYw_4kWiKaEn>Td@Xp{VLMe-Qfo`I7WbZTF;OGQ2%Up$HBZD8nkFng z;eIxsy{~G&PT!ODp2v6`Pc&V;ukruGfu{MBLO-j|pUkSH9eM6m-)8j(!JAapT#@T+ zJn`v9P`aXJjaL|_dBGA}TNY*R4XWoR-I{Qbvtql>)1CERk5+6D+uT{EczBMY!KS+4 zr2=+J+?Re@K9~@ zqe6o$f9$>e51d3}zPmW3oOGH7&Q*hiT9XgT>Z;z?<}v-=EaktaqGalK?uimS(Pq0_7MB}T{eQkk#P>(?nW=Bjeem_%=<@SG=^y3``b~9qk7j?>?;EUXSWSZ?G0wIM;_kLb>-Bi178nMy#9ar3H$U7 zT0D_EwY@n7=dQewcwt71$h2^!DXcz^x?&d@q$+aezfj1WU#85xJ0|(&(ODN#Gfzif z*?(eF^`>QeEuCd|t(nrgR4=_^tHZS$pS|pjxcfK#l8e{fyszPBR07lVt4`gEs>MuH zu4b4t3FfhE5@QautnvT&$MeCa1*}cqwA)m815Mdh?$)`uVa@R@!|3E+#wSj3W{2{5 z=1uu@GRR`}tLy8P7ap}^h+L_yEi81U*sZ=t$J^wfNx-}7EJKgpzi;o9TbGFLWz zTJq9|{qV}dxr-YXyr@ro8gbiu(Ii%93DE|f742r#mUm+}w(VZ^W!H*L7yh4mbjF3* zMKj`q;rAvDj!4VjD?XMweEL7VRYGZ2 z#On{!Wx1ZK}*(LXL;=HDbuDjAxIr}6Q5sMT#Uz8AK_;uBVmhH_nZYK3>?l#X|QrDHX(~D%p6MWSX|9k(1%_5g^`C7H|p+*uC9 zqv`c!-s-^(paX7YfZ?ldUk}fLQfIX4P6F_sn{3dk`upRa0C+h)+zA7>XfO;(O4zc# z521&id)n>`BAnPiHUZ#_qBT?5$a3rFHtunxN(LQ!knWYY|Fr9|%;^kr9r+GX%=ftw zyN&cZhxD{VMB_BD9@()Fsm zV#+DPYcId4rRSUYC%qSU=$fMPwSRr1<%EtJrWx~I?wDVYl^s%V{@hWFe%~D5LA+s^ zT8eZ=UFv(keSvcVM&uc$gi%!U6C z0YgC{ZWIRfF4rjT&2k#+QaSCnNqsi$Sm)3cih6)K9O|n5QxbHo<#%U{+z2xqmvHfT zvoVU7q;$fagLTzy%rQ^w`oqY5;^9gK&4~>AyO0rjCqSv*jZhc9Y^9e6P7Hj}GBJ9S zNLYnpWW2BJ$uP5sUBIz;|^Aj(wO5In*6+MEK0bFKm5RVX1x>&}>ps zDE;1&QAe+fsVtd&xHFkRoUJo{=#~Qdxo6er1VieH3Y{6xtN|a=6&)fVZ7Vn-Oe*Ni zyT{l%+MScVcghol?2tuDaw z^Eh5#DPQ&-WaiH-d?zjoh%lshyj)b2Yr)OmpIalcHzwqgj;Q?*NkG|GYoKcG3ymJIf$;wjZH7(ard-JDUkXqT^$`Qq?C(L2{*8(%> zVA4qO@RAkcIB8Otc64$PZn`>FWM>uhvvf@?UI$d8Yw&Hea zY1Xb9^L-9RZwL8g@2cWD>yfE-in?yltt=^+!hKax(mGxYnx8a* zE#b-BStxX@q#`1^IX2g6d$=cPpMA^(09zqS~E SaD=Qi_z2}dkWc+Sdgi}4Ac=GU delta 1305 zcmcJH>sOKo0LHzfgt{z6nu@N-wDL`+Eo)u8q$Z?LhA37lMBY89bTL=-dLb|=M_yhG zHJ6G<0qrFfn^p>Lw&XMu3)GF!mRjlLL{r=BZa5m1^&WeH?s1yEs{vT$MO z<~uH^7&GO~Y|AaBJv{d`VSbOAy^Cs;E6xK{T{A;@;)R?RzkgMvShOsHRO^D4QAqq% zGD$NRkhU=yxsO}u%93f_4!SbEmKCrnE>oXAI?WC|a^AV8xc?X`v%SIrC+2V_R#t|7 z{`d@k>l}};oSz~QYUQs(y<$~h)wLFh?S__IOgDS)j_)wON^lzlQyY{fVzO!VJXV1W3!X$hzaPq)ahSkT%9s$1W;uE~Qmr&)u*-V=KGb2;Ydt!m+oNdYS0csZu+sI=M%Z|#U-G6TQPkj zaA<~{bi}o;v*c{FzcC##e%C!I85TgX3x(&;hj!Vb|F*R~?w`zlD_^l6Q~Cv~SQ~7n z$)(9&K>8OwZyiD!LEaktaqG=EKIXOwyd)k_alUt4 zYmLkL=!0DLj30UVvwkQn-q5Bi)*92&@X0rSsR^I=wuGG$$*m1>|o%)0WIMVol$N!9(#HFlA_Y17cdq8Yr?t1M{c?-}o>giXG~t7oGd zRHAuD`RDPOq00geg-ni(n9`r2!5+HgLGfa5)g^Yzx9^^j-kTaRW&OF?%SGS(nQG{^ zd`=LnP}ou@-ouMxR{LwmiEHXeOP`{I!F>s&g z!JZp${-pl=XYiWCL6!Ms{jSgHvSK0UCTp)fD{%FkhW1yEoY26TTUOp`U;0{p|J3Po z?Rs)cn6BUR3^wKW4sU)`!u9>Tl$Gu*hKz*9H6P~|$p79@!aPq?CVivfi%k=6#>`gS zf1_1&SNEpy4KtQ}jnp~i#+LPearms3rS%Kjj-HD@xhm3f&bl+n-Qv(8JF$&;SxES~iI zQu2do|Ns7(85&`6voWh*g}48+Y16W-lU|SWt?MJ+ZxG(Be(6Z7QDphQO?@hn{*3(us37-r^>bP0l+XkK>8q&X literal 1531 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4rZW;-{LxVAQc|q6XFV_X#x1APtFCJD^n8W z7wo_wpkQF{9}u6AUr^uBKVklY^&9pdIDg^(gZCfiSKNBcz`$zm>EaktaqGIyHnWhuDDdps=ZZI{ebpWZ})0@_u zKDsbo7b@Yp+vGw z-XxL#-0p&sb{kH-3bQ@_PVMFq>n@Maji%aX56=1`SMlVB%sk0|doxz0C0b5M^pu~k zTsc|tlU-QiSEWm9*DVb{eZF42=(@C>`h$7j5*l51{)^eey0qiYy)SJ3$Fjqo7-vMh zH+H^vTx*TuXRAY>>V+Rk+jT6<61^6>F=E*~g@u290gawo81YG1M?87W#DG2bdCnfP z+8Y(Ak$$`OJx9QC$)ziAwUt(X?7ps4bkad)ol&+;Ma=qXrs+542YD3)c0a zJr^~XyCj92z8kePs*QE0-~6x@YOY_WPW1~3Pgr#<(Q)BSm6eqjmt?eG?vx9hv8ps; z!h4mIwYl&Ai0=NYxRP~2SLYY|S*r>oqC%^7w)M*HzR)-6lhRtg;|BV(eU0A;xZW_)`B}Mpy(8!S3Uiq(38AYe z#rItLW5RPgJn|4b>uZ*F=SSNlZ|Q%SA*=DkJ@k<18nNtfwNLl55-R7KzPuWE%XR*q zRj2tQ9v4Y+hr}KzU7sN@_U$BpUG0UZC6h0uM4SJfI9E})^0i)b=#AE+o4Y=4xX-%( zcd^CO-!F24x>6_CiFds(IaO8pK)NL9U4298zvBJgnLF-kss$dh`}In9G28i-%ejBd z_&MLk^hV3k$+D|H+W)U^%IDZJZ}p`gMbG&^&n-*(>N0D`4Yk#3OLy38&f<6S2uWL! zG4dga4z`AUv|B@4;Z|R woF=GxgNmBLMZMk+bketg8`E7iX9mpTew9;F_vXyfAD|-K)78&qol`;+00~I6q5uE@ 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 index 43fe14e4eb693191dd3a38cebe9694c02d1eb7e0..90e0d815f6fdb93e2692331920609e07dda1bde7 100644 GIT binary patch delta 847 zcmZ3^xtMdpddB+`H*BllUA9G9lcTlp4s*nD$MChQ7SH)!e=gUTBYn_8VIVAMlX79Qd*WyD~{a1f)Y^^Wge7*nL z)D621OV`w`-*@_Jq|W5$m3JZ*%dhralGj{uIqz$|cu)AHDQuvclC<=UMf7 zN~5127&*d-t`C0lCVl5jC9~ zwk){W7Z7_V;^DX4c>V1I}?hQB0-T&uBSlDk{HP>6C;oZh3f?rMdS+yKZ%iyxy_$^sS+J0Kplef2>)_fER z6SEJipYULNuu#}xyWaI{T3X#dUdps>4UJ=1eLO<7O3F;UUh0yH`___=GZ)v2-I#f_ zck0K>x6(Jh{Z#tKI3d`o+4jGH`m`#W_XlQQkDr=$--%&$^O9PX zKQKppy0j)^^^+AFZoS%FmM}4RH+QSO{gzX!L(e!wZ0V?1es`>fr?CF~Bzx<(KQz1r zR`0krJ?O_zt}_QBOyYP~b6-AnxHMU(z_83P+0uK_nMO@s*`$eu)~j{mbgpMqd41iJ zt;v!2^UDb(?}^Zsm? z-KD`G(Z$4Ivi)CedOhd8m)b50*? delta 851 zcmZ3?xtw#tdd9~SH*Bl_UAE=U8m`vF8LUgLEietaxZ?G{^~Kv~_D=Y4^Tz)*E#EKb z9sfA<)PlKu`<^S`oqu#yd%NB2WtN`5QgwgN>)&DDebwkqRalF5#Db-YAqNljPI6%7 zHWylz@X+U=Ro_;DbqSJ%tGCIl;t1Vv$n?t@JMSG$qOqSp)QalN;<{G2=AL~0Y>w~^ z|6cCnnszt#f&cSoyC++9mHyJo*LmK*RYdhZ`@LOpSAuma<^Ie63%Hr2`R%B}*+bu1 z^?JVUJF@=i&ZOF=0 zO-l?=i&&r;6VkUQymjN3PeBo4{AXihxAxUd%#Tw{Sbg(UYIfriv(tKTqKIR49pf6gWFv60j0 z-}2`|jNqY9n#Tj%jE)wqa}^IM>ut|c>JVL}rk`*6wBhUSEt%ENW3Goy>Qh<$ zOFVdQ&MBb@0IKk(lmGw# 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 index 29de617c38abf3afac68c64866345b74bce06f89..650ae9d86e7c7f35d964cab52c13aaaa3328f5df 100644 GIT binary patch delta 386 zcmbQuHjQns<-c6tmG-^mrWvbOZ?Rx&#`}m}5WVd;~T{1tS@V1YC zzw^v%{Ttu$zphsdTT>Q(f2C2ceB#knd#rM%FS9+e%Is)p^SZj0t@|x?%1ZxlxD{J? z*@pN1##^7hZOD}R#<#lj`|8>El1usdX7AR}O^UjgY-=YPHnH<+ZbI$JDUlJWoZMnM zFt?&Ieys)u0Zg&!{$eE>k2hEcjK_*WC1wJX^P6N+gG&#^xWKXQRLLuKM(Q QA_EY3y85}Sb4q9e02Z^fp8x;= delta 378 zcmbQnHk)n2V%ua-7srr_TW_z0GCeY2a5!kPBU<;n{R^+c^Y^`;KMe3>%kEH46?68O zso65&nWFMp>oox>OBlGtbRsq+#7^G8>({6kj3HLH@nyd#$Ll`sAEPH_$Y z-j<&|rVUejLzfw@iCHt{ziO*#oLhusgnTnAMEk+6#eowqXNdgn6;)4tbxXf%_Ojof z_g>wyduGo0?xXjTbZ1qW@-`d9$px8e>gitodGF!(q|M9TigBOz%~Jb&r0aH7)Ew^i zEp?q&>vhHC#a_#X>6D8%pHuCPPdxgmVEYX{-|CLgysq7BxnH@m|8I!MS^Y23boZl$ zl~Q)ertQa%F3Y@Oy~fI}Kkr}9)qOE%95a1Rr?0ub`jFPGP_eMR&0oErO=vB(IM4)h zE;{4ix`cy3v1thhCwDSs0>eQ|^^#<^S&-XiKJS^W0?kI<_KZGiU#4vny)uIV2s~Z= KT-G@yGywo!U#gn` 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 index b9ad9103bdea793f92f60354896d9dcb35e9ff72..133a33f07eea974746ae518fce14d83bb05114aa 100644 GIT binary patch literal 1484 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4rZW;-{LxVAQc|q6XFV_X#x1APtFCJD^n8W z7wo_wpkQF{9}u6AUr^uBKVklY^&9pdIDg^(gZCfiSKNBcz`)Ak>EaktaqG=EKIXOwyd)gY-6Cg?_asa7ohxg50 zYreuMQ6i1`ScLNwExsRj|L4A2+1xd0*1L&z|J>JJKKOb|gv89$={lGHADGy^?QdYj zFS`%(zPbKA%WnRmc|!teuqF z>{@ke?>0Xj#=Ol2?eC7lYbAG z;tgK1guOaxtqs(MZM=M>U_bkVeFsn3ez_)bo4I@@kJt_8h)tmh!kC|SpE0#hMEG#s$UBlVr9|} zwCYb}o%U(Qg6E2t4qRg2Sa2}U@t~oRPHxJFrii4iJ^NdJFKbloOIo>sd0CQZ$Cug1 z7tBf1VLX}^v$S4h+V4dtbR5r2_~jUwHSX8A9B!nt8LA6m9VRN z!LpJ+S|SqU5?;r3ZC{zPrD~%2h}A*_Zo=j1(S4e2!22IH6+h z(|rd{<(2-PnAf>fJ|XhNp*>5QOds|ow>m!zb$PL=`{@l|sffOZ))70|-LiWsyenn0 zi>xoM{IYppV<x$0*-Pgms?rN?INb>GW7Y0xe54Te#BZ`}w|c1m?!BHf z%kPQtf7O-Lxw*MK=HcU|&L1KlXp8xjOxm5$In{aN%CDbHHr(6t$9u;czP()uCp-Q0 z)H$nn?zypQ>(jY)S2u3T){R_#L36IM?3SvX51uWYbkQ%<&fod8^p~Q$Z?|y&dF>#7 zM*7y%Gq1KbtF3+i;5293Z`0Ey(_-JrURdEize7Zr=k43DatG0A*_)W+@9XJr59dz{ z-X4D7_1FGqXKEW(db|}EyY%JE%ig}P57PU$`-JViJ9BkZh{^obhx28_RwtTh9lif< z#U+X7TFFJFIn{yFoZP<_C;Moo_= z!YXnTu;ggf89dZhYp`2XSMOn&GSl@!VGO%tIP;lzE;@NfoFqVHvZt$`%Q~loCIJ8K Bq2T}k literal 1488 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4rZW;-{LxVAQc|q6XFV_X#x1APtFCJD^n8W z7wo_wpkQF{9}u6AUr^uBKVklY^&9pdIDg^(gZCfiSKNBcz`)Am>EaktaqGIyHnWhuDDdps=1~4_LbpWZ})0_18 zf0u2!qru%)C?jxP!nq?fpyFg)1tQ|`KI~b_-dK;`Ble$$6DuOF{kb4Z_53z zrBf?kacq0I>i-;`Kdupx++WT~J+3-nf2C9O^jYIb-T8s7&5rXUZf|_B>F7bVJr*0c z1v%+XzbI^Hy>Xwr!)DeS^CIpW8)*I121ofiEH{dsT+lb=z}$##kpLI3cSoX*HcV=t{QSDj9h=komD@IVEPNah`S10l z=b4F?YCn^6qP;YaGBbR>{mAUYnSz@V?+)8%m+x&{x=yzEkF$D<>4UxwDNXqc=Etrq zol`t>rJMOVm84|B!++%)c1Icu4h6~9R(#-c`)uQz+Ze=lao)xd ze>3ZgD^JlX4{6WeGDPK>4}M$Uj}Q!C&6s+_|%&7z|Bl{u@4eNlt5~1+M$<%eLC4$u#x# zJD<}Aj1xXxDUvjGs?nPsu%t<~FJY(rtTRGKKApeu&2)w6QJF=T_xUF(Qe_G8CpIGtJPq_eZz zbZ74il?^(38bg`3a=4evoL;*7M|fkMr1s~{zC7OV6Cypty`8@rh4DVEDmkA2;bqdv ziSJT%`ftD9+$Fn)%Vk%D*e3CfEHgBw-FpAP^-_f7jO!a)dg><1WtMGFnNhNF$>G(D zBQ9#M{ds89g0c-#d%VQ8n|~RErIcy#Up=S1`0pXD1wP{5QX2e8N7rrPGx%t?ovCRq z`_8UfmFA_t&c=EFn0xJ@ep$r@mI$e?(%tsSN2jTJX$T*jwe?Tw-)B?VB{9jKezd zOPOkTE;706&5du1R<(4z5YemFPlWV5OIzF%6aQ$<?e4 Bk#Yb4 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 index e827e8ae923396c3c9f6d1f28ef519ad9d13a89d..bd0a21e00b46f5993d0854e27f07ba2d7581ebd7 100644 GIT binary patch delta 801 zcmey#@ttGB`ufeDE{-7;x87WEXFL!f!n~kl?!MON_tT~~aWHVE?ALgEa*BtMw#c>l zQ+u6KPEJzwp0=h<2g0BVRX>d^W4><4cRyX)y#gCoC%xaCh~vhJ|Gn?wbbtSyLzVZ8;AcgZ7{j>w|f z9cCtGadE?@jR~0%KO-i{MDb=PrngCMT-G-A(4}}@p>;RfzP4P|4L^NtgWSf5y6;lc zaw2X<+}K-wxa!EGPR@DXU-{@4ZK$~aebF+@(|bwZ0o(%uaRES@LyiLh1YW5>|2E0sC2URU>XZIaIzbF_6dV{=z$}tL2}jb6aR!Yw-X7 zPvGzBzRx-}lXD(@`~7XnmCK8wHkA4Gr5viL>^2SQe0nrgDSvC*k?XR|Ci|Fl4sUeZ zmwalWcf!ZHBImYM=vYru$~*j9>96Z7=>sp;W$5IeRatDbk>S{e`TwsqzKw_w39FlN u?O4>>NVl~KQQ2y@?5+MX=1yM2yi;UV(0j`zGd^Bq00K`}KbLh*2~7aJFN*8{ delta 803 zcmey)@snf1`ueS&E{-7;x87VZXFOoQz-&++yHMbt$!>uk{Va|*^}VW%yj5l@vmTp2 zb*)3n$w{i-)7Cr%Q#z3+Ks-vRdeV(?0b06AuI#OIc$feOv%%TNVjIez|Be4#W@L5Z zlkmdqa+`tJ3wV#8ig-}F)hO)rza?&K%8z|Yl>BGTm2<48wEbxG?(|a;+aq-LR7iTK zu1V0*xi@QL-o}>YP2xwr|9dGmZRlDgpm{a&Z=jcdf^p_YN$)-X=It>xD$=?0`N-xk zY|BF)F^5%JM7sBFJ#hcck7Yg%#+v%qg!?j+>W>9YUaW5Ze+|n~CvBgWh|?deUtGHM zy(^2et98@HRO+;ZCzOzsF~@R4O;-ypDMH zBWq`b?$4h4nhA-#`x0Ni6g_k_Dx-f}pFH>0DaPU&WxJP`Y*4x2H>+dEYlVpVbqDXi zdysy_TSIJdy@}?R2OEDJJ9tQc_3{Xh-Rez8uPe7LtmS<5e4#HZcf9WN*{35!nrqp$ zlUFjV>1lns-g%?ZwbYNk4%rqj=fC^X zqJG1pt2OeG2`N*xMwyv>RQ|DcsJMW%4Q{O|3LOg6FloVWXbec_ysM^MrvOmW;`(n0JaaJbJHi>d-=K1|aZs^>bP0l+XkK<35H! 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 index 9e7ddf97b8258aaf600a88c3e3e344dc5be6c85a..01f85188825ba27ce4e744de8a25da51a95f81de 100644 GIT binary patch delta 1167 zcmV;A1aSM(4BiZos{x*othj%-+$au&VVIT#VX#7o`4#W|-{;nj;~uNAYna+OA8N&;Ht_np1ln-hO4TY7p1UpRT4 z6U(j8$*!DO5&a$@;sp|%NHeI+S(SY zEX(lCuqJ(O@B3af?vel?UEJfASSupo<5o)_{uNg7i8(y>eH4?T@2`i#8WiiXUMDo^ z@-W08?i~%697|Ez9A1Aza+0Tm_Gnn`2CD*?h8V8{KzzV*^R3hl0DL^b@(EanYix%J zzQ%Ve)RM&k;ZRst7%Y}gFomzbJD~W5HA3gVrr!$04VD{WZQR#;mb*v?{iZdYf>V#(B!K{hsb~Lc>(Aw)&Ma-!7Tq~ zfyQ%iXAkp^HC*R%Fs?9AK5BVYM(BEKO(^0OsnC zuts30MDu`E!)SWwBVou_9LTj)F9ip*q z`06q&3+jJnpfLL~Glb({sZ}dmtjbJ|_WriQep}F+{VdhU)~RCMuT}vTITY4g(obrz zLVoiP32GgJ6mo96Qwn_k()`3=)o^X0IS!U9-)D?fzD1MX)M|vu>3-Hq`k|F)d+-hL z`6YNs5QmIjR+zyZ?c2(0BU>!(qv!WaS^l_;z#4yah#{=E(%WTJ?#;F`^dFax9-z}z ztcx1B%CWH4&dCcb2@4NdvUVp-Xw)j}4F}LPA{ESL-=69?SnDvf+kpYm)XKF++epWI zZPG!^uETUc)MtD6+D$Lt?SFTCQODR6;n{vxsAG(^m)8kJQJA`EdA=2vjbi=Bxcxo! ziB(zeALPX9p+2$p%ux?-JJ+%h@Y znGwQlh6e|A=ZD^ h9t!)iC?%5;a{;axZM)*1U10zK002ovPDHLkV1fo#Kq3GD delta 1168 zcmV;B1aJG^4AKmcs{xXcthj%o-7E@*aa@*IKPARST;PBI*Ew5Kj$mw^edzFZ?_=cn zJ79n=|6?pYe$;!{y=n0mS3i#W`(RZ>wOp0>E6YOlZLxr>6vtJtsz=pk@Bl6zIMG)H zYx8BjCR*CDBqOt6g)*_$9WVv!082E$BE!`tSOsgZ)rZ{U%P`zyMMHlt3`A8)aa;vU zc^Jg0DpZ$?cX{|LcdFdqg*NgjKzhJlu?<5lL0!c+U#Ug4ySdBb4hlr?5ia zl4y^mM|Q_FfD~~JFyw!a)d0-an7ajNH&}9cM1uw3_5w>gz}vg&?Mhft`Q0Q%I?%ac z-P2qg0ybE|;R!ILSQdXw+`ywp&0x~igUlbx z5w#1baEM8_EnJEPRIl1?VM$sZL|#~yJ=P4(t0`0k>;9zm9xLSAjwutxI9YncNR;mo8c`1 zl^Yh&>~gsRF#Uf}Ut!h23#{roPJ|+KL++bNmKB*{X+XcojR%1DH~A@6^$M#R0rpsG z)0-w}a>J^Dm$_=!bR~TfOFpQq?agl$5AQa(d665|0QM-?{ljaie;3vS*jj{&*PKfj zk$rYp4*Gqbdg()~W`|`+1{kniAL);Fb#7QSpwm3z2n&C}gJ(k@V>vM5080$eKii!} zZdfDCX`Yt$(4L-WC8tTSG`Q#dw8;&t2AW;2FBh4B3`bZ?I)gVbf70#>wGa}JnPJHV z=6!C1U@|4DVESrpXi`uUwcEn|xA%j{3rkKwwa*RoM_4rJu7ej5R-J&#C7P-~`qTtf zK3Ku3-{*h3$#uV75jnX@)P6rp12SoZD&lQ4tRgF{HwO%_avgUd?Vt{a;B+M6)16Yb zTNdh_1gi#Xz~qA^FV}_0D&3-SZ{hn3%;AvLhv>HiBN8kF+@6A+gVx6TRvs2mx&5}X zZ_<3bj$XR%a{19sfLGxVt352&?()`65qH26Gr)g%?QSE$knY9w0C$lWmha_Z&(kZL zTBGGo6|!IzV<`)vS|J*klKUJ@K3Lw`zWV&0)&vr7l3fRBKC-zIFR(=L^hQ0`I4a&; z_22DpcU09F(^mS`ewJ>GF^v+(rK`G&yCak^0K1J66HsQL8LiXR+)VtAf=G)|$Hf z6O8g7Lkd>G`nerCumAuCf&g}&;`d*;bb>1Y$p}G{AqE_i@C8ki@C6nptbej($&w{Y i)}8|UvPvYA5_19TRBgg2ZQTR_00006+9OJ 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 index c07f1e09d48966cda557e361bb0daed33fb234e0..36cd4484f502b55c1dce5fd06a25194b120c93e1 100644 GIT binary patch literal 1537 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4rZW;-{LxVAQc|q6XFV_X#x1APtFCJD^n8W z7wo_wpkQF{9}u6AUr^uBKVklY^&9pdIDg^(gZCfiSKNBcz`$zj>EaktaqG=EKIXOwyd)k_alUt4 zYmLkL=!0DLj30UVvwkQn-q5Bi)*92&@X0rSsR^I=wuGG$$*m1>|o%)0WIMVol$N!9(#HFlA_Y17cdq8Yr?t1M{c?-}o>giXG~t7oGd zRHAuD`RDPOq00geg-ni(n9`r2!5+HgLGfa5)g^Yzx9^^j-kTaRW&OF?%SGS(nQG{^ zd`=LnP}ou@-ouMxR{LwmiEHXeOP`{I!F>s&g z!JZp${-pl=XYiWCL6!Ms{jSgHvSK0UCTp)fD{%FkhW1yEoY26TTUOp`U;0{p|J3Po z?Rs)cn6BUR3^wKW4sU)`!u9>Tl$Gu*hKz*9H6P~|$p79@!aPq?CVivfi%k=6#>`gS zf1_1&SNEpy4KtQ}jnp~i#+LPearms3rS%Kjj-HD@xhm3f&bl+n-Qv(8JF$&;SxES~iI zQu2do|Ns7(85&`6voWh*g}48+Y16W-lU|SWt?MJ+ZxG(Be(6Z7QDphQO?@hn{*3(us37-r^>bP0l+XkK>8q&X literal 1531 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4rZW;-{LxVAQc|q6XFV_X#x1APtFCJD^n8W z7wo_wpkQF{9}u6AUr^uBKVklY^&9pdIDg^(gZCfiSKNBcz`$zm>EaktaqGIyHnWhuDDdps=ZZI{ebpWZ})0@_u zKDsbo7b@Yp+vGw z-XxL#-0p&sb{kH-3bQ@_PVMFq>n@Maji%aX56=1`SMlVB%sk0|doxz0C0b5M^pu~k zTsc|tlU-QiSEWm9*DVb{eZF42=(@C>`h$7j5*l51{)^eey0qiYy)SJ3$Fjqo7-vMh zH+H^vTx*TuXRAY>>V+Rk+jT6<61^6>F=E*~g@u290gawo81YG1M?87W#DG2bdCnfP z+8Y(Ak$$`OJx9QC$)ziAwUt(X?7ps4bkad)ol&+;Ma=qXrs+542YD3)c0a zJr^~XyCj92z8kePs*QE0-~6x@YOY_WPW1~3Pgr#<(Q)BSm6eqjmt?eG?vx9hv8ps; z!h4mIwYl&Ai0=NYxRP~2SLYY|S*r>oqC%^7w)M*HzR)-6lhRtg;|BV(eU0A;xZW_)`B}Mpy(8!S3Uiq(38AYe z#rItLW5RPgJn|4b>uZ*F=SSNlZ|Q%SA*=DkJ@k<18nNtfwNLl55-R7KzPuWE%XR*q zRj2tQ9v4Y+hr}KzU7sN@_U$BpUG0UZC6h0uM4SJfI9E})^0i)b=#AE+o4Y=4xX-%( zcd^CO-!F24x>6_CiFds(IaO8pK)NL9U4298zvBJgnLF-kss$dh`}In9G28i-%ejBd z_&MLk^hV3k$+D|H+W)U^%IDZJZ}p`gMbG&^&n-*(>N0D`4Yk#3OLy38&f<6S2uWL! zG4dga4z`AUv|B@4;Z|R woF=GxgNmBLMZMk+bketg8`E7iX9mpTew9;F_vXyfAD|-K)78&qol`;+00~I6q5uE@ 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 index 9d2aca7cc84b7781e2db099a09643a9b21101d53..0065f0758965ebe91d28be6e7d89a3f8c62aca90 100644 GIT binary patch delta 1106 zcmV-Y1g-n846Y22uK{Y2u(*HS?zV|UaTsS_SVGCvAut&5bN|=5>5)>w>6v(={Q}GW zztk>LT7H^Gt!Z-`$MJS*tnp*X+3UuSb&> zDl|J3J+H8|)gy(?*B_AWu%cMyW_#3ukTA$$K z1DFntB^^#ig=U8n&=s-d-Qt~^?-kZ)|HZbFVh@ca9eQ1%-C)Q09^@7U?1V>$DL5F zEd%M$SeE4-3Kt4290BCfDR)>wMP+_5=ZLGd5(g5?Jv82OZ zS7>$Ei$)16q9d}gGGFi5SP58lg;s~FxRtQvb@gAzigJIKszRf~N{-z{vGn2573vQ4 zvHKTTIhA)*EPZ$shoYPnhQf;9z)GikQmPM+u24}%{Xp6J)3AMxHOTAJV3{3uxkE=;lxa-iPE7VwRxx;CL zCHUicv7~>XHs}g>9k%GmG?p(ukV**F_FZ#ctR5J2g}V;ZppT8l5(V=swCu3_QhI{y z$I%}js{z(QRk-Ug&w#hFb_%>|fuG3ozWTLtdesDzKRlL0CKZ|;$_8g*sVBp{Z^iw~ zgvN3SQQft*zfStsI4)KT zs0vkwszMvfMR8T@|9nu@J^0)-@_S3;PJrcgg+_!=EV7Wn6 zXmr>;#gc`7d%A|U^awO6*4#;xXTS>8$Gj^nmake1UApDmr}U(%*!4N*d(OAi`v1St z>01~3yB#_v0RRSq0CqfddH;e7Rd9O~36Yc01vZnA1zRS+lO;=*ELpPlP}rA6DVb23 Y3ry#a*Q2;Oz5oCK07*qoM6N<$f>4Aog#Z8m delta 1107 zcmV-Z1g!h646h83uK{b3u(*F2Hmiw4VVH!tm}06Jf(Y;b|DDS{v$yrUgllK+ehAAx zpKNuodFDJL9;es7@2|U-)i1OrPunl}f9>cAK-#se2Y}J!8L=>!JQILV%O`gXSuGaw zXLS&2lcV|ll$(IT9zj-(g=;=E2>D1m0FRde?2aHyIvh4D{PM|Q1blyx4k4?h)KrC$ zBUOpI!^v`#nyS!0HX;oGo-dD49YU6Q>NBX&>g&dbk9K7 zdam#|vdpZaQWfSq?EZhK!uoVN|2Uo8J6Y~@`QWcp`Ld`(sxaT-q(;&83x=p}BR;vG z?WU~reZnvLAGUZ|jq;%@%yro4n~e$!q@ai9r_IXxF+ww4Y~ivD#7Cw=)uA#O&9*Fi z!-~nTWz_@L46bllQGBc`)Ez35ep{9uAUv+YS4{|U#s{8dH4=Xyo?Tmg(1Zlh>dKbo zu~4ro%y%gEEXiV5b9lL)j}VkBC$7@#LFyp3>Y`;?EVSzi^Bo3VA4r^+bWQ|)3o2X

s@Ff2<+h}qWFE@+V9&0uGiU4$%NF2{U@++kEB!Lm94bN%+~ zPEEDZ%RFTaFM1 z!$TEPhctHIovakLDODB^Pf$8YC5_#;WQ84WN_lt&rGr#DS}12f4gDWZ)_!G~8FI5( zs|s0V%XbDWYkqUM)G}Y+jbKfD0S=2_fm~r^+zZ01UQxSr&q-P<1Fola)oAVce_N zE1yTOEE6DBwU_z^6uP1zBESt%hg>1c5*i4@<@A5g@6xwPJjraT?1kI8HbyvI26~Tz zWqANpA$3R<=4Bc8#F}ossA^|?()akqvoSl6Ww}CyemTq0V`R?C9If(HG7a{ z;<`fau-%lUsJj`ziGFJZ>PTyBrO7?Ws;dQSHgNRp8(bT%DRm(k8}=VKf8qXv_a8LY{%>PoVA=2K;uunK>&=BgMxbKW z4eSoBujdP^?5K}!NsXB$F?o@u=iJN(e>NFim~cP+;ESO8){@SpP|LFYx-|NqxUANKQGhEZ_y~VuMiA^g%O_tPIy|eqq?`jf>xNA zg<8gE|3wic)me?2)72IHbNjkYU0)qA+Hfy%VZ_cQ^HX&eFHc#Y`e)x$C%NTwz8Qoz zsW$9?9l2fOaMrn9>$W|;T-wT@8E<%{NpxW~!$hMs9~!Q{Om1pTNwB+F1)`DJJbC>eB0NVp2MRBe!1 zpisv4Yr6Cvo|$&fT2ot=$>~HMn4=&o@U(Od!{Up8L;3Yxw+d}h{R_TWEj zxBYWNNXu0xD@UORbq%SE(+|A;(A*dzJdM?Fg5ZSd8$aDUoxR0??M3#XNt|C^=pWCL z-!LoL=}h@#cD5I*4!u&8i?7i6|(qUK8UA*4Ee|IL6 z!>U7@4qRuG-+x$$p?X*DBK;{}=d`ZW)A8&QZO{|8J$g#)q4#=sUh9Sk$*=$&_BHjL z)$#j}7csmKyTiUAvi9&9r9}^>CQ2TiD_o^V^`J0#n{D+@RkbWVFyk{KL7t UPilu8Qb5J1r>mdKI;Vst04yU>_y7O^ literal 1401 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4rZW;-{LxVAQcke6XFV_sSZRQ)b0QpDqRxf z7wo_wpkQF{ACQn=P~Xr$Vg7>k8}=VKf8qXv_a8LY{%>PoVA<*E;uunK>&=CuiOdE9 zu8ysiX?OnpHZlIo^o;Yn)!U!GiRagb&bTG?dN=FK>fWpKny&b5VBr?iiP(_92__E; zf!GI|RKX;xH;B#3Jq=8X>44ZkWe}1U8h^>_{J!Bk|MWxQYirxKuGu}iZuKkiYq4uM z*T}4nOL!V_e?!q4yEQV|Y}&_Hg-0k}+wn>|tYfRntHWFy?N+62a9ZY`Q&5T1ECcaz0)m}WNc-#4M@ttS* zFZ>DpCYsPHeO7%<9^-Krm)!>!>++_o`Nwsmty<(|hSu-w16IQJwzu1BOMV}YlZ`kX z>XvC%FvsZnU!`QbJLlFcWLqQr@_mGIUb1oAhmFaKOQ+n92%fd+wW`^2<89v_D&0BW zl&c>s8ez+|#^!#DK$OJ>y}uXJ@;=W$`84iahvhGhCbQ{Dyb+a`wKlH1!Th%Gs(9}7 znE8fxR-3I~%;;M-|6`a%OEKMloPz~!T{390@ob>dc%c+eMv&+1vCbUjnc;M9I*%8&} zC%o3IXD@m^!FpNLhO}kVH-7uIW&QOj`>hWhTPe1t@u_jx_s4JFzI`mY=GjEqj-ONR zEn3OC+BG_Vb@-8cca>whm~s>7be!F z9N%yuq-Cl^*_xJrtZVpURi5!zID`v^rQF*n)*`-vY1Yq+1xjD)Bqq9-$}~mU|-kz)lbIsfn#$4{-gRC0}>+dW3cE&Kj#DOq9uHU;js*@3FtVkXeW!L4qwIwIJaiul%wFOhTL!RlUVFrJS5J l$SBuqc3jnwQzmgP{TcnGpmEJ-OMOrQ>gnp|vd$@?2>`7JV6y-K 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 index 29de617c38abf3afac68c64866345b74bce06f89..650ae9d86e7c7f35d964cab52c13aaaa3328f5df 100644 GIT binary patch delta 386 zcmbQuHjQns<-c6tmG-^mrWvbOZ?Rx&#`}m}5WVd;~T{1tS@V1YC zzw^v%{Ttu$zphsdTT>Q(f2C2ceB#knd#rM%FS9+e%Is)p^SZj0t@|x?%1ZxlxD{J? z*@pN1##^7hZOD}R#<#lj`|8>El1usdX7AR}O^UjgY-=YPHnH<+ZbI$JDUlJWoZMnM zFt?&Ieys)u0Zg&!{$eE>k2hEcjK_*WC1wJX^P6N+gG&#^xWKXQRLLuKM(Q QA_EY3y85}Sb4q9e02Z^fp8x;= delta 378 zcmbQnHk)n2V%ua-7srr_TW_z0GCeY2a5!kPBU<;n{R^+c^Y^`;KMe3>%kEH46?68O zso65&nWFMp>oox>OBlGtbRsq+#7^G8>({6kj3HLH@nyd#$Ll`sAEPH_$Y z-j<&|rVUejLzfw@iCHt{ziO*#oLhusgnTnAMEk+6#eowqXNdgn6;)4tbxXf%_Ojof z_g>wyduGo0?xXjTbZ1qW@-`d9$px8e>gitodGF!(q|M9TigBOz%~Jb&r0aH7)Ew^i zEp?q&>vhHC#a_#X>6D8%pHuCPPdxgmVEYX{-|CLgysq7BxnH@m|8I!MS^Y23boZl$ zl~Q)ertQa%F3Y@Oy~fI}Kkr}9)qOE%95a1Rr?0ub`jFPGP_eMR&0oErO=vB(IM4)h zE;{4ix`cy3v1thhCwDSs0>eQ|^^#<^S&-XiKJS^W0?kI<_KZGiU#4vny)uIV2s~Z= KT-G@yGywo!U#gn` 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 index 43fe14e4eb693191dd3a38cebe9694c02d1eb7e0..90e0d815f6fdb93e2692331920609e07dda1bde7 100644 GIT binary patch delta 847 zcmZ3^xtMdpddB+`H*BllUA9G9lcTlp4s*nD$MChQ7SH)!e=gUTBYn_8VIVAMlX79Qd*WyD~{a1f)Y^^Wge7*nL z)D621OV`w`-*@_Jq|W5$m3JZ*%dhralGj{uIqz$|cu)AHDQuvclC<=UMf7 zN~5127&*d-t`C0lCVl5jC9~ zwk){W7Z7_V;^DX4c>V1I}?hQB0-T&uBSlDk{HP>6C;oZh3f?rMdS+yKZ%iyxy_$^sS+J0Kplef2>)_fER z6SEJipYULNuu#}xyWaI{T3X#dUdps>4UJ=1eLO<7O3F;UUh0yH`___=GZ)v2-I#f_ zck0K>x6(Jh{Z#tKI3d`o+4jGH`m`#W_XlQQkDr=$--%&$^O9PX zKQKppy0j)^^^+AFZoS%FmM}4RH+QSO{gzX!L(e!wZ0V?1es`>fr?CF~Bzx<(KQz1r zR`0krJ?O_zt}_QBOyYP~b6-AnxHMU(z_83P+0uK_nMO@s*`$eu)~j{mbgpMqd41iJ zt;v!2^UDb(?}^Zsm? z-KD`G(Z$4Ivi)CedOhd8m)b50*? delta 851 zcmZ3?xtw#tdd9~SH*Bl_UAE=U8m`vF8LUgLEietaxZ?G{^~Kv~_D=Y4^Tz)*E#EKb z9sfA<)PlKu`<^S`oqu#yd%NB2WtN`5QgwgN>)&DDebwkqRalF5#Db-YAqNljPI6%7 zHWylz@X+U=Ro_;DbqSJ%tGCIl;t1Vv$n?t@JMSG$qOqSp)QalN;<{G2=AL~0Y>w~^ z|6cCnnszt#f&cSoyC++9mHyJo*LmK*RYdhZ`@LOpSAuma<^Ie63%Hr2`R%B}*+bu1 z^?JVUJF@=i&ZOF=0 zO-l?=i&&r;6VkUQymjN3PeBo4{AXihxAxUd%#Tw{Sbg(UYIfriv(tKTqKIR49pf6gWFv60j0 z-}2`|jNqY9n#Tj%jE)wqa}^IM>ut|c>JVL}rk`*6wBhUSEt%ENW3Goy>Qh<$ zOFVdQ&MBb@0IKk(lmGw# 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 index 81a24d16867ccbe12beeb0fac2f8e0cf37f34f44..c4e2511c514b5aa18803a77d726796f1b7775478 100644 GIT binary patch literal 1672 zcmd6o`#aMM9LK*dW7#2(GUbv8pifW9!UTa!F`R z*-o-!Nn`F6+Or6Urc|DE8O!Aq9Xfx*InVh#&*ynPpU)4U_Yd#qd4DeZEd-b*H)G;n48(i;tfw)5c!{07U5Hg*lWqK0oXr_YesvAXAT4tFPPf)mJErnCDi$ znKn2h<{n5-yC;F+zI68awI}nHaHmCm{UK^U+T`BK99~1O0$~TA7lP~oe)IntH zL+Ka;o&2V#Rwf07ZvR4ILnAx1j*Yhb=@3LXXlFN++-m=VRh`oVKEBjmPua{#Kb=f}wsWq}5-mfD?ICI^J< zj3DI{4lf`{2}cI#V+H zRfNqe!uY(xkhDeZuD4~|LPJMerk$?GDIlVzv!$e&CZ(!lO!TvqtA-Xwz%P`&&5=iv zHCgkOHM zex^LJHnDoAR=(;p2y*#_Vx3GCjGiJ%?ZmeWdgTRzER?U(V3nBE^sY;Z(Vm%`jAaxc zJkj|UHoZ4Q+hZgv#r5qS;~=52%BaZW_I`SALjk<{8Z+=sQEk*(DA8Hw56jFEfGv{} zlC=gxF2KpS<`x>5TnkvXMyrSnGRqaO;-%VdU5tBi}rC5kZAS7p+P6)?`e)B$cEw8$Nquq|H-Or{I8 zhh=28ylcT@9T>*XduS9X$WQg?ug>RHCBcFUuO#4TJO2{?IDlw-StvQ zC!j+O`@U^q^E0gs6A>s&ae&DOQD*?Ro{#L|RzBpeSY$ez8Sj?U(%gzki67>{P;51n zYiius#|;%as{X)HbP%Os5dlYezT)&nJ;!t4#&)FAX10P&~Di+dTJZbKmT}*W7 zG4eN!jzqu{g6AP(+$~`q!-X6kE=xLBkNe#HHMf^q;l>wrso;t}>IrYTfZ_Wj>pERo z_VK3#WP?tSm@KBPvV@80%tz_SRkB4>{?zn{ZMh0~#rfIOmWw>3)kOI3d1fti!iM{% z3%#8a{MlkBiL}3nx8|CTZ`oo?~hTd?M}G%2>~+ z&?g7gi)NQd9VwTvEkJA;jk;s;T@%F5f0ZnG4N`8(gGPB+{KU&NV>5Wp1;@5WI?iUW zp&_=qtB~538#PcoT`N;(M{1WQdv*|h^q^G=W8Q37k=;&7Auu q{QehUfBPQ2Y?tjz5d>e8E3f4vY^lWk=$3)af8vA1d)@W`bN>b>XV=>R literal 1659 zcmd6o{WsGK9LK*bJuGH-ArBL$tQ$t|7}w*SF1lM{&19Z38yA(Q5vKBx2w^U5Lf1oA zE%}NGms_5nvgsh!a4{+hX_@1ph^zjGd+z<5^FFV0KIeQspZ8DiPa4tbqykhO3IKot z{uetE0LY-fYA69m)1P{5J}gmm9pZS2L32o{Y{LL`>P2gJU(iM8e(GG`IbaVV zpO2Y^sGVDJO*Kux%2tlGKVMShR*qlg;lX`W($-@&PY_`1gs?y0t{QIA zd*tO+v6w%4qlZJDSUz<~V6_s~$aRP11t6)ZE%p^r z!ZyCai0Cbyx2Nk4k@xh8c!hqF&~$l@H6e4=;!kDV-%!vDa0pkkF$IQlc%3FFkA|_W z0BRp~RHX!(3=+e^V^I>xtl=xvz-NCtGk@FTI(OIZ(G9Nr%WO+zp?ab}u-b0IYlg4{ z-FmBs4V8`fBkt24?}?y^H7DuuO~_Z+Pu07hKA43q8ZsVl8k^e`nj#oU6sf_0IC=yi-W#6Y+6S(!pUF?AE1xE?TqcL~ za^3*ztm^sWRPz$A1$_DDRIN*jb{^6SL82`tu6IRu_)bu;I1ff{eKe94_&5vI4_~G* zN~@Igby3E_l95BlN9HL~`5#!QBMla4-c_5b?#qb#+HR$tgz?uWH6F$=9T(g(XA*3q zf|%vYyE2#{t*?0E(<)ToL`$>tI+n@=8q+w1*%;{l#7nk;m7EIOK4;ieC2^s&PS8WB z)QR9@WEc0I{;h}LUD2dCJG5BcxaS?4{Z6YCtKZ7N6iG}7b%jmtYk*br2qMmecUpTXR7S@(hQMFS`L3w_gf`~m* z`S>n9%p`ud_^3K1i(RiBCb*I?f_-A`vsX4=w?HsKHt3S*x zfp%F`gOnhCAK~{lgffk3Fcas32gx=Z()l~>vCYvvn~zu_8`8Sl7F&92pN{lR(-#<7 z;WhEigL46vkwgfv7b269EJ60QI3tvhp&t9L>U>SMn(S`@_y0iL_unk}t-uD%^zACk a;Sqpu$ywSgYQ_EQ3&Gnv**!Wzz4i|@4BJuw diff --git a/crates/resvg/tests/tests/text/text/zalgo.png b/crates/resvg/tests/tests/text/text/zalgo.png index 3e7ca99db98789a447878be9306e34e53b6852b2..b0ae2b0fd3aa24395d369f536385529e243ff38e 100644 GIT binary patch literal 3104 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 L#^mvF*XaKMVMUGX diff --git a/crates/usvg/Cargo.toml b/crates/usvg/Cargo.toml index 0cbff1599..7b177e999 100644 --- a/crates/usvg/Cargo.toml +++ b/crates/usvg/Cargo.toml @@ -36,11 +36,9 @@ 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 } -# Note: ttf-parser is re-exported from rustybuzz, but we need gvar-alloc for variable fonts -# with many variation axes (like Roboto Flex which has 13 axes) -ttf-parser = { version = "0.25.1", features = ["gvar-alloc"], optional = true } +fontdb = { path = "../fontdb", default-features = false, optional = true } +harfrust = { version = "0.5", optional = true } +skrifa = { version = "0.40", optional = true } unicode-bidi = { version = "0.3", optional = true } unicode-script = { version = "0.5", optional = true } unicode-vo = { version = "0.1", optional = true } @@ -52,7 +50,7 @@ once_cell = "1.21" default = ["text", "system-fonts", "memmap-fonts"] # Enables text-to-path conversion support. # Adds around 400KiB to your binary. -text = ["fontdb", "rustybuzz", "ttf-parser", "unicode-bidi", "unicode-script", "unicode-vo"] +text = ["fontdb", "harfrust", "skrifa", "unicode-bidi", "unicode-script", "unicode-vo"] # Enables system fonts loading. system-fonts = ["fontdb/fs", "fontdb/fontconfig"] # Enables font files memmaping for faster loading. diff --git a/crates/usvg/src/parser/converter.rs b/crates/usvg/src/parser/converter.rs index 38bdbdbff..234a81260 100644 --- a/crates/usvg/src/parser/converter.rs +++ b/crates/usvg/src/parser/converter.rs @@ -11,17 +11,17 @@ use fontdb::Database; #[cfg(feature = "text")] use fontdb::ID; #[cfg(feature = "text")] -use rustybuzz::ttf_parser::GlyphId; +use skrifa::GlyphId; use svgtypes::{Length, LengthUnit as Unit, PaintOrderKind, TransformOrigin}; use tiny_skia_path::PathBuilder; use super::svgtree::{self, AId, EId, FromValue, SvgNode}; use super::units::{self, convert_length}; use super::{Error, Options, marker}; -#[cfg(feature = "text")] -use crate::flatten::BitmapImage; use crate::parser::paint_server::process_paint; #[cfg(feature = "text")] +use crate::text::flatten::BitmapImage; +#[cfg(feature = "text")] use crate::text::flatten::DatabaseExt; use crate::*; diff --git a/crates/usvg/src/text/flatten.rs b/crates/usvg/src/text/flatten.rs index e5fa39f76..90b668b7e 100644 --- a/crates/usvg/src/text/flatten.rs +++ b/crates/usvg/src/text/flatten.rs @@ -5,12 +5,16 @@ 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, + instance::{LocationRef, Size as SkrifaSize}, + outline::{DrawSettings, OutlinePen, pen::ControlBoundsPen}, + setting::VariationSetting, +}; use tiny_skia_path::{NonZeroRect, Size, Transform}; -use xmlwriter::XmlWriter; -use crate::text::colr::GlyphPainter; use crate::*; fn resolve_rendering_mode(text: &Text) -> ShapeRendering { @@ -45,9 +49,9 @@ fn push_outline_paths( } } +/// Convert positioned glyphs to path outlines. pub(crate) fn flatten(text: &mut Text, cache: &mut Cache) -> Option<(Group, NonZeroRect)> { let mut new_children = vec![]; - let rendering_mode = resolve_rendering_mode(text); for span in &text.layouted { @@ -63,32 +67,29 @@ pub(crate) fn flatten(text: &mut Text, cache: &mut Cache) -> Option<(Group, NonZ new_children.push(Node::Path(Box::new(path))); } - // Instead of always processing each glyph separately, we always collect - // as many outline glyphs as possible by pushing them into the span_builder - // and only if we encounter a different glyph, or we reach the very end of the - // 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). let mut span_builder = tiny_skia_path::PathBuilder::new(); - // 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. + // Check if we need variations for this span. let has_explicit_variations = !span.variations.is_empty(); for glyph in &span.positioned_glyphs { + // Only use variations path if we have explicit variations OR + // if font-optical-sizing is auto AND the font has an opsz axis + let needs_variations = has_explicit_variations + || (span.font_optical_sizing == crate::FontOpticalSizing::Auto + && cache.has_opsz_axis(glyph.font)); + // A (best-effort conversion of a) COLR glyph. if let Some(tree) = cache.fontdb_colr(glyph.font, glyph.id) { let mut group = Group { transform: glyph.colr_transform(), ..Group::empty() }; - // TODO: Probably need to update abs_transform of children? group.children.push(Node::Group(Box::new(tree.root))); group.calculate_bounding_boxes(); - new_children.push(Node::Group(Box::new(group))); } - // An SVG glyph. Will return the usvg node containing the glyph descriptions. + // An SVG glyph. else if let Some(node) = cache.fontdb_svg(glyph.font, glyph.id) { push_outline_paths(span, &mut span_builder, &mut new_children, rendering_mode); @@ -96,10 +97,8 @@ pub(crate) fn flatten(text: &mut Text, cache: &mut Cache) -> Option<(Group, NonZ transform: glyph.svg_transform(), ..Group::empty() }; - // TODO: Probably need to update abs_transform of children? group.children.push(node); group.calculate_bounding_boxes(); - new_children.push(Node::Group(Box::new(group))); } // A bitmap glyph. @@ -108,20 +107,15 @@ pub(crate) fn flatten(text: &mut Text, cache: &mut Cache) -> Option<(Group, NonZ let transform = if img.is_sbix { glyph.sbix_transform( - img.x as f32, - img.y as f32, - img.glyph_bbox.map(|bbox| bbox.x_min).unwrap_or(0) as f32, - img.glyph_bbox.map(|bbox| bbox.y_min).unwrap_or(0) as f32, - img.pixels_per_em as f32, + img.x, + img.y, + img.glyph_bbox.map(|bbox| bbox.x_min as f32).unwrap_or(0.0), + img.glyph_bbox.map(|bbox| bbox.y_min as f32).unwrap_or(0.0), + img.pixels_per_em, img.image.size.height(), ) } else { - glyph.cbdt_transform( - img.x as f32, - img.y as f32, - img.pixels_per_em as f32, - img.image.size.height(), - ) + glyph.cbdt_transform(img.x, img.y, img.pixels_per_em) }; let mut group = Group { @@ -130,22 +124,13 @@ pub(crate) fn flatten(text: &mut Text, cache: &mut Cache) -> Option<(Group, NonZ }; group.children.push(Node::Image(Box::new(img.image))); group.calculate_bounding_boxes(); - new_children.push(Node::Group(Box::new(group))); } else { - // Only bypass cache if: explicit variations OR (auto opsz AND font has opsz axis) - let needs_variations = has_explicit_variations - || (span.font_optical_sizing == crate::FontOpticalSizing::Auto - && cache.has_opsz_axis(glyph.font)); - + // Regular outline glyph let outline = if needs_variations { - cache.fontdb.outline_with_variations( - glyph.font, - glyph.id, - &span.variations, - glyph.font_size(), - span.font_optical_sizing, - ) + cache + .fontdb + .outline_with_variations(glyph.font, glyph.id, &span.variations) } else { cache.fontdb_outline(glyph.font, glyph.id) }; @@ -180,11 +165,24 @@ pub(crate) fn flatten(text: &mut Text, cache: &mut Cache) -> Option<(Group, NonZ Some((group, stroke_bbox)) } -struct PathBuilder { +// SkrifaPen for outline drawing +struct SkrifaPen { builder: tiny_skia_path::PathBuilder, } -impl ttf_parser::OutlineBuilder for PathBuilder { +impl SkrifaPen { + fn new() -> Self { + Self { + builder: tiny_skia_path::PathBuilder::new(), + } + } + + fn finish(self) -> Option { + self.builder.finish() + } +} + +impl OutlinePen for SkrifaPen { fn move_to(&mut self, x: f32, y: f32) { self.builder.move_to(x, y); } @@ -193,12 +191,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) { @@ -206,6 +204,7 @@ impl ttf_parser::OutlineBuilder for PathBuilder { } } +// DatabaseExt trait for skrifa-based font operations pub(crate) trait DatabaseExt { fn outline(&self, id: ID, glyph_id: GlyphId) -> Option; fn outline_with_variations( @@ -213,8 +212,6 @@ pub(crate) trait DatabaseExt { id: ID, glyph_id: GlyphId, variations: &[crate::FontVariation], - font_size: f32, - font_optical_sizing: crate::FontOpticalSizing, ) -> Option; fn has_opsz_axis(&self, id: ID) -> bool; fn raster(&self, id: ID, glyph_id: GlyphId) -> Option; @@ -222,35 +219,38 @@ pub(crate) trait DatabaseExt { fn colr(&self, id: ID, glyph_id: GlyphId) -> Option; } +/// Bounding box for a glyph +#[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, - is_sbix: bool, + pub(crate) image: Image, + pub(crate) x: f32, + pub(crate) y: f32, + pub(crate) pixels_per_em: f32, + pub(crate) glyph_bbox: Option, + pub(crate) is_sbix: bool, } 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 mut font = ttf_parser::Face::parse(data, face_index).ok()?; - - // For variable fonts, we need to set default variation values to get proper outlines - if font.is_variable() { - for axis in font.variation_axes() { - font.set_variation(axis.tag, axis.def_value); - } - } - - let mut builder = PathBuilder { - builder: tiny_skia_path::PathBuilder::new(), - }; - - font.outline_glyph(glyph_id, &mut builder)?; - builder.builder.finish() + let font = FontRef::from_index(data, face_index).ok()?; + let outlines = font.outline_glyphs(); + let glyph = outlines.get(glyph_id)?; + + let mut pen = SkrifaPen::new(); + let settings = DrawSettings::unhinted(SkrifaSize::unscaled(), LocationRef::default()); + glyph.draw(settings, &mut pen).ok()?; + pen.finish() })? } @@ -260,50 +260,38 @@ impl DatabaseExt for Database { 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 mut font = ttf_parser::Face::parse(data, face_index).ok()?; - - for v in variations { - font.set_variation(ttf_parser::Tag::from_bytes(&v.tag), v.value); - } - - // 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 - if let Some(axes) = font.tables().fvar { - let has_opsz_axis = axes - .axes - .into_iter() - .any(|axis| axis.tag == ttf_parser::Tag::from_bytes(b"opsz")); - if has_opsz_axis { - font.set_variation(ttf_parser::Tag::from_bytes(b"opsz"), font_size); - } - } - } - } - - let mut builder = PathBuilder { - builder: tiny_skia_path::PathBuilder::new(), - }; - - font.outline_glyph(glyph_id, &mut builder)?; - builder.builder.finish() + 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 (auto-opsz is already included in variations) + let settings: Vec = variations + .iter() + .map(|v| VariationSetting::new(Tag::new(&v.tag), v.value)) + .collect(); + + // Use location_to_slice which applies avar table remapping + axes.location_to_slice(&settings, &mut coords); + + let location = LocationRef::new(&coords); + let mut pen = SkrifaPen::new(); + let draw_settings = DrawSettings::unhinted(SkrifaSize::unscaled(), location); + glyph.draw(draw_settings, &mut pen).ok()?; + pen.finish() })? } fn has_opsz_axis(&self, id: ID) -> bool { self.with_face_data(id, |data, face_index| -> Option { - let font = ttf_parser::Face::parse(data, face_index).ok()?; - let has_opsz = font.tables().fvar.map_or(false, |axes| { - axes.axes - .into_iter() - .any(|axis| axis.tag == ttf_parser::Tag::from_bytes(b"opsz")) - }); + let font = FontRef::from_index(data, face_index).ok()?; + let has_opsz = font.axes().iter().any(|a| a.tag() == Tag::new(b"opsz")); Some(has_opsz) }) .flatten() @@ -312,106 +300,209 @@ impl DatabaseExt for Database { 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(), - }; + let font = FontRef::from_index(data, face_index).ok()?; + + // Get largest strike (like ttf-parser's u16::MAX behavior) + let strikes = font.bitmap_strikes(); + let strike = strikes.iter().max_by(|a, b| { + a.ppem() + .partial_cmp(&b.ppem()) + .unwrap_or(std::cmp::Ordering::Equal) + })?; + + let bitmap_glyph = strike.get(glyph_id)?; + + // Only handle PNG format (matching original ttf-parser behavior) + let png_data = match bitmap_glyph.data { + BitmapData::Png(data) => data, + _ => return None, + }; - return Some(bitmap_image); - } + // Get dimensions from PNG header + let (width, height) = if let Ok(size) = imagesize::blob_size(png_data) { + (size.width as u32, size.height as u32) + } else { + let ppem = strike.ppem(); + (ppem as u32, ppem as u32) + }; - None + let glyph_bbox = { + let outlines = font.outline_glyphs(); + outlines.get(glyph_id).and_then(|glyph| { + let mut bounds_pen = ControlBoundsPen::new(); + let settings = + DrawSettings::unhinted(SkrifaSize::unscaled(), LocationRef::default()); + glyph.draw(settings, &mut bounds_pen).ok()?; + bounds_pen.bounding_box().map(|bb| GlyphBbox { + x_min: bb.x_min.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, + }) + }) + }; + + let is_sbix = font.table_data(Tag::new(b"sbix")).is_some(); + + let bitmap_image = BitmapImage { + image: Image { + id: String::new(), + visible: true, + size: Size::from_wh(width as f32, height as f32)?, + rendering_mode: ImageRendering::OptimizeQuality, + kind: ImageKind::PNG(Arc::new(png_data.to_vec())), + abs_transform: Transform::default(), + abs_bounding_box: NonZeroRect::from_xywh( + 0.0, + 0.0, + width as f32, + height as f32, + )?, + }, + x: bitmap_glyph.inner_bearing_x, + y: bitmap_glyph.inner_bearing_y, + pixels_per_em: strike.ppem(), + glyph_bbox, + is_sbix, + }; + + Some(bitmap_image) })? } fn svg(&self, id: ID, glyph_id: GlyphId) -> Option { - // TODO: Technically not 100% accurate because the SVG format in a OTF font - // is actually a subset/superset of a normal SVG, but it seems to work fine - // for Twitter Color Emoji, so might as well use what we already have. - - // TODO: Glyph records can contain the data for multiple glyphs. We should - // add a cache so we don't need to reparse the data every time. self.with_face_data(id, |data, face_index| -> Option { - let font = ttf_parser::Face::parse(data, face_index).ok()?; - let image = font.glyph_svg_image(glyph_id)?; - let tree = Tree::from_data(image.data, &Options::default()).ok()?; - - // Twitter Color Emoji seems to always have one SVG record per glyph, - // while Noto Color Emoji sometimes contains multiple ones. It's kind of hacky, - // but the best we have for now. - let node = if image.start_glyph_id == image.end_glyph_id { - Node::Group(Box::new(tree.root)) - } else { - tree.node_by_id(&format!("glyph{}", glyph_id.0)) - .log_none(|| { - log::warn!("Failed to find SVG glyph node for glyph {}", glyph_id.0); - }) - .cloned()? - }; + let font = FontRef::from_index(data, face_index).ok()?; + + let svg_table = font.table_data(Tag::new(b"SVG "))?; + let svg_data = svg_table.as_ref(); + + if svg_data.len() < 10 { + return None; + } + + 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; - Some(node) + 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; + + let entries_start = 2; + let glyph_id_val = glyph_id.to_u32() as u16; + + for i in 0..num_entries { + let entry_offset = entries_start + i * 12; + if entry_offset + 12 > doc_list.len() { + break; + } + + let entry = &doc_list[entry_offset..entry_offset + 12]; + let start_glyph = u16::from_be_bytes([entry[0], entry[1]]); + let end_glyph = u16::from_be_bytes([entry[2], entry[3]]); + let svg_doc_offset = + u32::from_be_bytes([entry[4], entry[5], entry[6], entry[7]]) as usize; + let svg_doc_length = + u32::from_be_bytes([entry[8], entry[9], entry[10], entry[11]]) as usize; + + if glyph_id_val >= start_glyph && glyph_id_val <= end_glyph { + 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]; + + let svg_bytes: std::borrow::Cow<[u8]> = + if svg_doc_data.starts_with(&[0x1f, 0x8b]) { + 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) + }; + + let tree = + crate::Tree::from_data(&svg_bytes, &crate::Options::default()).ok()?; + + let node = if start_glyph == end_glyph { + Node::Group(Box::new(tree.root)) + } else { + let glyph_node_id = format!("glyph{}", glyph_id_val); + tree.node_by_id(&glyph_node_id).cloned()? + }; + + return Some(node); + } + } + + None })? } fn colr(&self, id: ID, glyph_id: GlyphId) -> Option { - self.with_face_data(id, |data, face_index| -> Option { - let face = ttf_parser::Face::parse(data, face_index).ok()?; - - let mut svg = XmlWriter::new(xmlwriter::Options::default()); - - svg.start_element("svg"); - svg.write_attribute("xmlns", "http://www.w3.org/2000/svg"); - svg.write_attribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); - - let mut path_buf = String::with_capacity(256); - let gradient_index = 1; - let clip_path_index = 1; - - svg.start_element("g"); - - let mut glyph_painter = GlyphPainter { - face: &face, - svg: &mut svg, - path_buf: &mut path_buf, - gradient_index, - clip_path_index, - palette_index: 0, - transform: ttf_parser::Transform::default(), - outline_transform: ttf_parser::Transform::default(), - transforms_stack: vec![ttf_parser::Transform::default()], - }; - - face.paint_color_glyph( - glyph_id, - 0, - RgbaColor::new(0, 0, 0, 255), - &mut glyph_painter, - )?; - svg.end_element(); + let result = self.with_face_data(id, |data, face_index| { + super::skrifa_colr::paint_colr_glyph(data, face_index, glyph_id) + })?; + result + } +} - Tree::from_data(svg.end_document().as_bytes(), &Options::default()).ok() - })? +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_skrifa_variable_font() { + 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(); + + let charmap = font.charmap(); + let glyph_id = charmap.map('N').expect("Glyph not found"); + let glyph = outlines.get(glyph_id).expect("Outline not found"); + + let axes = font.axes(); + + let wdth_idx = axes + .iter() + .position(|a| a.tag() == Tag::new(b"wdth")) + .expect("wdth axis not found"); + + 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(); + + 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(); + + 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 448d6545d..338725a8d 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; @@ -71,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 @@ -152,9 +166,9 @@ pub struct Span { pub paint_order: PaintOrder, /// The font size of the span. pub font_size: NonZeroPositiveF32, - /// Font variation settings for variable fonts. + /// Font variation settings for variable fonts (uniform for all glyphs in span). pub variations: Vec, - /// Font optical sizing mode. + /// Font optical sizing mode for auto-opsz computation. pub font_optical_sizing: crate::FontOpticalSizing, /// The visibility of the span. pub visible: bool, @@ -347,12 +361,21 @@ 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: span.font.variations.clone(), + variations: effective_variations, font_optical_sizing: span.font_optical_sizing, visible: span.visible, positioned_glyphs, @@ -1203,89 +1226,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) } } @@ -1406,13 +1361,13 @@ fn shape_text_with_font( fontdb: &fontdb::Database, ) -> Option> { fontdb.with_face_data(font.id, |font_data, face_index| -> Option> { - let mut 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 + let mut final_variations: Vec = variations .iter() - .map(|v| rustybuzz::Variation { - tag: Tag::from_bytes(&v.tag), + .map(|v| harfrust::Variation { + tag: Tag::new(&v.tag), value: v.value, }) .collect(); @@ -1423,15 +1378,13 @@ fn shape_text_with_font( 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 the already parsed rb_font - if let Some(axes) = rb_font.tables().fvar { - let has_opsz_axis = axes - .axes - .into_iter() - .any(|axis| axis.tag == ttf_parser::Tag::from_bytes(b"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(rustybuzz::Variation { - tag: Tag::from_bytes(b"opsz"), + final_variations.push(harfrust::Variation { + tag: Tag::new(b"opsz"), value: font_size, }); } @@ -1439,10 +1392,22 @@ fn shape_text_with_font( } } - // Apply font variations for variable fonts - if !final_variations.is_empty() { - rb_font.set_variations(&final_variations); - } + // 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]; @@ -1458,31 +1423,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; @@ -1499,9 +1470,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, @@ -1616,7 +1590,7 @@ pub(crate) struct Glyph { impl Glyph { fn is_missing(&self) -> bool { - self.id.0 == 0 + self.id.to_u32() == 0 } } @@ -1665,6 +1639,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 @@ -1812,3 +1813,70 @@ 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 { + use super::flatten::DatabaseExt; + + 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 && fontdb.has_opsz_axis(font_id) { + 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..e33ec160b 100644 --- a/crates/usvg/src/text/mod.rs +++ b/crates/usvg/src/text/mod.rs @@ -11,7 +11,9 @@ use crate::{Cache, Font, FontStretch, FontStyle, Text}; pub(crate) mod flatten; -mod colr; +mod skrifa_colr; +mod skrifa_metrics; + /// Provides access to the layout of a text node. pub mod layout; @@ -171,13 +173,13 @@ impl FontResolver<'_> { let base_family = base_face .families .iter() - .find(|f| f.1 == fontdb::Language::English_UnitedStates) + .find(|f| f.1 == fontdb::Language::EnglishUnitedStates) .unwrap_or(&base_face.families[0]); let new_family = face .families .iter() - .find(|f| f.1 == fontdb::Language::English_UnitedStates) + .find(|f| f.1 == fontdb::Language::EnglishUnitedStates) .unwrap_or(&base_face.families[0]); log::warn!("Fallback from {} to {}.", base_family.0, new_family.0); diff --git a/crates/usvg/src/text/skrifa_colr.rs b/crates/usvg/src/text/skrifa_colr.rs new file mode 100644 index 000000000..1d3687ca5 --- /dev/null +++ b/crates/usvg/src/text/skrifa_colr.rs @@ -0,0 +1,421 @@ +// Copyright 2026 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(crate) 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 color_glyphs = font.color_glyphs(); + + // Try COLRv1 first, then fall back to COLRv0 + let color_glyph = color_glyphs + .get_with_format(glyph_id, ColorGlyphFormat::ColrV1) + .or_else(|| color_glyphs.get_with_format(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..e3bab2de7 --- /dev/null +++ b/crates/usvg/src/text/skrifa_metrics.rs @@ -0,0 +1,104 @@ +// Copyright 2026 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(crate) 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(crate) 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/tests/files/clip-path-with-complex-text-expected.svg b/crates/usvg/tests/files/clip-path-with-complex-text-expected.svg index 49f2ea86e..c5fb10474 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..3de7dcd08 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..2fa81f83c 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..2fa81f83c 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..9a0e75383 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 @@ - - - + + +