//! 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);
}