mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
* Build: line tables only for workspace crates, no debug info for dependencies * Tests: one integration-test binary per crate * xtask ci: nextest, doctests, test build without examples, one wasm check
75 lines
3.2 KiB
Rust
75 lines
3.2 KiB
Rust
//! Text anchoring of shapes without an `anchor` of their own: the model centres a shape's text,
|
|
//! a file without one puts it at the top (or where the theme's shape defaults say).
|
|
|
|
use std::io::{Cursor, Read, Write};
|
|
|
|
use deckcraft_model::text::Anchor;
|
|
use deckcraft_model::{Presentation, ShapeKind};
|
|
use zip::write::SimpleFileOptions;
|
|
|
|
fn sample() -> Presentation {
|
|
let mut s = deckcraft_engine::Session::new();
|
|
deckcraft_engine::sample::open_sample(&mut s).expect("sample");
|
|
s.doc().expect("doc").doc.as_ref().clone()
|
|
}
|
|
|
|
/// Every part of `bytes` with `edit` applied to the XML ones.
|
|
fn rewrite(bytes: &[u8], edit: impl Fn(&str, String) -> String) -> Vec<u8> {
|
|
let mut src = zip::ZipArchive::new(Cursor::new(bytes)).expect("zip");
|
|
let mut out = zip::ZipWriter::new(Cursor::new(Vec::new()));
|
|
for i in 0..src.len() {
|
|
let mut f = src.by_index(i).expect("entry");
|
|
let name = f.name().to_string();
|
|
let mut data = Vec::new();
|
|
f.read_to_end(&mut data).expect("read");
|
|
if name.ends_with(".xml") {
|
|
data = edit(&name, String::from_utf8(data).expect("utf8")).into_bytes();
|
|
}
|
|
out.start_file(name, SimpleFileOptions::default()).expect("start");
|
|
out.write_all(&data).expect("write");
|
|
}
|
|
out.finish().expect("finish").into_inner()
|
|
}
|
|
|
|
/// Anchors of the shapes with text that aren't placeholders, text boxes or tables.
|
|
fn anchors(p: &Presentation) -> Vec<Option<Anchor>> {
|
|
p.slides
|
|
.iter()
|
|
.flat_map(|s| s.shapes.iter())
|
|
.filter(|s| s.ph.is_none() && !s.text_box && !matches!(s.kind, ShapeKind::Table(_)))
|
|
.filter_map(|s| s.text.as_ref().map(|t| t.body.anchor))
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn shapes_keep_their_middle_anchor_and_files_without_one_anchor_at_the_top() {
|
|
let p = sample();
|
|
assert!(anchors(&p).contains(&None), "the sample has shapes with the model's default anchor");
|
|
let bytes = deckcraft_pptx::export(&p).expect("export");
|
|
|
|
// Written explicitly, so PowerPoint (which reads a missing anchor as top) centres them too.
|
|
let back = deckcraft_pptx::import(&bytes).expect("import");
|
|
let a = anchors(&back);
|
|
assert_eq!(a, anchors(&p), "the middle anchor survives (as the model's default)");
|
|
|
|
// The same file without the anchors: their text sits at the top.
|
|
let bare = rewrite(&bytes, |n, x| if n.starts_with("ppt/slides/slide") { x.replace(r#" anchor="ctr""#, "") } else { x });
|
|
let a = anchors(&deckcraft_pptx::import(&bare).expect("import"));
|
|
assert!(a.iter().all(|x| *x == Some(Anchor::Top)), "{a:?}");
|
|
|
|
// ...unless the theme's shape defaults centre them, as Office's themes do.
|
|
let themed = rewrite(&bare, |n, x| {
|
|
if n.starts_with("ppt/theme/") {
|
|
x.replace(
|
|
"<a:objectDefaults/>",
|
|
r#"<a:objectDefaults><a:spDef><a:spPr/><a:bodyPr rtlCol="0" anchor="ctr"/><a:lstStyle/></a:spDef></a:objectDefaults>"#,
|
|
)
|
|
} else {
|
|
x
|
|
}
|
|
});
|
|
let a = anchors(&deckcraft_pptx::import(&themed).expect("import"));
|
|
assert!(a.iter().all(|x| x.is_none() || *x == Some(Anchor::Middle)), "{a:?}");
|
|
assert!(a.iter().filter(|x| x.is_none()).count() >= 3, "{a:?}");
|
|
}
|