craft-idml: resources model (styles, swatches, stroke styles, colour groups, preferences) (#1017)

* craft-idml: a typed model of the layout (spreads, pages, guides, page items, graphics, geometry)

The first step of the typed IDML model. Spreads and parent spreads, pages, guides, page items
(nested), placed graphics and their links are typed; each keeps its attributes and Properties
as written and reads them through accessors, path geometry among them. Elements the model
does not type are kept where they stood, so a node read and written again reads the same.

* craft-idml: the story model (ranges, content, tables, notes, anchored items)

Stories as IDML writes them: paragraph and character ranges, Content with the special
characters InDesign writes as <?ACE n?>, breaks, the containers text sits in (hyperlink
sources, tracked changes, XML elements, cross-reference sources, footnotes, notes), tables with
their cells, and page items anchored in the text. Each node keeps its values as written; text
is gathered without recursion.

* craft-idml: read and write ICML (InCopy Markup)

The format layer of storytold/designcraft#2248: an ICML file told by its InCopyInterchange
instruction, read within the size of one package part, its references to package parts left
out with a warning, a file without a story refused, and written with the two instructions that
mark it. Choosing the story to place and mapping it stay with the application.

* craft-idml: the resources model (style groups, swatches, stroke styles, colour groups, preferences)

Style groups as trees of groups and styles for each kind, each style with its group path,
BasedOn and NextStyle; colours, tints, gradients with their stops, mixed inks and the None
swatch; inks, custom stroke styles, colour groups; and the preference elements by name. Values
are kept as written and read through accessors.

---------

Co-authored-by: ljkx <177671075+ljkx@users.noreply.github.com>
This commit is contained in:
Dylan M. Taylorandljkx authored and GitHub committed 2026-11-11 20:35:11 -05:00
1 parent b471b84fd4
commit 24903d320d
4 files changed
+603 -3

No files matched your search

+16 -2
View File
@@ -10,8 +10,8 @@ Implemented clean-room from the public IDML File Format Specification and from i
packages exported from synthetic documents.
This crate is the layer between the bytes and IDML's own element tree, with a typed model of
the layout and the stories (`model`). Styles, swatches and preferences are not modelled yet: a
caller walks those elements and maps them onto its own document. InCopy Markup (ICML), one
the layout, the stories and the resources (`model`): a caller reads it and maps it onto its own
document. InCopy Markup (ICML), one
story as one XML document, is read and written too (`icml`).
## Reading
@@ -153,6 +153,20 @@ assert_eq!(story.text(|a| (a == Ace::PageNumber).then_some('#')), "Page #\n");
Containers nest as deep as the XML reader allows (256), and text is gathered without
recursion.
## Resources
- `style_groups` gives the root style groups (`RootParagraphStyleGroup` and the character,
object, cell and table ones) as trees of `StyleGroup` and `Style`, written back exactly.
`StyleGroup::styles` lists every style with the names of the groups it is in; `Style` has
`based_on`, `next_style` and its other children (`other`: an object style's
`TextFramePreference`, its effect settings).
- `swatches` gives `Color`, `Tint`, `Gradient` (with `stops`), `MixedInk`, `MixedInkGroup` and the
None `Swatch`: `spot`, `space`, `color_value`, `base_color`, `tint_value`, `radial`.
- `stroke_styles` (dashed with `dash_array`, dotted, striped with `stripe_array`) and
`color_groups` (with `members`).
- `preference` finds a preference element (`DocumentPreference`, `GridPreference`, …) and
`named` every element of a name (`Ink`, `Layer`, `Section`, `Condition`, `NumberingList`).
## ICML
`icml::is_icml` sniffs the `SnippetType="InCopyInterchange"` instruction in the first 4 KiB.
+7 -1
View File
@@ -1,6 +1,7 @@
//! A typed model of IDML: spreads and parent (master) spreads, pages, guides, page items and the
//! graphics placed in them; stories with their paragraph and character ranges, text, tables,
//! notes and anchored items.
//! notes and anchored items; style groups and styles, swatches, inks, stroke styles, colour
//! groups and preferences.
//!
//! The model types what contains what. Each node keeps its attributes and `<Properties>`
//! children as written ([`Values`]) and reads them through typed accessors, path geometry among
@@ -12,10 +13,15 @@
//! [`spreads`]) and written back with `to_el`, then [`crate::part_xml`].
mod geometry;
mod resources;
mod spread;
mod story;
pub use geometry::{MAX_PATH_POINTS, PathGeometry, PathPoint, SubPath};
pub use resources::{
ColorGroup, ColorSpace, GradientStop, StrokeStyle, StrokeStyleKind, Style, StyleGroup, StyleKind, StyleNode, Swatch, SwatchKind, color_groups,
named, preference, stroke_styles, style_groups, swatches,
};
pub use spread::{Graphic, GraphicChild, GraphicKind, Guide, ItemChild, ItemKind, Link, Page, PageChild, PageItem, Spread, SpreadChild, spreads};
pub use story::{Ace, CONTAINERS, Cell, Container, ContentPart, Range, Story, Table, TableChild, TextNode, stories};
+401
View File
@@ -0,0 +1,401 @@
//! Resources: style groups and styles, swatches, inks, stroke styles, colour groups, and the
//! preference elements.
use craft_xml::{El, Local, Node};
use super::{Values, tidy};
use crate::value::nums;
/// The children of `el` other than `<Properties>`.
fn others(el: &El) -> impl Iterator<Item = &El> {
el.elements().filter(|e| !e.is(Local("Properties")))
}
/// What a style styles.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StyleKind {
Paragraph,
Character,
Object,
Cell,
Table,
}
impl StyleKind {
pub const ALL: [StyleKind; 5] = [StyleKind::Paragraph, StyleKind::Character, StyleKind::Object, StyleKind::Cell, StyleKind::Table];
/// The element name of a style: `ParagraphStyle`, ….
pub fn style(self) -> &'static str {
match self {
StyleKind::Paragraph => "ParagraphStyle",
StyleKind::Character => "CharacterStyle",
StyleKind::Object => "ObjectStyle",
StyleKind::Cell => "CellStyle",
StyleKind::Table => "TableStyle",
}
}
/// The element name of a group: `ParagraphStyleGroup`, ….
pub fn group(self) -> &'static str {
match self {
StyleKind::Paragraph => "ParagraphStyleGroup",
StyleKind::Character => "CharacterStyleGroup",
StyleKind::Object => "ObjectStyleGroup",
StyleKind::Cell => "CellStyleGroup",
StyleKind::Table => "TableStyleGroup",
}
}
/// The element name of the root group: `RootParagraphStyleGroup`, ….
pub fn root(self) -> &'static str {
match self {
StyleKind::Paragraph => "RootParagraphStyleGroup",
StyleKind::Character => "RootCharacterStyleGroup",
StyleKind::Object => "RootObjectStyleGroup",
StyleKind::Cell => "RootCellStyleGroup",
StyleKind::Table => "RootTableStyleGroup",
}
}
/// The kind whose root group is called `name`.
pub fn of_root(name: &str) -> Option<StyleKind> {
StyleKind::ALL.into_iter().find(|k| k.root() == name)
}
}
/// A style group: a root group (`RootParagraphStyleGroup`, …) or a group in one.
#[derive(Clone, Debug, PartialEq)]
pub struct StyleGroup {
pub kind: StyleKind,
/// The root group of its kind.
pub root: bool,
pub values: Values,
pub children: Vec<StyleNode>,
}
/// A child of a style group, in document order.
#[derive(Clone, Debug, PartialEq)]
pub enum StyleNode {
Group(StyleGroup),
Style(Style),
Other(El),
}
/// A style, its values as written: `Name`, `Self`, `Properties/BasedOn`, its attributes; and its
/// other children (an object style's `TextFramePreference`, effect settings, …).
#[derive(Clone, Debug, PartialEq)]
pub struct Style {
pub kind: StyleKind,
pub values: Values,
pub rest: Vec<El>,
}
impl StyleGroup {
/// The root group `el`; `None` when `el` is no root style group.
pub fn from_el(el: &El) -> Option<StyleGroup> {
let kind = StyleKind::of_root(el.local())?;
Some(Self::read(kind, true, el))
}
fn read(kind: StyleKind, root: bool, el: &El) -> StyleGroup {
let children = others(el)
.map(|c| match c.local() {
n if n == kind.group() => StyleNode::Group(Self::read(kind, false, c)),
n if n == kind.style() => StyleNode::Style(Style { kind, values: Values::of(c), rest: others(c).map(tidy).collect() }),
_ => StyleNode::Other(tidy(c)),
})
.collect();
StyleGroup { kind, root, values: Values::of(el), children }
}
pub fn to_el(&self) -> El {
let mut el = self.values.element(if self.root { self.kind.root() } else { self.kind.group() });
el.children.extend(self.children.iter().map(|c| {
Node::El(match c {
StyleNode::Group(g) => g.to_el(),
StyleNode::Style(s) => s.to_el(),
StyleNode::Other(e) => e.clone(),
})
}));
el
}
/// Every style in this group and the groups in it, in document order, with the names of
/// the groups it is in (outermost first, this group's own name left out for a root group).
pub fn styles(&self) -> Vec<(Vec<&str>, &Style)> {
let mut out = Vec::new();
let start: Vec<&str> = if self.root { Vec::new() } else { self.values.name().into_iter().collect() };
// Iterative: nesting is bounded by the XML reader, but there is no need to recurse.
let mut stack: Vec<(Vec<&str>, &StyleNode)> = self.children.iter().rev().map(|c| (start.clone(), c)).collect();
while let Some((path, node)) = stack.pop() {
match node {
StyleNode::Style(s) => out.push((path, s)),
StyleNode::Group(g) => {
let mut inner = path.clone();
inner.extend(g.values.name());
stack.extend(g.children.iter().rev().map(|c| (inner.clone(), c)));
}
StyleNode::Other(_) => {}
}
}
out
}
}
impl Style {
pub fn to_el(&self) -> El {
let mut el = self.values.element(self.kind.style());
el.children.extend(self.rest.iter().cloned().map(Node::El));
el
}
/// `Properties/BasedOn`: the `Self` of the style this one is based on (`$ID/[No paragraph
/// style]` and the like for none).
pub fn based_on(&self) -> Option<String> {
self.values.value("BasedOn")
}
/// `NextStyle` of a paragraph style.
pub fn next_style(&self) -> Option<String> {
self.values.value("NextStyle")
}
/// The child element `name` (`TextFramePreference`, `TransparencySetting`, …).
pub fn other(&self, name: &str) -> Option<&El> {
self.rest.iter().find(|e| e.local() == name)
}
}
/// The root style groups among `top`, in order.
pub fn style_groups(top: &[El]) -> Vec<StyleGroup> {
top.iter().filter_map(StyleGroup::from_el).collect()
}
/// What a swatch is.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SwatchKind {
Color,
Tint,
Gradient,
MixedInk,
MixedInkGroup,
/// `Swatch`: the None swatch.
Swatch,
}
impl SwatchKind {
pub fn of(name: &str) -> Option<SwatchKind> {
Some(match name {
"Color" => SwatchKind::Color,
"Tint" => SwatchKind::Tint,
"Gradient" => SwatchKind::Gradient,
"MixedInk" => SwatchKind::MixedInk,
"MixedInkGroup" => SwatchKind::MixedInkGroup,
"Swatch" => SwatchKind::Swatch,
_ => return None,
})
}
pub fn name(self) -> &'static str {
match self {
SwatchKind::Color => "Color",
SwatchKind::Tint => "Tint",
SwatchKind::Gradient => "Gradient",
SwatchKind::MixedInk => "MixedInk",
SwatchKind::MixedInkGroup => "MixedInkGroup",
SwatchKind::Swatch => "Swatch",
}
}
}
/// A swatch (`Color`, `Tint`, `Gradient`, …), its values as written and its other children
/// (a gradient's `GradientStop`s).
#[derive(Clone, Debug, PartialEq)]
pub struct Swatch {
pub kind: SwatchKind,
pub values: Values,
pub rest: Vec<El>,
}
/// A colour's space, as `Space` names it.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ColorSpace {
Cmyk,
Rgb,
Lab,
/// `MixedInk`.
MixedInk,
}
/// A stop of a gradient.
#[derive(Clone, Debug, PartialEq)]
pub struct GradientStop {
/// `StopColor`: the `Self` of a colour.
pub color: String,
/// `Location`, 0 to 100.
pub location: f64,
/// `Midpoint`, 0 to 100 (50 when absent).
pub midpoint: f64,
}
impl Swatch {
pub fn to_el(&self) -> El {
let mut el = self.values.element(self.kind.name());
el.children.extend(self.rest.iter().cloned().map(Node::El));
el
}
/// `Model` is `Spot`.
pub fn spot(&self) -> bool {
self.values.get("Model") == Some("Spot")
}
/// `Space`; CMYK when absent or unknown.
pub fn space(&self) -> ColorSpace {
match self.values.get("Space") {
Some("RGB") => ColorSpace::Rgb,
Some("LAB") => ColorSpace::Lab,
Some("MixedInk") => ColorSpace::MixedInk,
_ => ColorSpace::Cmyk,
}
}
/// `ColorValue`: CMYK in percent, RGB 0 to 255, Lab as L a b. Empty when absent or when a
/// number is not finite.
pub fn color_value(&self) -> Vec<f64> {
self.values.get("ColorValue").map(nums).unwrap_or_default()
}
/// `BaseColor` of a tint: the `Self` of its colour.
pub fn base_color(&self) -> Option<&str> {
self.values.get("BaseColor")
}
/// `TintValue` of a tint, in percent.
pub fn tint_value(&self) -> Option<f64> {
self.values.num("TintValue")
}
/// `Type` of a gradient is `Radial`; linear otherwise.
pub fn radial(&self) -> bool {
self.values.get("Type") == Some("Radial")
}
/// A gradient's stops in order; a stop without a colour or a finite location is left out.
pub fn stops(&self) -> Vec<GradientStop> {
self.rest
.iter()
.filter(|e| e.local() == "GradientStop")
.filter_map(|s| {
let v = Values::of(s);
Some(GradientStop {
color: v.get("StopColor")?.to_string(),
location: v.num("Location")?,
midpoint: v.num("Midpoint").unwrap_or(50.0),
})
})
.collect()
}
}
/// The swatches among `top`, in order.
pub fn swatches(top: &[El]) -> Vec<Swatch> {
top.iter().filter_map(|e| Some(Swatch { kind: SwatchKind::of(e.local())?, values: Values::of(e), rest: others(e).map(tidy).collect() })).collect()
}
/// What a stroke style is.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StrokeStyleKind {
Dashed,
Dotted,
Striped,
}
/// A custom stroke style.
#[derive(Clone, Debug, PartialEq)]
pub struct StrokeStyle {
pub kind: StrokeStyleKind,
pub values: Values,
pub rest: Vec<El>,
}
impl StrokeStyle {
pub fn to_el(&self) -> El {
let name = match self.kind {
StrokeStyleKind::Dashed => "DashedStrokeStyle",
StrokeStyleKind::Dotted => "DottedStrokeStyle",
StrokeStyleKind::Striped => "StripedStrokeStyle",
};
let mut el = self.values.element(name);
el.children.extend(self.rest.iter().cloned().map(Node::El));
el
}
/// `DashArray` of a dashed style: dash, gap, dash, gap, … in points.
pub fn dash_array(&self) -> Vec<f64> {
self.values.get("DashArray").map(nums).unwrap_or_default()
}
/// `StripeArray` of a striped style: each stripe's start and end, in percent of the weight.
pub fn stripe_array(&self) -> Vec<f64> {
self.values.get("StripeArray").map(nums).unwrap_or_default()
}
}
/// The custom stroke styles among `top`, in order.
pub fn stroke_styles(top: &[El]) -> Vec<StrokeStyle> {
top.iter()
.filter_map(|e| {
let kind = match e.local() {
"DashedStrokeStyle" => StrokeStyleKind::Dashed,
"DottedStrokeStyle" => StrokeStyleKind::Dotted,
"StripedStrokeStyle" => StrokeStyleKind::Striped,
_ => return None,
};
Some(StrokeStyle { kind, values: Values::of(e), rest: others(e).map(tidy).collect() })
})
.collect()
}
/// A colour group (`ColorGroup`): its values and the swatches it lists.
#[derive(Clone, Debug, PartialEq)]
pub struct ColorGroup {
pub values: Values,
pub rest: Vec<El>,
}
impl ColorGroup {
pub fn to_el(&self) -> El {
let mut el = self.values.element("ColorGroup");
el.children.extend(self.rest.iter().cloned().map(Node::El));
el
}
/// `IsRootColorGroup`.
pub fn is_root(&self) -> bool {
self.values.boolean("IsRootColorGroup") == Some(true)
}
/// The `Self`s of the swatches the group lists (`ColorGroupSwatch`'s `SwatchItemRef`), in
/// order.
pub fn members(&self) -> Vec<&str> {
self.rest.iter().filter(|e| e.local() == "ColorGroupSwatch").filter_map(|e| e.attr("SwatchItemRef")).collect()
}
}
/// The colour groups among `top`, in order.
pub fn color_groups(top: &[El]) -> Vec<ColorGroup> {
top.iter().filter(|e| e.local() == "ColorGroup").map(|e| ColorGroup { values: Values::of(e), rest: others(e).map(tidy).collect() }).collect()
}
/// The values of every element named `name` among `top`, in order: `Ink`, `Layer`, `Section`,
/// `Condition`, `NumberingList`, ….
pub fn named(top: &[El], name: &str) -> Vec<Values> {
top.iter().filter(|e| e.local() == name).map(Values::of).collect()
}
/// The first element named `name` among `top` (a preference: `DocumentPreference`,
/// `MarginPreference`, `TextFramePreference`, `GridPreference`, …), its values.
pub fn preference(top: &[El], name: &str) -> Option<Values> {
top.iter().find(|e| e.local() == name).map(Values::of)
}
+179
View File
@@ -0,0 +1,179 @@
//! Styles, swatches, stroke styles, colour groups and preferences.
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use craft_idml::model::{
ColorSpace, GradientStop, StrokeStyleKind, StyleGroup, StyleKind, StyleNode, SwatchKind, color_groups, named, preference, stroke_styles,
style_groups, swatches,
};
use craft_idml::{El, parse_part, part_root, part_xml};
const STYLES: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<idPkg:Styles xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging" DOMVersion="16.0">
<RootCharacterStyleGroup Self="u7a">
<CharacterStyle Self="CharacterStyle/$ID/[No character style]" Name="$ID/[No character style]" />
<CharacterStyle Self="CharacterStyle/Em" Name="Em" FontStyle="Italic"><Properties><BasedOn type="object">CharacterStyle/$ID/[No character style]</BasedOn></Properties></CharacterStyle>
<CharacterStyleGroup Self="CharacterStyleGroup/Inline" Name="Inline">
<CharacterStyleGroup Self="CharacterStyleGroup/Inline%3aCode" Name="Code">
<CharacterStyle Self="CharacterStyle/Inline%3aCode%3aMono" Name="Inline:Code:Mono" />
</CharacterStyleGroup>
</CharacterStyleGroup>
</RootCharacterStyleGroup>
<RootParagraphStyleGroup Self="u79">
<ParagraphStyle Self="ParagraphStyle/Head" Name="Head" PointSize="18" NextStyle="ParagraphStyle/Body">
<Properties><BasedOn type="string">$ID/[No paragraph style]</BasedOn><Leading type="unit">22</Leading></Properties>
</ParagraphStyle>
<ParagraphStyle Self="ParagraphStyle/Body" Name="Body" />
</RootParagraphStyleGroup>
<RootObjectStyleGroup Self="u8b">
<ObjectStyle Self="ObjectStyle/Frame" Name="Frame" FillColor="Color/Paper">
<TextFramePreference TextColumnCount="3" />
<TransparencySetting><BlendingSetting Opacity="50" /></TransparencySetting>
</ObjectStyle>
</RootObjectStyleGroup>
<RootCellStyleGroup Self="u8c"><CellStyle Self="CellStyle/$ID/[None]" Name="$ID/[None]" /></RootCellStyleGroup>
<RootTableStyleGroup Self="u8d"><TableStyle Self="TableStyle/$ID/[Basic Table]" Name="$ID/[Basic Table]" /></RootTableStyleGroup>
<TOCStyle Self="TOCStyle/$ID/DefaultTOCStyleName" Name="$ID/DefaultTOCStyleName" />
</idPkg:Styles>
"#;
fn top(xml: &str) -> Vec<El> {
parse_part(xml.as_bytes()).unwrap().elements().cloned().collect()
}
#[test]
fn style_groups_give_their_styles_with_their_group_path() {
let groups = style_groups(&top(STYLES));
let kinds: Vec<StyleKind> = groups.iter().map(|g| g.kind).collect();
assert_eq!(kinds, [StyleKind::Character, StyleKind::Paragraph, StyleKind::Object, StyleKind::Cell, StyleKind::Table]);
assert!(groups.iter().all(|g| g.root));
let chars = groups[0].styles();
assert_eq!(
chars.iter().map(|(path, s)| (path.join("/"), s.values.name().unwrap())).collect::<Vec<_>>(),
[(String::new(), "$ID/[No character style]"), (String::new(), "Em"), ("Inline/Code".to_string(), "Inline:Code:Mono"),]
);
let em = chars[1].1;
assert_eq!(
(em.kind, em.based_on().as_deref(), em.values.get("FontStyle")),
(StyleKind::Character, Some("CharacterStyle/$ID/[No character style]"), Some("Italic"))
);
let head = groups[1].styles()[0].1;
assert_eq!((head.based_on().as_deref(), head.next_style().as_deref()), (Some("$ID/[No paragraph style]"), Some("ParagraphStyle/Body")));
assert_eq!((head.values.num("PointSize"), head.values.num("Leading")), (Some(18.0), Some(22.0)));
let frame = groups[2].styles()[0].1;
assert_eq!(frame.other("TextFramePreference").and_then(|p| p.attr("TextColumnCount")), Some("3"));
assert!(frame.other("TransparencySetting").is_some());
// A group inside a group is its own node, with its own values.
let StyleNode::Group(inline) = &groups[0].children[2] else { panic!() };
assert!(!inline.root);
assert_eq!(inline.styles()[0].0, ["Inline", "Code"], "a group's own name starts its path");
// Not a root style group.
assert_eq!(StyleGroup::from_el(&El::new("TOCStyle")), None);
}
#[test]
fn style_groups_written_and_read_again_are_the_same() {
let groups = style_groups(&top(STYLES));
let mut part = part_root("Styles");
for g in &groups {
part.push(g.to_el());
}
assert_eq!(style_groups(&top(&String::from_utf8(part_xml(&part)).unwrap())), groups);
}
const GRAPHIC: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<idPkg:Graphic xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging" DOMVersion="16.0">
<Color Self="Color/Black" Model="Process" Space="CMYK" ColorValue="0 0 0 100" Name="Black" />
<Color Self="Color/Brand" Model="Spot" Space="RGB" ColorValue="255 128 0" Name="Brand" />
<Color Self="Color/Lab" Model="Process" Space="LAB" ColorValue="50 -20 30" Name="Lab" />
<Color Self="Color/Broken" Model="Process" Space="CMYK" ColorValue="0 NaN 0 0" Name="Broken" />
<Ink Self="Ink/$ID/Process Cyan" Name="$ID/Process Cyan" />
<Ink Self="Ink/Brand" Name="Brand" ConvertToProcess="true" AliasInkName="Other" />
<Tint Self="Tint/Brand 40" BaseColor="Color/Brand" TintValue="40" Name="" />
<Gradient Self="Gradient/Fade" Type="Radial" Name="Fade">
<GradientStop Self="Gradient/FadeGradientStop0" StopColor="Color/Black" Location="0" />
<GradientStop Self="Gradient/FadeGradientStop1" StopColor="Color/Brand" Location="100" Midpoint="30" />
<GradientStop Self="Gradient/FadeGradientStop2" Location="50" />
</Gradient>
<Swatch Self="Swatch/None" Name="None" />
<DashedStrokeStyle Self="DashedStrokeStyle/Dash" Name="Dash" DashArray="4 2 1 2" />
<StripedStrokeStyle Self="StripedStrokeStyle/Rails" Name="Rails" StripeArray="0 25 75 100" />
<DottedStrokeStyle Self="DottedStrokeStyle/Dots" Name="Dots" />
<ColorGroup Self="ColorGroup/[Root Color Group]" Name="[Root Color Group]" IsRootColorGroup="true">
<ColorGroupSwatch Self="u18ColorGroupSwatch0" SwatchItemRef="Swatch/None" />
<ColorGroupSwatch Self="u18ColorGroupSwatch1" SwatchItemRef="Color/Black" />
</ColorGroup>
</idPkg:Graphic>
"#;
#[test]
fn swatches_give_their_colours_tints_and_gradients() {
let t = top(GRAPHIC);
let sw = swatches(&t);
assert_eq!(
sw.iter().map(|s| s.kind).collect::<Vec<_>>(),
[SwatchKind::Color, SwatchKind::Color, SwatchKind::Color, SwatchKind::Color, SwatchKind::Tint, SwatchKind::Gradient, SwatchKind::Swatch]
);
assert_eq!((sw[0].spot(), sw[0].space(), sw[0].color_value()), (false, ColorSpace::Cmyk, vec![0.0, 0.0, 0.0, 100.0]));
assert_eq!((sw[1].spot(), sw[1].space(), sw[1].color_value()), (true, ColorSpace::Rgb, vec![255.0, 128.0, 0.0]));
assert_eq!(sw[2].space(), ColorSpace::Lab);
assert_eq!(sw[3].color_value(), Vec::<f64>::new(), "a value that is not finite is no value");
assert_eq!((sw[4].base_color(), sw[4].tint_value()), (Some("Color/Brand"), Some(40.0)));
assert!(sw[5].radial());
assert_eq!(
sw[5].stops(),
[
GradientStop { color: "Color/Black".into(), location: 0.0, midpoint: 50.0 },
GradientStop { color: "Color/Brand".into(), location: 100.0, midpoint: 30.0 },
]
);
let again: Vec<El> = top(&String::from_utf8(part_xml(&sw.iter().fold(part_root("Graphic"), |p, s| p.with_child(s.to_el())))).unwrap());
assert_eq!(swatches(&again), sw);
}
#[test]
fn inks_stroke_styles_and_colour_groups() {
let t = top(GRAPHIC);
let inks = named(&t, "Ink");
assert_eq!(inks.len(), 2);
assert_eq!((inks[1].boolean("ConvertToProcess"), inks[1].get("AliasInkName")), (Some(true), Some("Other")));
let strokes = stroke_styles(&t);
assert_eq!(strokes.iter().map(|s| s.kind).collect::<Vec<_>>(), [StrokeStyleKind::Dashed, StrokeStyleKind::Striped, StrokeStyleKind::Dotted]);
assert_eq!(strokes[0].dash_array(), [4.0, 2.0, 1.0, 2.0]);
assert_eq!(strokes[1].stripe_array(), [0.0, 25.0, 75.0, 100.0]);
let groups = color_groups(&t);
assert!(groups[0].is_root());
assert_eq!(groups[0].members(), ["Swatch/None", "Color/Black"]);
let again = top(&String::from_utf8(part_xml(&part_root("Graphic").with_child(groups[0].to_el()).with_child(strokes[1].to_el()))).unwrap());
assert_eq!(color_groups(&again), groups);
assert_eq!(stroke_styles(&again), [strokes[1].clone()]);
}
#[test]
fn preferences_are_found_by_name() {
let prefs = top(
r#"<idPkg:Preferences><DocumentPreference PageWidth="612" PageHeight="792" FacingPages="true"><Properties><ColumnDirection type="enumeration">Horizontal</ColumnDirection></Properties></DocumentPreference><DocumentPreference PageWidth="1"/><GridPreference BaselineDivision="12"/></idPkg:Preferences>"#,
);
let doc = preference(&prefs, "DocumentPreference").unwrap();
assert_eq!(
(doc.num("PageWidth"), doc.boolean("FacingPages"), doc.value("ColumnDirection").as_deref()),
(Some(612.0), Some(true), Some("Horizontal"))
);
assert_eq!(preference(&prefs, "GridPreference").and_then(|g| g.num("BaselineDivision")), Some(12.0));
assert_eq!(preference(&prefs, "MarginPreference"), None);
assert_eq!(named(&prefs, "DocumentPreference").len(), 2);
}
/// Style groups nest as deep as the XML reader allows; their styles are gathered without
/// recursion.
#[test]
fn deeply_nested_style_groups_are_read() {
let depth = 200;
let xml = format!(
"<RootParagraphStyleGroup>{}<ParagraphStyle Name=\"deep\"/>{}</RootParagraphStyleGroup>",
"<ParagraphStyleGroup Name=\"g\">".repeat(depth),
"</ParagraphStyleGroup>".repeat(depth)
);
let g = StyleGroup::from_el(&parse_part(xml.as_bytes()).unwrap()).unwrap();
let styles = g.styles();
assert_eq!((styles.len(), styles[0].0.len(), styles[0].1.values.name()), (1, depth, Some("deep")));
}