mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-29 04:36:47 -06:00
Merge PR #548
This commit is contained in:
@@ -125,6 +125,8 @@ pub struct FontFace {
|
||||
em: std::sync::OnceLock<((f64, f64), bool)>,
|
||||
/// The font menu group and native family name, read on first use.
|
||||
group: std::sync::OnceLock<(FontGroup, Option<String>)>,
|
||||
/// The PostScript name, read on first use.
|
||||
postscript: std::sync::OnceLock<Option<String>>,
|
||||
/// Format, class and PANOSE numbers (the font menus), read on first use.
|
||||
kind: std::sync::OnceLock<crate::kind::FaceKind>,
|
||||
}
|
||||
@@ -309,6 +311,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)
|
||||
@@ -342,6 +351,7 @@ struct CatalogEntry {
|
||||
/// The font menu group and native family name, read by the scan.
|
||||
group: FontGroup,
|
||||
native: Option<String>,
|
||||
postscript: Option<String>,
|
||||
}
|
||||
|
||||
/// What the scan reads of one face in a font file: its names and font menu group.
|
||||
@@ -352,6 +362,7 @@ struct ScannedFace {
|
||||
style: String,
|
||||
group: FontGroup,
|
||||
native: Option<String>,
|
||||
postscript: Option<String>,
|
||||
}
|
||||
|
||||
/// Process-wide font database: the fonts every document shares (bundled, installed, added), and
|
||||
@@ -628,7 +639,8 @@ 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 })
|
||||
let postscript = name(&f, &[StringId::POSTSCRIPT_NAME]);
|
||||
Some(ScannedFace { family, style, group, native, postscript })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -779,6 +791,7 @@ fn make_face(bytes: FontBytes, source: FontSource, index: u32, family: String, s
|
||||
bmp: std::sync::OnceLock::new(),
|
||||
em: std::sync::OnceLock::new(),
|
||||
group: std::sync::OnceLock::new(),
|
||||
postscript: std::sync::OnceLock::new(),
|
||||
kind: std::sync::OnceLock::new(),
|
||||
})
|
||||
}
|
||||
@@ -1063,7 +1076,14 @@ impl FontDb {
|
||||
continue;
|
||||
}
|
||||
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 });
|
||||
found.push(CatalogEntry {
|
||||
family: f.family,
|
||||
style: f.style,
|
||||
path: p.clone(),
|
||||
group: f.group,
|
||||
native: f.native,
|
||||
postscript: f.postscript,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1212,6 +1232,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)
|
||||
@@ -1503,6 +1528,21 @@ impl ScopedFonts<'_> {
|
||||
self.has_exact_family(family) || without_format_suffix(family).is_some_and(|base| self.has_exact_family(base))
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
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) {
|
||||
return true;
|
||||
|
||||
@@ -181,3 +181,23 @@ fn font_menus_group_installed_and_loaded_fonts_without_loading_them() {
|
||||
assert_eq!(infos.iter().filter(|i| i.family == "Groups Mincho").count(), 1);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[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,25 +73,54 @@ 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.
|
||||
/// as it would in a text frame. 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> {
|
||||
let bundled = usvg::FontResolver::default_font_selector();
|
||||
Box::new(move |font, db| {
|
||||
let fonts = designcraft_fonts::FontDb::global();
|
||||
for family in font.families() {
|
||||
if let usvg::FontFamily::Named(name) = family
|
||||
&& !db.faces().any(|f| f.families.iter().any(|(n, _)| n.eq_ignore_ascii_case(name)))
|
||||
&& fonts.has_family(name)
|
||||
&& !has_family(db, 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 !has_family(db, name) => {
|
||||
if let Some(id) = postscript_face(db, name) {
|
||||
return Some(id);
|
||||
}
|
||||
}
|
||||
// A family (or generic family) that matches: usvg's own selection.
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
bundled(font, db)
|
||||
})
|
||||
}
|
||||
|
||||
/// Does a face in the SVG's font database have family `name` (ignoring case)?
|
||||
fn has_family(db: &usvg::fontdb::Database, name: &str) -> bool {
|
||||
db.faces().any(|f| f.families.iter().any(|(n, _)| n.eq_ignore_ascii_case(name)))
|
||||
}
|
||||
|
||||
/// 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();
|
||||
@@ -351,6 +380,45 @@ mod tests {
|
||||
assert!(set.len() == 2 && set.iter().all(|(f, gid)| f != "Source Sans 3" && *gid != 0), "{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