diff --git a/ROADMAP.md b/ROADMAP.md index 6a412a42f..14a4c9351 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -143,6 +143,7 @@ Newest first. - **2026-10-15** · Find Font no longer reports a style missing when it differs from the installed one only in spacing or case (InDesign files can name `Regular ` with a trailing space); the text was already drawn in the right face. No readiness percentage change. - **2026-10-15** · A line of text is set when its baseline fits in the frame, as in InDesign: descenders may hang below the frame's bottom. A frame sized to the cap height of a big heading (103 pt in a 2.77 cm frame, as Zero2Graphic's magazine tutorial draws them) no longer oversets. No readiness percentage change. - **2026-10-15** · A story that ends with a return is no longer overset when only its empty last paragraph doesn't fit, as in InDesign (Preflight said "Overset text: 0 characters" for a caption InDesign shows with no errors). No readiness percentage change. +- **2026-10-15** · IDML: the CJK leading model InDesign writes on [No paragraph style] of every document (Aki Below) only applies when the document uses a Japanese composer, as in InDesign; a Western document's paragraphs after a size change no longer sit lower than in InDesign (Montilla Design's Flex Layout file: its captions came out overset). No readiness percentage change. - **2026-10-13** · Object › Fitting has InDesign's shortcuts: Fill Frame Proportionally ⌘⌥⇧C, Fit Content Proportionally ⌘⌥⇧E, Fit Frame to Content ⌘⌥C, Fit Content to Frame ⌘⌥E, Center Content ⌘⇧E. No readiness percentage change. - **2026-10-13** · Gradients between process colours blend as inks, as InDesign shows them: magenta to cyan passes through C 50 M 50 (a light lilac) on screen instead of the darker RGB mix, and a midpoint between CMYK or grey stops no longer turns the PDF shading RGB. No readiness percentage change. - **2026-10-14** · The Swatches panel has InDesign's header: a Fill/Stroke proxy, Formatting Affects Container/Text and the Tint of the colour in front (a tint alone keeps each object's own swatch). Clicking a swatch applies it to the fill or stroke in front (of the text, when formatting affects text), and the bottom row has New Color Group, New Swatch and Delete Swatch. No readiness percentage change. diff --git a/crates/idml/src/import.rs b/crates/idml/src/import.rs index 23afead97..36d8ed355 100644 --- a/crates/idml/src/import.rs +++ b/crates/idml/src/import.rs @@ -66,6 +66,8 @@ pub fn import_idml_report(bytes: &[u8], read_link: &dyn Fn(&str) -> Option = Vec::new(); let mut warnings = Vec::new(); + // Only the parts the import reads count (unreferenced entries are never decompressed). + let mut japanese_composer = names_japanese_composer(&dm); for c in root.elements() { if c.name.starts_with("idPkg:") { let Some(src) = c.get("src") else { continue }; @@ -73,6 +75,7 @@ pub fn import_idml_report(bytes: &[u8], read_link: &dyn Fn(&str) -> Option Option Package<'a> { } } +/// Does this package part apply one of InDesign's Japanese composers (`HL Composer J`, +/// `HL Single J`) anywhere? Only they use the CJK leading model. +fn names_japanese_composer(data: &[u8]) -> bool { + const KEY: &[u8] = b"Composer=\""; + let mut rest = data; + while let Some(at) = rest.windows(KEY.len()).position(|w| w == KEY) { + let value = rest.get(at + KEY.len()..).unwrap_or_default(); + let end = value.iter().position(|b| *b == b'"').unwrap_or(value.len()); + let name = String::from_utf8_lossy(value.get(..end).unwrap_or_default()); + if name.ends_with(" J") || name.contains("Japanese") { + return true; + } + rest = value.get(end..).unwrap_or_default(); + } + false +} + /// A package part name in the form the lookup compares: InDesign and other IDML writers differ /// in percent-encoding, case, path separators and a leading `./` or `/`. fn normalize_part_name(name: &str) -> String { @@ -205,6 +226,8 @@ struct Importer<'r> { read_link: &'r dyn Fn(&str) -> Option>, /// View Preferences › Ruler Origin: where a guide's Location is measured from. ruler_origin: RulerOrigin, + /// The package applies a Japanese composer somewhere (see [`names_japanese_composer`]). + japanese_composer: bool, next_id: u64, settings: DocSettings, default_margins: Option<(Margins, Columns)>, @@ -356,6 +379,7 @@ impl<'r> Importer<'r> { Importer { read_link, ruler_origin: RulerOrigin::Spread, + japanese_composer: false, next_id: 0, settings: DocSettings::default(), default_margins: None, @@ -989,7 +1013,13 @@ impl<'r> Importer<'r> { let based_on = if name == designcraft_doc::NO_PARA_STYLE { None } else { self.based_on(e, true) }; let next_style = e.get("NextStyle").map(|n| self.para_style_ref(n)).filter(|n| *n != name); let para = self.para_attrs(e); - let chars = self.char_attrs(e); + let mut chars = self.char_attrs(e); + // InDesign writes its CJK leading model (Aki Below) on [No paragraph style] of every + // document; only the Japanese composers use it, so a document set with the Paragraph or + // Single-line Composer spaces its lines baseline to baseline. + if name == designcraft_doc::NO_PARA_STYLE && !self.japanese_composer { + chars.leading_model = None; + } let s = ParagraphStyle { name: name.clone(), based_on, next_style, para, chars, shortcut: String::new() }; match self.styles.para_mut(&name) { Some(slot) => *slot = s, diff --git a/crates/idml/src/tests.rs b/crates/idml/src/tests.rs index 16a0f465f..ba0bad4eb 100644 --- a/crates/idml/src/tests.rs +++ b/crates/idml/src/tests.rs @@ -2355,3 +2355,35 @@ fn too_many_entries_are_refused() { let Err(err) = import_idml(&w.finish().unwrap().into_inner()) else { panic!("a package with 100 001 entries imported") }; assert!(err.to_string().contains("entries"), "{err}"); } + +/// InDesign writes `LeadingModel="LeadingModelAkiBelow"` on [No paragraph style] of every +/// document, but only its Japanese composers use the leading model: with the Paragraph Composer, +/// a 24 pt paragraph after a 32 pt one sits a Roman 28.8 pt below it (Montilla Design's Flex Layout +/// file: frames fitted to that came out overset). +#[test] +fn the_default_cjk_leading_model_applies_only_with_a_japanese_composer() { + let styles = |composer: &str| { + STYLES.replace( + r#"Name="$ID/[No paragraph style]" PointSize="12">"#, + &format!(r#"Name="$ID/[No paragraph style]" PointSize="12" LeadingModel="LeadingModelAkiBelow" Composer="{composer}">"#), + ) + }; + let model = |composer: &str| { + let s = styles(composer); + let pkg = zip_files(&[ + ("designmap.xml", DESIGNMAP), + ("Resources/Graphic.xml", GRAPHIC), + ("Resources/Styles.xml", &s), + ("Resources/Preferences.xml", PREFS), + ("MasterSpreads/MasterSpread_m1.xml", MASTER), + ("Spreads/Spread_sp1.xml", SPREAD), + ("Stories/Story_s1.xml", STORY), + ]); + let d = import_idml(&pkg).unwrap(); + d.styles.para(designcraft_doc::NO_PARA_STYLE).unwrap().chars.leading_model + }; + assert_eq!(model("HL Composer"), None, "the Paragraph Composer leads baseline to baseline"); + assert_eq!(model("HL Single"), None); + assert_eq!(model("$ID/HL Composer J"), Some(designcraft_doc::cjk::LeadingModel::AkiBelow)); + assert_eq!(model("$ID/HL Single J"), Some(designcraft_doc::cjk::LeadingModel::AkiBelow)); +}