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 layout(body: &str) -> Vec<LayoutedPage> {
let document_xml = 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>"#
);
let doc = dxpdf::docx::parse(&make_docx(&document_xml)).expect("parse");
dxpdf::render::resolve_and_layout(doc).1
}
const FILL: (u8, u8, u8) = (0xC0, 0xFF, 0xEE);
fn row_box(pages: &[LayoutedPage]) -> (f32, f32) {
let mut found: Vec<(f32, f32)> = pages
.iter()
.flat_map(|p| &p.commands)
.filter_map(|c| match c {
DrawCommand::Rect { rect, color } if (color.r, color.g, color.b) == FILL => {
Some((rect.origin.y.raw(), rect.size.height.raw()))
}
_ => None,
})
.collect();
assert_eq!(found.len(), 1, "expected one shaded cell, got {found:?}");
found.pop().unwrap()
}
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_bordered_edge(edge: &str, sz: u32, mar: u32) -> String {
let edges: String = ["top", "left", "bottom", "right", "insideH", "insideV"]
.iter()
.map(|e| {
if *e == edge {
format!(r#"<w:{e} w:val="single" w:sz="{sz}" w:space="0" w:color="auto"/>"#)
} else {
format!(r#"<w:{e} w:val="nil"/>"#)
}
})
.collect();
let margins: String = ["top", "left", "bottom", "right"]
.iter()
.map(|e| format!(r#"<w:{e} w:w="{mar}" w:type="dxa"/>"#))
.collect();
format!(
r#"<w:tbl>
<w:tblPr>
<w:tblW w:w="4000" w:type="dxa"/>
<w:tblBorders>{edges}</w:tblBorders>
<w:tblCellMar>{margins}</w:tblCellMar>
<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:shd w:val="clear" w:color="auto" w:fill="C0FFEE"/>
</w:tcPr>
<w:p><w:r><w:t>CELL</w:t></w:r></w:p>
</w:tc></w:tr>
</w:tbl>"#
)
}
const THIN: u32 = 4;
const THICK: u32 = 48;
const DIFFERENCE: f32 = 5.5;
#[test]
fn a_thicker_top_border_makes_the_row_taller_by_the_difference() {
let (_, thin) = row_box(&layout(&one_bordered_edge("top", THIN, 0)));
let (_, thick) = row_box(&layout(&one_bordered_edge("top", THICK, 0)));
assert_eq!(
thick - thin,
DIFFERENCE,
"w:sz {THIN}→{THICK} is {DIFFERENCE}pt of extra top inset, so the row \
box must be {DIFFERENCE}pt taller; got {thin} → {thick}"
);
}
#[test]
fn the_content_stays_inside_the_box_when_the_top_border_grows() {
let thin_pages = layout(&one_bordered_edge("top", THIN, 0));
let thick_pages = layout(&one_bordered_edge("top", THICK, 0));
let (thin_top, thin_h) = row_box(&thin_pages);
let (thick_top, thick_h) = row_box(&thick_pages);
let thin_base = baseline_of(&thin_pages, "CELL");
let thick_base = baseline_of(&thick_pages, "CELL");
assert_eq!(
thick_base - thin_base,
DIFFERENCE,
"the thicker border must push the content down by its extra {DIFFERENCE}pt"
);
assert_eq!(
(thick_top + thick_h) - thick_base,
(thin_top + thin_h) - thin_base,
"the same content must sit the same distance above the foot of its box, \
whatever the border above it is"
);
}
#[test]
fn a_top_margin_as_thick_as_the_border_absorbs_it() {
let (_, thin) = row_box(&layout(&one_bordered_edge("top", THIN, 120)));
let (_, thick) = row_box(&layout(&one_bordered_edge("top", THICK, 120)));
assert_eq!(
thick, thin,
"max(border, margin) is 6pt either way, so the row height cannot differ"
);
}
#[test]
fn a_thicker_bottom_border_makes_the_row_taller_by_the_difference() {
let (_, thin) = row_box(&layout(&one_bordered_edge("bottom", THIN, 0)));
let (_, thick) = row_box(&layout(&one_bordered_edge("bottom", THICK, 0)));
assert_eq!(thick - thin, DIFFERENCE, "got {thin} → {thick}");
}
#[test]
fn the_content_does_not_move_when_the_bottom_border_grows() {
let thin_pages = layout(&one_bordered_edge("bottom", THIN, 0));
let thick_pages = layout(&one_bordered_edge("bottom", THICK, 0));
let (thin_top, _) = row_box(&thin_pages);
let (thick_top, _) = row_box(&thick_pages);
assert_eq!(
baseline_of(&thick_pages, "CELL") - thick_top,
baseline_of(&thin_pages, "CELL") - thin_top,
"a bottom border is below the content and must not displace it"
);
}
#[test]
fn a_bottom_aligned_cell_sits_above_its_bottom_border() {
let cell = |sz: u32| {
format!(
r#"<w:tbl>
<w:tblPr>
<w:tblW w:w="4000" w:type="dxa"/>
<w:tblBorders>
<w:bottom w:val="single" w:sz="{sz}" w:space="0" w:color="auto"/>
<w:top w:val="nil"/><w:left w:val="nil"/><w:right w:val="nil"/>
<w:insideH w:val="nil"/><w:insideV w:val="nil"/>
</w:tblBorders>
<w:tblCellMar>
<w:top w:w="0" w:type="dxa"/><w:left w:w="0" w:type="dxa"/>
<w:bottom w:w="0" w:type="dxa"/><w:right w:w="0" w:type="dxa"/>
</w:tblCellMar>
<w:tblLayout w:type="fixed"/>
</w:tblPr>
<w:tblGrid><w:gridCol w:w="4000"/></w:tblGrid>
<w:tr>
<w:trPr><w:trHeight w:val="2400" w:hRule="exact"/></w:trPr>
<w:tc>
<w:tcPr>
<w:tcW w:w="4000" w:type="dxa"/>
<w:vAlign w:val="bottom"/>
<w:shd w:val="clear" w:color="auto" w:fill="C0FFEE"/>
</w:tcPr>
<w:p><w:r><w:t>CELL</w:t></w:r></w:p>
</w:tc>
</w:tr>
</w:tbl>"#
)
};
let thin_pages = layout(&cell(THIN));
let thick_pages = layout(&cell(THICK));
let (thin_top, thin_h) = row_box(&thin_pages);
let (thick_top, thick_h) = row_box(&thick_pages);
assert_eq!(
thin_h, thick_h,
"an exact row height is the author's, whatever the border"
);
assert_eq!(
(thin_top + thin_h - baseline_of(&thin_pages, "CELL"))
- (thick_top + thick_h - baseline_of(&thick_pages, "CELL")),
-DIFFERENCE,
"the thicker bottom border must lift the bottom-aligned content by its \
extra {DIFFERENCE}pt, not leave it under the border"
);
}
#[test]
fn a_thicker_left_border_narrows_the_content_box() {
let cell = |sz: u32| {
format!(
r#"<w:tbl>
<w:tblPr>
<w:tblW w:w="900" w:type="dxa"/>
<w:tblBorders>
<w:left w:val="single" w:sz="{sz}" w:space="0" w:color="auto"/>
<w:top w:val="nil"/><w:bottom w:val="nil"/><w:right w:val="nil"/>
<w:insideH w:val="nil"/><w:insideV w:val="nil"/>
</w:tblBorders>
<w:tblCellMar>
<w:top w:w="0" w:type="dxa"/><w:left w:w="0" w:type="dxa"/>
<w:bottom w:w="0" w:type="dxa"/><w:right w:w="0" w:type="dxa"/>
</w:tblCellMar>
<w:tblLayout w:type="fixed"/>
</w:tblPr>
<w:tblGrid><w:gridCol w:w="900"/></w:tblGrid>
<w:tr><w:tc>
<w:tcPr>
<w:tcW w:w="900" w:type="dxa"/>
<w:shd w:val="clear" w:color="auto" w:fill="C0FFEE"/>
</w:tcPr>
<w:p><w:r><w:t>MMMMMM MMMMMM</w:t></w:r></w:p>
</w:tc></w:tr>
</w:tbl>"#
)
};
let (_, narrow) = row_box(&layout(&cell(320)));
let (_, wide) = row_box(&layout(&cell(0)));
assert!(
narrow > wide,
"a 40pt left border must narrow the content box enough to add lines; \
got {wide} unbordered vs {narrow} bordered"
);
}
fn one_shared_vertical(sz: u32) -> String {
let edges: String = ["top", "left", "bottom", "right", "insideH"]
.iter()
.map(|e| format!(r#"<w:{e} w:val="nil"/>"#))
.collect();
let cell = |label: &str| {
format!(
r#"<w:tc><w:tcPr><w:tcW w:w="2000" w:type="dxa"/></w:tcPr>
<w:p><w:r><w:t>{label}</w:t></w:r></w:p></w:tc>"#
)
};
format!(
r#"<w:tbl>
<w:tblPr>
<w:tblW w:w="4000" w:type="dxa"/>
<w:tblBorders>{edges}<w:insideV w:val="single" w:sz="{sz}" w:space="0" w:color="auto"/></w:tblBorders>
<w:tblCellMar>
<w:top w:w="0" w:type="dxa"/><w:left w:w="0" w:type="dxa"/>
<w:bottom w:w="0" w:type="dxa"/><w:right w:w="0" w:type="dxa"/>
</w:tblCellMar>
<w:tblLayout w:type="fixed"/>
</w:tblPr>
<w:tblGrid><w:gridCol w:w="2000"/><w:gridCol w:w="2000"/></w:tblGrid>
<w:tr>{}{}</w:tr>
</w:tbl>"#,
cell("LEFT"),
cell("RIGHT")
)
}
fn glyph_x(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.x.raw()),
_ => None,
})
.unwrap_or_else(|| panic!("no text {needle:?} on the page"))
}
#[test]
fn a_shared_vertical_is_charged_half_to_each_of_the_cells_that_meet_on_it() {
let thin = layout(&one_shared_vertical(THIN));
let thick = layout(&one_shared_vertical(THICK));
assert_eq!(
glyph_x(&thick, "RIGHT") - glyph_x(&thin, "RIGHT"),
DIFFERENCE / 2.0,
"the following cell is charged half the shared border, not none of it"
);
assert_eq!(
glyph_x(&thick, "LEFT"),
glyph_x(&thin, "LEFT"),
"the leading cell's content box does not start on the shared edge"
);
}