M6: Sketch import: text with styled runs and font names

Text layers keep their string and styled runs (font, size, colour, line
height, letter spacing, decorations, text transform; ranges in UTF-16
units, clamped), paragraph alignment and spacing, vertical alignment and
how the box sizes. Fonts named by PostScript name become family and
style; the engine then matches families to installed ones ignoring
spaces (PingFangSC → PingFang SC). Fonts embedded in newer files are
named in the import report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jindřich Šaršon
2026-10-22 00:27:20 +02:00
co-authored by Claude Opus 5.5
parent ff9d966ea6
commit a0542e581c
8 changed files with 442 additions and 1 deletions
+35
View File
@@ -558,11 +558,46 @@ fn load_with(
fn load_sketch(bytes: &[u8], name: &str) -> Option<Result<figcraft_format::Loaded>> {
let r = figcraft_format::sketch::try_import(bytes, name)?;
Some(r.map_err(|e| EngineError::Other(e.to_string())).map(|(mut doc, warnings)| {
match_font_families(&mut doc);
link_instances(&mut doc);
figcraft_format::Loaded { issues: warnings, ..doc.into() }
}))
}
/// Font families a `.sketch` import guessed from PostScript names ("Ping Fang SC" from
/// "PingFangSC-Semibold") take the installed family's name when they differ only in spaces and
/// case.
fn match_font_families(doc: &mut Document) {
let installed = figcraft_text::fontdb::family_names();
let mut cache: std::collections::BTreeMap<String, Option<String>> = std::collections::BTreeMap::new();
let mut fix = |style: &mut figcraft_doc::TextStyle| {
let found = cache.entry(style.font_family.clone()).or_insert_with(|| installed_family(&style.font_family, &installed));
if let Some(f) = found {
style.font_family = f.clone();
}
};
let ids: Vec<NodeId> = doc.nodes().filter(|n| matches!(n.kind, NodeKind::Text(_))).map(|n| n.id).collect();
for id in ids {
if let Some(NodeKind::Text(td)) = doc.node_mut(id).map(|n| &mut n.kind) {
fix(&mut td.style);
for r in &mut td.runs {
fix(&mut r.style);
}
}
}
}
/// The installed family `family` means when it isn't installed under that exact name: the one
/// whose name is the same without spaces, ignoring case.
pub(crate) fn installed_family(family: &str, installed: &[String]) -> Option<String> {
let key = |s: &str| s.chars().filter(|c| !c.is_whitespace()).flat_map(char::to_lowercase).collect::<String>();
if installed.iter().any(|f| f.eq_ignore_ascii_case(family)) {
return None;
}
let want = key(family);
installed.iter().find(|f| key(f) == want).cloned()
}
/// Gives `None` when `bytes` are not a `.fig` file. Then other readers can try.
pub(crate) fn load_fig(bytes: &[u8], name: &str) -> Option<Result<figcraft_format::Loaded>> {
let r = figcraft_format::fig::try_import(bytes, name)?;
+11
View File
@@ -134,3 +134,14 @@ fn sketch_masks_show_their_content() {
assert!(m.alpha() > 200 && m.red() > 150, "mask fill: {m:?}");
assert!(px(2, 2).alpha() < 50);
}
/// Family names guessed from PostScript names find the installed family whatever its spaces.
#[test]
fn sketch_font_families_match_installed_ones() {
let installed = ["PingFang SC".to_string(), "Helvetica Neue".to_string(), "IBM Plex Sans".to_string()];
let m = |f: &str| crate::cmd::file::installed_family(f, &installed);
assert_eq!(m("Ping Fang SC").as_deref(), Some("PingFang SC"));
assert_eq!(m("Helvetica Neue"), None, "already right");
assert_eq!(m("IBMPlex Sans").as_deref(), Some("IBM Plex Sans"));
assert_eq!(m("Comic Sans"), None);
}