use std::io::Write;
use dxpdf::render::layout::draw_command::{DrawCommand, LayoutedPage};
const RED: &str = "#FF0000";
const BLUE: &str = "#0000FF";
fn make_docx(document_xml: &str, styles_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"/>
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+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="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
</Relationships>"#,
)
.unwrap();
zip.start_file("word/styles.xml", o).unwrap();
zip.write_all(styles_xml.as_bytes()).unwrap();
zip.start_file("word/document.xml", o).unwrap();
zip.write_all(document_xml.as_bytes()).unwrap();
zip.finish().unwrap();
}
buf
}
fn styles() -> &'static str {
r#"<?xml version="1.0" encoding="UTF-8"?>
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:style w:type="table" w:styleId="TestTbl">
<w:name w:val="Test Table"/>
<w:tblPr/>
<w:tblStylePr w:type="firstCol">
<w:tcPr><w:shd w:val="clear" w:color="auto" w:fill="FF0000"/></w:tcPr>
</w:tblStylePr>
<w:tblStylePr w:type="lastCol">
<w:tcPr><w:shd w:val="clear" w:color="auto" w:fill="0000FF"/></w:tcPr>
</w:tblStylePr>
</w:style>
</w:styles>"#
}
fn document(rows: &str) -> String {
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:tbl>
<w:tblPr>
<w:tblStyle w:val="TestTbl"/>
<w:tblW w:w="8000" w:type="dxa"/>
<w:tblLook w:firstRow="0" w:lastRow="0" w:firstColumn="1"
w:lastColumn="1" w:noHBand="1" w:noVBand="1"/>
</w:tblPr>
<w:tblGrid>
<w:gridCol w:w="2000"/><w:gridCol w:w="2000"/>
<w:gridCol w:w="2000"/><w:gridCol w:w="2000"/>
</w:tblGrid>
{rows}
</w:tbl>
<w:sectPr>
<w:pgSz w:w="12240" w:h="15840"/>
<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"
w:header="720" w:footer="720" w:gutter="0"/>
</w:sectPr>
</w:body>
</w:document>"#
)
}
fn cell(span: u32) -> String {
let pr = if span > 1 {
format!(r#"<w:tcPr><w:gridSpan w:val="{span}"/></w:tcPr>"#)
} else {
String::new()
};
format!("<w:tc>{pr}<w:p><w:r><w:t>x</w:t></w:r></w:p></w:tc>")
}
fn layout(document_xml: &str) -> Vec<LayoutedPage> {
let doc = dxpdf::docx::parse(&make_docx(document_xml, styles())).expect("parse");
dxpdf::render::resolve_and_layout(doc).1
}
fn shaded(pages: &[LayoutedPage]) -> Vec<(String, i32, i32)> {
let mut v: Vec<(String, i32, i32)> = pages
.iter()
.flat_map(|p| &p.commands)
.filter_map(|c| match c {
DrawCommand::Rect { rect, color, .. } => {
let hex = format!("#{:02X}{:02X}{:02X}", color.r, color.g, color.b);
(hex == RED || hex == BLUE).then(|| {
(
hex,
rect.origin.x.raw().round() as i32,
rect.size.width.raw().round() as i32,
)
})
}
_ => None,
})
.collect();
v.sort();
v
}
#[test]
fn a_span_that_stops_short_of_the_last_grid_column_is_not_the_last_column() {
let pages = layout(&document(&format!("<w:tr>{}</w:tr>", cell(3))));
assert_eq!(
shaded(&pages),
vec![(RED.to_string(), 72, 300)],
"grid columns 0..2 of 4: firstCol only, over the cell's full 3-column width"
);
}
#[test]
fn a_span_that_reaches_the_last_grid_column_is_the_last_column() {
let row = format!("<w:tr>{}{}{}</w:tr>", cell(1), cell(1), cell(2));
let pages = layout(&document(&row));
assert_eq!(
shaded(&pages),
vec![(BLUE.to_string(), 272, 200), (RED.to_string(), 72, 100),],
"grid column 0 is firstCol; the cell covering 2..3 reaches the last column"
);
}
#[test]
fn grid_before_moves_the_first_cell_out_of_the_first_column_region() {
let row = format!(
r#"<w:tr><w:trPr><w:gridBefore w:val="1"/></w:trPr>{}{}{}</w:tr>"#,
cell(1),
cell(1),
cell(1)
);
let pages = layout(&document(&row));
assert_eq!(
shaded(&pages),
vec![(BLUE.to_string(), 372, 100)],
"the row's first cell sits at grid column 1, so nothing is firstCol"
);
}
#[test]
fn one_cell_per_grid_column_marks_the_outer_two() {
let row = format!("<w:tr>{}{}{}{}</w:tr>", cell(1), cell(1), cell(1), cell(1));
let pages = layout(&document(&row));
assert_eq!(
shaded(&pages),
vec![(BLUE.to_string(), 372, 100), (RED.to_string(), 72, 100)],
);
}
fn cell_with_raw_span(span: &str) -> String {
format!(
r#"<w:tc><w:tcPr><w:gridSpan w:val="{span}"/></w:tcPr>
<w:p><w:r><w:t>x</w:t></w:r></w:p></w:tc>"#
)
}
#[test]
fn a_zero_grid_span_covers_one_grid_column_like_an_absent_one() {
let with_zero = layout(&document(&format!(
"<w:tr>{}{}{}{}</w:tr>",
cell_with_raw_span("0"),
cell(1),
cell(1),
cell(1)
)));
let with_one = layout(&document(&format!(
"<w:tr>{}{}{}{}</w:tr>",
cell_with_raw_span("1"),
cell(1),
cell(1),
cell(1)
)));
assert_eq!(
shaded(&with_zero),
shaded(&with_one),
"a zero gridSpan must address the grid exactly as a span of one"
);
assert_eq!(
shaded(&with_zero),
vec![(BLUE.to_string(), 372, 100), (RED.to_string(), 72, 100)],
"and that grid is the plain one-cell-per-column grid: firstCol at the \
left margin, lastCol over the fourth column"
);
}