M6: anchored objects in IDML (inline / above line), verified in InDesign 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5a9fa2d16a
commit
252ae0df32
|
|
@ -21,7 +21,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
|||
- Cross-references and text anchors: Insert Cross-Reference (paragraph by style, or text anchor), 11 formats with InDesign's building blocks (full/partial paragraph, paragraph text/number, page number, anchor name, chapter, file name) plus user formats; resolved live at composition — never out of date, no Update step; unresolved destinations show `??`; PDF link annotations to the destination page; IDML export/import as InDesign cross-reference sources, hyperlinks, text destinations and formats (verified in InDesign 2026); `xref.*` / `anchor.*` commands.
|
||||
- Index: page references (topic from the selection or up to 4 levels, sort keys; current page, to end of story, next n paragraphs, suppressed, See / See also), Generate / Update Index (section headings, nested or run-in, page ranges merged, Index Title / Section Head / Level 1–4 styles); IDML export/import as InDesign topics, page references and topic cross-references (verified in InDesign 2026); `index.*` commands.
|
||||
- Text clipboard keeps formatting (character/paragraph formats, footnotes, cross-references, index markers, tables) and falls back to plain text when the system clipboard changed; Paste without Formatting (⇧⌘V); Change Case (UPPERCASE, lowercase, Title Case, Sentence case — formatting kept); Type › Insert Special Character / White Space / Break Character submenus.
|
||||
- Anchored objects: items flow in the text inline (on the baseline, with Y offset; lines grow to fit) or above the line (left/center/right, space before/after); paste copied items into text to anchor them; Anchored Object Options and Release (back onto the page where shown); rendered and exported to PDF; copy/paste and threading carry them; `anchored.*` commands.
|
||||
- Anchored objects: items flow in the text inline (on the baseline, with Y offset; lines grow to fit) or above the line (left/center/right, space before/after); paste copied items into text to anchor them; Anchored Object Options and Release (back onto the page where shown); rendered, exported to PDF and to/from IDML (InDesign anchored object settings; verified in InDesign 2026); copy/paste and threading carry them; `anchored.*` commands.
|
||||
- Menus expose the engine's features (TOC, numbering & sections, text variables, hyperlinks, cross-references, footnotes, step and repeat, spelling, fitting/content, effects, Data Merge, Preflight, EPUB); any menu command with parameters gets a dialog generated from its parameter documentation.
|
||||
- Table of contents (generate/update, dot leaders) and text variables (running headers, last page number, chapter number, file name, dates, custom) resolved per page.
|
||||
- Soft effects: blurred drop shadow, inner shadow, outer glow, basic feather.
|
||||
|
|
|
|||
|
|
@ -1476,6 +1476,34 @@ impl<'a> Ex<'a> {
|
|||
.attr("DestinationUniqueKey", a.id & 0xFFFF_FFFF),
|
||||
));
|
||||
}
|
||||
} else if ch == designcraft_doc::OBJECT_MARK {
|
||||
// An anchored object: the item inside the range with its anchoring settings.
|
||||
flush_text(&mut cur, &mut pending);
|
||||
flush_content(&mut pending, &mut out);
|
||||
let k = s.text[..seg_start + ci].matches(designcraft_doc::OBJECT_MARK).count();
|
||||
if let Some(o) = s.objects.get(k) {
|
||||
let mut e = self.item_el(&o.item, o.item.xf);
|
||||
use designcraft_doc::anchored::AnchorAlign as A;
|
||||
let setting = match &o.position {
|
||||
designcraft_doc::AnchorPosition::Inline { y_offset } => {
|
||||
El::new("AnchoredObjectSetting").attr("AnchoredPosition", "InlinePosition").attr("AnchorYoffset", num(*y_offset))
|
||||
}
|
||||
designcraft_doc::AnchorPosition::AboveLine { align, space_before, space_after } => El::new("AnchoredObjectSetting")
|
||||
.attr("AnchoredPosition", "AboveLine")
|
||||
.attr(
|
||||
"HorizontalAlignment",
|
||||
match align {
|
||||
A::Left => "LeftAlign",
|
||||
A::Center => "CenterAlign",
|
||||
A::Right => "RightAlign",
|
||||
},
|
||||
)
|
||||
.attr("AnchorSpaceAbove", num(*space_before))
|
||||
.attr("AnchorYoffset", num(*space_after)),
|
||||
};
|
||||
e.push(setting);
|
||||
out.push(Node::El(e));
|
||||
}
|
||||
} else if ch == designcraft_doc::INDEX_MARK {
|
||||
flush_text(&mut cur, &mut pending);
|
||||
flush_content(&mut pending, &mut out);
|
||||
|
|
|
|||
|
|
@ -987,9 +987,10 @@ impl<'r> Importer<'r> {
|
|||
anchors: Vec::new(),
|
||||
xrefs: Vec::new(),
|
||||
index_refs: Vec::new(),
|
||||
objects: Vec::new(),
|
||||
};
|
||||
self.walk_story(e, &mut b, &ParaFormat::default(), &CharAttrs::default(), &CharFormat::default(), None);
|
||||
let StoryBuilder { text, mut paras, runs, last, tables: tbls, notes, anchors, xrefs, index_refs, .. } = b;
|
||||
let StoryBuilder { text, mut paras, runs, last, tables: tbls, notes, anchors, xrefs, index_refs, objects, .. } = b;
|
||||
let mut chars: Vec<CharRun> = runs.into_iter().filter(|r| r.len > 0).collect();
|
||||
if chars.is_empty() {
|
||||
chars.push(CharRun { len: 0, format: last });
|
||||
|
|
@ -1026,7 +1027,7 @@ impl<'r> Importer<'r> {
|
|||
anchors: anchor_list,
|
||||
xrefs: xref_list,
|
||||
index_refs: index_refs.into_iter().map(Arc::new).collect(),
|
||||
objects: vec![],
|
||||
objects: objects.into_iter().map(Arc::new).collect(),
|
||||
};
|
||||
st.fix_notes();
|
||||
st.fix_marks();
|
||||
|
|
@ -1250,17 +1251,30 @@ impl<'r> Importer<'r> {
|
|||
b.push(&designcraft_doc::FOOTNOTE_REF.to_string(), &rcf);
|
||||
}
|
||||
// Not supported yet: skip their content entirely.
|
||||
"Properties"
|
||||
| "Note"
|
||||
| "Rectangle"
|
||||
| "Oval"
|
||||
| "Polygon"
|
||||
| "GraphicLine"
|
||||
| "TextFrame"
|
||||
| "Group"
|
||||
| "StoryPreference"
|
||||
| "InCopyExportOption"
|
||||
| "TextVariableInstance" => {}
|
||||
"Rectangle" | "Oval" | "Polygon" | "GraphicLine" | "Group" => {
|
||||
// An anchored object (text frames inside stories aren't supported yet).
|
||||
let has_text = c.find_all("TextFrame").next().is_some();
|
||||
if !has_text && let Some(item) = self.item(c, designcraft_geom::Affine::IDENTITY) {
|
||||
use designcraft_doc::anchored::AnchorAlign as A;
|
||||
let set = c.find("AnchoredObjectSetting");
|
||||
let get = |k: &str| set.and_then(|s| s.num(k)).unwrap_or(0.0);
|
||||
let position = match set.and_then(|s| s.get("AnchoredPosition")) {
|
||||
Some("AboveLine") => designcraft_doc::AnchorPosition::AboveLine {
|
||||
align: match set.and_then(|s| s.get("HorizontalAlignment")) {
|
||||
Some("CenterAlign") => A::Center,
|
||||
Some("RightAlign") => A::Right,
|
||||
_ => A::Left,
|
||||
},
|
||||
space_before: get("AnchorSpaceAbove"),
|
||||
space_after: get("AnchorYoffset"),
|
||||
},
|
||||
_ => designcraft_doc::AnchorPosition::Inline { y_offset: get("AnchorYoffset") },
|
||||
};
|
||||
b.objects.push(designcraft_doc::AnchoredObject::new(item, position));
|
||||
b.push(&designcraft_doc::OBJECT_MARK.to_string(), cf);
|
||||
}
|
||||
}
|
||||
"Properties" | "Note" | "TextFrame" | "StoryPreference" | "InCopyExportOption" | "TextVariableInstance" => {}
|
||||
_ => self.walk_story(c, b, pf, pchars, cf, brk),
|
||||
},
|
||||
Node::Pi(t, v) if t == "ACE" => {
|
||||
|
|
@ -1751,6 +1765,7 @@ struct StoryBuilder {
|
|||
anchors: Vec<(String, String)>,
|
||||
index_refs: Vec<designcraft_doc::IndexRef>,
|
||||
xrefs: Vec<(String, String)>,
|
||||
objects: Vec<designcraft_doc::AnchoredObject>,
|
||||
}
|
||||
|
||||
impl StoryBuilder {
|
||||
|
|
|
|||
|
|
@ -490,3 +490,34 @@ fn round_trips_index_references() {
|
|||
assert_eq!(s.text.replace(designcraft_doc::INDEX_MARK, ""), "Kerning adjusts pairs.\nMore.");
|
||||
assert!(s.text.starts_with(&format!("{0}{0}Ke{0}rning", designcraft_doc::INDEX_MARK)), "{:?}", s.text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trips_anchored_objects() {
|
||||
use designcraft_doc::{AnchorPosition, AnchoredObject, anchored::AnchorAlign};
|
||||
let mut d = Document::new(&NewDocument::default());
|
||||
let lid = d.default_layer();
|
||||
let (_, sid) =
|
||||
d.add_text_frame(SpreadRef::Doc(0), Rect::new(40.0, 40.0, 400.0, 400.0), lid, "Inline here.\nAbove.", ParaFormat::default()).unwrap();
|
||||
let mut a =
|
||||
designcraft_doc::Item::new(designcraft_doc::ItemId(d.alloc()), lid, Shape::Rectangle, shapes::rectangle(Rect::new(0.0, 0.0, 20.0, 16.0)));
|
||||
a.fill = Fill::swatch(designcraft_color::swatch::BLACK);
|
||||
let b = designcraft_doc::Item::new(designcraft_doc::ItemId(d.alloc()), lid, Shape::Oval, shapes::ellipse(Rect::new(0.0, 0.0, 60.0, 30.0)));
|
||||
{
|
||||
let s = d.story_mut(sid).unwrap();
|
||||
s.insert_object(7, AnchoredObject::new(a, AnchorPosition::Inline { y_offset: 2.0 }));
|
||||
let at = s.text.find("Above").unwrap();
|
||||
s.insert_object(at, AnchoredObject::new(b, AnchorPosition::AboveLine { align: AnchorAlign::Center, space_before: 3.0, space_after: 4.0 }));
|
||||
}
|
||||
d.check().unwrap();
|
||||
let back = import_idml(&export_idml(&d)).unwrap();
|
||||
back.check().unwrap();
|
||||
let s = back.stories.values().find(|s| !s.objects.is_empty()).unwrap();
|
||||
assert_eq!(s.text, d.story(sid).unwrap().text);
|
||||
assert_eq!(s.objects.len(), 2);
|
||||
let (w, h) = s.objects[0].size();
|
||||
assert!((w - 20.0).abs() < 1e-6 && (h - 16.0).abs() < 1e-6);
|
||||
assert_eq!(s.objects[0].position, AnchorPosition::Inline { y_offset: 2.0 });
|
||||
assert_eq!(s.objects[0].item.fill.swatch, designcraft_color::swatch::BLACK);
|
||||
assert_eq!(s.objects[1].item.shape, Shape::Oval);
|
||||
assert_eq!(s.objects[1].position, AnchorPosition::AboveLine { align: AnchorAlign::Center, space_before: 3.0, space_after: 4.0 });
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue