//! Original nested-picture controls. No native output or external assets are embedded. use craft_wordmodel::para::{Anchor, CanvasStyle, Float, GroupChild, ShapeKind, Wrap}; use craft_wordmodel::props::Rgb; use craft_wordmodel::{Document, InlineObject, Paragraph, PartKind, Pos, StoryRef, TextBody, para_block}; use std::{io::Write, sync::Arc}; // Original solid RGB(0,72,192), generated in code for these controls. const BLUE_PNG: &[u8] = &[ 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 64, 0, 0, 0, 16, 8, 2, 0, 0, 0, 41, 133, 238, 126, 0, 0, 0, 54, 73, 68, 65, 84, 120, 156, 237, 207, 65, 9, 0, 48, 12, 192, 192, 202, 155, 236, 73, 171, 136, 61, 142, 65, 32, 2, 46, 51, 231, 254, 157, 23, 52, 160, 5, 13, 104, 65, 3, 90, 208, 128, 22, 52, 160, 5, 13, 104, 65, 3, 90, 240, 214, 2, 129, 33, 32, 61, 59, 114, 68, 120, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130, ]; const INLINE_XML: &str = r#" 2533 Plain | red parent 180 pt wide | saved height 80 pt9144001270000Parent"#; fn picture() -> InlineObject { InlineObject::Image { media: "original-blue.png".into(), w: 40.0, h: 10.0, alt: String::new(), float: Default::default(), crop: [0.0; 4], fx: Default::default(), ole: None, } } fn model_box() -> Document { let mut doc = Document::new(); doc.media.insert("original-blue.png".into(), Arc::new(BLUE_PNG.to_vec())); let id = doc.add_part(PartKind::TextBox, vec![para_block(Paragraph::with_text("Parent", Default::default()))]); doc.parts.get_mut(&id).unwrap().body = TextBody { auto_fit: true, inset: Some([0; 4]), ..Default::default() }; doc.insert_object(&Pos { story: StoryRef::Part(id), path: craft_wordmodel::Path(vec![0]), off: 0 }, picture(), &Default::default()).unwrap(); let parent = InlineObject::Shape { kind: ShapeKind::TextBox, w: 180.0, h: 80.0, fill: Some(craft_wordmodel::fill::Fill::solid(Rgb(240, 248, 240))), stroke: Some(Rgb(192, 0, 0)), stroke_width: 1.0, float: Float { wrap: Wrap::InFrontOfText, h_rel: Anchor::Page, v_rel: Anchor::Page, x: 72.0, y: 100.0, ..Default::default() }, story: Some(id), freeform: None, effects: Default::default(), extra: Default::default(), }; doc.insert_object(&Pos::body(0, 0), parent, &Default::default()).unwrap(); doc } fn check_box(doc: &Document) { let objects = &doc.body[0].as_para().unwrap().objects; assert_eq!(objects.len(), 1); let InlineObject::Shape { kind, w, h, float, story: Some(id), stroke, stroke_width, .. } = &objects[0] else { panic!("outer textbox lost: {:?}", objects[0]); }; assert_eq!((*kind, *w, *h, *stroke, *stroke_width), (ShapeKind::TextBox, 180.0, 80.0, Some(Rgb(192, 0, 0)), 1.0)); assert_eq!((float.wrap, float.h_rel, float.v_rel, float.x, float.y), (Wrap::InFrontOfText, Anchor::Page, Anchor::Page, 72.0, 100.0)); let part = &doc.parts[id]; assert_eq!(part.kind, PartKind::TextBox); assert!(part.body.auto_fit); assert_eq!(part.body.inset, Some([0; 4])); assert_eq!(doc.parts.len(), 1); let para = part.blocks[0].as_para().unwrap(); assert_eq!(para.text, "\u{fffc}Parent"); assert_eq!(para.objects.len(), 1); let InlineObject::Image { w, h, float, media, .. } = ¶.objects[0] else { panic!("inline picture lost"); }; assert_eq!((*w, *h, float.wrap), (40.0, 10.0, Wrap::Inline)); assert_eq!(doc.media[media].as_slice(), BLUE_PNG); } fn package(xml: &str) -> Vec { let mut zip = zip::ZipWriter::new(std::io::Cursor::new(Vec::new())); let rels = br#""#; let docrels = br#""#; let types = br#""#; for (name, bytes) in [ ("_rels/.rels", rels.as_slice()), ("[Content_Types].xml", types.as_slice()), ("word/document.xml", xml.as_bytes()), ("word/_rels/document.xml.rels", docrels.as_slice()), ("word/media/isolation-blue.png", BLUE_PNG), ] { zip.start_file(name, zip::write::SimpleFileOptions::default()).unwrap(); zip.write_all(bytes).unwrap(); } zip.finish().unwrap().into_inner() } #[test] fn model_nested_picture_roundtrip() { let doc = model_box(); check_box(&doc); let bytes = craft_docx::write(&doc).unwrap(); check_box(&craft_docx::read(&bytes).unwrap()); } #[test] fn original_inline_xml_keeps_box() { let doc = craft_docx::read(&package(INLINE_XML)).unwrap(); check_box(&doc); check_box(&craft_docx::read(&craft_docx::write(&doc).unwrap()).unwrap()); } #[test] fn picture_group_canvas_keep_kind() { let group = |canvas| InlineObject::Group { w: 100.0, h: 50.0, float: Default::default(), ch_w: 100.0, ch_h: 50.0, children: vec![GroupChild { x: 3.0, y: 4.0, obj: picture() }], canvas, }; for original in [picture(), group(None), group(Some(CanvasStyle::default()))] { let mut doc = Document::new(); doc.media.insert("original-blue.png".into(), Arc::new(BLUE_PNG.to_vec())); doc.insert_object(&Pos::body(0, 0), original.clone(), &Default::default()).unwrap(); let got = craft_docx::read(&craft_docx::write(&doc).unwrap()).unwrap(); let object = &got.body[0].as_para().unwrap().objects[0]; match (&original, object) { (InlineObject::Image { .. }, InlineObject::Image { w, h, media, .. }) => { assert_eq!((*w, *h), (40.0, 10.0)); assert_eq!(got.media[media].as_slice(), BLUE_PNG); } (InlineObject::Group { canvas: expected, .. }, InlineObject::Group { w, h, children, canvas, .. }) => { assert_eq!((*w, *h), (100.0, 50.0)); assert_eq!(canvas.is_some(), expected.is_some()); assert_eq!(children.len(), 1); let child = &children[0]; assert_eq!((child.x, child.y), (3.0, 4.0)); let InlineObject::Image { w, h, media, .. } = &child.obj else { panic!("picture member changed kind"); }; assert_eq!((*w, *h), (40.0, 10.0)); assert_eq!(got.media[media].as_slice(), BLUE_PNG); } _ => panic!("payload changed kind: {object:?}"), } assert!(got.parts.is_empty()); } } #[test] fn own_picture_fill_keeps_image() { // Keep the existing WPS picture-fill fallback, distinct from a nested picture. let start = INLINE_XML.find("").unwrap(); let end = INLINE_XML.find("").unwrap(); let payload = r#""#; let xml = format!("{}{}{}", &INLINE_XML[..start], payload, &INLINE_XML[end..]); let doc = craft_docx::read(&package(&xml)).unwrap(); assert!(doc.parts.is_empty()); let objects = &doc.body[0].as_para().unwrap().objects; assert_eq!(objects.len(), 1); let InlineObject::Image { w, h, media, float, .. } = &objects[0] else { panic!("own picture fill lost: {:?}", objects[0]); }; assert_eq!((*w, *h), (180.0, 80.0)); assert_eq!((float.wrap, float.x, float.y), (Wrap::InFrontOfText, 72.0, 100.0)); assert_eq!(doc.media[media].as_slice(), BLUE_PNG); }