mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-15 06:43:41 +00:00
Merge PR #935
# Conflicts: # crates/fonts/src/fontdb.rs # crates/fonts/src/tests_docfonts.rs
This commit is contained in:
commit
35ec71f04f
3 files changed
+111
-12
No files matched your search
+66
-11
@@ -93,6 +93,9 @@ pub struct FontFace {
|
||||
pub family: String,
|
||||
/// Typographic style name (e.g. "Semibold", "Italic").
|
||||
pub style: String,
|
||||
/// The WWS family and style names (name IDs 21 and 22) when they differ from the typographic
|
||||
/// ones: InDesign names an optical-size face by them ("Kepler Std Display" / "Light").
|
||||
pub wws: Option<(String, String)>,
|
||||
/// usWeightClass-style weight (400 = regular).
|
||||
pub weight: f32,
|
||||
pub italic: bool,
|
||||
@@ -347,6 +350,7 @@ impl FontFace {
|
||||
struct CatalogEntry {
|
||||
family: String,
|
||||
style: String,
|
||||
wws: Option<(String, String)>,
|
||||
path: std::path::PathBuf,
|
||||
/// The font menu group and native family name, read by the scan.
|
||||
group: FontGroup,
|
||||
@@ -360,6 +364,7 @@ struct CatalogEntry {
|
||||
struct ScannedFace {
|
||||
family: String,
|
||||
style: String,
|
||||
wws: Option<(String, String)>,
|
||||
group: FontGroup,
|
||||
native: Option<String>,
|
||||
postscript: Option<String>,
|
||||
@@ -574,6 +579,31 @@ fn has_face(faces: &[Arc<FontFace>], family: &str, style: &str) -> bool {
|
||||
faces.iter().any(|f| f.family.eq_ignore_ascii_case(family) && f.style.eq_ignore_ascii_case(style))
|
||||
}
|
||||
|
||||
/// A face's WWS family and style names (name IDs 21 and 22), if it has a WWS family and it differs
|
||||
/// from `family`/`style`.
|
||||
fn wws_names(f: &skrifa::FontRef<'_>, family: &str, style: &str) -> Option<(String, String)> {
|
||||
let wf = name(f, &[StringId::WWS_FAMILY_NAME])?;
|
||||
let ws = name(f, &[StringId::WWS_SUBFAMILY_NAME]).unwrap_or_else(|| "Regular".into());
|
||||
(!wf.eq_ignore_ascii_case(family) || !ws.eq_ignore_ascii_case(style)).then_some((wf, ws))
|
||||
}
|
||||
|
||||
/// The family and style a face is known by: its typographic names, or its WWS names (if it has
|
||||
/// them) when `by_wws`.
|
||||
fn known_as<'a>(family: &'a str, style: &'a str, wws: Option<&'a (String, String)>, by_wws: bool) -> Option<(&'a str, &'a str)> {
|
||||
if by_wws { wws.map(|w| (w.0.as_str(), w.1.as_str())) } else { Some((family, style)) }
|
||||
}
|
||||
|
||||
/// Is `family` the WWS family name of `f`?
|
||||
fn has_wws_family(f: &FontFace, family: &str) -> bool {
|
||||
f.wws.as_ref().is_some_and(|w| w.0.eq_ignore_ascii_case(family))
|
||||
}
|
||||
|
||||
/// Is `family` the typographic or the WWS family name of `c`?
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn in_catalog_family(c: &CatalogEntry, family: &str) -> bool {
|
||||
c.family.eq_ignore_ascii_case(family) || c.wws.as_ref().is_some_and(|w| w.0.eq_ignore_ascii_case(family))
|
||||
}
|
||||
|
||||
fn face_names(f: &skrifa::FontRef<'_>) -> Option<(String, String)> {
|
||||
let family = name(f, &[StringId::TYPOGRAPHIC_FAMILY_NAME, StringId::FAMILY_NAME])?;
|
||||
let style = name(f, &[StringId::TYPOGRAPHIC_SUBFAMILY_NAME, StringId::SUBFAMILY_NAME]).unwrap_or_else(|| "Regular".into());
|
||||
@@ -640,7 +670,8 @@ fn file_face_names(path: &std::path::Path) -> Vec<ScannedFace> {
|
||||
}
|
||||
};
|
||||
let postscript = name(&f, &[StringId::POSTSCRIPT_NAME]);
|
||||
Some(ScannedFace { family, style, group, native, postscript })
|
||||
let wws = wws_names(&f, &family, &style);
|
||||
Some(ScannedFace { family, style, wws, group, native, postscript })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -752,6 +783,7 @@ fn make_face(bytes: FontBytes, source: FontSource, index: u32, family: String, s
|
||||
let m = f.metrics(Size::unscaled(), &location);
|
||||
let upem = m.units_per_em.max(1) as f64;
|
||||
let a = f.attributes();
|
||||
let wws = wws_names(&f, &family, &style);
|
||||
let hb = harfrust::FontRef::from_index(data, index).ok()?;
|
||||
let shaper = harfrust::ShaperData::new(&hb);
|
||||
let instance = (!coords.is_empty())
|
||||
@@ -766,6 +798,7 @@ fn make_face(bytes: FontBytes, source: FontSource, index: u32, family: String, s
|
||||
id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
|
||||
family,
|
||||
style,
|
||||
wws,
|
||||
weight,
|
||||
italic,
|
||||
upem,
|
||||
@@ -1085,6 +1118,7 @@ impl FontDb {
|
||||
found.push(CatalogEntry {
|
||||
family: f.family,
|
||||
style: f.style,
|
||||
wws: f.wws,
|
||||
path: p.clone(),
|
||||
group: f.group,
|
||||
native: f.native,
|
||||
@@ -1104,7 +1138,7 @@ impl FontDb {
|
||||
fn load_cataloged(&self, family: &str) -> bool {
|
||||
let paths: Vec<std::path::PathBuf> = {
|
||||
let cat = self.read_catalog();
|
||||
let mut p: Vec<_> = cat.iter().filter(|c| c.family.eq_ignore_ascii_case(family)).map(|c| c.path.clone()).collect();
|
||||
let mut p: Vec<_> = cat.iter().filter(|c| in_catalog_family(c, family)).map(|c| c.path.clone()).collect();
|
||||
p.sort();
|
||||
p.dedup();
|
||||
p
|
||||
@@ -1264,7 +1298,7 @@ impl FontDb {
|
||||
}
|
||||
|
||||
fn is_loaded(&self, family: &str) -> bool {
|
||||
self.read_faces().iter().any(|f| f.family.eq_ignore_ascii_case(family))
|
||||
self.read_faces().iter().any(|f| f.family.eq_ignore_ascii_case(family) || has_wws_family(f, family))
|
||||
}
|
||||
|
||||
/// A shared variable font's axes (see [`ScopedFonts::axes`]).
|
||||
@@ -1464,17 +1498,30 @@ impl ScopedFonts<'_> {
|
||||
}
|
||||
|
||||
fn exact_style_traits(&self, family: &str) -> Vec<(bool, f32, String)> {
|
||||
let v = self.named_style_traits(family, false);
|
||||
// No face of that typographic family: the family may be a WWS name (see `find`).
|
||||
if v.is_empty() { self.named_style_traits(family, true) } else { v }
|
||||
}
|
||||
|
||||
/// [`ScopedFonts::exact_style_traits`] by the faces' typographic names, or by their WWS names
|
||||
/// when `by_wws`.
|
||||
fn named_style_traits(&self, family: &str, by_wws: bool) -> Vec<(bool, f32, String)> {
|
||||
let mut v: Vec<(bool, f32, String)> = self
|
||||
.own()
|
||||
.iter()
|
||||
.chain(self.db.read_faces().iter())
|
||||
.filter(|f| f.family.eq_ignore_ascii_case(family) && !f.style.contains('{'))
|
||||
.map(|f| (f.italic, f.weight, f.style.clone()))
|
||||
.filter(|f| !f.style.contains('{'))
|
||||
.filter_map(|f| known_as(&f.family, &f.style, f.wws.as_ref(), by_wws).map(|n| (f, n)))
|
||||
.filter(|(_, (fam, _))| fam.eq_ignore_ascii_case(family))
|
||||
.map(|(f, (_, style))| (f.italic, f.weight, style.to_string()))
|
||||
.collect();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
for c in self.db.read_catalog().iter() {
|
||||
if c.family.eq_ignore_ascii_case(family) && !v.iter().any(|(_, _, s)| s.eq_ignore_ascii_case(&c.style)) {
|
||||
v.push((style_italic(&c.style), style_weight(&c.style), c.style.clone()));
|
||||
if let Some((fam, style)) = known_as(&c.family, &c.style, c.wws.as_ref(), by_wws)
|
||||
&& fam.eq_ignore_ascii_case(family)
|
||||
&& !v.iter().any(|(_, _, s)| s.eq_ignore_ascii_case(style))
|
||||
{
|
||||
v.push((style_italic(style), style_weight(style), style.to_string()));
|
||||
}
|
||||
}
|
||||
v.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.total_cmp(&b.1)).then(a.2.cmp(&b.2)));
|
||||
@@ -1565,11 +1612,11 @@ impl ScopedFonts<'_> {
|
||||
}
|
||||
|
||||
fn has_exact_family(&self, family: &str) -> bool {
|
||||
if self.own().iter().any(|f| f.family.eq_ignore_ascii_case(family)) || self.db.is_loaded(family) {
|
||||
if self.own().iter().any(|f| f.family.eq_ignore_ascii_case(family) || has_wws_family(f, family)) || self.db.is_loaded(family) {
|
||||
return true;
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if self.db.read_catalog().iter().any(|c| c.family.eq_ignore_ascii_case(family)) {
|
||||
if self.db.read_catalog().iter().any(|c| in_catalog_family(c, family)) {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
@@ -1636,12 +1683,20 @@ impl ScopedFonts<'_> {
|
||||
let faces = self.db.read_faces();
|
||||
// The document's fonts first: they stand in for other fonts of the same name.
|
||||
let named = |f: &&Arc<FontFace>| f.family.eq_ignore_ascii_case(family);
|
||||
let cands: Vec<&Arc<FontFace>> = self.own().iter().filter(named).chain(faces.iter().filter(named)).collect();
|
||||
let mut cands: Vec<&Arc<FontFace>> = self.own().iter().filter(named).chain(faces.iter().filter(named)).collect();
|
||||
// No face of that typographic family: the family may be a WWS name, whose faces carry
|
||||
// their WWS style names.
|
||||
let by_wws = cands.is_empty();
|
||||
if by_wws {
|
||||
let aliased = |f: &&Arc<FontFace>| has_wws_family(f, family);
|
||||
cands = self.own().iter().filter(aliased).chain(faces.iter().filter(aliased)).collect();
|
||||
}
|
||||
if cands.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let style_of = |f: &FontFace| -> String { norm(f.wws.as_ref().filter(|_| by_wws).map_or(&f.style, |w| &w.1)) };
|
||||
let ns = norm(style);
|
||||
if let Some(f) = cands.iter().find(|f| norm(&f.style) == ns) {
|
||||
if let Some(f) = cands.iter().find(|f| style_of(f) == ns) {
|
||||
return Some((*f).clone());
|
||||
}
|
||||
let (tw, ti) = (style_weight(style), style_italic(style));
|
||||
|
||||
@@ -67,6 +67,30 @@ pub fn font_mapping(family: &str, glyphs: &[(char, char)]) -> Option<Vec<u8>> {
|
||||
assemble(base, tables)
|
||||
}
|
||||
|
||||
/// [`font_with`] for an optical-size face: typographic names `family` / `style` (name IDs 16 and
|
||||
/// 17) and WWS names `wws_family` / `wws_style` (IDs 21 and 22), the way InDesign names it
|
||||
/// ("Kepler Std" / "Light Display" and "Kepler Std Display" / "Light"). `None` if the bundled font
|
||||
/// can't be read.
|
||||
pub fn font_with_wws(family: &str, style: &str, wws_family: &str, wws_style: &str, chars: &[char]) -> Option<Vec<u8>> {
|
||||
let base = font_with(family, chars)?;
|
||||
let entries = [(1u16, family), (2, style), (16, family), (17, style), (21, wws_family), (22, wws_style)];
|
||||
let count = u16::try_from(entries.len()).ok()?;
|
||||
let mut name = Vec::new();
|
||||
for v in [0u16, count, 6 + count * 12] {
|
||||
name.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
let mut at = 0usize;
|
||||
for (id, s) in entries {
|
||||
let len = s.encode_utf16().count() * 2;
|
||||
for v in [3u16, 1, 0x409, id, u16::try_from(len).ok()?, u16::try_from(at).ok()?] {
|
||||
name.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
at += len;
|
||||
}
|
||||
entries.iter().for_each(|(_, s)| name.extend(s.encode_utf16().flat_map(u16::to_be_bytes)));
|
||||
with_tables(&base, vec![(*b"name", name)])
|
||||
}
|
||||
|
||||
/// Every table of the font file `font`: (tag, bytes).
|
||||
fn tables_of(font: &[u8]) -> Option<Vec<([u8; 4], Vec<u8>)>> {
|
||||
let be16 = |b: &[u8], at: usize| b.get(at..at + 2).and_then(|s| s.try_into().ok()).map(u16::from_be_bytes);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::*;
|
||||
use crate::testing::{font_with, with_fs_type};
|
||||
use crate::testing::{font_with, font_with_wws, with_fs_type};
|
||||
|
||||
/// An empty temporary folder unique to `name` and this process.
|
||||
fn temp_dir(name: &str) -> PathBuf {
|
||||
@@ -190,3 +190,23 @@ fn the_access_check_vets_the_folder_and_every_file_before_it_is_opened() {
|
||||
assert_eq!((r.faces, r.scope, r.skipped), (0, 0, vec!["no".to_string()]));
|
||||
let _ = std::fs::remove_dir_all(&folder);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optical_size_face_is_found_by_its_wws_names() {
|
||||
let folder = temp_dir("wws");
|
||||
let bytes = font_with_wws("DocFont Kepler", "Light Display", "DocFont Kepler Display", "Light", &['k']).unwrap();
|
||||
std::fs::write(folder.join("light-display.otf"), bytes).unwrap();
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
let r = db.load_document_fonts(&folder, &|_| Ok(()));
|
||||
assert_eq!((r.faces, r.skipped.len()), (1, 0), "{:?}", r.skipped);
|
||||
let fonts = db.scoped(r.scope);
|
||||
assert!(fonts.has_family("DocFont Kepler Display"), "the WWS family counts as available");
|
||||
assert_eq!(fonts.face("DocFont Kepler Display", "Light").style, "Light Display");
|
||||
// `font.list` checks the style against these: "Light" isn't a missing style.
|
||||
assert_eq!(fonts.styles("DocFont Kepler Display"), ["Light"]);
|
||||
// The typographic names still find it.
|
||||
assert!(fonts.has_family("DocFont Kepler"));
|
||||
assert_eq!(fonts.face("DocFont Kepler", "Light Display").style, "Light Display");
|
||||
assert_eq!(fonts.styles("DocFont Kepler"), ["Light Display"]);
|
||||
let _ = std::fs::remove_dir_all(&folder);
|
||||
}
|
||||
Reference in new issue
Block a user