render: retain authored webfont identity
This commit is contained in:
1 parent
20be62bca9
commit
1b313ecb49
3 files changed
+271
-62
No files matched your search
@@ -410,6 +410,24 @@ pub fn layout_dom_with_resources(
|
|||||||
viewport: (f32, f32),
|
viewport: (f32, f32),
|
||||||
intrinsic: &HashMap<NodeId, (f32, f32)>,
|
intrinsic: &HashMap<NodeId, (f32, f32)>,
|
||||||
fonts: &[Vec<u8>],
|
fonts: &[Vec<u8>],
|
||||||
|
) -> DomLayout {
|
||||||
|
let fonts: Vec<_> = fonts
|
||||||
|
.iter()
|
||||||
|
.map(|data| crate::inline::WebFont {
|
||||||
|
data: data.clone(),
|
||||||
|
family: None,
|
||||||
|
weight: None,
|
||||||
|
italic: None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
layout_dom_with_web_fonts(tree, viewport, intrinsic, &fonts)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn layout_dom_with_web_fonts(
|
||||||
|
tree: &DomTree,
|
||||||
|
viewport: (f32, f32),
|
||||||
|
intrinsic: &HashMap<NodeId, (f32, f32)>,
|
||||||
|
fonts: &[crate::inline::WebFont],
|
||||||
) -> DomLayout {
|
) -> DomLayout {
|
||||||
let timing = std::env::var("OBSCURA_RENDER_TIMING").is_ok();
|
let timing = std::env::var("OBSCURA_RENDER_TIMING").is_ok();
|
||||||
|
|
||||||
@@ -467,7 +485,7 @@ pub fn layout_dom_with_resources(
|
|||||||
let mut taffy_tree: TaffyTree<usize> = TaffyTree::new();
|
let mut taffy_tree: TaffyTree<usize> = TaffyTree::new();
|
||||||
let mut id_map: HashMap<taffy::NodeId, NodeId> = HashMap::new();
|
let mut id_map: HashMap<taffy::NodeId, NodeId> = HashMap::new();
|
||||||
let mut words: HashMap<taffy::NodeId, (NodeId, String)> = HashMap::new();
|
let mut words: HashMap<taffy::NodeId, (NodeId, String)> = HashMap::new();
|
||||||
let mut engine = crate::inline::TextEngine::new_with_fonts(fonts);
|
let mut engine = crate::inline::TextEngine::new_with_web_fonts(fonts);
|
||||||
let mut ifc_items = IfcRegistry::default();
|
let mut ifc_items = IfcRegistry::default();
|
||||||
|
|
||||||
// The document node itself is not an element; lay out from the first
|
// The document node itself is not an element; lay out from the first
|
||||||
|
|||||||
@@ -80,14 +80,31 @@ fn resolve_font_family(fam: Option<&str>) -> &'static str {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct LoadedFamily {
|
struct LoadedFamily {
|
||||||
|
faces: Vec<LoadedFace>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct LoadedFace {
|
||||||
name: Arc<str>,
|
name: Arc<str>,
|
||||||
weights: Vec<u16>,
|
min_weight: u16,
|
||||||
|
max_weight: u16,
|
||||||
|
shape_weight: u16,
|
||||||
|
italic: bool,
|
||||||
variable: bool,
|
variable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct WebFont {
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
pub family: Option<String>,
|
||||||
|
pub weight: Option<(u16, u16)>,
|
||||||
|
pub italic: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_loaded_font(
|
fn resolve_loaded_font(
|
||||||
fam: Option<&str>,
|
fam: Option<&str>,
|
||||||
requested_weight: u16,
|
requested_weight: u16,
|
||||||
|
requested_italic: bool,
|
||||||
loaded: &HashMap<String, LoadedFamily>,
|
loaded: &HashMap<String, LoadedFamily>,
|
||||||
) -> (Arc<str>, u16) {
|
) -> (Arc<str>, u16) {
|
||||||
if let Some(stack) = fam {
|
if let Some(stack) = fam {
|
||||||
@@ -97,12 +114,35 @@ fn resolve_loaded_font(
|
|||||||
.trim_matches(|c| c == '"' || c == '\'')
|
.trim_matches(|c| c == '"' || c == '\'')
|
||||||
.trim();
|
.trim();
|
||||||
if let Some(family) = loaded.get(&name.to_ascii_lowercase()) {
|
if let Some(family) = loaded.get(&name.to_ascii_lowercase()) {
|
||||||
let weight = if family.variable {
|
let exact_style: Vec<_> = family
|
||||||
requested_weight
|
.faces
|
||||||
|
.iter()
|
||||||
|
.filter(|face| face.italic == requested_italic)
|
||||||
|
.collect();
|
||||||
|
let candidates: Vec<_> = if exact_style.is_empty() {
|
||||||
|
family.faces.iter().collect()
|
||||||
} else {
|
} else {
|
||||||
match_font_weight(requested_weight, &family.weights)
|
exact_style
|
||||||
};
|
};
|
||||||
return (Arc::clone(&family.name), weight);
|
if let Some(face) = candidates.iter().copied().find(|face| {
|
||||||
|
(face.min_weight..=face.max_weight).contains(&requested_weight)
|
||||||
|
}) {
|
||||||
|
let weight = if face.variable {
|
||||||
|
requested_weight
|
||||||
|
} else {
|
||||||
|
face.shape_weight
|
||||||
|
};
|
||||||
|
return (Arc::clone(&face.name), weight);
|
||||||
|
}
|
||||||
|
let available: Vec<_> =
|
||||||
|
candidates.iter().map(|face| face.min_weight).collect();
|
||||||
|
let matched = match_font_weight(requested_weight, &available);
|
||||||
|
if let Some(face) = candidates
|
||||||
|
.into_iter()
|
||||||
|
.find(|face| face.min_weight == matched)
|
||||||
|
{
|
||||||
|
return (Arc::clone(&face.name), face.shape_weight);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -316,42 +356,76 @@ impl TextEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_with_fonts(fonts: &[Vec<u8>]) -> Self {
|
pub fn new_with_fonts(fonts: &[Vec<u8>]) -> Self {
|
||||||
// Build a database from our four embedded faces only. Never call
|
let fonts: Vec<_> = fonts
|
||||||
|
.iter()
|
||||||
|
.map(|data| WebFont {
|
||||||
|
data: data.clone(),
|
||||||
|
family: None,
|
||||||
|
weight: None,
|
||||||
|
italic: None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Self::new_with_web_fonts(&fonts)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn new_with_web_fonts(fonts: &[WebFont]) -> Self {
|
||||||
|
// Build a database from embedded and page-provided faces. Never call
|
||||||
// load_system_fonts: a host's font set would make layout differ
|
// load_system_fonts: a host's font set would make layout differ
|
||||||
// machine to machine and add a multi-millisecond startup scan.
|
// machine to machine and add a multi-millisecond startup scan.
|
||||||
let mut db = cosmic_text::fontdb::Database::new();
|
let mut db = cosmic_text::fontdb::Database::new();
|
||||||
|
let mut declarations = Vec::new();
|
||||||
for bytes in [
|
for bytes in [
|
||||||
SANS_R, SANS_B, SANS_O, SANS_BO,
|
SANS_R, SANS_B, SANS_O, SANS_BO,
|
||||||
SERIF_R, SERIF_B, SERIF_O, SERIF_BO,
|
SERIF_R, SERIF_B, SERIF_O, SERIF_BO,
|
||||||
MONO_R, MONO_B, MONO_O, MONO_BO,
|
MONO_R, MONO_B, MONO_O, MONO_BO,
|
||||||
FALLBACK,
|
FALLBACK,
|
||||||
] {
|
] {
|
||||||
db.load_font_source(cosmic_text::fontdb::Source::Binary(Arc::new(bytes)));
|
for id in db.load_font_source(cosmic_text::fontdb::Source::Binary(Arc::new(bytes))) {
|
||||||
|
declarations.push((id, None, None, None));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for bytes in fonts {
|
for font in fonts {
|
||||||
db.load_font_source(cosmic_text::fontdb::Source::Binary(Arc::new(
|
for id in db.load_font_source(cosmic_text::fontdb::Source::Binary(Arc::new(
|
||||||
bytes.clone(),
|
font.data.clone(),
|
||||||
)));
|
))) {
|
||||||
|
declarations.push((
|
||||||
|
id,
|
||||||
|
font.family.clone(),
|
||||||
|
font.weight,
|
||||||
|
font.italic,
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let mut loaded_families = HashMap::new();
|
let mut loaded_families = HashMap::new();
|
||||||
let faces: Vec<_> = db
|
for (id, declared_family, declared_weight, declared_italic) in declarations {
|
||||||
.faces()
|
let Some(face) = db.face(id) else { continue };
|
||||||
.map(|face| (face.id, face.families.clone(), face.weight.0))
|
let names = face.families.clone();
|
||||||
.collect();
|
let internal_name = names
|
||||||
for (id, names, weight) in faces {
|
.first()
|
||||||
|
.map(|(name, _)| Arc::<str>::from(name.as_str()))
|
||||||
|
.unwrap_or_else(|| Arc::from(FAMILY));
|
||||||
|
let shape_weight = face.weight.0;
|
||||||
|
let italic = declared_italic
|
||||||
|
.unwrap_or(!matches!(face.style, cosmic_text::fontdb::Style::Normal));
|
||||||
let variable = db
|
let variable = db
|
||||||
.with_face_data(id, face_is_variable)
|
.with_face_data(id, face_is_variable)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
for (name, _) in names {
|
let weight = declared_weight.unwrap_or((shape_weight, shape_weight));
|
||||||
|
let declared_names: Vec<String> = declared_family
|
||||||
|
.map(|name| vec![name])
|
||||||
|
.unwrap_or_else(|| names.into_iter().map(|(name, _)| name).collect());
|
||||||
|
for name in declared_names {
|
||||||
let family = loaded_families
|
let family = loaded_families
|
||||||
.entry(name.to_ascii_lowercase())
|
.entry(name.to_ascii_lowercase())
|
||||||
.or_insert_with(|| LoadedFamily {
|
.or_insert_with(|| LoadedFamily { faces: Vec::new() });
|
||||||
name: Arc::from(name.as_str()),
|
family.faces.push(LoadedFace {
|
||||||
weights: Vec::new(),
|
name: Arc::clone(&internal_name),
|
||||||
variable: false,
|
min_weight: weight.0,
|
||||||
});
|
max_weight: weight.1,
|
||||||
family.weights.push(weight);
|
shape_weight,
|
||||||
family.variable |= variable;
|
italic,
|
||||||
|
variable,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
db.set_sans_serif_family(FAMILY);
|
db.set_sans_serif_family(FAMILY);
|
||||||
@@ -387,6 +461,7 @@ impl TextEngine {
|
|||||||
let (family, weight) = resolve_loaded_font(
|
let (family, weight) = resolve_loaded_font(
|
||||||
base.font_family.as_deref(),
|
base.font_family.as_deref(),
|
||||||
crate::style::used_font_weight(base),
|
crate::style::used_font_weight(base),
|
||||||
|
base.font_style_italic.unwrap_or(false),
|
||||||
&self.loaded_families,
|
&self.loaded_families,
|
||||||
);
|
);
|
||||||
let ctx = base_span_ctx(
|
let ctx = base_span_ctx(
|
||||||
@@ -436,6 +511,7 @@ impl TextEngine {
|
|||||||
let (family, weight) = resolve_loaded_font(
|
let (family, weight) = resolve_loaded_font(
|
||||||
base.font_family.as_deref(),
|
base.font_family.as_deref(),
|
||||||
crate::style::used_font_weight(base),
|
crate::style::used_font_weight(base),
|
||||||
|
base.font_style_italic.unwrap_or(false),
|
||||||
&self.loaded_families,
|
&self.loaded_families,
|
||||||
);
|
);
|
||||||
let ctx = base_span_ctx(
|
let ctx = base_span_ctx(
|
||||||
@@ -775,6 +851,9 @@ fn collect_node_spans(
|
|||||||
resolve_loaded_font(
|
resolve_loaded_font(
|
||||||
Some(family),
|
Some(family),
|
||||||
requested_weight,
|
requested_weight,
|
||||||
|
style
|
||||||
|
.and_then(|style| style.font_style_italic)
|
||||||
|
.unwrap_or(ctx.italic),
|
||||||
loaded_families,
|
loaded_families,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -1224,6 +1303,45 @@ mod tests {
|
|||||||
assert_eq!(match_font_weight(500, &[400, 500, 700]), 500);
|
assert_eq!(match_font_weight(500, &[400, 500, 700]), 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn declared_family_selects_a_face_with_a_different_internal_name() {
|
||||||
|
let family = LoadedFamily {
|
||||||
|
faces: vec![
|
||||||
|
LoadedFace {
|
||||||
|
name: Arc::from("Poppins"),
|
||||||
|
min_weight: 400,
|
||||||
|
max_weight: 400,
|
||||||
|
shape_weight: 400,
|
||||||
|
italic: false,
|
||||||
|
variable: false,
|
||||||
|
},
|
||||||
|
LoadedFace {
|
||||||
|
name: Arc::from("Poppins Medium"),
|
||||||
|
min_weight: 500,
|
||||||
|
max_weight: 500,
|
||||||
|
shape_weight: 500,
|
||||||
|
italic: false,
|
||||||
|
variable: false,
|
||||||
|
},
|
||||||
|
LoadedFace {
|
||||||
|
name: Arc::from("Poppins"),
|
||||||
|
min_weight: 700,
|
||||||
|
max_weight: 700,
|
||||||
|
shape_weight: 700,
|
||||||
|
italic: false,
|
||||||
|
variable: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let loaded = HashMap::from([("poppins".to_string(), family)]);
|
||||||
|
let medium = resolve_loaded_font(Some("Poppins, sans-serif"), 500, false, &loaded);
|
||||||
|
assert_eq!(medium.0.as_ref(), "Poppins Medium");
|
||||||
|
assert_eq!(medium.1, 500);
|
||||||
|
let semibold = resolve_loaded_font(Some("Poppins"), 600, false, &loaded);
|
||||||
|
assert_eq!(semibold.0.as_ref(), "Poppins");
|
||||||
|
assert_eq!(semibold.1, 700);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clip_fill_only_for_transparent_clip_text() {
|
fn clip_fill_only_for_transparent_clip_text() {
|
||||||
let mut s = LayoutStyle::default();
|
let mut s = LayoutStyle::default();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ static FONT_BYTES: &[u8] = include_bytes!("../assets/liberation-sans.ttf");
|
|||||||
static SERIF_FONT_BYTES: &[u8] = include_bytes!("../assets/liberation-serif.ttf");
|
static SERIF_FONT_BYTES: &[u8] = include_bytes!("../assets/liberation-serif.ttf");
|
||||||
static MONO_FONT_BYTES: &[u8] = include_bytes!("../assets/liberation-mono.ttf");
|
static MONO_FONT_BYTES: &[u8] = include_bytes!("../assets/liberation-mono.ttf");
|
||||||
|
|
||||||
use crate::layout_dom_with_resources;
|
use crate::dom::layout_dom_with_web_fonts;
|
||||||
|
|
||||||
/// Render `tree` at `viewport` (width, height) in CSS pixels to a Pixmap, or
|
/// Render `tree` at `viewport` (width, height) in CSS pixels to a Pixmap, or
|
||||||
/// None if the viewport is zero-sized. `base_url`, when given, resolves the
|
/// None if the viewport is zero-sized. `base_url`, when given, resolves the
|
||||||
@@ -36,7 +36,7 @@ pub fn paint_dom(tree: &DomTree, viewport: (f32, f32), base_url: Option<&str>) -
|
|||||||
// each URL is still fetched at most once.
|
// each URL is still fetched at most once.
|
||||||
let intrinsic = collect_image_intrinsics(tree, base_url, &mut image_cache);
|
let intrinsic = collect_image_intrinsics(tree, base_url, &mut image_cache);
|
||||||
let fonts = collect_web_fonts(tree, base_url);
|
let fonts = collect_web_fonts(tree, base_url);
|
||||||
let mut laid = layout_dom_with_resources(tree, viewport, &intrinsic, &fonts);
|
let mut laid = layout_dom_with_web_fonts(tree, viewport, &intrinsic, &fonts);
|
||||||
let root_font_size = tree
|
let root_font_size = tree
|
||||||
.query_selector("html")
|
.query_selector("html")
|
||||||
.ok()
|
.ok()
|
||||||
@@ -953,15 +953,42 @@ fn fetch_bytes(
|
|||||||
/// filtering is load-bearing for performance: generated font packages commonly
|
/// filtering is load-bearing for performance: generated font packages commonly
|
||||||
/// emit six or seven script subsets per face, while an English page needs only
|
/// emit six or seven script subsets per face, while an English page needs only
|
||||||
/// the subset containing ASCII.
|
/// the subset containing ASCII.
|
||||||
fn collect_web_fonts(tree: &DomTree, base_url: Option<&str>) -> Vec<Vec<u8>> {
|
fn collect_web_fonts(tree: &DomTree, base_url: Option<&str>) -> Vec<crate::inline::WebFont> {
|
||||||
let mut cache = std::collections::HashMap::new();
|
let mut cache = std::collections::HashMap::new();
|
||||||
let mut seen = std::collections::HashSet::new();
|
let mut seen = std::collections::HashSet::new();
|
||||||
let mut fonts = Vec::new();
|
let mut fonts = Vec::new();
|
||||||
|
let mut rules = Vec::new();
|
||||||
|
|
||||||
|
for nid in tree.descendants(tree.document()) {
|
||||||
|
let Some(node) = tree.get_node(nid) else { continue };
|
||||||
|
if node
|
||||||
|
.as_element()
|
||||||
|
.map(|element| element.local.as_ref() != "style")
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let css = tree.text_content(nid);
|
||||||
|
for face in font_face_blocks(&css) {
|
||||||
|
if !font_face_covers_ascii(face) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(src) = font_face_urls(face).into_iter().next() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
rules.push((
|
||||||
|
font_resource_key(&src, base_url),
|
||||||
|
src,
|
||||||
|
font_face_family(face),
|
||||||
|
font_face_weight(face),
|
||||||
|
font_face_italic(face),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Critical web fonts are normally preloaded from the document with a URL
|
// Critical web fonts are normally preloaded from the document with a URL
|
||||||
// already resolved relative to the HTML. Prefer that browser-authored list:
|
// already resolved relative to the HTML. Fetch those first, while retaining
|
||||||
// CSS gathered from external stylesheets is flattened into <style> text in
|
// the matching @font-face descriptors needed for CSS family/weight lookup.
|
||||||
// the live DOM, where a relative src no longer carries its stylesheet URL.
|
|
||||||
let mut preloads = Vec::new();
|
let mut preloads = Vec::new();
|
||||||
for nid in tree.descendants(tree.document()) {
|
for nid in tree.descendants(tree.document()) {
|
||||||
let Some(node) = tree.get_node(nid) else { continue };
|
let Some(node) = tree.get_node(nid) else { continue };
|
||||||
@@ -985,50 +1012,52 @@ fn collect_web_fonts(tree: &DomTree, base_url: Option<&str>) -> Vec<Vec<u8>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for src in preloads.iter().take(16) {
|
for src in preloads.iter().take(16) {
|
||||||
if !seen.insert(src.clone()) {
|
let key = font_resource_key(src, base_url);
|
||||||
|
if !seen.insert(key.clone()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(decoded) = fetch_and_decode_font(src, base_url, &mut cache) {
|
if let Some(decoded) = fetch_and_decode_font(src, base_url, &mut cache) {
|
||||||
fonts.push(decoded);
|
let metadata = rules.iter().find(|rule| rule.0 == key);
|
||||||
|
fonts.push(crate::inline::WebFont {
|
||||||
|
data: decoded,
|
||||||
|
family: metadata.and_then(|rule| rule.2.clone()),
|
||||||
|
weight: metadata.and_then(|rule| rule.3),
|
||||||
|
italic: metadata.and_then(|rule| rule.4),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !preloads.is_empty() {
|
|
||||||
return fonts;
|
|
||||||
}
|
|
||||||
|
|
||||||
for nid in tree.descendants(tree.document()) {
|
for (key, src, family, weight, italic) in rules {
|
||||||
let Some(node) = tree.get_node(nid) else { continue };
|
if fonts.len() >= 16 {
|
||||||
if node
|
break;
|
||||||
.as_element()
|
}
|
||||||
.map(|element| element.local.as_ref() != "style")
|
if !seen.insert(key) {
|
||||||
.unwrap_or(true)
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let css = tree.text_content(nid);
|
if let Some(decoded) = fetch_and_decode_font(&src, base_url, &mut cache) {
|
||||||
for face in font_face_blocks(&css) {
|
fonts.push(crate::inline::WebFont {
|
||||||
if !font_face_covers_ascii(face) {
|
data: decoded,
|
||||||
continue;
|
family,
|
||||||
}
|
weight,
|
||||||
let Some(src) = font_face_urls(face).into_iter().next() else {
|
italic,
|
||||||
continue;
|
});
|
||||||
};
|
|
||||||
if !seen.insert(src.clone()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Bound pathological pages without penalizing normal generated
|
|
||||||
// font packages (family x weight x style usually stays under 20).
|
|
||||||
if fonts.len() >= 12 {
|
|
||||||
return fonts;
|
|
||||||
}
|
|
||||||
if let Some(decoded) = fetch_and_decode_font(&src, base_url, &mut cache) {
|
|
||||||
fonts.push(decoded);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fonts
|
fonts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn font_resource_key(src: &str, base_url: Option<&str>) -> String {
|
||||||
|
url::Url::parse(src)
|
||||||
|
.ok()
|
||||||
|
.or_else(|| {
|
||||||
|
base_url
|
||||||
|
.and_then(|base| url::Url::parse(base).ok())
|
||||||
|
.and_then(|base| base.join(src).ok())
|
||||||
|
})
|
||||||
|
.map(|url| url.to_string())
|
||||||
|
.unwrap_or_else(|| src.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
fn fetch_and_decode_font(
|
fn fetch_and_decode_font(
|
||||||
src: &str,
|
src: &str,
|
||||||
base_url: Option<&str>,
|
base_url: Option<&str>,
|
||||||
@@ -1108,6 +1137,45 @@ fn font_face_declaration<'a>(face: &'a str, name: &str) -> Option<&'a str> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn font_face_family(face: &str) -> Option<String> {
|
||||||
|
font_face_declaration(face, "font-family")
|
||||||
|
.map(|family| family.trim().trim_matches(|ch| matches!(ch, '"' | '\'')).to_string())
|
||||||
|
.filter(|family| !family.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn font_face_weight(face: &str) -> Option<(u16, u16)> {
|
||||||
|
fn parse(value: &str) -> Option<u16> {
|
||||||
|
match value.to_ascii_lowercase().as_str() {
|
||||||
|
"normal" => Some(400),
|
||||||
|
"bold" => Some(700),
|
||||||
|
value => value
|
||||||
|
.parse::<f32>()
|
||||||
|
.ok()
|
||||||
|
.filter(|weight| weight.is_finite() && (1.0..=1000.0).contains(weight))
|
||||||
|
.map(|weight| weight.round() as u16),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut values = font_face_declaration(face, "font-weight")?
|
||||||
|
.split_ascii_whitespace()
|
||||||
|
.filter_map(parse);
|
||||||
|
let first = values.next()?;
|
||||||
|
let second = values.next().unwrap_or(first);
|
||||||
|
Some((first.min(second), first.max(second)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn font_face_italic(face: &str) -> Option<bool> {
|
||||||
|
font_face_declaration(face, "font-style").and_then(|style| {
|
||||||
|
let style = style.trim().to_ascii_lowercase();
|
||||||
|
if style == "normal" {
|
||||||
|
Some(false)
|
||||||
|
} else if style == "italic" || style.starts_with("oblique") {
|
||||||
|
Some(true)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn font_face_covers_ascii(face: &str) -> bool {
|
fn font_face_covers_ascii(face: &str) -> bool {
|
||||||
let Some(range) = font_face_declaration(face, "unicode-range") else {
|
let Some(range) = font_face_declaration(face, "unicode-range") else {
|
||||||
return true;
|
return true;
|
||||||
@@ -3187,6 +3255,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Example";
|
font-family: "Example";
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 350 650;
|
||||||
src: url(data:font/woff2;base64,d09GMg==) format("woff2"),
|
src: url(data:font/woff2;base64,d09GMg==) format("woff2"),
|
||||||
url("./example-latin.woff") format("woff");
|
url("./example-latin.woff") format("woff");
|
||||||
unicode-range: U+??, U+2000-206F;
|
unicode-range: U+??, U+2000-206F;
|
||||||
@@ -3196,6 +3266,9 @@ mod tests {
|
|||||||
assert_eq!(faces.len(), 2);
|
assert_eq!(faces.len(), 2);
|
||||||
assert!(!font_face_covers_ascii(faces[0]));
|
assert!(!font_face_covers_ascii(faces[0]));
|
||||||
assert!(font_face_covers_ascii(faces[1]));
|
assert!(font_face_covers_ascii(faces[1]));
|
||||||
|
assert_eq!(font_face_family(faces[1]).as_deref(), Some("Example"));
|
||||||
|
assert_eq!(font_face_weight(faces[1]), Some((350, 650)));
|
||||||
|
assert_eq!(font_face_italic(faces[1]), Some(true));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
font_face_urls(faces[1]),
|
font_face_urls(faces[1]),
|
||||||
vec![
|
vec![
|
||||||
|
|||||||
Reference in new issue
Block a user