pub mod common;
use ciclostile::{Docx, Image, Length, error::InvalidImage};
use common::{CONTENT_TYPES, DOCUMENT_RELS, FOOTER_RELS, Fixture, JPEG, PNG};
const IMAGE: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
fn png() -> Image {
Image::from_bytes(PNG)
.unwrap()
.width(Length::from_cm(3.0))
.alt_text("Logo")
}
fn jpeg() -> Image {
Image::from_bytes(JPEG).unwrap()
}
fn relationship(id: &str, target: &str) -> String {
format!(r#"<Relationship Id="{id}" Type="{IMAGE}" Target="{target}"/>"#)
}
fn appended(rels: &str, relationship: &str) -> String {
rels.replace(
"</Relationships>",
&format!("{relationship}</Relationships>"),
)
}
#[test]
fn image_replaces_its_placeholders_in_every_story() {
let template = Fixture::template().zip();
let baseline = common::untouched(&template);
let mut docx = Docx::from_bytes(template).unwrap();
assert_eq!(docx.replace_with_image("{{logo}}", &png()), 2);
let output = docx.to_bytes();
let added = common::assert_kept_except(
&baseline,
&output,
&[
"word/document.xml",
"word/_rels/document.xml.rels",
"word/header1.xml",
],
);
assert_eq!(
added,
["word/_rels/header1.xml.rels", "word/media/image2.png"]
);
assert_eq!(common::read(&output, "word/media/image2.png"), PNG);
assert_eq!(
common::read_text(&output, "word/_rels/document.xml.rels"),
appended(DOCUMENT_RELS, &relationship("rId11", "media/image2.png"))
);
assert_eq!(
common::read_text(&output, "word/_rels/header1.xml.rels"),
concat!(
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n",
r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">"#,
r#"<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image""#,
r#" Target="media/image2.png"/></Relationships>"#,
)
);
let document = common::read_text(&output, "word/document.xml");
let header = common::read_text(&output, "word/header1.xml");
assert!(!document.contains("{{logo}}") && !header.contains("logo}}"));
assert_eq!(common::count(&document, r#"<a:blip r:embed="rId11"/>"#), 1);
assert_eq!(common::count(&header, r#"<a:blip r:embed="rId1"/>"#), 1);
for xml in [&document, &header] {
assert_eq!(
common::count(xml, r#"<wp:extent cx="1080000" cy="720000"/>"#),
1
);
assert_eq!(common::count(xml, r#" descr="Logo"/>"#), 1);
}
let mut ids = common::drawing_ids(&document);
ids.extend(common::drawing_ids(&header));
ids.sort_unstable();
assert_eq!(ids, [1, 2, 3, 4]);
Docx::from_bytes(output).unwrap();
}
#[test]
fn image_in_a_story_with_relationships_gets_the_next_id() {
let template = Fixture::template().zip();
let baseline = common::untouched(&template);
let mut docx = Docx::from_bytes(template).unwrap();
assert_eq!(docx.replace_with_image("{{signature}}", &jpeg()), 1);
let output = docx.to_bytes();
let added = common::assert_kept_except(
&baseline,
&output,
&[
"[Content_Types].xml",
"word/footer1.xml",
"word/_rels/footer1.xml.rels",
],
);
assert_eq!(added, ["word/media/image1.jpeg"]);
assert_eq!(
common::read_text(&output, "word/_rels/footer1.xml.rels"),
appended(FOOTER_RELS, &relationship("rId2", "media/image1.jpeg"))
);
assert_eq!(
common::read_text(&output, "[Content_Types].xml"),
CONTENT_TYPES.replace(
r#"<Default Extension="xml" ContentType="application/xml"/>"#,
concat!(
r#"<Default Extension="xml" ContentType="application/xml"/>"#,
r#"<Default Extension="jpeg" ContentType="image/jpeg"/>"#,
),
)
);
let footer = common::read_text(&output, "word/footer1.xml");
assert_eq!(common::count(&footer, r#"<a:blip r:embed="rId2"/>"#), 1);
assert_eq!(common::drawing_ids(&footer), [3]);
assert_eq!(
common::count(&footer, r#"<wp:extent cx="28575" cy="19050"/>"#),
1
);
}
#[test]
fn each_call_adds_its_own_media_part() {
let template = Fixture::template().zip();
let baseline = common::untouched(&template);
let mut docx = Docx::from_bytes(template).unwrap();
assert_eq!(docx.replace_with_image("{{logo}}", &png()), 2);
assert_eq!(docx.replace_with_image("{{signature}}", &png()), 1);
let output = docx.to_bytes();
let added = common::assert_kept_except(
&baseline,
&output,
&[
"word/document.xml",
"word/_rels/document.xml.rels",
"word/header1.xml",
"word/footer1.xml",
"word/_rels/footer1.xml.rels",
],
);
assert_eq!(
added,
[
"word/_rels/header1.xml.rels",
"word/media/image2.png",
"word/media/image3.png"
]
);
assert_eq!(
common::read_text(&output, "word/_rels/footer1.xml.rels"),
appended(FOOTER_RELS, &relationship("rId2", "media/image3.png"))
);
let mut ids: Vec<u32> = ["word/document.xml", "word/header1.xml", "word/footer1.xml"]
.into_iter()
.flat_map(|name| common::drawing_ids(&common::read_text(&output, name)))
.collect();
ids.sort_unstable();
assert_eq!(ids, [1, 2, 3, 4, 5]);
}
#[test]
fn media_names_skip_taken_names_ignoring_case() {
let fixture = Fixture::template()
.with("word/media/IMAGE2.PNG", PNG)
.with("word/media/image3.png", PNG);
let template = fixture.zip();
let baseline = common::untouched(&template);
let mut docx = Docx::from_bytes(template).unwrap();
assert_eq!(docx.replace_with_image("{{signature}}", &png()), 1);
let added = common::assert_kept_except(
&baseline,
&docx.to_bytes(),
&["word/footer1.xml", "word/_rels/footer1.xml.rels"],
);
assert_eq!(added, ["word/media/image4.png"]);
}
#[test]
fn missing_placeholder_adds_nothing() {
let template = Fixture::template().zip();
let baseline = common::untouched(&template);
let mut docx = Docx::from_bytes(template).unwrap();
assert_eq!(docx.replace_with_image("{{nowhere}}", &jpeg()), 0);
assert_eq!(docx.to_bytes(), baseline);
assert_eq!(docx.replace_with_image("{{signature}}", &jpeg()), 1);
assert!(common::contains(&docx.to_bytes(), "word/media/image1.jpeg"));
}
#[test]
fn empty_placeholder_matches_nothing() {
let template = Fixture::template().zip();
let baseline = common::untouched(&template);
let mut docx = Docx::from_bytes(template).unwrap();
assert_eq!(docx.replace_with_image("", &png()), 0);
assert_eq!(docx.to_bytes(), baseline);
}
#[test]
fn new_drawings_skip_ids_used_by_comments_and_the_glossary() {
let fixture = Fixture::template()
.with(
"word/_rels/document.xml.rels",
appended(DOCUMENT_RELS, common::COMMENTS_AND_GLOSSARY_RELS),
)
.with("word/comments.xml", common::COMMENTS)
.with("word/glossary/document.xml", common::GLOSSARY);
let template = fixture.zip();
let baseline = common::untouched(&template);
let mut docx = Docx::from_bytes(template).unwrap();
assert_eq!(docx.replace_with_image("{{logo}}", &png()), 2);
let output = docx.to_bytes();
let added = common::assert_kept_except(
&baseline,
&output,
&[
"word/document.xml",
"word/_rels/document.xml.rels",
"word/header1.xml",
],
);
assert_eq!(
added,
["word/_rels/header1.xml.rels", "word/media/image2.png"]
);
let mut ids: Vec<u32> = ["word/document.xml", "word/header1.xml"]
.into_iter()
.flat_map(|name| common::drawing_ids(&common::read_text(&output, name)))
.collect();
ids.sort_unstable();
assert_eq!(ids, [1, 2, 5, 6]);
}
#[test]
fn missing_comments_and_glossary_are_skipped() {
let fixture = Fixture::template().with(
"word/_rels/document.xml.rels",
appended(DOCUMENT_RELS, common::COMMENTS_AND_GLOSSARY_RELS),
);
let mut docx = Docx::from_bytes(fixture.zip()).unwrap();
assert_eq!(docx.replace_with_image("{{signature}}", &png()), 1);
let footer = common::read_text(&docx.to_bytes(), "word/footer1.xml");
assert_eq!(common::drawing_ids(&footer), [3]);
}
#[test]
fn gif_files_are_unsupported() {
let gif = b"GIF89a\x03\x00\x02\x00\x80\x00\x00\xff\x00\x00\x00\x00\xff\x3b";
assert!(matches!(
Image::from_bytes(&gif[..]),
Err(InvalidImage::UnsupportedFormat)
));
}
#[test]
fn media_follow_the_main_part_and_targets_are_relative() {
let main = common::DOCUMENT.replace(
r#"<w:footerReference w:type="default" r:id="rId10"/>"#,
r#"<w:footerReference w:type="default" r:id="rId2"/>"#,
);
let fixture = Fixture::new([("[Content_Types].xml", CONTENT_TYPES.as_bytes())])
.with(
"_rels/.rels",
common::PACKAGE_RELS.replace("word/document.xml", "content/main.xml"),
)
.with("content/main.xml", main)
.with(
"content/_rels/main.xml.rels",
concat!(
r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">"#,
r#"<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" Target="../shared/footer.xml"/>"#,
"</Relationships>",
),
)
.with("shared/footer.xml", common::FOOTER);
let mut docx = Docx::from_bytes(fixture.zip()).unwrap();
assert_eq!(docx.replace_with_image("{{logo}}", &png()), 1);
assert_eq!(docx.replace_with_image("{{signature}}", &png()), 1);
let output = docx.to_bytes();
assert!(common::contains(&output, "content/media/image1.png"));
assert!(common::contains(&output, "content/media/image2.png"));
assert!(
common::read_text(&output, "content/_rels/main.xml.rels")
.contains(&relationship("rId3", "media/image1.png"))
);
assert!(
common::read_text(&output, "shared/_rels/footer.xml.rels")
.contains(&relationship("rId1", "../content/media/image2.png"))
);
}