mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 18:26:50 -06:00
IDML: import and export ruler guides
Guides in an IDML page (<Guide> children of <Page>) were dropped on import, and DesignCraft's own guides weren't written. Montilla Design's Flex Layout file keeps its 16 column guides on the B-Parent page; none came in. Import: Location is measured from View Preferences › Ruler Origin (the spread's top left, the page's, or the spine); Orientation, FitToPage (off: a spread guide), Locked and ItemLayer are kept. Export writes each page's guides under its <Page> in spread-origin coordinates, which is the ruler origin DesignCraft writes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d07cbbe94c
commit
099fbd23ee
@@ -132,6 +132,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
|
||||
|
||||
Newest first.
|
||||
|
||||
- **2026-10-15** · IDML: ruler guides import and export (they were dropped). Each page's guides come in measured from the document's ruler origin (spread, page or spine), with Fit to Page, Locked and their layer, parent pages' included, and go out again under their pages. 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.
|
||||
|
||||
@@ -1667,6 +1667,26 @@ impl<'a> Ex<'a> {
|
||||
.attr("GeometricBounds", format!("0 0 {} {}", num(pg.height), num(pg.width)))
|
||||
.attr("ItemTransform", format!("1 0 0 1 {} {}", num(pg.x - ox), num(-oy)))
|
||||
.child(margin_el(pg));
|
||||
let mut pe = pe;
|
||||
// Ruler guides, measured from the spread's top left (RulerOrigin is SpreadOrigin).
|
||||
let left = sp.pages.iter().map(|p| p.x).fold(f64::INFINITY, f64::min);
|
||||
let left = if left.is_finite() { left } else { 0.0 };
|
||||
for g in &pg.guides {
|
||||
let vertical = g.orientation == designcraft_doc::Orientation::Vertical;
|
||||
let mut ge = El::new("Guide")
|
||||
.attr("Self", format!("{}g{}", uid(pg.id.0), pe.elements().count()))
|
||||
.attr("Orientation", if vertical { "Vertical" } else { "Horizontal" })
|
||||
.attr("Location", num(if vertical { g.position - left } else { g.position }))
|
||||
.attr("FitToPage", bool_s(!g.spread))
|
||||
.attr("ViewThreshold", "5")
|
||||
.attr("Locked", bool_s(g.locked))
|
||||
.attr("PageIndex", (first_page + i).to_string())
|
||||
.attr("GuideType", "Ruler");
|
||||
if let Some(l) = g.layer {
|
||||
ge.set("ItemLayer", uid(l.0));
|
||||
}
|
||||
pe.push(ge);
|
||||
}
|
||||
el.push(pe);
|
||||
}
|
||||
let origin = Affine::translate((-ox, -oy));
|
||||
|
||||
@@ -9,8 +9,8 @@ use designcraft_color::{Color, ColorType, Gradient, GradientKind, GradientStop,
|
||||
use designcraft_doc::{
|
||||
Asset, AssetId, BaselineGrid, CellRange, CharAttrs, CharFormat, CharRun, CharacterStyle, Columns, ColumnsKind, Content, DocSettings, Document,
|
||||
DropShadow, Effects, Fill, GridAlign, GridRelative, Guide, Item, ItemId, Kerning, LAYER_COLORS, Layer, LayerId, ListType, Margins, ObjectStyle,
|
||||
Page, PageId, PageSide, ParaAttrs, ParaFormat, ParagraphStyle, ParentInfo, RowHeightMode, Rule, Section, Shape, SpanColumns, Spread, SpreadId,
|
||||
Story, StoryId, Stroke, Styles, TabStop, Table, TextFrame, TextFrameOptions, TextWrap, story as st,
|
||||
Orientation, Page, PageId, PageSide, ParaAttrs, ParaFormat, ParagraphStyle, ParentInfo, RowHeightMode, Rule, Section, Shape, SpanColumns, Spread,
|
||||
SpreadId, Story, StoryId, Stroke, Styles, TabStop, Table, TextFrame, TextFrameOptions, TextWrap, story as st,
|
||||
};
|
||||
use designcraft_geom::corners::{Corner, CornerOptions};
|
||||
use designcraft_geom::{Affine, Anchor, PathData, Point, Rect, SubPath};
|
||||
@@ -116,6 +116,14 @@ pub fn import_idml_report(bytes: &[u8], read_link: &dyn Fn(&str) -> Option<Vec<u
|
||||
Ok(Imported { document, warnings })
|
||||
}
|
||||
|
||||
/// View Preferences › Ruler Origin.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum RulerOrigin {
|
||||
Spread,
|
||||
Page,
|
||||
Spine,
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -159,6 +167,8 @@ struct ItemCtx {
|
||||
|
||||
struct Importer<'r> {
|
||||
read_link: &'r dyn Fn(&str) -> Option<Vec<u8>>,
|
||||
/// View Preferences › Ruler Origin: where a guide's Location is measured from.
|
||||
ruler_origin: RulerOrigin,
|
||||
next_id: u64,
|
||||
settings: DocSettings,
|
||||
default_margins: Option<(Margins, Columns)>,
|
||||
@@ -275,6 +285,7 @@ impl<'r> Importer<'r> {
|
||||
styles.paragraph.iter_mut().for_each(|s| s.based_on = None);
|
||||
Importer {
|
||||
read_link,
|
||||
ruler_origin: RulerOrigin::Spread,
|
||||
next_id: 0,
|
||||
settings: DocSettings::default(),
|
||||
default_margins: None,
|
||||
@@ -362,6 +373,11 @@ impl<'r> Importer<'r> {
|
||||
if let Some(v) = e.num("CursorKeyIncrement") {
|
||||
self.settings.keyboard_increment = v;
|
||||
}
|
||||
self.ruler_origin = match e.get("RulerOrigin") {
|
||||
Some("PageOrigin") => RulerOrigin::Page,
|
||||
Some("SpineOrigin") => RulerOrigin::Spine,
|
||||
_ => RulerOrigin::Spread,
|
||||
};
|
||||
}
|
||||
"GridPreference" => self.grid_prefs(e),
|
||||
"PasteboardPreference" => {
|
||||
@@ -1964,6 +1980,33 @@ impl<'r> Importer<'r> {
|
||||
|
||||
// ---------- spreads and items ----------
|
||||
|
||||
/// Ruler guides of a `<Page>` (its `<Guide>` children), `page` its rectangle in our spread
|
||||
/// coordinates. A guide's Location is measured from the ruler origin: the spread's top left, the
|
||||
/// page's, or the spine. Fit to Page off: the guide crosses the spread.
|
||||
fn guides_of(&self, p: &El, page: Rect, spine: f64) -> Vec<Guide> {
|
||||
p.find_all("Guide")
|
||||
.filter_map(|g| {
|
||||
let at = g.num("Location").filter(|v| v.is_finite() && v.abs() < 1e7)?;
|
||||
let orientation = if g.get("Orientation") == Some("Horizontal") { Orientation::Horizontal } else { Orientation::Vertical };
|
||||
let position = match (orientation, self.ruler_origin) {
|
||||
(Orientation::Horizontal, RulerOrigin::Page) => page.y0 + at,
|
||||
(Orientation::Horizontal, _) => at,
|
||||
(Orientation::Vertical, RulerOrigin::Page) => page.x0 + at,
|
||||
(Orientation::Vertical, RulerOrigin::Spine) => spine + at,
|
||||
(Orientation::Vertical, RulerOrigin::Spread) => at,
|
||||
};
|
||||
Some(Guide {
|
||||
orientation,
|
||||
position,
|
||||
spread: g.get("FitToPage") == Some("false"),
|
||||
locked: g.get("Locked") == Some("true"),
|
||||
layer: g.get("ItemLayer").and_then(|l| self.layer_ids.get(l)).copied(),
|
||||
liquid: false,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn spread(&mut self, e: &El, id: SpreadId, master: bool) -> Spread {
|
||||
let facing = self.settings.facing_pages;
|
||||
// Page rectangles in IDML spread coordinates.
|
||||
@@ -1981,6 +2024,9 @@ impl<'r> Importer<'r> {
|
||||
let min_y = pages.iter().map(|(r, _)| r.y0).fold(f64::INFINITY, f64::min);
|
||||
let (min_x, min_y) = if min_x.is_finite() { (min_x, min_y) } else { (0.0, 0.0) };
|
||||
let shift = Affine::translate((-min_x, -min_y));
|
||||
// The spine: between a left and a right page (else the spread's left edge).
|
||||
let spine = pages.iter().map(|(r, _)| r.x0 - min_x).filter(|x| *x > 1e-6).fold(f64::INFINITY, f64::min);
|
||||
let spine = if spine.is_finite() && facing { spine } else { 0.0 };
|
||||
let show = e.get("ShowMasterItems") != Some("false");
|
||||
let first_abs: usize = self.spreads.iter().map(|s| s.pages.len()).sum();
|
||||
let mut out_pages = Vec::new();
|
||||
@@ -2013,7 +2059,7 @@ impl<'r> Importer<'r> {
|
||||
parent,
|
||||
side,
|
||||
overridden: vec![],
|
||||
guides: Vec::<Guide>::new(),
|
||||
guides: self.guides_of(p, Rect::new(r.x0 - min_x, r.y0 - min_y, r.x1 - min_x, r.y1 - min_y), spine),
|
||||
show_parent_items: show,
|
||||
liquid: Default::default(),
|
||||
transition: None,
|
||||
|
||||
@@ -2169,3 +2169,54 @@ fn frame_preference_property_color_resolves_for_frames_and_object_styles() {
|
||||
assert_eq!(colors, vec!["Near", "Near", "Far", designcraft_color::swatch::NONE]);
|
||||
assert_eq!(d.styles.object_style("Child").unwrap().text_frame.as_ref().unwrap().column_rule_color, "Near");
|
||||
}
|
||||
|
||||
/// A two-page spread with ruler guides on its pages (and one on the parent), in `origin` ruler
|
||||
/// coordinates.
|
||||
fn guides_package(origin: &str) -> Vec<u8> {
|
||||
let prefs = PREFS.replace("</idPkg:Preferences>", &format!(r#"<ViewPreference RulerOrigin="{origin}"/></idPkg:Preferences>"#));
|
||||
let spread = SPREAD.replace(
|
||||
r#"<Page Self="p2" AppliedMaster="m1" GeometricBounds="0 0 700 500" ItemTransform="1 0 0 1 0 -350"/>"#,
|
||||
r#"<Page Self="p2" AppliedMaster="m1" GeometricBounds="0 0 700 500" ItemTransform="1 0 0 1 0 -350">
|
||||
<Guide Self="g1" Orientation="Vertical" Location="40" FitToPage="true" ItemLayer="L1" Locked="true"/>
|
||||
<Guide Self="g2" Orientation="Horizontal" Location="100" FitToPage="false"/>
|
||||
</Page>"#,
|
||||
);
|
||||
let master = MASTER.replace(
|
||||
r#"<Page Self="mp1" GeometricBounds="0 0 700 500" ItemTransform="1 0 0 1 -500 -350"/>"#,
|
||||
r#"<Page Self="mp1" GeometricBounds="0 0 700 500" ItemTransform="1 0 0 1 -500 -350"><Guide Self="mg" Orientation="Vertical" Location="84"/></Page>"#,
|
||||
);
|
||||
zip_files(&[
|
||||
("designmap.xml", DESIGNMAP),
|
||||
("Resources/Graphic.xml", GRAPHIC),
|
||||
("Resources/Styles.xml", STYLES),
|
||||
("Resources/Preferences.xml", &prefs),
|
||||
("MasterSpreads/MasterSpread_m1.xml", &master),
|
||||
("Spreads/Spread_sp1.xml", &spread),
|
||||
("Stories/Story_s1.xml", STORY),
|
||||
])
|
||||
}
|
||||
|
||||
/// Ruler guides come in from IDML (they were dropped): on their page, measured from the ruler
|
||||
/// origin, with Fit to Page, Locked and their layer; parent pages' too. They go out again.
|
||||
#[test]
|
||||
fn ruler_guides_import_and_round_trip() {
|
||||
use designcraft_doc::Orientation::{Horizontal, Vertical};
|
||||
let guides = |d: &Document| {
|
||||
let p = &d.spreads[0].pages[1];
|
||||
p.guides.iter().map(|g| (g.orientation, g.position, g.spread, g.locked, g.layer.is_some())).collect::<Vec<_>>()
|
||||
};
|
||||
// The second page starts at x 500 in our spread coordinates.
|
||||
let d = import_idml(&guides_package("PageOrigin")).unwrap();
|
||||
assert_eq!(guides(&d), [(Vertical, 540.0, false, true, true), (Horizontal, 100.0, true, false, false)]);
|
||||
let d = import_idml(&guides_package("SpineOrigin")).unwrap();
|
||||
assert_eq!(guides(&d)[0].1, 540.0, "the spine is the second page's left edge");
|
||||
let d = import_idml(&guides_package("SpreadOrigin")).unwrap();
|
||||
assert_eq!(guides(&d)[0].1, 40.0);
|
||||
assert_eq!(d.parents[0].pages[0].guides.len(), 1, "a parent page's guide");
|
||||
assert_eq!(d.parents[0].pages[0].guides[0].position, 84.0);
|
||||
// Round trip: written under each page, read back where they were.
|
||||
let d = import_idml(&guides_package("PageOrigin")).unwrap();
|
||||
let back = import_idml(&export_idml(&d)).unwrap();
|
||||
assert_eq!(guides(&back), guides(&d));
|
||||
assert_eq!(back.parents[0].pages[0].guides.len(), 1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user