diff --git a/Cargo.lock b/Cargo.lock index ecb8a7191..4900bdd48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1638,6 +1638,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ + "phf_macros", "phf_shared", ] @@ -1661,6 +1662,19 @@ dependencies = [ "rand", ] +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "phf_shared" version = "0.11.2" @@ -2541,6 +2555,7 @@ dependencies = [ "log", "once_cell", "palette", + "phf", "rayon", "regex", "roxmltree 0.19.0", diff --git a/Cargo.toml b/Cargo.toml index 0e1eb9382..130fb5ee6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,7 @@ palette = { version = "0.7.3", default-features = false, features = ["approx", " parking_lot = "0.12.1" pathdiff = "0.2" pdf-writer = "0.9.2" +phf = { version = "0.11", features = ["macros"] } pixglyph = "0.3" proc-macro2 = "1" pulldown-cmark = "0.9" diff --git a/crates/typst/Cargo.toml b/crates/typst/Cargo.toml index f04110acb..b9e3b494c 100644 --- a/crates/typst/Cargo.toml +++ b/crates/typst/Cargo.toml @@ -41,6 +41,7 @@ lipsum = { workspace = true } log = { workspace = true } once_cell = { workspace = true } palette = { workspace = true } +phf = { workspace = true } rayon = { workspace = true } regex = { workspace = true } roxmltree = { workspace = true } diff --git a/crates/typst/src/text/font/book.rs b/crates/typst/src/text/font/book.rs index 2e64533e2..14b385835 100644 --- a/crates/typst/src/text/font/book.rs +++ b/crates/typst/src/text/font/book.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; use ttf_parser::{name_id, PlatformId, Tag}; use unicode_segmentation::UnicodeSegmentation; +use super::exceptions::find_exception; use crate::text::{Font, FontStretch, FontStyle, FontVariant, FontWeight}; /// Metadata about a collection of fonts. @@ -206,6 +207,8 @@ impl FontInfo { /// Compute metadata for a single ttf-parser face. pub(super) fn from_ttf(ttf: &ttf_parser::Face) -> Option { + let ps_name = find_name(ttf, name_id::POST_SCRIPT_NAME); + let exception = ps_name.as_deref().and_then(find_exception); // We cannot use Name ID 16 "Typographic Family", because for some // fonts it groups together more than just Style / Weight / Stretch // variants (e.g. Display variants of Noto fonts) and then some @@ -222,45 +225,43 @@ impl FontInfo { // because Name ID 1 "Family" sometimes contains "Display" and // sometimes doesn't for the Display variants and that mixes things // up. - let family = { - let mut family = find_name(ttf, name_id::FAMILY)?; - if family.starts_with("Noto") - || family.starts_with("NewCM") - || family.starts_with("NewComputerModern") - { - family = find_name(ttf, name_id::FULL_NAME)?; - } - typographic_family(&family).to_string() - }; + let family = + exception.and_then(|c| c.family.map(str::to_string)).or_else(|| { + let mut family = find_name(ttf, name_id::FAMILY)?; + if family.starts_with("Noto") { + family = find_name(ttf, name_id::FULL_NAME)?; + } + Some(typographic_family(&family).to_string()) + })?; let variant = { - let mut full = find_name(ttf, name_id::FULL_NAME).unwrap_or_default(); - full.make_ascii_lowercase(); + let style = exception.and_then(|c| c.style).unwrap_or_else(|| { + let mut full = find_name(ttf, name_id::FULL_NAME).unwrap_or_default(); + full.make_ascii_lowercase(); - // Some fonts miss the relevant bits for italic or oblique, so - // we also try to infer that from the full name. - let italic = ttf.is_italic() || full.contains("italic"); - let oblique = - ttf.is_oblique() || full.contains("oblique") || full.contains("slanted"); + // Some fonts miss the relevant bits for italic or oblique, so + // we also try to infer that from the full name. + let italic = ttf.is_italic() || full.contains("italic"); + let oblique = ttf.is_oblique() + || full.contains("oblique") + || full.contains("slanted"); - let style = match (italic, oblique) { - (false, false) => FontStyle::Normal, - (true, _) => FontStyle::Italic, - (_, true) => FontStyle::Oblique, - }; - - let weight = { - let mut number = ttf.weight().to_number(); - if (family.starts_with("NewCM") - || family.starts_with("New Computer Modern")) - && full.contains("book") - { - number += 50; + match (italic, oblique) { + (false, false) => FontStyle::Normal, + (true, _) => FontStyle::Italic, + (_, true) => FontStyle::Oblique, } - FontWeight::from_number(number) - }; + }); + + let weight = exception.and_then(|c| c.weight).unwrap_or_else(|| { + let number = ttf.weight().to_number(); + FontWeight::from_number(number) + }); + + let stretch = exception + .and_then(|c| c.stretch) + .unwrap_or_else(|| FontStretch::from_number(ttf.width().to_number())); - let stretch = FontStretch::from_number(ttf.width().to_number()); FontVariant { style, weight, stretch } }; @@ -355,12 +356,6 @@ fn typographic_family(mut family: &str) -> &str { "narrow", "condensed", "cond", "cn", "cd", "compressed", "expanded", "exp" ]; - let mut extra = [].as_slice(); - let newcm = family.starts_with("NewCM") || family.starts_with("NewComputerModern"); - if newcm { - extra = &["book"]; - } - // Trim spacing and weird leading dots in Apple fonts. family = family.trim().trim_start_matches('.'); @@ -376,7 +371,7 @@ fn typographic_family(mut family: &str) -> &str { // Find style suffix. let mut t = trimmed; let mut shortened = false; - while let Some(s) = SUFFIXES.iter().chain(extra).find_map(|s| t.strip_suffix(s)) { + while let Some(s) = SUFFIXES.iter().find_map(|s| t.strip_suffix(s)) { shortened = true; t = s; } @@ -403,20 +398,7 @@ fn typographic_family(mut family: &str) -> &str { // Apply style suffix trimming. family = &family[..len]; - if newcm { - family = family.trim_end_matches("10"); - } - - // Fix bad names. - match family { - "Noto Sans Symbols2" => "Noto Sans Symbols 2", - "NewComputerModern" => "New Computer Modern", - "NewComputerModernMono" => "New Computer Modern Mono", - "NewComputerModernSans" => "New Computer Modern Sans", - "NewComputerModernMath" => "New Computer Modern Math", - "NewCMUncial" | "NewComputerModernUncial" => "New Computer Modern Uncial", - other => other, - } + family } /// How many words the two strings share in their prefix. diff --git a/crates/typst/src/text/font/exceptions.rs b/crates/typst/src/text/font/exceptions.rs new file mode 100644 index 000000000..363c6bfe9 --- /dev/null +++ b/crates/typst/src/text/font/exceptions.rs @@ -0,0 +1,130 @@ +use serde::Deserialize; + +use super::{FontStretch, FontStyle, FontWeight}; + +#[derive(Debug, Default, Deserialize)] +pub struct Exception { + pub family: Option<&'static str>, + pub style: Option, + pub weight: Option, + pub stretch: Option, +} + +impl Exception { + pub const fn new() -> Self { + Self { + family: None, + style: None, + weight: None, + stretch: None, + } + } + + const fn family(self, family: &'static str) -> Self { + Self { family: Some(family), ..self } + } + + const fn style(self, style: FontStyle) -> Self { + Self { style: Some(style), ..self } + } + + const fn weight(self, weight: u16) -> Self { + Self { weight: Some(FontWeight(weight)), ..self } + } + + #[allow(unused)] // left for future use + const fn stretch(self, stretch: u16) -> Self { + Self { stretch: Some(FontStretch(stretch)), ..self } + } +} + +pub fn find_exception(postscript_name: &str) -> Option<&'static Exception> { + EXCEPTION_MAP.get(postscript_name) +} + +/// A map which keys are PostScript name and values are override entries. +static EXCEPTION_MAP: phf::Map<&'static str, Exception> = phf::phf_map! { + "NewCM08-Book" => Exception::new() + .family("New Computer Modern 08") + .weight(450), + "NewCM08-BookItalic" => Exception::new() + .family("New Computer Modern 08") + .weight(450), + "NewCM08-Italic" => Exception::new() + .family("New Computer Modern 08"), + "NewCM08-Regular" => Exception::new() + .family("New Computer Modern 08"), + "NewCM10-Bold" => Exception::new() + .family("New Computer Modern"), + "NewCM10-BoldItalic" => Exception::new() + .family("New Computer Modern"), + "NewCM10-Book" => Exception::new() + .family("New Computer Modern") + .weight(450), + "NewCM10-BookItalic" => Exception::new() + .family("New Computer Modern") + .weight(450), + "NewCM10-Italic" => Exception::new() + .family("New Computer Modern"), + "NewCM10-Regular" => Exception::new() + .family("New Computer Modern"), + "NewCMMath-Book" => Exception::new() + .family("New Computer Modern Math") + .weight(450), + "NewCMMath-Regular" => Exception::new() + .family("New Computer Modern Math"), + "NewCMMono10-Bold" => Exception::new() + .family("New Computer Modern Mono"), + "NewCMMono10-BoldOblique" => Exception::new() + .family("New Computer Modern Mono"), + "NewCMMono10-Book" => Exception::new() + .family("New Computer Modern Mono") + .weight(450), + "NewCMMono10-BookItalic" => Exception::new() + .family("New Computer Modern Mono") + .weight(450), + "NewCMMono10-Italic" => Exception::new() + .family("New Computer Modern Mono"), + "NewCMMono10-Regular" => Exception::new() + .family("New Computer Modern Mono"), + "NewCMSans08-Book" => Exception::new() + .family("New Computer Modern Sans 08") + .weight(450), + "NewCMSans08-BookOblique" => Exception::new() + .family("New Computer Modern Sans 08") + .weight(450), + "NewCMSans08-Oblique" => Exception::new() + .family("New Computer Modern Sans 08"), + "NewCMSans08-Regular" => Exception::new() + .family("New Computer Modern Sans 08"), + "NewCMSans10-Bold" => Exception::new() + .family("New Computer Modern Sans"), + "NewCMSans10-BoldOblique" => Exception::new() + .family("New Computer Modern Sans"), + "NewCMSans10-Book" => Exception::new() + .family("New Computer Modern Sans") + .weight(450), + "NewCMSans10-BookOblique" => Exception::new() + .family("New Computer Modern Sans") + .weight(450) + .style(FontStyle::Oblique), + "NewCMSans10-Oblique" => Exception::new() + .family("New Computer Modern Sans") + .style(FontStyle::Oblique), + "NewCMSans10-Regular" => Exception::new() + .family("New Computer Modern Sans"), + "NewCMUncial08-Bold" => Exception::new() + .family("New Computer Modern Uncial 08"), + "NewCMUncial08-Book" => Exception::new() + .family("New Computer Modern Uncial 08") + .weight(450), + "NewCMUncial08-Regular" => Exception::new() + .family("New Computer Modern Uncial 08"), + "NewCMUncial10-Bold" => Exception::new() + .family("New Computer Modern Uncial"), + "NewCMUncial10-Book" => Exception::new() + .family("New Computer Modern Uncial") + .weight(450), + "NewCMUncial10-Regular" => Exception::new() + .family("New Computer Modern Uncial"), +}; diff --git a/crates/typst/src/text/font/mod.rs b/crates/typst/src/text/font/mod.rs index d9eb044b9..42a87b7ec 100644 --- a/crates/typst/src/text/font/mod.rs +++ b/crates/typst/src/text/font/mod.rs @@ -1,6 +1,7 @@ //! Font handling. mod book; +mod exceptions; mod variant; pub use self::book::{Coverage, FontBook, FontFlags, FontInfo}; diff --git a/crates/typst/src/text/font/variant.rs b/crates/typst/src/text/font/variant.rs index f96f648d1..e34d17b6b 100644 --- a/crates/typst/src/text/font/variant.rs +++ b/crates/typst/src/text/font/variant.rs @@ -77,7 +77,7 @@ impl From for FontStyle { #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Serialize, Deserialize)] #[serde(transparent)] -pub struct FontWeight(u16); +pub struct FontWeight(pub(super) u16); impl FontWeight { /// Thin weight (100). @@ -180,7 +180,7 @@ cast! { #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Serialize, Deserialize)] #[serde(transparent)] -pub struct FontStretch(u16); +pub struct FontStretch(pub(super) u16); impl FontStretch { /// Ultra-condensed stretch (50%).