Implement automatic font fallback support for next/font/local
(#47463)
Fixes WEB-787. This implements automatic font fallbacks for `next/font/local`. It uses the `allsorts` library to query font metrics across woff and ttf fonts. Test Plan: Now passes 24 Next.js integration tests, up from 14.
This commit is contained in:
parent
38953adb11
commit
56ebe97ae4
5 changed files with 558 additions and 8 deletions
165
packages/next-swc/Cargo.lock
generated
165
packages/next-swc/Cargo.lock
generated
|
@ -48,6 +48,55 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aliasable"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "alloc-no-stdlib"
|
||||||
|
version = "2.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "alloc-stdlib"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
|
||||||
|
dependencies = [
|
||||||
|
"alloc-no-stdlib",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "allsorts"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e926a9819dcf2211da0c19f5ca06a8f5c883e3bdb5ccc51afead3a7d995f023"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"bitreader",
|
||||||
|
"brotli-decompressor",
|
||||||
|
"byteorder",
|
||||||
|
"encoding_rs",
|
||||||
|
"flate2",
|
||||||
|
"glyph-names",
|
||||||
|
"itertools",
|
||||||
|
"lazy_static",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"num-traits",
|
||||||
|
"ouroboros",
|
||||||
|
"pathfinder_geometry",
|
||||||
|
"rustc-hash",
|
||||||
|
"tinyvec",
|
||||||
|
"ucd-trie",
|
||||||
|
"unicode-canonical-combining-class",
|
||||||
|
"unicode-general-category",
|
||||||
|
"unicode-joining-type",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android_system_properties"
|
name = "android_system_properties"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
@ -486,6 +535,15 @@ version = "2.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1"
|
checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitreader"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d84ea71c85d1fe98fe67a9b9988b1695bc24c0b0d3bfb18d4c510f44b4b09941"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blake3"
|
name = "blake3"
|
||||||
version = "1.3.3"
|
version = "1.3.3"
|
||||||
|
@ -523,6 +581,16 @@ dependencies = [
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "brotli-decompressor"
|
||||||
|
version = "2.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744"
|
||||||
|
dependencies = [
|
||||||
|
"alloc-no-stdlib",
|
||||||
|
"alloc-stdlib",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "browserslist-rs"
|
name = "browserslist-rs"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
|
@ -1611,6 +1679,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
|
checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
|
"libz-sys",
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1906,6 +1975,12 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glyph-names"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3531d702d6c1a3ba92a5fb55a404c7b8c476c8e7ca249951077afcbe4bc807f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.3.16"
|
version = "0.3.16"
|
||||||
|
@ -3076,6 +3151,7 @@ dependencies = [
|
||||||
name = "next-core"
|
name = "next-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"allsorts",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"auto-hash-map",
|
"auto-hash-map",
|
||||||
"futures",
|
"futures",
|
||||||
|
@ -3486,6 +3562,29 @@ version = "6.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267"
|
checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ouroboros"
|
||||||
|
version = "0.15.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1358bd1558bd2a083fed428ffeda486fbfb323e698cdda7794259d592ca72db"
|
||||||
|
dependencies = [
|
||||||
|
"aliasable",
|
||||||
|
"ouroboros_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ouroboros_macro"
|
||||||
|
version = "0.15.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f7d21ccd03305a674437ee1248f3ab5d4b1db095cf1caf49f1713ddf61956b7"
|
||||||
|
dependencies = [
|
||||||
|
"Inflector",
|
||||||
|
"proc-macro-error",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "output_vt100"
|
name = "output_vt100"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
|
@ -3563,6 +3662,25 @@ version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathfinder_geometry"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"pathfinder_simd",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathfinder_simd"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39fe46acc5503595e5949c17b818714d26fdf9b4920eacf3b2947f0199f4a6ff"
|
||||||
|
dependencies = [
|
||||||
|
"rustc_version 0.3.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "patricia_tree"
|
name = "patricia_tree"
|
||||||
version = "0.5.5"
|
version = "0.5.5"
|
||||||
|
@ -4205,6 +4323,15 @@ dependencies = [
|
||||||
"semver 0.9.0",
|
"semver 0.9.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc_version"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
|
||||||
|
dependencies = [
|
||||||
|
"semver 0.11.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
@ -4362,7 +4489,16 @@ version = "0.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
|
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"semver-parser",
|
"semver-parser 0.7.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
|
||||||
|
dependencies = [
|
||||||
|
"semver-parser 0.10.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -4380,6 +4516,15 @@ version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver-parser"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
|
||||||
|
dependencies = [
|
||||||
|
"pest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sentry"
|
name = "sentry"
|
||||||
version = "0.27.0"
|
version = "0.27.0"
|
||||||
|
@ -7176,6 +7321,18 @@ version = "0.3.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
|
checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-canonical-combining-class"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6925586af9268182c711e47c0853ed84131049efaca41776d0ca97f983865c32"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-general-category"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-id"
|
name = "unicode-id"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
|
@ -7188,6 +7345,12 @@ version = "1.0.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
|
checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-joining-type"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22f8cb47ccb8bc750808755af3071da4a10dcd147b68fc874b7ae4b12543f6f5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-linebreak"
|
name = "unicode-linebreak"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
|
|
|
@ -91,6 +91,8 @@ chromiumoxide = { version = "0.4.0", features = [
|
||||||
# sync with chromiumoxide's tungstenite requirement.
|
# sync with chromiumoxide's tungstenite requirement.
|
||||||
tungstenite = "0.17.3"
|
tungstenite = "0.17.3"
|
||||||
|
|
||||||
|
# flate2_zlib requires zlib, use flate2_rust
|
||||||
|
allsorts = { version = "0.14.0", default_features = false, features = ["outline", "flate2_rust"] }
|
||||||
anyhow = "1.0.69"
|
anyhow = "1.0.69"
|
||||||
assert_cmd = "2.0.8"
|
assert_cmd = "2.0.8"
|
||||||
async-compression = { version = "0.3.13", default-features = false, features = [
|
async-compression = { version = "0.3.13", default-features = false, features = [
|
||||||
|
|
|
@ -9,6 +9,7 @@ edition = "2021"
|
||||||
bench = false
|
bench = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
allsorts = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
auto-hash-map = { workspace = true }
|
auto-hash-map = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
|
|
|
@ -1,14 +1,32 @@
|
||||||
use anyhow::Result;
|
use allsorts::{
|
||||||
|
font_data::{DynamicFontTableProvider, FontData},
|
||||||
|
Font,
|
||||||
|
};
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
use turbo_tasks::primitives::{StringVc, StringsVc, U32Vc};
|
use turbo_tasks::primitives::{StringVc, StringsVc, U32Vc};
|
||||||
|
use turbo_tasks_fs::{FileContent, FileSystemPathVc};
|
||||||
|
|
||||||
use super::{options::NextFontLocalOptionsVc, request::AdjustFontFallback};
|
use super::{
|
||||||
|
options::{FontDescriptor, FontDescriptors, FontWeight, NextFontLocalOptionsVc},
|
||||||
|
request::AdjustFontFallback,
|
||||||
|
};
|
||||||
use crate::next_font::{
|
use crate::next_font::{
|
||||||
font_fallback::{AutomaticFontFallback, FontFallback, FontFallbacksVc},
|
font_fallback::{
|
||||||
|
AutomaticFontFallback, DefaultFallbackFont, FontAdjustment, FontFallback, FontFallbacksVc,
|
||||||
|
DEFAULT_SANS_SERIF_FONT, DEFAULT_SERIF_FONT,
|
||||||
|
},
|
||||||
util::{get_scoped_font_family, FontFamilyType},
|
util::{get_scoped_font_family, FontFamilyType},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// From
|
||||||
|
// https://github.com/vercel/next.js/blob/7457be0c74e64b4d0617943ed27f4d557cc916be/packages/font/src/local/get-fallback-metrics-from-font-file.ts#L34
|
||||||
|
static AVG_CHARACTERS: &str = "aaabcdeeeefghiijklmnnoopqrrssttuvwxyz ";
|
||||||
|
static NORMAL_WEIGHT: f64 = 400.0;
|
||||||
|
static BOLD_WEIGHT: f64 = 700.0;
|
||||||
|
|
||||||
#[turbo_tasks::function]
|
#[turbo_tasks::function]
|
||||||
pub(super) async fn get_font_fallbacks(
|
pub(super) async fn get_font_fallbacks(
|
||||||
|
context: FileSystemPathVc,
|
||||||
options_vc: NextFontLocalOptionsVc,
|
options_vc: NextFontLocalOptionsVc,
|
||||||
request_hash: U32Vc,
|
request_hash: U32Vc,
|
||||||
) -> Result<FontFallbacksVc> {
|
) -> Result<FontFallbacksVc> {
|
||||||
|
@ -26,7 +44,9 @@ pub(super) async fn get_font_fallbacks(
|
||||||
AutomaticFontFallback {
|
AutomaticFontFallback {
|
||||||
scoped_font_family,
|
scoped_font_family,
|
||||||
local_font_family: StringVc::cell("Arial".to_owned()),
|
local_font_family: StringVc::cell("Arial".to_owned()),
|
||||||
adjustment: None,
|
adjustment: Some(
|
||||||
|
get_font_adjustment(context, options_vc, &DEFAULT_SANS_SERIF_FONT).await?,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
.cell(),
|
.cell(),
|
||||||
)
|
)
|
||||||
|
@ -37,7 +57,9 @@ pub(super) async fn get_font_fallbacks(
|
||||||
AutomaticFontFallback {
|
AutomaticFontFallback {
|
||||||
scoped_font_family,
|
scoped_font_family,
|
||||||
local_font_family: StringVc::cell("Times New Roman".to_owned()),
|
local_font_family: StringVc::cell("Times New Roman".to_owned()),
|
||||||
adjustment: None,
|
adjustment: Some(
|
||||||
|
get_font_adjustment(context, options_vc, &DEFAULT_SERIF_FONT).await?,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
.cell(),
|
.cell(),
|
||||||
)
|
)
|
||||||
|
@ -52,3 +74,365 @@ pub(super) async fn get_font_fallbacks(
|
||||||
|
|
||||||
Ok(FontFallbacksVc::cell(font_fallbacks))
|
Ok(FontFallbacksVc::cell(font_fallbacks))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_font_adjustment(
|
||||||
|
context: FileSystemPathVc,
|
||||||
|
options: NextFontLocalOptionsVc,
|
||||||
|
fallback_font: &DefaultFallbackFont,
|
||||||
|
) -> Result<FontAdjustment> {
|
||||||
|
let options = &*options.await?;
|
||||||
|
let main_descriptor = pick_font_for_fallback_generation(&options.fonts)?;
|
||||||
|
let font_file = &*context.join(&main_descriptor.path).read().await?;
|
||||||
|
let font_file_rope = match font_file {
|
||||||
|
FileContent::NotFound => bail!("Expected font file content"),
|
||||||
|
FileContent::Content(file) => file.content(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let font_file_binary = font_file_rope.to_bytes()?;
|
||||||
|
let scope = allsorts::binary::read::ReadScope::new(&font_file_binary);
|
||||||
|
let mut font = Font::new(scope.read::<FontData>()?.table_provider(0)?)?.context(format!(
|
||||||
|
"Unable to read font metrics from font file at {}",
|
||||||
|
&main_descriptor.path,
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let az_avg_width = calc_average_width(&mut font);
|
||||||
|
let units_per_em = font
|
||||||
|
.head_table()?
|
||||||
|
.context(format!(
|
||||||
|
"Unable to read font scale from font file at {}",
|
||||||
|
&main_descriptor.path
|
||||||
|
))?
|
||||||
|
.units_per_em as f64;
|
||||||
|
|
||||||
|
let fallback_avg_width = fallback_font.az_avg_width / fallback_font.units_per_em as f64;
|
||||||
|
let size_adjust = match az_avg_width {
|
||||||
|
Some(az_avg_width) => az_avg_width as f64 / units_per_em / fallback_avg_width,
|
||||||
|
None => 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(FontAdjustment {
|
||||||
|
ascent: font.hhea_table.ascender as f64 / (units_per_em * size_adjust),
|
||||||
|
descent: font.hhea_table.descender as f64 / (units_per_em * size_adjust),
|
||||||
|
line_gap: font.hhea_table.line_gap as f64 / (units_per_em * size_adjust),
|
||||||
|
size_adjust,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calc_average_width(font: &mut Font<DynamicFontTableProvider>) -> Option<f32> {
|
||||||
|
let has_all_glyphs = AVG_CHARACTERS.chars().all(|c| {
|
||||||
|
font.lookup_glyph_index(c, allsorts::font::MatchingPresentation::NotRequired, None)
|
||||||
|
.0
|
||||||
|
> 0
|
||||||
|
});
|
||||||
|
if !has_all_glyphs {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(
|
||||||
|
font.map_glyphs(
|
||||||
|
AVG_CHARACTERS,
|
||||||
|
allsorts::tag::LATN,
|
||||||
|
allsorts::font::MatchingPresentation::NotRequired,
|
||||||
|
)
|
||||||
|
.iter()
|
||||||
|
.map(|g| font.horizontal_advance(g.glyph_index).unwrap())
|
||||||
|
.sum::<u16>() as f32
|
||||||
|
/ AVG_CHARACTERS.len() as f32,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// From https://github.com/vercel/next.js/blob/dbdf47cf617b8d7213ffe1ff28318ea8eb88c623/packages/font/src/local/pick-font-file-for-fallback-generation.ts#L59
|
||||||
|
///
|
||||||
|
/// If multiple font files are provided for a font family, we need to pick
|
||||||
|
/// one to use for the automatic fallback generation. This function returns
|
||||||
|
/// the font file that is most likely to be used for the bulk of the text on
|
||||||
|
/// a page.
|
||||||
|
///
|
||||||
|
/// There are some assumptions here about the text on a page when picking the
|
||||||
|
/// font file:
|
||||||
|
/// - Most of the text will have normal weight, use the one closest to 400
|
||||||
|
/// - Most of the text will have normal style, prefer normal over italic
|
||||||
|
/// - If two font files have the same distance from normal weight, the thinner
|
||||||
|
/// one will most likely be the bulk of the text
|
||||||
|
fn pick_font_for_fallback_generation(
|
||||||
|
font_descriptors: &FontDescriptors,
|
||||||
|
) -> Result<&FontDescriptor> {
|
||||||
|
match font_descriptors {
|
||||||
|
FontDescriptors::One(descriptor) => Ok(descriptor),
|
||||||
|
FontDescriptors::Many(descriptors) => {
|
||||||
|
let mut used_descriptor = descriptors
|
||||||
|
.first()
|
||||||
|
.context("At least one font is required")?;
|
||||||
|
|
||||||
|
for current_descriptor in descriptors.iter().skip(1) {
|
||||||
|
let used_font_distance = get_distance_from_normal_weight(&used_descriptor.weight)?;
|
||||||
|
let current_font_distance =
|
||||||
|
get_distance_from_normal_weight(¤t_descriptor.weight)?;
|
||||||
|
|
||||||
|
// Prefer normal style if they have the same weight
|
||||||
|
if used_font_distance == current_font_distance
|
||||||
|
&& current_descriptor.style != Some("italic".to_owned())
|
||||||
|
{
|
||||||
|
used_descriptor = current_descriptor;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let abs_used_distance = used_font_distance.abs();
|
||||||
|
let abs_current_distance = current_font_distance.abs();
|
||||||
|
|
||||||
|
// Use closest absolute distance to normal weight
|
||||||
|
if abs_current_distance < abs_used_distance {
|
||||||
|
used_descriptor = current_descriptor;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer the thinner font if both have the same absolute
|
||||||
|
if abs_used_distance == abs_current_distance
|
||||||
|
&& current_font_distance < used_font_distance
|
||||||
|
{
|
||||||
|
used_descriptor = current_descriptor;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(used_descriptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// From https://github.com/vercel/next.js/blob/dbdf47cf617b8d7213ffe1ff28318ea8eb88c623/packages/font/src/local/pick-font-file-for-fallback-generation.ts#L18
|
||||||
|
///
|
||||||
|
/// Get the distance from normal (400) weight for the provided weight.
|
||||||
|
/// If it's not a variable font we can just return the distance.
|
||||||
|
/// If it's a variable font we need to compare its weight range to 400.
|
||||||
|
fn get_distance_from_normal_weight(weight: &Option<FontWeight>) -> Result<f64> {
|
||||||
|
let Some(weight) = weight else {
|
||||||
|
return Ok(0.0)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(match weight {
|
||||||
|
FontWeight::Fixed(val) => parse_weight_string(val)? - NORMAL_WEIGHT,
|
||||||
|
FontWeight::Variable(start, end) => {
|
||||||
|
let start = parse_weight_string(start)?;
|
||||||
|
let end = parse_weight_string(end)?;
|
||||||
|
|
||||||
|
// Normal weight is within variable font range
|
||||||
|
if NORMAL_WEIGHT > start && NORMAL_WEIGHT < end {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
let start_distance = start - NORMAL_WEIGHT;
|
||||||
|
let end_distance = end - NORMAL_WEIGHT;
|
||||||
|
|
||||||
|
if start_distance.abs() < end_distance.abs() {
|
||||||
|
start_distance
|
||||||
|
} else {
|
||||||
|
end_distance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// From https://github.com/vercel/next.js/blob/dbdf47cf617b8d7213ffe1ff28318ea8eb88c623/packages/font/src/local/pick-font-file-for-fallback-generation.ts#L6
|
||||||
|
///
|
||||||
|
/// Convert the weight string to a number so it can be used for comparison.
|
||||||
|
/// Weights can be defined as a number, 'normal' or 'bold'. https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-weight
|
||||||
|
fn parse_weight_string(weight_str: &str) -> Result<f64> {
|
||||||
|
if weight_str == "normal" {
|
||||||
|
Ok(NORMAL_WEIGHT)
|
||||||
|
} else if weight_str == "bold" {
|
||||||
|
Ok(BOLD_WEIGHT)
|
||||||
|
} else {
|
||||||
|
match weight_str.parse::<f64>() {
|
||||||
|
Ok(parsed) => Ok(parsed),
|
||||||
|
Err(_) => {
|
||||||
|
bail!(
|
||||||
|
"Invalid weight value in src array: `{}`. Expected `normal`, `bold` or a \
|
||||||
|
number",
|
||||||
|
weight_str
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// From https://github.com/vercel/next.js/blob/7457be0c74e64b4d0617943ed27f4d557cc916be/packages/font/src/local/pick-font-file-for-fallback-generation.test.ts
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::next_font::local::{
|
||||||
|
font_fallback::pick_font_for_fallback_generation,
|
||||||
|
options::{FontDescriptor, FontDescriptors, FontWeight},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn generate_font_descriptor(weight: &FontWeight, style: &Option<String>) -> FontDescriptor {
|
||||||
|
FontDescriptor {
|
||||||
|
ext: "ttf".to_owned(),
|
||||||
|
path: "foo.ttf".to_owned(),
|
||||||
|
style: style.clone(),
|
||||||
|
weight: Some(weight.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_picks_weight_closest_to_400() -> Result<()> {
|
||||||
|
assert_eq!(
|
||||||
|
pick_font_for_fallback_generation(&FontDescriptors::Many(vec![
|
||||||
|
generate_font_descriptor(&FontWeight::Fixed("300".to_owned()), &None),
|
||||||
|
generate_font_descriptor(&FontWeight::Fixed("600".to_owned()), &None)
|
||||||
|
]))?,
|
||||||
|
&generate_font_descriptor(&FontWeight::Fixed("300".to_owned()), &None)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
pick_font_for_fallback_generation(&FontDescriptors::Many(vec![
|
||||||
|
generate_font_descriptor(&FontWeight::Fixed("200".to_owned()), &None),
|
||||||
|
generate_font_descriptor(&FontWeight::Fixed("500".to_owned()), &None)
|
||||||
|
]))?,
|
||||||
|
&generate_font_descriptor(&FontWeight::Fixed("500".to_owned()), &None)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
pick_font_for_fallback_generation(&FontDescriptors::Many(vec![
|
||||||
|
generate_font_descriptor(&FontWeight::Fixed("normal".to_owned()), &None),
|
||||||
|
generate_font_descriptor(&FontWeight::Fixed("700".to_owned()), &None)
|
||||||
|
]))?,
|
||||||
|
&generate_font_descriptor(&FontWeight::Fixed("normal".to_owned()), &None)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
pick_font_for_fallback_generation(&FontDescriptors::Many(vec![
|
||||||
|
generate_font_descriptor(&FontWeight::Fixed("bold".to_owned()), &None),
|
||||||
|
generate_font_descriptor(&FontWeight::Fixed("900".to_owned()), &None)
|
||||||
|
]))?,
|
||||||
|
&generate_font_descriptor(&FontWeight::Fixed("bold".to_owned()), &None)
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_picks_thinner_weight_if_same_distance_to_400() -> Result<()> {
|
||||||
|
assert_eq!(
|
||||||
|
pick_font_for_fallback_generation(&FontDescriptors::Many(vec![
|
||||||
|
generate_font_descriptor(&FontWeight::Fixed("300".to_owned()), &None),
|
||||||
|
generate_font_descriptor(&FontWeight::Fixed("500".to_owned()), &None)
|
||||||
|
]))?,
|
||||||
|
&generate_font_descriptor(&FontWeight::Fixed("300".to_owned()), &None)
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_picks_variable_closest_to_400() -> Result<()> {
|
||||||
|
assert_eq!(
|
||||||
|
pick_font_for_fallback_generation(&FontDescriptors::Many(vec![
|
||||||
|
generate_font_descriptor(
|
||||||
|
&FontWeight::Variable("100".to_owned(), "300".to_owned()),
|
||||||
|
&None
|
||||||
|
),
|
||||||
|
generate_font_descriptor(
|
||||||
|
&FontWeight::Variable("600".to_owned(), "900".to_owned()),
|
||||||
|
&None
|
||||||
|
)
|
||||||
|
]))?,
|
||||||
|
&generate_font_descriptor(
|
||||||
|
&FontWeight::Variable("100".to_owned(), "300".to_owned()),
|
||||||
|
&None
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
pick_font_for_fallback_generation(&FontDescriptors::Many(vec![
|
||||||
|
generate_font_descriptor(
|
||||||
|
&FontWeight::Variable("100".to_owned(), "200".to_owned()),
|
||||||
|
&None
|
||||||
|
),
|
||||||
|
generate_font_descriptor(
|
||||||
|
&FontWeight::Variable("500".to_owned(), "800".to_owned()),
|
||||||
|
&None
|
||||||
|
)
|
||||||
|
]))?,
|
||||||
|
&generate_font_descriptor(
|
||||||
|
&FontWeight::Variable("500".to_owned(), "800".to_owned()),
|
||||||
|
&None
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
pick_font_for_fallback_generation(&FontDescriptors::Many(vec![
|
||||||
|
generate_font_descriptor(
|
||||||
|
&FontWeight::Variable("100".to_owned(), "900".to_owned()),
|
||||||
|
&None
|
||||||
|
),
|
||||||
|
generate_font_descriptor(
|
||||||
|
&FontWeight::Variable("300".to_owned(), "399".to_owned()),
|
||||||
|
&None
|
||||||
|
)
|
||||||
|
]))?,
|
||||||
|
&generate_font_descriptor(
|
||||||
|
&FontWeight::Variable("100".to_owned(), "900".to_owned()),
|
||||||
|
&None
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_prefer_normal_over_italic() -> Result<()> {
|
||||||
|
assert_eq!(
|
||||||
|
pick_font_for_fallback_generation(&FontDescriptors::Many(vec![
|
||||||
|
generate_font_descriptor(
|
||||||
|
&FontWeight::Fixed("400".to_owned()),
|
||||||
|
&Some("normal".to_owned())
|
||||||
|
),
|
||||||
|
generate_font_descriptor(
|
||||||
|
&FontWeight::Fixed("400".to_owned()),
|
||||||
|
&Some("italic".to_owned())
|
||||||
|
)
|
||||||
|
]))?,
|
||||||
|
&generate_font_descriptor(
|
||||||
|
&FontWeight::Fixed("400".to_owned()),
|
||||||
|
&Some("normal".to_owned())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_errors_on_invalid_weight() -> Result<()> {
|
||||||
|
match pick_font_for_fallback_generation(&FontDescriptors::Many(vec![
|
||||||
|
generate_font_descriptor(
|
||||||
|
&FontWeight::Variable("normal".to_owned(), "bold".to_owned()),
|
||||||
|
&None,
|
||||||
|
),
|
||||||
|
generate_font_descriptor(
|
||||||
|
&FontWeight::Variable("400".to_owned(), "bold".to_owned()),
|
||||||
|
&None,
|
||||||
|
),
|
||||||
|
generate_font_descriptor(
|
||||||
|
&FontWeight::Variable("normal".to_owned(), "700".to_owned()),
|
||||||
|
&None,
|
||||||
|
),
|
||||||
|
generate_font_descriptor(
|
||||||
|
&FontWeight::Variable("100".to_owned(), "abc".to_owned()),
|
||||||
|
&None,
|
||||||
|
),
|
||||||
|
])) {
|
||||||
|
Ok(_) => panic!(),
|
||||||
|
Err(err) => {
|
||||||
|
assert_eq!(
|
||||||
|
err.to_string(),
|
||||||
|
"Invalid weight value in src array: `abc`. Expected `normal`, `bold` or a \
|
||||||
|
number"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -75,7 +75,7 @@ impl ImportMappingReplacement for NextFontLocalReplacer {
|
||||||
|
|
||||||
let request_hash = get_request_hash(*query_vc);
|
let request_hash = get_request_hash(*query_vc);
|
||||||
let options_vc = font_options_from_query_map(*query_vc);
|
let options_vc = font_options_from_query_map(*query_vc);
|
||||||
let font_fallbacks = get_font_fallbacks(options_vc, request_hash);
|
let font_fallbacks = get_font_fallbacks(context, options_vc, request_hash);
|
||||||
let properties =
|
let properties =
|
||||||
&*get_font_css_properties(options_vc, font_fallbacks, request_hash).await?;
|
&*get_font_css_properties(options_vc, font_fallbacks, request_hash).await?;
|
||||||
let file_content = formatdoc!(
|
let file_content = formatdoc!(
|
||||||
|
@ -164,7 +164,7 @@ impl ImportMappingReplacement for NextFontLocalCssModuleReplacer {
|
||||||
"/{}.module.css",
|
"/{}.module.css",
|
||||||
get_request_id(options.font_family(), request_hash).await?
|
get_request_id(options.font_family(), request_hash).await?
|
||||||
));
|
));
|
||||||
let fallback = get_font_fallbacks(options, request_hash);
|
let fallback = get_font_fallbacks(context, options, request_hash);
|
||||||
|
|
||||||
let stylesheet = build_stylesheet(
|
let stylesheet = build_stylesheet(
|
||||||
font_options_from_query_map(*query_vc),
|
font_options_from_query_map(*query_vc),
|
||||||
|
|
Loading…
Reference in a new issue