use std::io::Write;
use dxpdf::render::layout::draw_command::{DrawCommand, LayoutedPage};
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()
}
pub fn table_doc(tbl_w: &str, grid_cols: &str, rows: &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>
<w:tbl>
<w:tblPr>
{tbl_w}
<w:tblLayout w:type="fixed"/>
<w:tblBorders>
<w:top w:val="single" w:sz="4" w:color="000000"/>
<w:left w:val="single" w:sz="4" w:color="000000"/>
<w:bottom w:val="single" w:sz="4" w:color="000000"/>
<w:right w:val="single" w:sz="4" w:color="000000"/>
<w:insideH w:val="single" w:sz="4" w:color="000000"/>
<w:insideV w:val="single" w:sz="4" w:color="000000"/>
</w:tblBorders>
</w:tblPr>
<w:tblGrid>{grid_cols}</w:tblGrid>
{rows}
</w:tbl>
</w:body>
</w:document>"#
))
}
pub fn grid(widths: &[i32]) -> String {
widths
.iter()
.map(|w| format!(r#"<w:gridCol w:w="{w}"/>"#))
.collect()
}
pub fn cell(text: &str, tcw: Option<(i32, &str)>, span: Option<i32>) -> String {
let w = match tcw {
Some((v, t)) => format!(r#"<w:tcW w:w="{v}" w:type="{t}"/>"#),
None => String::new(),
};
let s = match span {
Some(n) => format!(r#"<w:gridSpan w:val="{n}"/>"#),
None => String::new(),
};
format!(r#"<w:tc><w:tcPr>{w}{s}</w:tcPr><w:p><w:r><w:t>{text}</w:t></w:r></w:p></w:tc>"#)
}
pub fn row(cells: &str) -> String {
format!("<w:tr>{cells}</w:tr>")
}
pub fn row_with(tr_pr: &str, cells: &str) -> String {
format!("<w:tr><w:trPr>{tr_pr}</w:trPr>{cells}</w:tr>")
}
pub fn layout(bytes: &[u8]) -> Vec<LayoutedPage> {
let parsed = dxpdf::docx::parse(bytes).expect("parse");
dxpdf::render::resolve_and_layout(parsed).1
}
pub fn first_row_cells(pages: &[LayoutedPage]) -> Vec<(f32, f32)> {
let rects: Vec<(f32, f32, f32, f32)> = pages
.iter()
.flat_map(|p| &p.commands)
.filter_map(|c| match c {
DrawCommand::Rect { rect, .. } => {
let (x, y) = (rect.origin.x.raw(), rect.origin.y.raw());
let (w, h) = (rect.size.width.raw(), rect.size.height.raw());
(w.min(h) <= 1.0).then_some((x, x + w, y, y + h))
}
_ => None,
})
.collect();
let is_vertical =
|&(x0, x1, y0, y1): &(f32, f32, f32, f32)| x1 - x0 <= 1.0 && y1 - y0 > x1 - x0;
let first = rects
.iter()
.filter(|r| is_vertical(r))
.map(|r| r.2)
.fold(f32::INFINITY, f32::min);
if !first.is_finite() {
return Vec::new();
}
let mut bands: Vec<(f32, f32)> = rects
.iter()
.filter(|r| is_vertical(r) && (r.2 - first).abs() < 0.01)
.map(|(x0, x1, ..)| (*x0, *x1))
.collect();
bands.sort_by(|p, q| p.0.total_cmp(&q.0));
bands.dedup_by(|a, b| (a.0 - b.0).abs() < 0.01);
if bands.len() < 2 {
return Vec::new();
}
let lines: Vec<f32> = bands.iter().map(|&(x0, x1)| (x0 + x1) * 0.5).collect();
lines.windows(2).map(|p| (p[0], p[1] - p[0])).collect()
}
const HALF_BORDER: f32 = 0.26;
pub fn assert_cells(got: &[(f32, f32)], want: &[(f32, f32)], what: &str) {
assert_eq!(
got.len(),
want.len(),
"{what}: expected {} cells, drew {}: {got:?}",
want.len(),
got.len()
);
for (i, (g, w)) in got.iter().zip(want).enumerate() {
assert!(
(g.0 - w.0).abs() < HALF_BORDER && (g.1 - w.1).abs() < 2.0 * HALF_BORDER,
"{what}: cell {i} drawn at x={:.2} w={:.2}, expected x={:.2} w={:.2} — all: {got:?}",
g.0,
g.1,
w.0,
w.1
);
}
}
#[test]
fn a_grid_that_seats_every_cell_is_scaled_proportionally() {
let pages = layout(&table_doc(
r#"<w:tblW w:w="6000" w:type="dxa"/>"#,
&grid(&[500, 1000, 1500]),
&row(&format!(
"{}{}{}",
cell("a", None, None),
cell("b", None, None),
cell("c", None, None)
)),
));
assert_cells(
&first_row_cells(&pages),
&[(72.0, 50.0), (122.0, 100.0), (222.0, 150.0)],
"declared grid scaled to tblW",
);
}
#[test]
fn grid_span_counts_toward_seating() {
let pages = layout(&table_doc(
r#"<w:tblW w:w="6000" w:type="dxa"/>"#,
&grid(&[2000, 2000, 2000]),
&row(&format!(
"{}{}",
cell("a", None, None),
cell("b", None, Some(2))
)),
));
assert_cells(
&first_row_cells(&pages),
&[(72.0, 100.0), (172.0, 200.0)],
"gridSpan seats the grid",
);
}
#[test]
fn grid_before_and_after_count_toward_seating() {
let pages = layout(&table_doc(
r#"<w:tblW w:w="8000" w:type="dxa"/>"#,
&grid(&[2000, 2000, 2000, 2000]),
&row_with(
r#"<w:gridBefore w:val="1"/><w:gridAfter w:val="2"/>"#,
&cell("a", None, None),
),
));
assert_cells(
&first_row_cells(&pages),
&[(172.0, 100.0)],
"gridBefore offsets the cell and seats the grid",
);
}
#[test]
fn a_row_shorter_than_the_grid_is_left_short() {
let pages = layout(&table_doc(
r#"<w:tblW w:w="8000" w:type="dxa"/>"#,
&grid(&[2000, 2000, 2000, 2000]),
&row(&format!(
"{}{}",
cell("a", Some((4000, "dxa")), None),
cell("b", Some((4000, "dxa")), None)
)),
));
assert_cells(
&first_row_cells(&pages),
&[(72.0, 100.0), (172.0, 100.0)],
"a short row keeps its grid columns and stops",
);
}
#[test]
fn a_cell_the_grid_cannot_seat_still_gets_a_column() {
let pages = layout(&table_doc(
r#"<w:tblW w:w="9600" w:type="dxa"/>"#,
&grid(&[4800, 4800]),
&row(&format!(
"{}{}{}{}",
cell("a", Some((2400, "dxa")), None),
cell("b", Some((2400, "dxa")), None),
cell("c", Some((2400, "dxa")), None),
cell("d", Some((2400, "dxa")), None)
)),
));
let cells = first_row_cells(&pages);
assert_eq!(cells.len(), 4, "four cells were declared: {cells:?}");
for (i, (x, w)) in cells.iter().enumerate() {
assert!(
*w > 0.0,
"cell {i} drew at zero width — the grid could not seat it and it \
was dropped rather than given a column: {cells:?}"
);
assert!(x.is_finite(), "cell {i} has no position: {cells:?}");
}
for i in 1..cells.len() {
assert!(
cells[i].0 > cells[i - 1].0 + 0.01,
"cells {} and {i} collapsed onto the same column: {cells:?}",
i - 1
);
}
}
#[test]
fn an_unseated_cell_is_sized_from_its_declared_tcw() {
let pages = layout(&table_doc(
r#"<w:tblW w:w="9600" w:type="dxa"/>"#,
&grid(&[4800, 4800]),
&row(&format!(
"{}{}{}{}",
cell("a", Some((4800, "dxa")), None),
cell("b", Some((4800, "dxa")), None),
cell("c", Some((1200, "dxa")), None),
cell("d", Some((3600, "dxa")), None)
)),
));
assert_cells(
&first_row_cells(&pages),
&[(72.0, 160.0), (232.0, 160.0), (392.0, 40.0), (432.0, 120.0)],
"appended columns take the unseated cells' declared tcW",
);
}
#[test]
fn a_span_the_grid_cannot_seat_gets_every_column_it_declares() {
let pages = layout(&table_doc(
r#"<w:tblW w:w="9600" w:type="dxa"/>"#,
&grid(&[4800, 4800]),
&row(&format!(
"{}{}",
cell("a", Some((4800, "dxa")), None),
cell("b", Some((7200, "dxa")), Some(3))
)),
));
assert_cells(
&first_row_cells(&pages),
&[(72.0, 192.0), (264.0, 288.0)],
"the span's missing columns share what its tcW leaves over",
);
}
#[test]
fn a_cell_with_no_declared_width_still_gets_a_column() {
let pages = layout(&table_doc(
r#"<w:tblW w:w="9600" w:type="dxa"/>"#,
&grid(&[4800, 4800]),
&row(&format!(
"{}{}{}",
cell("a", None, None),
cell("b", None, None),
cell("c", None, None)
)),
));
assert_cells(
&first_row_cells(&pages),
&[(72.0, 160.0), (232.0, 160.0), (392.0, 160.0)],
"a cell with no tcW takes the mean of the declared columns",
);
}
#[test]
fn a_table_with_no_grid_takes_its_columns_from_tcw() {
let pages = layout(&table_doc(
r#"<w:tblW w:w="9600" w:type="dxa"/>"#,
"",
&row(&format!(
"{}{}{}",
cell("a", Some((1600, "dxa")), None),
cell("b", Some((4800, "dxa")), None),
cell("c", Some((3200, "dxa")), None)
)),
));
assert_cells(
&first_row_cells(&pages),
&[(72.0, 80.0), (152.0, 240.0), (392.0, 160.0)],
"an absent grid is rebuilt from the declared cell widths",
);
}
#[test]
fn the_widest_claim_on_an_appended_column_wins() {
let rows = format!(
"{}{}",
row(&format!(
"{}{}{}",
cell("a", Some((4800, "dxa")), None),
cell("b", Some((4800, "dxa")), None),
cell("c", Some((4800, "dxa")), None)
)),
row(&format!(
"{}{}{}",
cell("d", Some((4800, "dxa")), None),
cell("e", Some((4800, "dxa")), None),
cell("f", Some((2400, "dxa")), None)
))
);
let pages = layout(&table_doc(
r#"<w:tblW w:w="9600" w:type="dxa"/>"#,
&grid(&[4800, 4800]),
&rows,
));
assert_cells(
&first_row_cells(&pages),
&[(72.0, 160.0), (232.0, 160.0), (392.0, 160.0)],
"the appended column is as wide as the widest row claims",
);
}
#[test]
fn grid_before_can_be_what_unseats_a_cell() {
let pages = layout(&table_doc(
r#"<w:tblW w:w="9600" w:type="dxa"/>"#,
&grid(&[4800, 4800]),
&row_with(
r#"<w:gridBefore w:val="1"/>"#,
&format!(
"{}{}",
cell("a", Some((4800, "dxa")), None),
cell("b", Some((2400, "dxa")), None)
),
),
));
assert_cells(
&first_row_cells(&pages),
&[(264.0, 192.0), (456.0, 96.0)],
"gridBefore counts toward the demand",
);
}
#[test]
fn a_grid_after_the_grid_cannot_hold_is_not_repaired() {
let pages = layout(&table_doc(
r#"<w:tblW w:w="9600" w:type="dxa"/>"#,
&grid(&[4800]),
&row_with(r#"<w:gridAfter w:val="1"/>"#, &cell("a", None, None)),
));
assert_cells(
&first_row_cells(&pages),
&[(72.0, 480.0)],
"gridAfter must not append a column",
);
}