use std::io::Write;
use dxpdf::render::layout::draw_command::{DrawCommand, LayoutedPage};
const RED_DOT_PNG: &[u8] = &[
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1, 8, 2, 0,
0, 0, 144, 119, 83, 222, 0, 0, 0, 12, 73, 68, 65, 84, 120, 218, 99, 248, 207, 192, 0, 0, 3, 1,
1, 0, 247, 3, 65, 67, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,
];
fn make_docx(document_xml: &str) -> Vec<u8> {
let mut buf = Vec::new();
{
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut buf));
let o = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
zip.start_file("[Content_Types].xml", o).unwrap();
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Default Extension="png" ContentType="image/png"/>
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
</Types>"#,
)
.unwrap();
zip.start_file("_rels/.rels", o).unwrap();
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
</Relationships>"#,
)
.unwrap();
zip.start_file("word/_rels/document.xml.rels", o).unwrap();
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId7" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.png"/>
</Relationships>"#,
)
.unwrap();
zip.start_file("word/media/image1.png", o).unwrap();
zip.write_all(RED_DOT_PNG).unwrap();
zip.start_file("word/document.xml", o).unwrap();
zip.write_all(document_xml.as_bytes()).unwrap();
zip.finish().unwrap();
}
buf
}
fn document(run_body: &str) -> String {
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape">
<w:body>
<w:p><w:r>{run_body}</w:r></w:p>
</w:body>
</w:document>"#
)
}
fn vml_rect(label: &str) -> String {
format!(
r##"<w:pict>
<v:rect style="position:absolute;margin-left:100pt;margin-top:20pt;width:120pt;height:50pt"
fillcolor="#DDDDDD">
<v:textbox><w:txbxContent>
<w:p><w:r><w:t>{label}</w:t></w:r></w:p>
</w:txbxContent></v:textbox>
</v:rect>
</w:pict>"##
)
}
fn wps_shape(label: &str) -> String {
format!(
r#"<w:drawing>
<wp:anchor distT="0" distB="0" distL="0" distR="0" simplePos="0"
relativeHeight="1" behindDoc="0" locked="0" layoutInCell="1" allowOverlap="1">
<wp:simplePos x="0" y="0"/>
<wp:positionH relativeFrom="margin"><wp:posOffset>1270000</wp:posOffset></wp:positionH>
<wp:positionV relativeFrom="paragraph"><wp:posOffset>254000</wp:posOffset></wp:positionV>
<wp:extent cx="1524000" cy="635000"/>
<wp:wrapNone/>
<wp:docPr id="1" name="Box"/>
<a:graphic><a:graphicData uri="http://schemas.microsoft.com/office/word/2010/wordprocessingShape">
<wps:wsp>
<wps:cNvSpPr/>
<wps:spPr>
<a:xfrm><a:off x="0" y="0"/><a:ext cx="1524000" cy="635000"/></a:xfrm>
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
<a:solidFill><a:srgbClr val="DDDDDD"/></a:solidFill>
</wps:spPr>
<wps:txbx><w:txbxContent>
<w:p><w:r><w:t>{label}</w:t></w:r></w:p>
</w:txbxContent></wps:txbx>
<wps:bodyPr rot="0" vert="horz" wrap="square" lIns="0" tIns="0" rIns="0" bIns="0" anchor="t"/>
</wps:wsp>
</a:graphicData></a:graphic>
</wp:anchor>
</w:drawing>"#
)
}
fn unpositioned_vml_rect(label: &str) -> String {
format!(
r##"<w:pict>
<v:rect style="width:120pt;height:50pt" fillcolor="#DDDDDD">
<v:textbox><w:txbxContent>
<w:p><w:r><w:t>{label}</w:t></w:r></w:p>
</w:txbxContent></v:textbox>
</v:rect>
</w:pict>"##
)
}
fn vml_image() -> String {
r##"<w:pict>
<v:shape type="#_x0000_t75"
style="position:absolute;margin-left:60pt;margin-top:10pt;width:40pt;height:40pt">
<v:imagedata r:id="rId7"/>
</v:shape>
</w:pict>"##
.to_string()
}
fn inline_wps_shape() -> String {
r#"<w:drawing>
<wp:inline distT="0" distB="0" distL="0" distR="0">
<wp:extent cx="1524000" cy="635000"/>
<wp:docPr id="2" name="InlineBox"/>
<a:graphic><a:graphicData uri="http://schemas.microsoft.com/office/word/2010/wordprocessingShape">
<wps:wsp>
<wps:cNvSpPr/>
<wps:spPr>
<a:xfrm><a:off x="0" y="0"/><a:ext cx="1524000" cy="635000"/></a:xfrm>
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
</wps:spPr>
</wps:wsp>
</a:graphicData></a:graphic>
</wp:inline>
</w:drawing>"#
.to_string()
}
fn alternate_content(requires: &str, choice: &str, fallback: &str) -> String {
format!(
r#"<mc:AlternateContent>
<mc:Choice Requires="{requires}">{choice}</mc:Choice>
<mc:Fallback>{fallback}</mc:Fallback>
</mc:AlternateContent>"#
)
}
fn layout(run_body: &str) -> Vec<LayoutedPage> {
let doc = dxpdf::docx::parse(&make_docx(&document(run_body))).expect("parse");
dxpdf::render::resolve_and_layout(doc).1
}
fn text_count(pages: &[LayoutedPage], label: &str) -> usize {
pages
.iter()
.flat_map(|p| p.commands.iter())
.filter(|c| matches!(c, DrawCommand::Text { text, .. } if text.trim() == label))
.count()
}
fn path_count(pages: &[LayoutedPage]) -> usize {
pages
.iter()
.flat_map(|p| p.commands.iter())
.filter(|c| matches!(c, DrawCommand::Path { .. }))
.count()
}
fn image_count(pages: &[LayoutedPage]) -> usize {
pages
.iter()
.flat_map(|p| p.commands.iter())
.filter(|c| matches!(c, DrawCommand::Image { .. }))
.count()
}
#[test]
fn a_bare_vml_rect_draws_its_geometry_and_its_text_once() {
let pages = layout(&vml_rect("Fallback"));
assert_eq!(path_count(&pages), 1, "the rect's own geometry");
assert_eq!(text_count(&pages, "Fallback"), 1, "its text box, once");
}
#[test]
fn an_unmeetable_choice_hands_the_whole_fallback_over() {
let pages = layout(&alternate_content("x99", "", &vml_rect("Fallback")));
assert_eq!(
path_count(&pages),
1,
"the Fallback's rect is the live branch — its geometry must be drawn",
);
assert_eq!(
text_count(&pages, "Fallback"),
1,
"and its text exactly once"
);
}
#[test]
fn an_unmeetable_choice_hands_over_a_vml_image_too() {
let bare = layout(&vml_image());
let wrapped = layout(&alternate_content("x99", "", &vml_image()));
assert_eq!(
image_count(&bare),
1,
"precondition: the bare pict places it"
);
assert_eq!(
image_count(&wrapped),
1,
"the Fallback's image is the live branch",
);
}
#[test]
fn a_drawable_choice_leaves_a_vml_fallback_image_inert() {
let pages = layout(&alternate_content(
"wps",
&wps_shape("Choice"),
&vml_image(),
));
assert_eq!(image_count(&pages), 0, "the Fallback's image is not live");
assert_eq!(text_count(&pages, "Choice"), 1);
}
#[test]
fn a_live_fallback_renders_exactly_like_the_bare_pict_it_is() {
let bare = layout(&vml_rect("Fallback"));
let wrapped = layout(&alternate_content("x99", "", &vml_rect("Fallback")));
assert_eq!(path_count(&wrapped), path_count(&bare));
assert_eq!(
text_count(&wrapped, "Fallback"),
text_count(&bare, "Fallback"),
);
}
#[test]
fn a_meetable_choice_with_nothing_drawable_still_yields_to_the_fallback() {
let pages = layout(&alternate_content(
"wps",
&inline_wps_shape(),
&vml_rect("Fallback"),
));
assert_eq!(text_count(&pages, "Fallback"), 1, "the Fallback is live");
assert!(
path_count(&pages) >= 1,
"and its rect reaches the page as geometry",
);
}
#[test]
fn a_fallback_with_no_anchored_content_still_reaches_the_page_inline() {
let pages = layout(&alternate_content(
"x99",
"",
&unpositioned_vml_rect("Fallback"),
));
assert_eq!(path_count(&pages), 0, "there is no geometry to draw");
assert_eq!(text_count(&pages, "Fallback"), 1, "but the text survives");
}
#[test]
fn a_drawable_choice_leaves_its_vml_fallback_completely_inert() {
let pages = layout(&alternate_content(
"wps",
&wps_shape("Choice"),
&vml_rect("Fallback"),
));
assert_eq!(path_count(&pages), 1, "one rectangle, drawn once");
assert_eq!(text_count(&pages, "Choice"), 1, "the Choice's text body");
assert_eq!(
text_count(&pages, "Fallback"),
0,
"the Fallback describes the same object — none of it is live",
);
}
#[test]
fn neither_branch_drawable_draws_nothing_and_does_not_panic() {
let pages = layout(&alternate_content("x99", "", ""));
assert_eq!(path_count(&pages), 0);
assert!(!pages.is_empty(), "the document still lays out");
}
#[test]
fn an_unmeetable_choice_with_no_fallback_draws_nothing() {
let pages = layout(
r#"<mc:AlternateContent><mc:Choice Requires="x99"></mc:Choice></mc:AlternateContent>"#,
);
assert_eq!(path_count(&pages), 0);
assert!(!pages.is_empty());
}