//! Legacy VML drawings in a `w:pict`: every shape in one, and `v:group` members in the space //! `coordsize`/`coordorigin` give them. Fixtures are hand-written from ECMA-376 Part 4 //! (VML, §14.1 `v:group`, `coordsize`, `coordorigin`; §14.2 shapes). use std::io::Write; use wordcraft_doc::para::ShapeKind; use wordcraft_doc::{Document, InlineObject, Paragraph}; fn zip(entries: &[(&str, &[u8])]) -> Vec { let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new())); for (name, bytes) in entries { zw.start_file(*name, zip::write::SimpleFileOptions::default()).unwrap(); zw.write_all(bytes).unwrap(); } zw.finish().unwrap().into_inner() } /// A document whose body is `body`. fn read_body(body: &str) -> Document { let root = r#""#; let doc = format!( r#"{body}"# ); wordcraft_docx::read(&zip(&[("_rels/.rels", root.as_bytes()), ("word/document.xml", doc.as_bytes())])).unwrap() } fn paras(d: &Document) -> Vec<&Paragraph> { d.body.iter().filter_map(|b| b.as_para()).collect() } /// The objects of the body's first paragraph. fn objects(d: &Document) -> &[InlineObject] { ¶s(d).first().expect("a paragraph").objects } fn pict(inner: &str) -> String { format!(r#"{inner}"#) } fn kinds(objs: &[InlineObject]) -> Vec { objs.iter() .map(|o| match o { InlineObject::Shape { kind, .. } => *kind, o => panic!("{o:?}"), }) .collect() } #[test] fn every_shape_in_a_pict_is_read() { // A `w:pict` may hold any number of shapes; each one is its own object. A `v:shapetype` is // a definition, not a shape, and is not one of them. let d = read_body(&pict(concat!( r#""#, r#""#, r#""#, r#""#, ))); let objs = objects(&d); assert_eq!(kinds(objs), [ShapeKind::Rectangle, ShapeKind::Ellipse, ShapeKind::RoundedRectangle], "all three, in document order"); let size = |o: &InlineObject| match o { InlineObject::Shape { w, h, .. } => (*w, *h), o => panic!("{o:?}"), }; assert_eq!(objs.iter().map(size).collect::>(), [(10.0, 20.0), (30.0, 40.0), (50.0, 60.0)], "each keeps its own size"); } #[test] fn a_vml_group_keeps_its_members() { // `coordsize` is the members' space, mapped onto the group's own box; `left`/`top` place a // member in that space. let d = read_body(&pict(concat!( r#""#, r#""#, r#""#, r#""#, ))); let InlineObject::Group { w, h, ch_w, ch_h, children, canvas, .. } = &objects(&d)[0] else { panic!("{:?}", objects(&d)) }; assert_eq!((*w, *h, *ch_w, *ch_h), (100.0, 50.0, 1000.0, 500.0), "its own box, and its members' space"); assert_eq!(*canvas, None, "a group is not a Drawing Canvas"); assert_eq!(children.len(), 2, "both members"); assert_eq!((children[0].x, children[0].y), (0.0, 0.0)); assert_eq!((children[1].x, children[1].y), (600.0, 100.0), "placed in the members' space, not scaled"); assert_eq!(kinds(&children.iter().map(|c| c.obj.clone()).collect::>()), [ShapeKind::Rectangle, ShapeKind::Ellipse]); } #[test] fn a_vml_group_shifts_its_members_by_its_coordorigin() { // `coordorigin` is the top-left of the members' space, so a member's offset is from there. let d = read_body(&pict(concat!( r#""#, r#""#, r#""#, r#""#, ))); let InlineObject::Group { ch_w, children, .. } = &objects(&d)[0] else { panic!() }; assert_eq!(*ch_w, 1000.0); assert_eq!((children[0].x, children[0].y), (0.0, 0.0), "a member at the origin sits at the group's corner"); assert_eq!((children[1].x, children[1].y), (500.0, 500.0)); } #[test] fn nested_vml_groups_are_flattened_into_the_outer_one() { // An inner group's members' space maps onto the inner group's own box, which is itself in // the outer group's space: 500 wide in a 1000-wide space inside a 1000-wide box halves it. let d = read_body(&pict(concat!( r#""#, r#""#, r#""#, r#""#, r#""#, r#""#, ))); let InlineObject::Group { children, .. } = &objects(&d)[0] else { panic!() }; assert_eq!(children.len(), 2, "the inner group's member comes into the outer group"); assert_eq!((children[0].x, children[0].y), (100.0, 100.0)); // 400 + 200 × (500/1000) = 500; 400 + 600 × 0.5 = 700. assert_eq!((children[1].x, children[1].y), (500.0, 700.0), "mapped through the inner group"); let InlineObject::Shape { w, h, .. } = children[1].obj else { panic!() }; assert_eq!((w, h), (50.0, 50.0), "and scaled by it"); } #[test] fn a_vml_group_with_no_member_we_can_show_is_left_out() { // Only a shape definition, and a shape we draw nothing for (no stroke, no fill). let d = read_body(&pict(concat!( r#""#, r#""#, r#""#, r#""#, ))); assert!(objects(&d).is_empty(), "nothing to show: {:?}", objects(&d)); } #[test] fn deeply_nested_vml_groups_stop_at_the_depth_bound() { // A hostile file nests groups without end; reading one must not run away. let depth = 64; let mut inner = r#""#.to_string(); for _ in 0..depth { inner = format!(r#"{inner}"#); } let d = read_body(&pict(&format!(r#"{inner}"#))); // Past the bound nothing is read, so the outermost group has no member and is left out. assert!(objects(&d).is_empty(), "{:?}", objects(&d)); } #[test] fn a_vml_group_survives_being_saved() { let body = pict(concat!( r#""#, r#""#, r#""#, r#""#, )); let d = read_body(&body); let InlineObject::Group { children, .. } = &objects(&d)[0] else { panic!("{:?}", objects(&d)) }; assert_eq!(children.len(), 2, "read with both its members to begin with"); let back = wordcraft_docx::read(&wordcraft_docx::write(&d).unwrap()).unwrap(); assert_eq!(objects(&back), objects(&d), "saved as DrawingML and read again, it is the same group"); } #[test] fn a_pict_of_a_rect_a_text_box_and_a_group_all_come_through() { // The three shapes of issue #1643, which a round trip dropped the last of. The group's // member gives its size in points, not in the group's own coordinates, as Word writes it. let d = read_body(&format!( "{}{}{}", pict(r##""##), pict( r#"boxed"# ), pict(concat!( r#""#, r##""##, r#""#, )), )); let ps = paras(&d); assert_eq!(ps.len(), 3, "a paragraph each"); assert_eq!(kinds(&ps[0].objects), [ShapeKind::Rectangle]); assert_eq!(kinds(&ps[1].objects), [ShapeKind::TextBox]); let InlineObject::Group { children, .. } = &ps[2].objects[0] else { panic!("{:?}", ps[2].objects) }; assert_eq!(kinds(&children.iter().map(|c| c.obj.clone()).collect::>()), [ShapeKind::Rectangle], "the green rectangle is read"); let InlineObject::Shape { fill, .. } = children[0].obj else { panic!() }; assert_eq!(fill, Some(wordcraft_doc::Rgb(0x00, 0xff, 0x00)), "with its colour"); // And it is still there after a save, which is what the round trip lost. let back = wordcraft_docx::read(&wordcraft_docx::write(&d).unwrap()).unwrap(); assert_eq!(paras(&back).iter().map(|p| p.objects.len()).collect::>(), [1, 1, 1]); let InlineObject::Group { children, .. } = ¶s(&back)[2].objects[0] else { panic!() }; assert_eq!(children.len(), 1, "the group keeps its member"); } /// The text of text box story `id`, its paragraphs joined by newlines. fn part_text(d: &Document, id: u32) -> String { d.parts.get(&id).map(|p| p.blocks.iter().filter_map(|b| b.as_para()).map(|p| p.plain_text()).collect::>().join("\n")).unwrap_or_default() } #[test] fn a_group_of_several_text_boxes_keeps_every_one() { // Reported on #1643 by @KNUFFIXX, whose fixture is a `_x0000_t202` pair side by side in one // inline `v:group`: only box A came through a convert. `vml_textbox` recurses to the first // box with content, so before this every later box in a group was dropped -- and a text box // is the one member a group used to keep, which is why this case looked half-working. let shape = |left: u32, height: u32, label: &str| { let paras: String = (1..=3).map(|n| format!(r#"{label} line {n}"#)).collect(); format!( r##"{paras}"## ) }; // The control fixture, and the overflow one where box B is too short for its text; the // height of a box must not decide whether it is read. for (height_b, label_b) in [(1600, "Box B (tall)"), (400, "Box B (short)")] { let d = read_body(&pict(&format!( concat!( r##""##, r##"{}{}"##, ), shape(100, 1600, "Box A (tall)"), shape(4200, height_b, label_b), ))); let InlineObject::Group { ch_w, ch_h, children, .. } = &objects(&d)[0] else { panic!("{:?}", objects(&d)) }; assert_eq!((*ch_w, *ch_h), (8000.0, 2000.0), "the group's member space"); assert_eq!(children.len(), 2, "both boxes, with box B {height_b} high"); assert_eq!((children[0].x, children[1].x), (100.0, 4200.0), "side by side"); let stories: Vec = children.iter().filter_map(|c| c.obj.text_box()).collect(); assert_eq!(stories.len(), 2, "each box has its own story"); assert_ne!(stories[0], stories[1]); assert_eq!(part_text(&d, stories[0]), "Box A (tall) line 1\nBox A (tall) line 2\nBox A (tall) line 3"); assert_eq!(part_text(&d, stories[1]), format!("{label_b} line 1\n{label_b} line 2\n{label_b} line 3")); // Saved and read again -- the step the report loses the text at. let back = wordcraft_docx::read(&wordcraft_docx::write(&d).unwrap()).unwrap(); let InlineObject::Group { children: again, .. } = &objects(&back)[0] else { panic!() }; let back_stories: Vec = again.iter().filter_map(|c| c.obj.text_box()).collect(); assert_eq!(back_stories.len(), 2, "both boxes survive the save"); assert_eq!(part_text(&back, back_stories[0]), part_text(&d, stories[0])); assert_eq!(part_text(&back, back_stories[1]), part_text(&d, stories[1])); } } #[test] fn a_group_member_finds_its_shapetype_beside_the_group() { // Word writes each `v:shapetype` once, at its first use, as a child of the `w:pict` -- so a // group's member refers to one that is its *parent's* sibling, not its own. The member here // carries no geometry and no colours of its own: its diamond shape comes from the // shapetype's `o:spt` (4) and its fill from the shapetype's `fillcolor`. let d = read_body(&pict(concat!( r##""##, r#""#, r##""##, r#""#, ))); let InlineObject::Group { children, .. } = &objects(&d)[0] else { panic!("{:?}", objects(&d)) }; assert_eq!(children.len(), 1, "the member is read"); let InlineObject::Shape { kind, fill, .. } = children[0].obj else { panic!("{:?}", children[0].obj) }; assert_eq!(kind, ShapeKind::Diamond, "the preset comes from the shapetype's o:spt"); assert_eq!(fill, Some(wordcraft_doc::Rgb(0x00, 0x00, 0xff)), "and the fill from its fillcolor"); } #[test] fn a_top_level_shape_still_finds_its_shapetype() { // The same lookup for a shape that is not in a group, which is what it was written for. let d = read_body(&pict(concat!( r##""##, r##""##, ))); let InlineObject::Shape { kind, fill, .. } = objects(&d)[0] else { panic!("{:?}", objects(&d)) }; assert_eq!((kind, fill), (ShapeKind::Diamond, Some(wordcraft_doc::Rgb(0x00, 0xff, 0x00)))); }