mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 22:42:46 +00:00
* Add craft-drawingml: units, colours, preset shapes, custom geometry and text warps The parts of DrawingML that need no document model, moved from DeckCraft (60f91f78): crates/color, crates/geom (preset, warp, Xfrm and the EMU conversions) and crates/pptx/src/custgeom.rs with CustomPath from crates/model. The code is unchanged except that custgeom reads craft-xml trees and writes to a craft-xml Writer instead of DeckCraft's own XML helper, and matches elements by local name. The 59 tests of the moved code come with it; six more cover the custom geometry limits. * drawingml: evaluate a custom geometry's guide lists once per coordinate space read_cust_geom rebuilt Guides and evaluated avLst and gdLst again for every path: up to 1,000 paths times up to 10,000 guides, each name copied and hashed. Guides are now cached per path coordinate space (w, h). At most 16 spaces and 4 MiB of guide names are kept per geometry; paths in further spaces are left out. The first space is always kept, and a geometry without guides is not capped. Ported from storytold/deckcraft#1789. A list counts as having guides only when it has gd children, as that PR's review asked. * drawingml: the model types for fills, lines, text, tables, charts and diagrams Moved from DeckCraft (60f91f78) crates/model/src: style.rs, text.rs, table.rs, chart.rs, numfmt.rs and trendline.rs, plus Diagram, DiagramNode and the MediaId and SlideId newtypes from lib.rs. The files are unchanged except that deckcraft_color:: is crate::color:: and Fill and TableBorder allow clippy::large_enum_variant. Their 60 tests come with them. Readers and writers follow. * drawingml: the theme model (colour, font and format schemes) Moved from DeckCraft crates/model/src/theme.rs at 60f91f78: FontSet, ScriptFont, FontScheme, FormatScheme, Theme and default_format. The app's theme gallery, its variants and its own default theme stay in the app. Theme::default() is a plain theme built in place, so nothing here depends on a gallery. * drawingml: readers and writers for fills, lines, text, charts, themes, table styles and diagrams Moved from DeckCraft crates/pptx/src at 60f91f78: read/dml.rs, write/dml.rs, read/chart.rs, read/chartex.rs, write/chart.rs, read/theme.rs, write/theme.rs, tables.rs and dgm.rs, onto craft-xml. Package lookups (Imp, Part, Exp, Out) are the caller's part::Source and part::Sink. The chart frame budget and kept-part capture stay with the presentation reader. * drawingml: what WordCraft and GridCraft read that DeckCraft's copy did not Every ST_PresetColorVal name has its colour, in any case and with the dk/lt/med abbreviations; a sysClr without lastClr takes the usual value of its name; a Strict percentage (50%) reads anywhere a percentage does; a chart series keeps its c:f formulas and the writer uses them. * craft-drawingml: extra named theme colours and explicit tint rows Ported from the format part of storytold/deckcraft#1746 by @jagjordi: ColorScheme::{extra, tints, computed_tints}, ExtraColor, GalleryVariant, variants_of, and the a:clrScheme extension that keeps them in a file. * drawingml xmlx: copy only used, bounded namespace declarations onto kept fragments and mc branches From storytold/deckcraft#1740 (988a4920), which landed after the extraction commit 60f91f78. * Add craft-pptx: the PresentationML model (from DeckCraft) Presentation, masters, layouts, slides, shapes, placeholders, media, comments, sections, show settings, properties, embedded fonts, kept parts and drawing guides from DeckCraft's crates/model/src/lib.rs, and transitions and animations from anim.rs. The DrawingML types come from craft-drawingml. The placeholder matching from DeckCraft's resolve.rs moves as placeholder::parents, without the rest of resolve. Presentation::default() is now an empty 16:9 deck with no masters; the starting deck stays with the application. * craft-pptx: the reader (from DeckCraft), on craft-opc and craft-drawingml DeckCraft's crates/pptx reader (read.rs, read/shapes.rs and the chart frame code of read/chart.rs) moved onto craft-opc's Package and craft-xml trees, with the DrawingML readers of craft-drawingml behind a part::Source over the import state. What DeckCraft took from its own defaults (a master for a file without one, the theme of a master that names none) comes from a caller-supplied Defaults; Plain is the neutral one. Placeholder geometry inheritance uses placeholder::parents. Transitions, animations, notes and comments follow in later changes. * craft-pptx: drop DeckCraft issue numbers from reader comments * craft-pptx: link the package budgets from the tag budget's doc * craft-pptx: the writer (from DeckCraft) DeckCraft's crates/pptx writer (write.rs, write/shapes.rs and the kept chart parts of write/chart.rs) moved onto craft-opc's PackageOut and craft-xml's Writer, with craft-drawingml's DrawingML writers behind a part::Sink over the export state. What DeckCraft took from its own defaults (a master for a deck without one, master and default text styles, its name in docProps/app.xml and in table style names) comes from Defaults. export and export_as take the defaults. Transitions, animations, notes slides and comments follow in later changes. * craft-pptx: transitions and animations (from DeckCraft) DeckCraft's read/timing.rs, write/timing.rs and maps.rs on craft-xml: p:transition with the PowerPoint 2010+ effects, and p:timing flattened into anim::Animation on reading and built back on writing, with the media settings of the timing tree. --------- Co-authored-by: Jordi Altayó <jordiag@kth.se>
92 lines
3.9 KiB
Rust
92 lines
3.9 KiB
Rust
//! Transitions and animations, written and read back.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use craft_pptx::anim::{ANIMATIONS, AnimStart, Animation, TRANSITIONS, TextBuild, Transition};
|
|
use craft_pptx::{Defaults, Plain, Presentation, Shape, ShapeId, Slide, SlideId, TextBody, Xfrm};
|
|
|
|
/// One slide per transition option, then one per animation option, each with a three-paragraph
|
|
/// box the animation targets.
|
|
fn deck() -> Presentation {
|
|
let mut p = Presentation::default();
|
|
let (m, last) = Plain.master(p.slide_size, 0);
|
|
let layout = m.layouts[0].id;
|
|
p.masters.push(Arc::new(m));
|
|
p.next_id = last;
|
|
let slide = |p: &mut Presentation, f: &mut dyn FnMut(&mut Slide, ShapeId)| {
|
|
let id = SlideId(p.alloc_id().unwrap());
|
|
let shape = ShapeId(p.alloc_id().unwrap());
|
|
let mut s = Slide { id, layout, ..Default::default() };
|
|
s.shapes.push(Shape {
|
|
id: shape,
|
|
xfrm: Some(Xfrm::new(10.0, 10.0, 200.0, 100.0)),
|
|
text: Some(TextBody::from_text("One\nTwo\nThree")),
|
|
..Default::default()
|
|
});
|
|
f(&mut s, shape);
|
|
p.slides.push(Arc::new(s));
|
|
};
|
|
// `none` is no transition at all, and reads back as such.
|
|
for (kind, _, _, dur, opts) in TRANSITIONS.iter().filter(|t| t.0 != "none") {
|
|
let opts: Vec<&str> = if opts.is_empty() { vec![""] } else { opts.to_vec() };
|
|
for (i, o) in opts.iter().enumerate() {
|
|
slide(&mut p, &mut |s, _| {
|
|
s.transition = Some(Transition {
|
|
kind: kind.to_string(),
|
|
option: o.to_string(),
|
|
duration_ms: *dur,
|
|
advance_after_ms: (i % 2 == 1).then_some(3000),
|
|
..Default::default()
|
|
});
|
|
});
|
|
}
|
|
}
|
|
for (k, (effect, _, class, _, dur, opts)) in ANIMATIONS.iter().enumerate() {
|
|
let opts: Vec<&str> = if opts.is_empty() { vec![""] } else { opts.to_vec() };
|
|
for (i, o) in opts.iter().enumerate() {
|
|
slide(&mut p, &mut |s, shape| {
|
|
let start = [AnimStart::OnClick, AnimStart::WithPrevious, AnimStart::AfterPrevious][(k + i) % 3];
|
|
let text_build = if (k + i) % 2 == 0 { TextBuild::AsOne } else { TextBuild::ByParagraph };
|
|
s.animations.push(Animation {
|
|
shape,
|
|
class: *class,
|
|
effect: effect.to_string(),
|
|
option: o.to_string(),
|
|
start,
|
|
duration_ms: *dur,
|
|
text_build,
|
|
..Default::default()
|
|
});
|
|
});
|
|
}
|
|
}
|
|
p
|
|
}
|
|
|
|
#[test]
|
|
fn every_transition_and_animation_survives_writing_and_reading() {
|
|
let p = deck();
|
|
let q = craft_pptx::import(&craft_pptx::export(&p, &Plain).unwrap(), &Plain).unwrap();
|
|
assert_eq!(q.slides.len(), p.slides.len());
|
|
for (a, b) in p.slides.iter().zip(&q.slides) {
|
|
let (ta, tb) = (a.transition.as_ref(), b.transition.as_ref());
|
|
assert_eq!(ta.map(|t| (&t.kind, &t.option, t.advance_after_ms)), tb.map(|t| (&t.kind, &t.option, t.advance_after_ms)), "slide {}", a.id);
|
|
assert_eq!(a.animations.len(), b.animations.len(), "slide {}", a.id);
|
|
for (x, y) in a.animations.iter().zip(&b.animations) {
|
|
// Effects and options are kept as far as PowerPoint's preset ids and subtypes tell
|
|
// them apart (`circle` reads back as `shape`, `boxIn` as `circleIn`): compare the
|
|
// preset.
|
|
let preset = |a: &Animation| craft_pptx::anim::animation_info(&a.effect, a.class).map(|i| i.3);
|
|
assert_eq!(
|
|
(preset(x), x.class, x.start, x.text_build),
|
|
(preset(y), y.class, y.start, y.text_build),
|
|
"slide {}: {} / {}",
|
|
a.id,
|
|
x.effect,
|
|
y.effect
|
|
);
|
|
assert_eq!(y.shape, b.shapes[0].id);
|
|
}
|
|
}
|
|
}
|