mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-26 03:06:56 -06:00
Merge idml-font-names: IDML: font family names keep their spaces; FontStyle Nothing inherits (#558)
Conflict: ROADMAP.md: both progress lines Conflict: crates/fonts/src/fontdb.rs: next's style_traits/traits_of/linked_style, canonical_family lookup, family_kind and postscript_face; #558's alternate_names (as written, then trimmed) now chains plain_family ($ID/ prefix and format suffix) and is the fallback in style_traits, face (canonical_family, then substitute_family) and has_family Conflict: crates/idml/src/import.rs: #558's CR/LF-only stripping (next's f.trim() dropped), next's composite_names lookup (by the trimmed name) and names::font_family_in; #558's FontStyle Nothing → unset Conflict: crates/idml/src/tests.rs: both tests
This commit is contained in:
@@ -138,6 +138,7 @@ Newest first.
|
||||
- **2026-10-12** · Properties › Transform shows rotation, shear and X/Y scale, and Rotate 90° and Flip Horizontal/Vertical buttons, as InDesign's does. No readiness percentage change.
|
||||
- **2026-10-12** · Underlines and strikethroughs are drawn as one bar per stretch of text on screen, as in PDF export: a bar per letter left faint seams between the letters. No readiness percentage change.
|
||||
- **2026-10-12** · The section marker character shows its section's marker text on document pages (nothing when the section has none) instead of the word "Section", which stays on parent pages; editing the marker recomposes (#556). No readiness percentage change.
|
||||
- **2026-10-12** · IDML import keeps a font family name exactly as written, trailing spaces included, and font lookup falls back to the name without surrounding spaces; `FontStyle="Nothing"` leaves the style to the paragraph (#558). No readiness percentage change.
|
||||
- **2026-10-12** · Words in capitals hyphenate like capitalized words (FERRUM COM-MODI), as in InDesign; Hyphenate Capitalized Words off keeps them whole. No readiness percentage change.
|
||||
- **2026-10-11 (printing):** `file.printers` and `file.print` use craft-libs/craft-print for CUPS and Windows. DesignCraft retains PDF export, page selection, spreads, marks and bleed; dry runs create no temporary PDF or print job. Windows selects matching or default driver paper and shrinks oversized sheets without enlarging smaller ones; queue duplex defaults are preserved. Headless tests exercise exported bytes, job settings, dry runs and refusals; shared native Windows CI passes, including paused-queue submission and cleanup. PostScript, production print controls and physical output validation remain open. No readiness percentage change.
|
||||
- **2026-10-11** · The View menu follows InDesign's: proofing first, then zoom, Rotate Spread, Screen Mode, Display Performance, rulers, Extras, Grids & Guides and Structure; the view items that sat at the end of the Type menu moved there. No readiness percentage change.
|
||||
|
||||
@@ -924,6 +924,13 @@ fn without_format_suffix(family: &str) -> Option<&str> {
|
||||
(!base.is_empty() && FORMATS.iter().any(|f| f.eq_ignore_ascii_case(suffix.trim()))).then_some(base)
|
||||
}
|
||||
|
||||
/// The names `family` is looked up under when no font has it as written: without surrounding
|
||||
/// spaces, then without an `$ID/` prefix or a format suffix (see [`plain_family`]).
|
||||
fn alternate_names(family: &str) -> impl Iterator<Item = &str> {
|
||||
let trimmed = family.trim();
|
||||
(trimmed != family && !trimmed.is_empty()).then_some(trimmed).into_iter().chain(plain_family(trimmed))
|
||||
}
|
||||
|
||||
/// The named style part of a style with axis settings (`Bold {wght:650}` → `Bold`).
|
||||
pub fn base_style(style: &str) -> &str {
|
||||
style.split('{').next().unwrap_or(style).trim()
|
||||
@@ -1633,10 +1640,10 @@ impl ScopedFonts<'_> {
|
||||
/// Regular first, then by weight.
|
||||
fn style_traits(&self, family: &str) -> Vec<(bool, f32, String)> {
|
||||
let v = self.exact_style_traits(family);
|
||||
match plain_family(family) {
|
||||
Some(base) if v.is_empty() => self.exact_style_traits(base),
|
||||
_ => v,
|
||||
if !v.is_empty() {
|
||||
return v;
|
||||
}
|
||||
alternate_names(family).map(|f| self.exact_style_traits(f)).find(|v| !v.is_empty()).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn exact_style_traits(&self, family: &str) -> Vec<(bool, f32, String)> {
|
||||
@@ -1662,13 +1669,13 @@ impl ScopedFonts<'_> {
|
||||
|
||||
/// Resolve a family + style to a face, falling back to the closest style of the family, then to
|
||||
/// Source Sans 3 Regular. Installed system fonts are found by name whatever ran before. A
|
||||
/// family with InDesign's `$ID/` prefix or a format suffix (`Minion Pro (OTF)`) that isn't
|
||||
/// found is looked up without them.
|
||||
/// family that isn't found as written is looked up without surrounding spaces, then without
|
||||
/// InDesign's `$ID/` prefix or a format suffix (`Minion Pro (OTF)` → `Minion Pro`).
|
||||
pub fn face(&self, family: &str, style: &str) -> Arc<FontFace> {
|
||||
let canonical = self
|
||||
.canonical_family(family)
|
||||
.or_else(|| plain_family(family).and_then(|base| self.canonical_family(base)))
|
||||
.unwrap_or_else(|| self.substitute_family(plain_family(family).unwrap_or(family)));
|
||||
.or_else(|| alternate_names(family).find_map(|f| self.canonical_family(f)))
|
||||
.unwrap_or_else(|| self.substitute_family(alternate_names(family).last().unwrap_or(family)));
|
||||
if let Some(f) = self.find_or_load(&canonical, style) {
|
||||
return f;
|
||||
}
|
||||
@@ -1742,9 +1749,9 @@ impl ScopedFonts<'_> {
|
||||
}
|
||||
|
||||
/// 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`])?
|
||||
/// under an alternate name (see [`ScopedFonts::face`])?
|
||||
pub fn has_family(&self, family: &str) -> bool {
|
||||
self.canonical_family(family).is_some() || plain_family(family).is_some_and(|base| self.canonical_family(base).is_some())
|
||||
self.canonical_family(family).is_some() || alternate_names(family).any(|f| self.canonical_family(f).is_some())
|
||||
}
|
||||
|
||||
/// A variable font's axes: (tag, name, min, default, max), empty for static fonts.
|
||||
|
||||
@@ -481,6 +481,19 @@ mod tests {
|
||||
assert!(!db.has_family("$ID/DC No Such Family"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_family_name_keeps_its_spaces_and_falls_back_to_the_trimmed_name() {
|
||||
let db = FontDb::with_font_dirs(Vec::new());
|
||||
db.set_system_fallback(false);
|
||||
db.add_font(testing::font_with("DC Test Space ", &['a']).unwrap());
|
||||
db.add_font(testing::font_with("DC Test Space", &['a']).unwrap());
|
||||
assert_eq!(db.face("DC Test Space ", "Regular").family, "DC Test Space ");
|
||||
assert_eq!(db.face("DC Test Space", "Regular").family, "DC Test Space");
|
||||
assert_eq!(db.face(&format!(" {DEFAULT_FAMILY}\n"), "Regular").family, DEFAULT_FAMILY);
|
||||
assert!(db.has_family(&format!("{DEFAULT_FAMILY} ")));
|
||||
assert_eq!(db.styles(&format!("{DEFAULT_FAMILY} ")), db.styles(DEFAULT_FAMILY));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outlines_and_metrics() {
|
||||
let db = FontDb::global();
|
||||
|
||||
@@ -1362,12 +1362,13 @@ impl<'r> Importer<'r> {
|
||||
fn char_attrs(&mut self, e: &El) -> CharAttrs {
|
||||
let mut a = CharAttrs::default();
|
||||
if let Some(f) = e.prop("AppliedFont") {
|
||||
let f = f.trim();
|
||||
// Some writers append the style after a tab.
|
||||
let fam = f.split('\t').next().unwrap_or(f);
|
||||
a.font_family = self.composite_names.get(fam).cloned().or_else(|| names::font_family_in(fam));
|
||||
// Some writers append the style after a tab. Spaces are part of a family's name
|
||||
// ("Minion Pro " is another font than "Minion Pro"); line breaks around it aren't.
|
||||
let fam = f.split('\t').next().unwrap_or(&f).trim_matches(['\n', '\r']);
|
||||
a.font_family = self.composite_names.get(fam.trim()).cloned().or_else(|| names::font_family_in(fam));
|
||||
}
|
||||
a.font_style = e.prop("FontStyle");
|
||||
// "Nothing" leaves the style unset: it comes from the paragraph style.
|
||||
a.font_style = e.prop("FontStyle").filter(|s| !matches!(s.trim(), "Nothing" | "$ID/Nothing"));
|
||||
a.size = e.num("PointSize");
|
||||
a.leading = e.prop("Leading").and_then(|l| names::leading_in(l.trim()));
|
||||
// Automatic kerning may carry an inactive numeric value (including the
|
||||
|
||||
@@ -3042,4 +3042,25 @@ fn hostile_custom_wrap_boundary_is_dropped() {
|
||||
assert_eq!(it.wrap.path, None);
|
||||
assert!(first_line_x(&d) >= 100.0 - 1e-6);
|
||||
}
|
||||
/// A family name keeps its spaces ("Minion Pro " is a font of its own) and `FontStyle="Nothing"`
|
||||
/// leaves the style to the paragraph, also after an export.
|
||||
#[test]
|
||||
fn applied_font_keeps_spaces_and_font_style_nothing_inherits() {
|
||||
let story = r#"<idPkg:Story xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging"><Story Self="s1">
|
||||
<ParagraphStyleRange AppliedParagraphStyle="ParagraphStyle/Text%3aBody" FontStyle="Bold">
|
||||
<CharacterStyleRange AppliedCharacterStyle="CharacterStyle/$ID/[No character style]" FontStyle="Nothing"><Properties><AppliedFont type="string">DC Space </AppliedFont></Properties><Content>Spaced</Content></CharacterStyleRange>
|
||||
<CharacterStyleRange AppliedCharacterStyle="CharacterStyle/$ID/[No character style]" FontStyle="$ID/Nothing"><Content> plain</Content></CharacterStyleRange>
|
||||
</ParagraphStyleRange></Story></idPkg:Story>"#;
|
||||
let check = |d: &Document| {
|
||||
let s = d.stories.values().find(|s| s.text.contains("Spaced")).unwrap();
|
||||
let (_, base) = d.styles.resolve_para(&s.paras[0]);
|
||||
let runs: Vec<_> = s.runs().map(|(_, f)| f.clone()).collect();
|
||||
assert_eq!(runs[0].over.font_family.as_deref(), Some("DC Space "));
|
||||
for f in &runs {
|
||||
assert_eq!(d.styles.resolve_char(&base, f).font_style, "Bold", "the enclosing range's style");
|
||||
}
|
||||
};
|
||||
let d = import_idml(&fixture_with_story(story)).unwrap();
|
||||
check(&d);
|
||||
check(&import_idml(&export_idml(&d)).unwrap());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user