use std::io::Write;
use dxpdf::render::layout::draw_command::DrawCommand;
fn make_docx(document_xml: &str) -> Vec<u8> {
let buf = std::io::Cursor::new(Vec::new());
let mut zip = zip::ZipWriter::new(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"/>
<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/document.xml", o).unwrap();
zip.write_all(document_xml.as_bytes()).unwrap();
zip.finish().unwrap().into_inner()
}
fn doc(body: &str) -> Vec<u8> {
make_docx(&format!(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>{body}</w:body>
</w:document>"#
))
}
fn layout(bytes: &[u8]) -> Vec<dxpdf::render::layout::draw_command::LayoutedPage> {
let parsed = dxpdf::docx::parse(bytes).expect("parse");
dxpdf::render::resolve_and_layout(parsed).1
}
fn y_of(pages: &[dxpdf::render::layout::draw_command::LayoutedPage], needle: &str) -> f32 {
pages
.iter()
.flat_map(|p| &p.commands)
.find_map(|c| match c {
DrawCommand::Text { text, position, .. } if &**text == needle => Some(position.y.raw()),
_ => None,
})
.unwrap_or_else(|| panic!("no text command {needle:?}"))
}
fn lone_cell_table(vmerge: &str) -> Vec<u8> {
doc(&format!(
r#"<w:p><w:r><w:t>before</w:t></w:r></w:p>
<w:tbl>
<w:tblPr><w:tblW w:w="8000" w:type="dxa"/></w:tblPr>
<w:tblGrid><w:gridCol w:w="8000"/></w:tblGrid>
<w:tr><w:tc><w:tcPr><w:tcW w:w="8000" w:type="dxa"/>{vmerge}</w:tcPr>
<w:p><w:r><w:t>AAA</w:t></w:r></w:p>
<w:p><w:r><w:t>BBB</w:t></w:r></w:p>
<w:p><w:r><w:t>CCC</w:t></w:r></w:p>
</w:tc></w:tr>
</w:tbl>
<w:p><w:r><w:t>after</w:t></w:r></w:p>"#
))
}
#[test]
fn lone_vmerge_restart_does_not_let_following_content_overlap_the_table() {
let pages = layout(&lone_cell_table(r#"<w:vMerge w:val="restart"/>"#));
let last_row_text = y_of(&pages, "CCC");
let after = y_of(&pages, "after");
assert!(
after > last_row_text,
"\"after\" must follow the table's last line (y={last_row_text:.1}), \
but was drawn at y={after:.1} — on top of it"
);
}
#[test]
fn lone_vmerge_restart_matches_the_unmerged_layout() {
let restart = layout(&lone_cell_table(r#"<w:vMerge w:val="restart"/>"#));
let control = layout(&lone_cell_table(""));
assert!(
(y_of(&restart, "after") - y_of(&control, "after")).abs() < 0.01,
"lone restart moved the following paragraph: {:.1} vs control {:.1}",
y_of(&restart, "after"),
y_of(&control, "after"),
);
}
#[test]
fn oversized_cant_split_row_does_not_emit_a_blank_leading_page() {
let paras: String = (0..70)
.map(|i| format!("<w:p><w:r><w:t>line {i}</w:t></w:r></w:p>"))
.collect();
let pages = layout(&doc(&format!(
r#"<w:tbl>
<w:tblPr><w:tblW w:w="8000" w:type="dxa"/></w:tblPr>
<w:tblGrid><w:gridCol w:w="8000"/></w:tblGrid>
<w:tr><w:trPr><w:cantSplit/></w:trPr><w:tc>
<w:tcPr><w:tcW w:w="8000" w:type="dxa"/></w:tcPr>
{paras}
</w:tc></w:tr>
</w:tbl>
<w:p><w:r><w:t>after</w:t></w:r></w:p>"#
)));
assert!(
!pages[0].commands.is_empty(),
"page 0 is blank — the table advanced to a page that gave it no more room"
);
}
#[test]
fn table_after_body_text_still_moves_to_the_next_page_when_it_does_not_fit() {
let filler: String = (0..60)
.map(|i| format!("<w:p><w:r><w:t>filler {i}</w:t></w:r></w:p>"))
.collect();
let rows: String = (0..12)
.map(|i| {
format!(
r#"<w:tr><w:trPr><w:cantSplit/></w:trPr><w:tc>
<w:tcPr><w:tcW w:w="8000" w:type="dxa"/></w:tcPr>
<w:p><w:r><w:t>row {i}</w:t></w:r></w:p></w:tc></w:tr>"#
)
})
.collect();
let pages = layout(&doc(&format!(
r#"{filler}
<w:tbl>
<w:tblPr><w:tblW w:w="8000" w:type="dxa"/></w:tblPr>
<w:tblGrid><w:gridCol w:w="8000"/></w:tblGrid>
{rows}
</w:tbl>"#
)));
assert!(
pages.len() > 1,
"filler plus a 12-row table should not fit on one page"
);
for (i, page) in pages.iter().enumerate() {
assert!(!page.commands.is_empty(), "page {i} is blank");
}
}