use std::io::Write;
use dxpdf::render::layout::draw_command::{DrawCommand, LayoutedPage};
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"/>
<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();
}
buf
}
fn document(body: &str) -> String {
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
{body}
<w:sectPr><w:pgSz w:w="11906" w:h="16838"/></w:sectPr>
</w:body>
</w:document>"#
)
}
fn layout(body: &str) -> Vec<LayoutedPage> {
let doc = dxpdf::docx::parse(&make_docx(&document(body))).expect("parse");
dxpdf::render::resolve_and_layout(doc).1
}
fn rects(pages: &[LayoutedPage]) -> Vec<(f32, f32, f32, f32)> {
pages
.iter()
.flat_map(|p| &p.commands)
.filter_map(|c| match c {
DrawCommand::Rect { rect, .. } => Some((
rect.origin.x.raw(),
rect.origin.y.raw(),
rect.size.width.raw(),
rect.size.height.raw(),
)),
_ => None,
})
.collect()
}
fn baseline_of(pages: &[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 {needle:?} on the page"))
}
fn one_cell_table(style: &str) -> String {
let edges: String = ["top", "left", "bottom", "right", "insideH", "insideV"]
.iter()
.map(|e| format!(r#"<w:{e} w:val="{style}" w:sz="24" w:space="0" w:color="auto"/>"#))
.collect();
format!(
r#"<w:tbl>
<w:tblPr>
<w:tblW w:w="4000" w:type="dxa"/>
<w:tblBorders>{edges}</w:tblBorders>
<w:tblLayout w:type="fixed"/>
</w:tblPr>
<w:tblGrid><w:gridCol w:w="4000"/></w:tblGrid>
<w:tr><w:tc>
<w:tcPr><w:tcW w:w="4000" w:type="dxa"/></w:tcPr>
<w:p><w:r><w:t>CELL</w:t></w:r></w:p>
</w:tc></w:tr>
</w:tbl>"#
)
}
type Run = (f32, f32);
fn edge_runs(pages: &[LayoutedPage]) -> (Vec<Run>, Vec<Run>) {
let all = rects(pages);
let distinct = |mut v: Vec<Run>| -> Vec<Run> {
v.sort_by(|a: &Run, b: &Run| a.0.total_cmp(&b.0).then(a.1.total_cmp(&b.1)));
v.dedup_by(|a, b| (a.0 - b.0).abs() < 1e-4 && (a.1 - b.1).abs() < 1e-4);
v
};
let horizontal = all
.iter()
.filter(|(_, _, w, h)| w > h)
.map(|&(_, y, _, h)| (y, h))
.collect();
let vertical = all
.iter()
.filter(|(_, _, w, h)| w <= h)
.map(|&(x, _, w, _)| (x, w))
.collect();
(distinct(horizontal), distinct(vertical))
}
#[test]
fn a_double_border_is_three_rules_wide_each_of_the_declared_sz() {
let (single_h, single_v) = edge_runs(&layout(&one_cell_table("single")));
let (double_h, double_v) = edge_runs(&layout(&one_cell_table("double")));
assert_eq!(
single_h.len(),
2,
"the control's top and bottom edges: {single_h:?}"
);
assert_eq!(single_v.len(), 2, "its left and right: {single_v:?}");
assert!(
single_h.iter().chain(&single_v).all(|&(_, t)| t == 3.0),
"w:sz=24 is 3pt; control was {single_h:?} / {single_v:?}"
);
assert_eq!(
double_h.len(),
4,
"two lines per horizontal edge: {double_h:?}"
);
assert_eq!(double_v.len(), 4, "and per vertical: {double_v:?}");
assert!(
double_h.iter().chain(&double_v).all(|&(_, t)| t == 3.0),
"each rule is the declared w:sz, unreduced; got {double_h:?} / {double_v:?}"
);
for (pair, control, axis) in [
(&double_h[..2], single_h[0], "top"),
(&double_h[2..], single_h[1], "bottom"),
(&double_v[..2], single_v[0], "left"),
(&double_v[2..], single_v[1], "right"),
] {
assert_eq!(
pair[1].0 - (pair[0].0 + pair[0].1),
control.1,
"{axis}: one w:sz of clear space between the two rules"
);
assert_eq!(
(pair[1].0 + pair[1].1) - pair[0].0,
control.1 * 3.0,
"{axis}: the whole edge is three times the single's"
);
}
}
fn valign_row(tag: &str, twips: u32) -> String {
let cells: String = [("T", "top"), ("C", "center"), ("B", "bottom")]
.iter()
.map(|(suffix, align)| {
format!(
r#"<w:tc>
<w:tcPr><w:tcW w:w="2000" w:type="dxa"/><w:vAlign w:val="{align}"/></w:tcPr>
<w:p><w:r><w:t>{tag}{suffix}</w:t></w:r></w:p>
</w:tc>"#
)
})
.collect();
format!(
r#"<w:tr>
<w:trPr><w:trHeight w:val="{twips}" w:hRule="exact"/></w:trPr>
{cells}
</w:tr>"#
)
}
#[test]
fn valign_offsets_scale_with_the_row_height_and_centre_is_half_of_bottom() {
let table = format!(
r#"<w:tbl>
<w:tblPr><w:tblW w:w="6000" w:type="dxa"/><w:tblLayout w:type="fixed"/></w:tblPr>
<w:tblGrid><w:gridCol w:w="2000"/><w:gridCol w:w="2000"/><w:gridCol w:w="2000"/></w:tblGrid>
{}
{}
</w:tbl>"#,
valign_row("A", 1200),
valign_row("B", 2400)
);
let pages = layout(&table);
let offsets = |tag: &str| -> (f32, f32) {
let top = baseline_of(&pages, &format!("{tag}T"));
(
baseline_of(&pages, &format!("{tag}C")) - top,
baseline_of(&pages, &format!("{tag}B")) - top,
)
};
let (a_centre, a_bottom) = offsets("A");
let (b_centre, b_bottom) = offsets("B");
assert!(
a_bottom > 0.0,
"the 1200-twip row must have spare height for the alignment to \
distribute, got {a_bottom}"
);
assert_eq!(
a_centre,
a_bottom / 2.0,
"60pt row: centre is half of bottom"
);
assert_eq!(
b_centre,
b_bottom / 2.0,
"120pt row: centre is half of bottom"
);
assert_eq!(
b_bottom - a_bottom,
60.0,
"the same content in a row 60pt taller drops 60pt further; \
a={a_bottom}, b={b_bottom}"
);
}
#[test]
fn valign_moves_nothing_in_a_row_that_is_exactly_its_content() {
let table = r#"<w:tbl>
<w:tblPr><w:tblW w:w="6000" w:type="dxa"/><w:tblLayout w:type="fixed"/></w:tblPr>
<w:tblGrid><w:gridCol w:w="2000"/><w:gridCol w:w="2000"/><w:gridCol w:w="2000"/></w:tblGrid>
<w:tr>
<w:tc><w:tcPr><w:tcW w:w="2000" w:type="dxa"/><w:vAlign w:val="top"/></w:tcPr>
<w:p><w:r><w:t>NT</w:t></w:r></w:p></w:tc>
<w:tc><w:tcPr><w:tcW w:w="2000" w:type="dxa"/><w:vAlign w:val="center"/></w:tcPr>
<w:p><w:r><w:t>NC</w:t></w:r></w:p></w:tc>
<w:tc><w:tcPr><w:tcW w:w="2000" w:type="dxa"/><w:vAlign w:val="bottom"/></w:tcPr>
<w:p><w:r><w:t>NB</w:t></w:r></w:p></w:tc>
</w:tr>
</w:tbl>"#;
let pages = layout(table);
let top = baseline_of(&pages, "NT");
assert_eq!(
baseline_of(&pages, "NC"),
top,
"centre has nothing to centre"
);
assert_eq!(baseline_of(&pages, "NB"), top, "bottom has nothing to drop");
}