IDML: layers import and export in stacking order; font names with a format suffix (#247)
* IDML: layers import and export in stacking order * Fonts: a family named with a format suffix such as (OTF) resolves to the installed family
This commit is contained in:
parent
fb68b7cc2d
commit
4d16a8ce3d
|
|
@ -190,4 +190,39 @@ mod tests {
|
|||
assert_eq!(r["changed"], 1);
|
||||
assert_eq!(s.doc().unwrap().doc.styles.para("Odd").unwrap().chars.font_family.as_deref(), Some(avail.as_str()));
|
||||
}
|
||||
|
||||
/// InDesign names a family installed in several formats `Family (OTF)`; the plain family
|
||||
/// stands for it, and a family that isn't available under either name is still missing.
|
||||
#[test]
|
||||
fn a_family_with_a_format_suffix_uses_the_plain_family() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let r = s.execute("frame.create", &json!({"rect": [72, 72, 400, 300], "content": "text", "text": "Some text here."})).unwrap();
|
||||
let sid = s.doc().unwrap().doc.item(designcraft_doc::ItemId(r["id"].as_u64().unwrap())).unwrap().text_frame().unwrap().story;
|
||||
s.execute("text.select", &json!({"story": sid.0, "anchor": 0, "focus": 15})).unwrap();
|
||||
s.execute("type.char", &json!({"fontFamily": "Source Serif 4 (OTF)", "fontStyle": "Bold"})).unwrap();
|
||||
let l = s.execute("font.list", &json!({})).unwrap();
|
||||
let entry = l.as_array().unwrap().iter().find(|f| f["family"] == "Source Serif 4 (OTF)").unwrap();
|
||||
assert_eq!((&entry["missing"], &entry["styleMissing"], &entry["source"]), (&json!(false), &json!(false), &json!("bundled")), "{l}");
|
||||
let st = s.doc().unwrap();
|
||||
let cs = s.cache.get(&st.doc, sid, None);
|
||||
let faces: Vec<_> =
|
||||
cs.frames.iter().flat_map(|f| &f.lines).flat_map(|l| &l.glyphs).filter(|g| g.visible && g.len > 0).map(|g| g.face.get()).collect();
|
||||
assert!(!faces.is_empty() && faces.iter().all(|f| f.family == "Source Serif 4" && f.style == "Bold"));
|
||||
let kinds = |s: &mut Session| {
|
||||
s.execute("preflight.run", &json!({})).unwrap()["issues"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|i| i["kind"].as_str().unwrap().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
assert!(!kinds(&mut s).iter().any(|k| k == "missingFont"));
|
||||
|
||||
s.execute("text.select", &json!({"story": sid.0, "anchor": 0, "focus": 4})).unwrap();
|
||||
s.execute("type.char", &json!({"fontFamily": "Nope (OTF)"})).unwrap();
|
||||
let l = s.execute("font.list", &json!({})).unwrap();
|
||||
assert_eq!((&l[0]["family"], &l[0]["missing"]), (&json!("Nope (OTF)"), &json!(true)), "{l}");
|
||||
assert!(kinds(&mut s).iter().any(|k| k == "missingFont"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -712,6 +712,15 @@ fn make_face(bytes: FontBytes, source: FontSource, index: u32, family: String, s
|
|||
})
|
||||
}
|
||||
|
||||
/// `family` without the font-format suffix layout apps append when a family is installed in
|
||||
/// several formats (`Minion Pro (OTF)` → `Minion Pro`), if it has one.
|
||||
fn without_format_suffix(family: &str) -> Option<&str> {
|
||||
const FORMATS: [&str; 5] = ["OTF", "TT", "TTF", "TTC", "T1"];
|
||||
let (base, suffix) = family.trim_end().strip_suffix(')')?.rsplit_once('(')?;
|
||||
let base = base.trim_end();
|
||||
(!base.is_empty() && FORMATS.iter().any(|f| f.eq_ignore_ascii_case(suffix.trim()))).then_some(base)
|
||||
}
|
||||
|
||||
/// 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()
|
||||
|
|
@ -1218,6 +1227,14 @@ impl ScopedFonts<'_> {
|
|||
|
||||
/// Style names available for `family` (Regular first, then by weight).
|
||||
pub fn styles(&self, family: &str) -> Vec<String> {
|
||||
let v = self.exact_styles(family);
|
||||
match without_format_suffix(family) {
|
||||
Some(base) if v.is_empty() => self.exact_styles(base),
|
||||
_ => v,
|
||||
}
|
||||
}
|
||||
|
||||
fn exact_styles(&self, family: &str) -> Vec<String> {
|
||||
let mut v: Vec<(bool, f32, String)> = self
|
||||
.own()
|
||||
.iter()
|
||||
|
|
@ -1237,15 +1254,10 @@ 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.
|
||||
/// Source Sans 3 Regular. Installed system fonts are found by name whatever ran before. A
|
||||
/// family with a format suffix (`Minion Pro (OTF)`) that isn't found is looked up without it.
|
||||
pub fn face(&self, family: &str, style: &str) -> Arc<FontFace> {
|
||||
if let Some(f) = self.find(family, style) {
|
||||
return f;
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if self.db.load_cataloged(family)
|
||||
&& let Some(f) = self.find(family, style)
|
||||
{
|
||||
if let Some(f) = self.find_or_load(family, style).or_else(|| without_format_suffix(family).and_then(|base| self.find_or_load(base, style))) {
|
||||
return f;
|
||||
}
|
||||
self.find(FALLBACK_FAMILY, style)
|
||||
|
|
@ -1254,8 +1266,24 @@ impl ScopedFonts<'_> {
|
|||
.unwrap_or_else(last_resort_face)
|
||||
}
|
||||
|
||||
/// Is `family` available (the document's, loaded, or installed on the system)?
|
||||
fn find_or_load(&self, family: &str, style: &str) -> Option<Arc<FontFace>> {
|
||||
if let Some(f) = self.find(family, style) {
|
||||
return Some(f);
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if self.db.load_cataloged(family) {
|
||||
return self.find(family, style);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Is `family` available (the document's, loaded, or installed on the system), as named or
|
||||
/// without a format suffix (see [`ScopedFonts::face`])?
|
||||
pub fn has_family(&self, family: &str) -> bool {
|
||||
self.has_exact_family(family) || without_format_suffix(family).is_some_and(|base| self.has_exact_family(base))
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -425,6 +425,18 @@ mod tests {
|
|||
assert_eq!(static_face.style, "Regular", "static fonts ignore axis settings");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_exact_family_name_wins_over_the_name_without_its_format_suffix() {
|
||||
let db = FontDb::with_font_dirs(Vec::new());
|
||||
db.set_system_fallback(false);
|
||||
db.add_font(testing::font_with("DC Test Suffix (OTF)", &['a']).unwrap());
|
||||
assert_eq!(db.face("DC Test Suffix (OTF)", "Regular").family, "DC Test Suffix (OTF)");
|
||||
assert_eq!(db.face(&format!("{DEFAULT_FAMILY} (TT)"), "Regular").family, DEFAULT_FAMILY);
|
||||
assert!(db.has_family(&format!("{DEFAULT_FAMILY} (T1)")));
|
||||
assert!(!db.has_family(&format!("{DEFAULT_FAMILY} (Bold)")));
|
||||
assert!(!db.has_family("DC Test Suffix"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outlines_and_metrics() {
|
||||
let db = FontDb::global();
|
||||
|
|
|
|||
|
|
@ -346,7 +346,8 @@ impl<'a> Ex<'a> {
|
|||
let n = format!("<?AID 001b?>TV {name}");
|
||||
root.push(El::new("TextVariable").attr("Self", format!("dTextVariablen{n}")).attr("Name", &n).attr("VariableType", ty));
|
||||
}
|
||||
for l in &d.layers {
|
||||
// IDML lists layers back to front.
|
||||
for l in d.layers.iter().rev() {
|
||||
let color = match names::layer_color_out(l.color) {
|
||||
Some(n) => p("LayerColor", "enumeration", n),
|
||||
None => {
|
||||
|
|
|
|||
|
|
@ -356,6 +356,8 @@ impl<'r> Importer<'r> {
|
|||
self.layer_ids.insert(s.to_string(), id);
|
||||
}
|
||||
}
|
||||
// IDML lists layers back to front; `Document::layers` is front to back.
|
||||
self.layers.reverse();
|
||||
if self.layers.is_empty() {
|
||||
let id = LayerId(self.alloc());
|
||||
self.layers.push(Layer {
|
||||
|
|
|
|||
|
|
@ -859,3 +859,38 @@ fn arabic_story_and_table_directions_survive_idml_export() {
|
|||
assert_eq!(st.direction, designcraft_doc::TextDirection::RightToLeft);
|
||||
assert_eq!(st.tables.values().next().unwrap().options.direction, designcraft_doc::TextDirection::RightToLeft);
|
||||
}
|
||||
|
||||
/// InDesign's designmap lists layers back to front; `Document::layers[0]` is the front layer.
|
||||
#[test]
|
||||
fn layers_import_and_export_in_stacking_order() {
|
||||
let designmap = r#"<Document xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging" Self="d">
|
||||
<Layer Self="L1" Name="Background"/>
|
||||
<Layer Self="L2" Name="Content"/>
|
||||
<idPkg:Spread src="Spreads/Spread_s.xml"/>
|
||||
</Document>"#;
|
||||
let spread = r#"<idPkg:Spread xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging">
|
||||
<Spread Self="s">
|
||||
<Page Self="p" GeometricBounds="0 0 100 100" ItemTransform="1 0 0 1 0 0"/>
|
||||
<Rectangle Self="r" ItemLayer="L1">
|
||||
<Properties><PathGeometry><GeometryPathType PathOpen="false"><PathPointArray>
|
||||
<PathPointType Anchor="0 0"/><PathPointType Anchor="0 100"/>
|
||||
<PathPointType Anchor="100 100"/><PathPointType Anchor="100 0"/>
|
||||
</PathPointArray></GeometryPathType></PathGeometry></Properties>
|
||||
</Rectangle>
|
||||
</Spread>
|
||||
</idPkg:Spread>"#;
|
||||
let d = import_idml(&zip_files(&[("designmap.xml", designmap), ("Spreads/Spread_s.xml", spread)])).unwrap();
|
||||
let names = |d: &Document| d.layers.iter().map(|l| l.name.clone()).collect::<Vec<_>>();
|
||||
assert_eq!(names(&d), ["Content", "Background"]);
|
||||
assert_eq!(d.spreads[0].items[0].layer, d.layers[1].id, "the rectangle stays on Background");
|
||||
|
||||
let bytes = export_idml(&d);
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(bytes.as_slice())).unwrap();
|
||||
let mut map = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("designmap.xml").unwrap(), &mut map).unwrap();
|
||||
let (bg, content) = (map.find(r#"Name="Background""#).unwrap(), map.find(r#"Name="Content""#).unwrap());
|
||||
assert!(bg < content, "designmap lists the back layer first");
|
||||
let back = import_idml(&bytes).unwrap();
|
||||
assert_eq!(names(&back), ["Content", "Background"]);
|
||||
assert_eq!(back.spreads[0].items[0].layer, back.layers[1].id);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue