PDF/.ai import: fonts are found by their PostScript name, whatever hyphens the family holds
A document names its fonts by PostScript name, which the importer split at the first hyphen into family and style. A family with hyphens in its name was never found: "Rounded-X-Mplus-1c-black" became family "rounded", style "x-mplus-1c-black", and the text fell back to another font. Common Japanese families have hyphens in their names. The system font scan now also indexes each installed face's PostScript name (read from the same name table it already reads). The importer asks for the face with that exact name first (`FontDb::by_postscript_name`) and splits the name only when no installed face has it. Test: installed faces are found by PostScript name, in any case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
025315f4bf
commit
caba979b01
|
|
@ -206,6 +206,10 @@ impl Families {
|
||||||
Some((tag, rest)) if tag.len() == 6 && tag.chars().all(|c| c.is_ascii_uppercase()) => rest,
|
Some((tag, rest)) if tag.len() == 6 && tag.chars().all(|c| c.is_ascii_uppercase()) => rest,
|
||||||
_ => name,
|
_ => name,
|
||||||
};
|
};
|
||||||
|
// An installed face of that exact PostScript name: its own family and style.
|
||||||
|
if let Some((family, style)) = vectorcraft_text::FontDb::global().by_postscript_name(name) {
|
||||||
|
return (family, style, true);
|
||||||
|
}
|
||||||
let (fam, style) = name.split_once(['-', ',']).unwrap_or((name, ""));
|
let (fam, style) = name.split_once(['-', ',']).unwrap_or((name, ""));
|
||||||
let found = [fam, strip_ps(fam)].iter().find_map(|f| self.0.get(&norm(f)).cloned());
|
let found = [fam, strip_ps(fam)].iter().find_map(|f| self.0.get(&norm(f)).cloned());
|
||||||
let style = match spaced(strip_ps(style)).as_str() {
|
let style = match spaced(strip_ps(style)).as_str() {
|
||||||
|
|
|
||||||
|
|
@ -153,11 +153,16 @@ struct CatalogFamily {
|
||||||
/// [`FontDb::face`] does). Always empty on wasm, which has no system fonts.
|
/// [`FontDb::face`] does). Always empty on wasm, which has no system fonts.
|
||||||
type Catalog = HashMap<String, CatalogFamily>;
|
type Catalog = HashMap<String, CatalogFamily>;
|
||||||
|
|
||||||
|
/// The installed faces' (family, style) by ASCII-lowercased PostScript name.
|
||||||
|
type PostScriptNames = HashMap<String, (String, String)>;
|
||||||
|
|
||||||
/// Process-wide font database.
|
/// Process-wide font database.
|
||||||
pub struct FontDb {
|
pub struct FontDb {
|
||||||
faces: RwLock<Vec<Arc<FontFace>>>,
|
faces: RwLock<Vec<Arc<FontFace>>>,
|
||||||
outlines: Mutex<HashMap<(u32, u32), Arc<BezPath>>>,
|
outlines: Mutex<HashMap<(u32, u32), Arc<BezPath>>>,
|
||||||
catalog: RwLock<Catalog>,
|
catalog: RwLock<Catalog>,
|
||||||
|
/// The installed faces by PostScript name (documents name fonts so: PDF, EPS, .ai).
|
||||||
|
postscript: RwLock<PostScriptNames>,
|
||||||
/// The folders the system font scan reads (the platform's font folders for [`FontDb::global`]).
|
/// The folders the system font scan reads (the platform's font folders for [`FontDb::global`]).
|
||||||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||||
font_dirs: Vec<PathBuf>,
|
font_dirs: Vec<PathBuf>,
|
||||||
|
|
@ -224,6 +229,13 @@ fn face_names(f: &skrifa::FontRef<'_>) -> Option<(String, String)> {
|
||||||
Some((family, style))
|
Some((family, style))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A face's (family, style, PostScript name).
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn face_names_ps(f: &skrifa::FontRef<'_>) -> Option<(String, String, Option<String>)> {
|
||||||
|
let (family, style) = face_names(f)?;
|
||||||
|
Some((family, style, name(f, &[StringId::POSTSCRIPT_NAME])))
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse every face in `data` (a font file or collection). Returns `(index, family, style)`.
|
/// Parse every face in `data` (a font file or collection). Returns `(index, family, style)`.
|
||||||
fn enumerate_faces(data: &[u8]) -> Vec<(u32, String, String)> {
|
fn enumerate_faces(data: &[u8]) -> Vec<(u32, String, String)> {
|
||||||
let count = match FileRef::new(data) {
|
let count = match FileRef::new(data) {
|
||||||
|
|
@ -239,10 +251,11 @@ fn enumerate_faces(data: &[u8]) -> Vec<(u32, String, String)> {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// (family, style) of every face in the font file at `path`, reading only its table directories
|
/// (family, style, PostScript name) of every face in the font file at `path`, reading only its
|
||||||
/// and `name` tables: a scan opens hundreds of font files, many of them megabytes long.
|
/// table directories and `name` tables: a scan opens hundreds of font files, many of them
|
||||||
|
/// megabytes long.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn file_face_names(path: &Path) -> Vec<(String, String)> {
|
fn file_face_names(path: &Path) -> Vec<(String, String, Option<String>)> {
|
||||||
use std::io::{Read, Seek, SeekFrom};
|
use std::io::{Read, Seek, SeekFrom};
|
||||||
/// Caps on what a (possibly damaged) file can make the scan read.
|
/// Caps on what a (possibly damaged) file can make the scan read.
|
||||||
const MAX_FACES: u32 = 256;
|
const MAX_FACES: u32 = 256;
|
||||||
|
|
@ -284,7 +297,7 @@ fn file_face_names(path: &Path) -> Vec<(String, String)> {
|
||||||
font.extend_from_slice(&u32::to_be_bytes(v));
|
font.extend_from_slice(&u32::to_be_bytes(v));
|
||||||
}
|
}
|
||||||
font.extend_from_slice(&table);
|
font.extend_from_slice(&table);
|
||||||
face_names(&skrifa::FontRef::new(&font).ok()?)
|
face_names_ps(&skrifa::FontRef::new(&font).ok()?)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
@ -394,6 +407,7 @@ impl FontDb {
|
||||||
faces: RwLock::new(faces),
|
faces: RwLock::new(faces),
|
||||||
outlines: Mutex::new(HashMap::new()),
|
outlines: Mutex::new(HashMap::new()),
|
||||||
catalog: RwLock::new(Catalog::new()),
|
catalog: RwLock::new(Catalog::new()),
|
||||||
|
postscript: RwLock::new(PostScriptNames::new()),
|
||||||
font_dirs,
|
font_dirs,
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
cataloged: std::sync::OnceLock::new(),
|
cataloged: std::sync::OnceLock::new(),
|
||||||
|
|
@ -540,6 +554,7 @@ impl FontDb {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn scan_font_dirs(&self) -> usize {
|
fn scan_font_dirs(&self) -> usize {
|
||||||
let mut catalog = Catalog::new();
|
let mut catalog = Catalog::new();
|
||||||
|
let mut postscript = PostScriptNames::new();
|
||||||
let mut n = 0;
|
let mut n = 0;
|
||||||
let mut stack = self.font_dirs.clone();
|
let mut stack = self.font_dirs.clone();
|
||||||
// Each folder once, however links lead back to it.
|
// Each folder once, however links lead back to it.
|
||||||
|
|
@ -559,7 +574,10 @@ impl FontDb {
|
||||||
if !matches!(ext.as_deref(), Some("ttf" | "otf" | "ttc" | "otc")) {
|
if !matches!(ext.as_deref(), Some("ttf" | "otf" | "ttc" | "otc")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (family, style) in file_face_names(&p) {
|
for (family, style, ps) in file_face_names(&p) {
|
||||||
|
if let Some(ps) = ps {
|
||||||
|
postscript.insert(ps.to_ascii_lowercase(), (family.clone(), style.clone()));
|
||||||
|
}
|
||||||
let entry = catalog.entry(family.to_ascii_lowercase()).or_default();
|
let entry = catalog.entry(family.to_ascii_lowercase()).or_default();
|
||||||
if entry.name.is_empty() {
|
if entry.name.is_empty() {
|
||||||
entry.name = family;
|
entry.name = family;
|
||||||
|
|
@ -571,10 +589,19 @@ impl FontDb {
|
||||||
}
|
}
|
||||||
log::debug!("cataloged {n} system font faces");
|
log::debug!("cataloged {n} system font faces");
|
||||||
*self.catalog.write().unwrap_or_else(|e| e.into_inner()) = catalog;
|
*self.catalog.write().unwrap_or_else(|e| e.into_inner()) = catalog;
|
||||||
|
*self.postscript.write().unwrap_or_else(|e| e.into_inner()) = postscript;
|
||||||
self.changed();
|
self.changed();
|
||||||
n
|
n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The (family, style) of the installed face whose PostScript name is `name` (ignoring ASCII
|
||||||
|
/// case). Documents name fonts this way, and a family can hold hyphens
|
||||||
|
/// ("Rounded-X-Mplus-1c-black" is Rounded-X M+ 1c, black), so the name can't be split.
|
||||||
|
pub fn by_postscript_name(&self, name: &str) -> Option<(String, String)> {
|
||||||
|
self.ensure_catalog();
|
||||||
|
self.postscript.read().unwrap_or_else(|e| e.into_inner()).get(&name.to_ascii_lowercase()).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
/// Load the files of the installed `family`. Returns whether any face was added.
|
/// Load the files of the installed `family`. Returns whether any face was added.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn load_cataloged(&self, family: &str) -> bool {
|
fn load_cataloged(&self, family: &str) -> bool {
|
||||||
|
|
|
||||||
|
|
@ -142,3 +142,15 @@ fn installed_styles_of_a_loaded_family_load_when_asked_for() {
|
||||||
// A style nobody has still gets the closest one.
|
// A style nobody has still gets the closest one.
|
||||||
assert_eq!(db.face(FAMILY, "Black").unwrap().style, "Bold");
|
assert_eq!(db.face(FAMILY, "Black").unwrap().style, "Bold");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Documents name fonts by PostScript name, and families can hold hyphens: the installed face of
|
||||||
|
/// that exact name gives the family and style, however the name splits.
|
||||||
|
#[test]
|
||||||
|
fn installed_faces_are_found_by_postscript_name() {
|
||||||
|
let dir = font_dir("postscript");
|
||||||
|
let db = FontDb::with_font_dirs(vec![dir]);
|
||||||
|
// The renamed Source Sans 3 files keep their PostScript names.
|
||||||
|
assert_eq!(db.by_postscript_name("SourceSans3-Bold"), Some((FAMILY.to_string(), "Bold".to_string())));
|
||||||
|
assert_eq!(db.by_postscript_name("sourcesans3-regular"), Some((FAMILY.to_string(), "Regular".to_string())), "any case");
|
||||||
|
assert_eq!(db.by_postscript_name("Rounded-X-Mplus-1c-black"), None);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue