CJK-MOJI9: preserve CID preferences and report unsupported glyph classification

This commit is contained in:
Shao Wenbin
2026-10-08 20:24:59 +08:00
parent 1e32767cef
commit 20ce4f5ecd
9 changed files with 124 additions and 5 deletions
+1
View File
@@ -1,5 +1,6 @@
# DesignCraft roadmap
- CJK-MOJI9: retain the CID Mojikumi compatibility preference and diagnose actual Adobe-Japan1 font use; exact CID class mapping remains unsupported.
- CJK-MOJI8: preserve shaped narrow punctuation and manual Tsume instead of restoring nonexistent full-width blank space.
- CJK-MOJI7: reject malformed Mojikumi interchange values and check nested note/cell paragraph rules in preflight.
- CJK-MOJI6: apply spacing at shaped-cluster boundaries and distinguish centered Traditional Chinese punctuation from Simplified Chinese side bearings.
+5
View File
@@ -352,6 +352,10 @@ pub struct DocSettings {
/// Documents saved before the setting existed read as on, the way they were drawn.
#[serde(default = "yes")]
pub glyph_fallback: bool,
/// Preserve IDML's CID-based Mojikumi request. Unicode classification is
/// currently used; preflight reports actual Adobe-Japan1 font use.
#[serde(default)]
pub use_cid_mojikumi: bool,
}
impl Default for DocSettings {
@@ -384,6 +388,7 @@ impl Default for DocSettings {
overprint_black: true,
track_changes: false,
glyph_fallback: false,
use_cid_mojikumi: false,
}
}
}
+44
View File
@@ -110,6 +110,19 @@ fn overset_location(s: &Session, target: designcraft_doc::ItemId) -> Option<(des
None
}
fn rendered_faces(cs: &ComposedStory, faces: &mut Vec<FaceRef>) {
let mut pending: Vec<_> = cs.frames.iter().collect();
while let Some(frame) = pending.pop() {
for g in frame.lines.iter().flat_map(|line| &line.glyphs).filter(|g| g.visible) {
if !faces.contains(&g.face) {
faces.push(g.face);
}
}
pending.extend(frame.notes.iter().flat_map(|n| &n.text.frames));
pending.extend(frame.tables.iter().flat_map(|t| t.cells.iter().flat_map(|c| &c.text.frames)));
}
}
pub fn check(s: &Session, min_ppi: f64) -> Vec<Issue> {
let Some(st) = s.active() else { return vec![] };
let d = &st.doc;
@@ -117,10 +130,15 @@ pub fn check(s: &Session, min_ppi: f64) -> Vec<Issue> {
// Stories: overset and fonts.
let mut missing_fonts: Vec<String> = Vec::new();
let mut unsupported_typography = std::collections::BTreeSet::new();
let mut cid_faces = Vec::new();
let mut uses_mojikumi = false;
let mut missing: Vec<(FaceRef, Vec<char>, designcraft_doc::ItemId)> = Vec::new();
for story in d.stories.values() {
let cs = s.cache.get(d, story.id, None);
missing_glyphs(&cs, None, 0, &mut missing);
if d.settings.use_cid_mojikumi {
rendered_faces(&cs, &mut cid_faces);
}
if cs.is_overset() {
let last = story.frames.last().copied();
let location = last.and_then(|f| overset_location(s, f));
@@ -144,6 +162,7 @@ pub fn check(s: &Session, min_ppi: f64) -> Vec<Issue> {
let ranges = story.para_ranges();
for (pi, pf) in story.paras.iter().enumerate() {
let (para, base) = d.styles.resolve_para(pf);
uses_mojikumi |= !matches!(para.mojikumi.as_str(), "" | "Nothing" | "None");
let Some(range) = ranges.get(pi) else { continue };
let has_rtl = story.text.get(range.clone()).is_some_and(|text| text.chars().any(designcraft_fonts::is_rtl));
if has_rtl {
@@ -207,6 +226,14 @@ pub fn check(s: &Session, min_ppi: f64) -> Vec<Issue> {
for f in missing_fonts {
out.push(Issue { severity: "error", kind: "missingFont", message: format!("Missing font: {f}"), item: None, page: None });
}
if uses_mojikumi {
for face in cid_faces.iter().filter(|face| face.uses_japan1_cids()) {
unsupported_typography.insert(format!(
"CID-based Mojikumi requested for {} {}; Unicode classes are used because CID class mapping is not implemented",
face.family, face.style
));
}
}
for message in unsupported_typography {
out.push(Issue { severity: "warning", kind: "unsupportedTypography", message, item: None, page: None });
}
@@ -428,3 +455,20 @@ mod mojikumi_nested_tests {
assert_eq!(issues.iter().filter(|i| i.kind == "unsupportedTypography" && i.message.contains("Unrecognized")).count(), 1);
}
}
#[cfg(test)]
mod cid_preference_tests {
use super::*;
#[test]
fn cid_request_is_undoable_and_does_not_warn_for_unicode_only_fonts() {
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
let r = s.execute("frame.create", &json!({"rect":[0,0,300,200], "content":"text", "text":"ABC"})).unwrap();
s.execute("text.select", &json!({"story":r["story"],"anchor":0,"focus":3})).unwrap();
s.execute("type.para", &json!({"attrs":{"mojikumi":"SimpChineseDefault"}})).unwrap();
assert_eq!(s.execute("document.preferences", &json!({"useCidMojikumi":true})).unwrap()["useCidMojikumi"], true);
assert!(!check(&s, 150.0).iter().any(|i| i.message.contains("CID-based")));
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(s.execute("document.preferences", &json!({})).unwrap()["useCidMojikumi"], false);
}
}
+2 -1
View File
@@ -21,6 +21,7 @@ const DOC_KEYS: &[&str] = &[
"advancedType",
"overprintBlack",
"glyphFallback",
"useCidMojikumi",
];
pub fn specs() -> Vec<CommandSpec> {
@@ -62,7 +63,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Document Preferences",
[],
None,
"{horizontalUnits?, verticalUnits?: points|picas|inches|millimeters|…, keyboardIncrement? (pt), baselineGrid?: {start, increment, relativeTo, color, viewThreshold}, grid?: {horizontal, vertical, subdivisions, color, inBack}, pasteboard?: [h, v], marginColor?, columnColor?, bleedColor?, slugColor?: [r, g, b], advancedType?: {superscriptSize, superscriptPosition, subscriptSize, subscriptPosition} (%), overprintBlack?, glyphFallback? (draw characters the font lacks from fallback fonts; off: as the font's missing-glyph box)} → those settings",
"{horizontalUnits?, verticalUnits?: points|picas|inches|millimeters|…, keyboardIncrement? (pt), baselineGrid?: {start, increment, relativeTo, color, viewThreshold}, grid?: {horizontal, vertical, subdivisions, color, inBack}, pasteboard?: [h, v], marginColor?, columnColor?, bleedColor?, slugColor?: [r, g, b], advancedType?: {superscriptSize, superscriptPosition, subscriptSize, subscriptPosition} (%), overprintBlack?, useCidMojikumi? (preserve CID classification request; unsupported font collections are diagnosed), glyphFallback? (draw characters the font lacks from fallback fonts; off: as the font's missing-glyph box)} → those settings",
has_doc,
|s, p| {
let cur = serde_json::to_value(&s.doc()?.doc.settings).map_err(|e| bad("document.preferences", e.to_string()))?;
+35
View File
@@ -181,6 +181,13 @@ impl std::fmt::Debug for FontFace {
}
impl FontFace {
/// Adobe-Japan1 CFF fonts are the collection for CID-based Mojikumi.
/// Other CID collections still use Unicode classes under Adobe's policy.
pub fn uses_japan1_cids(&self) -> bool {
use skrifa::raw::TableProvider;
self.skrifa().and_then(|f| f.cff().ok()).is_some_and(cff_uses_japan1_cids)
}
pub fn data(&self) -> &[u8] {
match &self.bytes {
FontBytes::Static(b) => b,
@@ -1567,3 +1574,31 @@ mod tests_docfonts;
#[cfg(test)]
#[path = "tests_vertical.rs"]
mod tests_vertical;
fn cff_uses_japan1_cids(cff: skrifa::raw::tables::cff::Cff<'_>) -> bool {
use skrifa::raw::ps::cff::dict;
let Some(top) = cff.top_dicts().get(0) else { return false };
dict::entries(top, None).any(|entry| match entry {
Ok(dict::Entry::Ros { registry, ordering, .. }) => cff.string(registry) == Some(b"Adobe") && cff.string(ordering) == Some(b"Japan1"),
_ => false,
})
}
#[cfg(test)]
mod mojikumi_cid_tests {
use super::*;
use skrifa::raw::{FontData, FontRead, tables::cff::Cff};
#[test]
fn recognizes_the_collection_in_cff_metadata_without_reading_outlines() {
// Minimal synthetic CFF indices: one name, a ROS dict, two strings,
// and one return-only subroutine. SID 391/392 reference the two strings.
let mut data = vec![1, 0, 4, 4, 0, 1, 1, 1, 2, b'T', 0, 1, 1, 1, 8, 248, 27, 248, 28, 139, 12, 30, 0, 2, 1, 1, 6, 12];
data.extend_from_slice(b"AdobeJapan1");
data.extend_from_slice(&[0, 1, 1, 1, 2, 11]);
assert!(cff_uses_japan1_cids(Cff::read(FontData::new(&data)).unwrap()));
let end = data.len() - 6;
data[end - 6..end].copy_from_slice(b"CNS1-X");
assert!(!cff_uses_japan1_cids(Cff::read(FontData::new(&data)).unwrap()));
assert!(!FontDb::global().face(crate::DEFAULT_FAMILY, "Regular").uses_japan1_cids());
}
}
+1
View File
@@ -799,6 +799,7 @@ impl<'a> Ex<'a> {
let a = &s.advanced_type;
root.push(
El::new("TextPreference")
.attr("UseCidMojikumi", s.use_cid_mojikumi)
.attr("SuperscriptSize", num(a.superscript_size))
.attr("SuperscriptPosition", num(a.superscript_position))
.attr("SubscriptSize", num(a.subscript_size))
+3
View File
@@ -273,6 +273,9 @@ impl<'r> Importer<'r> {
}
}
"TextPreference" => {
if let Some(v) = e.boolean("UseCidMojikumi") {
self.settings.use_cid_mojikumi = v;
}
let a = &mut self.settings.advanced_type;
for (k, v) in [
("SuperscriptSize", &mut a.superscript_size),
+8
View File
@@ -968,3 +968,11 @@ fn malformed_mojikumi_rows_are_not_silently_coerced() {
assert!(err.contains("Mojikumi"), "{key}={value}: {err}");
}
}
#[test]
fn cid_mojikumi_preference_survives_interchange() {
let mut d = import_idml(&fixture_with_story(STORY)).unwrap();
d.settings.use_cid_mojikumi = true;
let restored = import_idml(&export_idml(&d)).unwrap();
assert!(restored.settings.use_cid_mojikumi);
}
+25 -4
View File
@@ -26,8 +26,29 @@ Regression tests cover preset boundary differences, custom override precedence,
IDML reference round trips, and unsupported names. The implementation must not be
represented as a byte-for-byte reproduction of Adobe's full preset matrices.
## Audit work
## Composition and validation
Further audit areas are regional punctuation metrics, shaped cluster boundaries,
equal-priority allocation, discrete line fitting, input validation, and diagnostics
for compatibility modes whose glyph classification cannot yet be reproduced.
Spacing is attached to complete shaped clusters. Regional punctuation side
bearings, shaped narrow forms, tracking, manual Tsume and explicit character aki
are handled before line fitting. Continuous gaps of equal priority share one
allocation pool; non-floating gaps reach discrete endpoints. Leading-only
compression is included for ragged lines. Custom rows retain their dimensions.
Import rejects malformed integers, non-finite amounts, inverted ranges and invalid
priorities. Native document validation uses the same range policy. Unknown integer
classes remain preservable and receive diagnostics, including in nested notes and
cells. The bounded supported spacing range is -1 to 100 em.
## Compatibility limits
[Adobe's TextPreference API](https://developer.adobe.com/indesign/uxp/omv/t/TextPreference/)
defines CID-based classification. The IDML `UseCidMojikumi` preference now survives
round trips and is accessible through `document.preferences`. The engine currently
uses Unicode classes. Preflight reports the incompatibility when the document
requests CID mode and actually renders Adobe-Japan1 CFF fonts with Mojikumi in use;
it does not flag ordinary Unicode fonts merely because the preference is enabled.
Exact Adobe CID-to-Mojikumi class mapping, complete proprietary preset pair matrices,
and legacy pre-CS2 vertical-scaling compatibility have not been reproduced or
certified. Public CMap resources map character codes to CIDs; they are not a
CID-to-Mojikumi-class oracle. No claim of complete Adobe output parity is made.