mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
Conflict: crates/fonts/src/fontdb.rs: FontFace, CatalogEntry and ScannedFace keep next's aliases and kind plus the PR's postscript (both catalog paths fill it); next's family_kind and has_family, plus the PR's ScopedFonts::postscript_face Conflict: crates/fonts/src/tests_sysfonts.rs: both tests Conflict: crates/images/src/lib.rs: next's case-insensitive query (#404) with the PR's PostScript lookups, using next's spelled() in place of the PR's has_family helper; both tests
This commit is contained in:
@@ -132,6 +132,8 @@ pub struct FontFace {
|
||||
style_aliases: Vec<String>,
|
||||
/// Format, class and PANOSE numbers (the font menus), read on first use.
|
||||
kind: std::sync::OnceLock<crate::kind::FaceKind>,
|
||||
/// The PostScript name, read on first use.
|
||||
postscript: std::sync::OnceLock<Option<String>>,
|
||||
}
|
||||
|
||||
/// A cheap `Copy` handle to a face. Faces are never unloaded, so the handle lives for the rest of
|
||||
@@ -327,6 +329,13 @@ impl FontFace {
|
||||
let (g, n) = self.group.get_or_init(|| self.skrifa().map(|f| crate::group::classify(&f, &self.family)).unwrap_or_default());
|
||||
(*g, n.as_deref())
|
||||
}
|
||||
/// The PostScript name (`name` ID 6), read from the font once. None for a variable font's
|
||||
/// named instance: the file's name belongs to its default instance.
|
||||
pub fn postscript_name(&self) -> Option<&str> {
|
||||
self.postscript
|
||||
.get_or_init(|| if self.is_variable() { None } else { self.skrifa().and_then(|f| name(&f, &[StringId::POSTSCRIPT_NAME])) })
|
||||
.as_deref()
|
||||
}
|
||||
/// Glyph id for `c` (0 = .notdef).
|
||||
pub fn glyph_for(&self, c: char) -> u32 {
|
||||
self.skrifa().and_then(|f| f.charmap().map(c)).map(|g| g.to_u32()).unwrap_or(0)
|
||||
@@ -361,6 +370,7 @@ struct CatalogEntry {
|
||||
group: FontGroup,
|
||||
native: Option<String>,
|
||||
aliases: Vec<String>,
|
||||
postscript: Option<String>,
|
||||
}
|
||||
|
||||
/// What the scan reads of one face in a font file: its names and font menu group.
|
||||
@@ -372,6 +382,7 @@ struct ScannedFace {
|
||||
group: FontGroup,
|
||||
native: Option<String>,
|
||||
aliases: Vec<String>,
|
||||
postscript: Option<String>,
|
||||
}
|
||||
|
||||
/// Process-wide font database: the fonts every document shares (bundled, installed, added), and
|
||||
@@ -648,7 +659,15 @@ fn file_face_names(path: &std::path::Path) -> Vec<ScannedFace> {
|
||||
crate::group::classify(&skrifa::FontRef::new(&font).ok()?, &family)
|
||||
}
|
||||
};
|
||||
Some(ScannedFace { family, style, group, native, aliases: aliases(&f, &[StringId::TYPOGRAPHIC_FAMILY_NAME, StringId::FAMILY_NAME]) })
|
||||
let postscript = name(&f, &[StringId::POSTSCRIPT_NAME]);
|
||||
Some(ScannedFace {
|
||||
family,
|
||||
style,
|
||||
group,
|
||||
native,
|
||||
aliases: aliases(&f, &[StringId::TYPOGRAPHIC_FAMILY_NAME, StringId::FAMILY_NAME]),
|
||||
postscript,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -836,6 +855,7 @@ fn make_face(bytes: FontBytes, source: FontSource, index: u32, family: String, s
|
||||
icf: std::sync::OnceLock::new(),
|
||||
group: std::sync::OnceLock::new(),
|
||||
kind: std::sync::OnceLock::new(),
|
||||
postscript: std::sync::OnceLock::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1148,6 +1168,7 @@ impl FontDb {
|
||||
group: f.group,
|
||||
native: f.native,
|
||||
aliases: f.aliases,
|
||||
postscript: f.postscript,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1156,7 +1177,15 @@ impl FontDb {
|
||||
// decide what is a font.
|
||||
for p in unscanned_font_files((self.font_files)(), &mut scanned) {
|
||||
for f in file_face_names(&p) {
|
||||
found.push(CatalogEntry { family: f.family, style: f.style, path: p.clone(), group: f.group, native: f.native, aliases: f.aliases });
|
||||
found.push(CatalogEntry {
|
||||
family: f.family,
|
||||
style: f.style,
|
||||
path: p.clone(),
|
||||
group: f.group,
|
||||
native: f.native,
|
||||
aliases: f.aliases,
|
||||
postscript: f.postscript,
|
||||
});
|
||||
}
|
||||
}
|
||||
let n = found.len();
|
||||
@@ -1304,6 +1333,11 @@ impl FontDb {
|
||||
self.scoped(0).face(family, style)
|
||||
}
|
||||
|
||||
/// The shared face with PostScript name `name` (see [`ScopedFonts::postscript_face`]).
|
||||
pub fn postscript_face(&self, name: &str) -> Option<(String, String)> {
|
||||
self.scoped(0).postscript_face(name)
|
||||
}
|
||||
|
||||
/// Is `family` among the shared fonts (loaded, or installed on the system)?
|
||||
pub fn has_family(&self, family: &str) -> bool {
|
||||
self.scoped(0).has_family(family)
|
||||
@@ -1692,6 +1726,21 @@ impl ScopedFonts<'_> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The family and style of the face whose PostScript name is `name`, ignoring case (the
|
||||
/// document's, loaded, or installed on the system, not loading it). SVGs exported from
|
||||
/// Illustrator name fonts this way (`font-family="'MyriadPro-Regular'"`).
|
||||
pub fn postscript_face(&self, name: &str) -> Option<(String, String)> {
|
||||
let named = |ps: Option<&str>| ps.is_some_and(|ps| ps.eq_ignore_ascii_case(name));
|
||||
if let Some(f) = self.own().iter().chain(self.db.read_faces().iter()).find(|f| named(f.postscript_name())) {
|
||||
return Some((f.family.clone(), f.style.clone()));
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(c) = self.db.read_catalog().iter().find(|c| named(c.postscript.as_deref())) {
|
||||
return Some((c.family.clone(), c.style.clone()));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Is `family` available (the document's, loaded, or installed on the system), as named or
|
||||
/// without an `$ID/` prefix or a format suffix (see [`ScopedFonts::face`])?
|
||||
pub fn has_family(&self, family: &str) -> bool {
|
||||
|
||||
@@ -350,3 +350,23 @@ fn a_rescan_does_not_cache_an_older_in_flight_glyph_miss() {
|
||||
assert_eq!(db.fallback_for(c, 0, None).unwrap().family, family);
|
||||
std::fs::remove_dir_all(db.font_dirs.first().unwrap()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installed_and_loaded_fonts_are_found_by_postscript_name() {
|
||||
let dir = std::env::temp_dir().join(format!("dc-sysfonts-postscript-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("ps.ttf"), crate::testing::font_with("DC Test PS Installed", &['a']).unwrap()).unwrap();
|
||||
let db = FontDb::with_font_dirs(vec![dir]);
|
||||
let found = Some(("DC Test PS Installed".to_string(), "Regular".to_string()));
|
||||
// From the catalog, without loading the font; case doesn't matter.
|
||||
assert_eq!(db.postscript_face("dctestpsinstalled-regular"), found);
|
||||
assert!(!db.is_loaded("DC Test PS Installed"));
|
||||
// Bundled and added (loaded) faces.
|
||||
assert_eq!(db.postscript_face("SourceSerif4-Semibold"), Some(("Source Serif 4".to_string(), "Semibold".to_string())));
|
||||
db.add_font(crate::testing::font_with("DC Test PS Added", &['a']).unwrap());
|
||||
assert_eq!(db.postscript_face("DCTestPSAdded-Regular"), Some(("DC Test PS Added".to_string(), "Regular".to_string())));
|
||||
// A family name isn't a PostScript name.
|
||||
assert_eq!(db.postscript_face("Source Serif 4"), None);
|
||||
assert_eq!(db.postscript_face("NoSuchFont-Regular"), None);
|
||||
}
|
||||
|
||||
@@ -73,7 +73,11 @@ fn svg_options() -> &'static usvg::Options<'static> {
|
||||
}
|
||||
|
||||
/// A family the SVG names that isn't bundled comes from the shared fonts (installed ones too),
|
||||
/// as it would in a text frame, and matches in any case, as it does there.
|
||||
/// as it would in a text frame, and matches in any case, as it does there. A name that is no
|
||||
/// family's but a face's PostScript name (as Illustrator's SVG export writes them:
|
||||
/// `font-family="'MyriadPro-Regular'"`) sets the text in that face, whatever font-weight and
|
||||
/// font-style say: the name already picks the weight and style, and a family's other faces would
|
||||
/// contradict it. Families listed before it come first.
|
||||
fn select_font() -> usvg::FontSelectionFn<'static> {
|
||||
use usvg::fontdb::{Family, Query, Stretch, Style, Weight};
|
||||
Box::new(move |font, db| {
|
||||
@@ -84,13 +88,29 @@ fn select_font() -> usvg::FontSelectionFn<'static> {
|
||||
for family in font.families() {
|
||||
if let usvg::FontFamily::Named(name) = family
|
||||
&& spelled(db, name).is_none()
|
||||
&& fonts.has_family(name)
|
||||
{
|
||||
for style in fonts.styles(name) {
|
||||
add_face(db, fonts.face(name, &style));
|
||||
if fonts.has_family(name) {
|
||||
for style in fonts.styles(name) {
|
||||
add_face(db, fonts.face(name, &style));
|
||||
}
|
||||
} else if postscript_face(db, name).is_none()
|
||||
&& let Some((family, style)) = fonts.postscript_face(name)
|
||||
{
|
||||
add_face(db, fonts.face(&family, &style));
|
||||
}
|
||||
}
|
||||
}
|
||||
for family in font.families() {
|
||||
match family {
|
||||
usvg::FontFamily::Named(name) if spelled(db, name).is_none() => {
|
||||
if let Some(id) = postscript_face(db, name) {
|
||||
return Some(id);
|
||||
}
|
||||
}
|
||||
// A family (or generic family) that matches: usvg's own selection.
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
// usvg's default query, but with each named family spelled as the database spells it:
|
||||
// fontdb compares names exactly, so "DeJaVu SaNs" would quietly fall to the serif.
|
||||
let names: Vec<Option<String>> = font
|
||||
@@ -135,6 +155,11 @@ fn select_font() -> usvg::FontSelectionFn<'static> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The face in the SVG's font database with PostScript name `name` (ignoring case).
|
||||
fn postscript_face(db: &usvg::fontdb::Database, name: &str) -> Option<usvg::fontdb::ID> {
|
||||
db.faces().find(|f| f.post_script_name.eq_ignore_ascii_case(name)).map(|f| f.id)
|
||||
}
|
||||
|
||||
/// Characters the chosen fonts lack (Korean, Arabic…) fall back to a shared face that has them.
|
||||
fn select_fallback() -> usvg::FallbackSelectionFn<'static> {
|
||||
let loaded = usvg::FontResolver::default_fallback_selector();
|
||||
@@ -406,6 +431,45 @@ mod tests {
|
||||
assert!(set.len() == 2 && set.iter().all(|(f, _)| f == "Source Serif 4"), "{set:?}");
|
||||
}
|
||||
|
||||
/// The PostScript name of the face that sets the text of `svg` (one face for all of it).
|
||||
fn text_face(svg_attrs: &str) -> String {
|
||||
let svg =
|
||||
format!(r#"<svg xmlns="http://www.w3.org/2000/svg" width="100" height="40"><text y="30" font-size="20"{svg_attrs}>ab</text></svg>"#);
|
||||
let tree = svg_tree(svg.as_bytes()).unwrap();
|
||||
let mut faces = Vec::new();
|
||||
for n in tree.root().children() {
|
||||
if let usvg::Node::Text(t) = n {
|
||||
for g in t.layouted().iter().flat_map(|s| &s.positioned_glyphs) {
|
||||
faces.extend(tree.fontdb().face(g.font).map(|f| f.post_script_name.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
faces.dedup();
|
||||
assert_eq!(faces.len(), 1, "{faces:?}");
|
||||
faces.remove(0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn svg_text_finds_fonts_by_postscript_name() {
|
||||
// Bundled, as Illustrator's SVG export names it; any case.
|
||||
assert_eq!(text_face(r#" font-family="'SourceSerif4-Semibold'""#), "SourceSerif4-Semibold");
|
||||
assert_eq!(text_face(r#" font-family="sourceserif4-semibold""#), "SourceSerif4-Semibold");
|
||||
// A shared font (as an installed one would be).
|
||||
let font = designcraft_fonts::testing::font_with("DC Test SVG PostScript", &['a', 'b']).unwrap();
|
||||
designcraft_fonts::FontDb::global().add_font(font);
|
||||
assert_eq!(text_face(r#" font-family="DCTestSVGPostScript-Regular""#), "DCTestSVGPostScript-Regular");
|
||||
// The name picks that face: font-weight and font-style don't change it.
|
||||
assert_eq!(text_face(r#" font-family="SourceSerif4-Semibold" font-weight="bold" font-style="italic""#), "SourceSerif4-Semibold");
|
||||
// A family listed before it still comes first; one after it is a fallback.
|
||||
assert_eq!(text_face(r#" font-family="'Source Sans 3', SourceSerif4-Bold""#), "SourceSans3-Regular");
|
||||
assert_eq!(text_face(r#" font-family="NoSuch-Font, SourceSerif4-Bold, 'Source Sans 3'""#), "SourceSerif4-Bold");
|
||||
// Family names work as before.
|
||||
assert_eq!(text_face(r#" font-family="'Source Serif 4'" font-weight="bold""#), "SourceSerif4-Bold");
|
||||
// An unknown name falls back as before (the serif default).
|
||||
assert_eq!(text_face(r#" font-family="NoSuch-Font""#), text_face(r#" font-family="No Such Font""#));
|
||||
assert_eq!(text_face(r#" font-family="NoSuch-Font""#), "SourceSerif4-Regular");
|
||||
}
|
||||
|
||||
/// A minimal Photoshop file: 2×1 RGB, no layers, raw merged image.
|
||||
fn tiny_psd() -> Vec<u8> {
|
||||
let mut b = Vec::new();
|
||||
|
||||
Reference in New Issue
Block a user