use std::collections::HashMap;
use std::io::Write;
use dxpdf::render::layout::draw_command::{DrawCommand, LayoutedPage};
fn make_docx(document_xml: &str, styles_xml: Option<&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();
if let Some(styles) = styles_xml {
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.as_bytes()).unwrap();
}
zip.start_file("word/document.xml", o).unwrap();
zip.write_all(document_xml.as_bytes()).unwrap();
zip.finish().unwrap();
}
buf
}
const GRID: &str = r#"<w:gridCol w:w="1000"/><w:gridCol w:w="2000"/><w:gridCol w:w="3000"/>"#;
fn table(bidi: bool, extra_tbl_pr: &str, rows: &str) -> String {
let flag = if bidi { "<w:bidiVisual/>" } else { "" };
format!(
r#"<w:tbl>
<w:tblPr>
<w:tblW w:w="6000" w:type="dxa"/>
<w:tblLayout w:type="fixed"/>
{flag}{extra_tbl_pr}
</w:tblPr>
<w:tblGrid>{GRID}</w:tblGrid>
{rows}
</w:tbl>"#
)
}
fn layout(body: &str, styles: Option<&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="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>"#
);
let doc = dxpdf::docx::parse(&make_docx(&document_xml, styles)).expect("parse");
dxpdf::render::resolve_and_layout(doc).1
}
fn cell(fill: &str, extra_tc_pr: &str) -> String {
format!(
r#"<w:tc>
<w:tcPr><w:shd w:val="clear" w:color="auto" w:fill="{fill}"/>{extra_tc_pr}</w:tcPr>
<w:p><w:r><w:t>x</w:t></w:r></w:p>
</w:tc>"#
)
}
type Rect = (f32, f32, f32, f32);
fn boxes(pages: &[LayoutedPage]) -> HashMap<(u8, u8, u8), Rect> {
let mut out = HashMap::new();
for c in pages.iter().flat_map(|p| &p.commands) {
if let DrawCommand::Rect { rect, color } = c {
out.entry((color.r, color.g, color.b)).or_insert((
rect.origin.x.raw(),
rect.origin.y.raw(),
rect.size.width.raw(),
rect.size.height.raw(),
));
}
}
out
}
const RED: (u8, u8, u8) = (0xFF, 0x00, 0x00);
const GREEN: (u8, u8, u8) = (0x00, 0xFF, 0x00);
const BLUE: (u8, u8, u8) = (0x00, 0x00, 0xFF);
fn extent(rects: impl Iterator<Item = Rect>) -> (f32, f32) {
rects.fold(
(f32::INFINITY, f32::NEG_INFINITY),
|(l, r), (x, _, w, _)| (l.min(x), r.max(x + w)),
)
}
fn span(rects: &HashMap<(u8, u8, u8), Rect>) -> (f32, f32) {
extent(rects.values().copied())
}
fn assert_mirrored(ltr: &HashMap<(u8, u8, u8), Rect>, rtl: &HashMap<(u8, u8, u8), Rect>) {
assert_eq!(ltr.len(), rtl.len(), "same cells either way");
let (l, r) = span(ltr);
let (rl, rr) = span(rtl);
assert_eq!(rr - rl, r - l, "a mirrored table is the same width");
for (colour, &(x, _, w, _)) in ltr {
let &(rx, _, rw, _) = rtl
.get(colour)
.unwrap_or_else(|| panic!("no {colour:?} cell in the mirrored table"));
assert_eq!(rw, w, "{colour:?}: a mirrored cell keeps its width");
assert_eq!(
rx - rl,
r - (x + w),
"{colour:?}: {x}..{} sits {} from the control's right edge, so its \
mirror must sit that far from the mirrored table's left edge",
x + w,
r - (x + w)
);
}
}
#[test]
fn the_first_cell_of_a_row_becomes_the_rightmost() {
let row = format!(
"<w:tr>{}{}{}</w:tr>",
cell("FF0000", ""),
cell("00FF00", ""),
cell("0000FF", "")
);
let ltr = boxes(&layout(&table(false, "", &row), None));
let rtl = boxes(&layout(&table(true, "", &row), None));
assert_eq!(ltr[&RED].2, 50.0, "1000 twips");
assert_eq!(ltr[&GREEN].2, 100.0, "2000 twips");
assert_eq!(ltr[&BLUE].2, 150.0, "3000 twips");
assert!(
ltr[&RED].0 < ltr[&GREEN].0 && ltr[&GREEN].0 < ltr[&BLUE].0,
"without the flag the cells run left to right"
);
assert_mirrored(<r, &rtl);
assert!(
rtl[&RED].0 > rtl[&GREEN].0 && rtl[&GREEN].0 > rtl[&BLUE].0,
"with it they run right to left: {rtl:?}"
);
}
#[test]
fn a_grid_span_covers_the_mirrored_run_of_slots() {
let row = format!(
"<w:tr>{}{}</w:tr>",
cell("FF0000", r#"<w:gridSpan w:val="2"/>"#),
cell("00FF00", "")
);
let ltr = boxes(&layout(&table(false, "", &row), None));
let rtl = boxes(&layout(&table(true, "", &row), None));
assert_eq!(ltr[&RED].2, 150.0, "1000 + 2000 twips");
assert_eq!(ltr[&GREEN].2, 150.0, "3000 twips");
assert_mirrored(<r, &rtl);
}
#[test]
fn a_grid_before_gap_moves_to_the_visual_right() {
let rows = format!(
"<w:tr><w:trPr><w:gridBefore w:val=\"1\"/></w:trPr>{}</w:tr><w:tr>{}</w:tr>",
cell("FF0000", r#"<w:gridSpan w:val="2"/>"#),
cell("00FF00", r#"<w:gridSpan w:val="3"/>"#),
);
let ltr = boxes(&layout(&table(false, "", &rows), None));
let rtl = boxes(&layout(&table(true, "", &rows), None));
assert_eq!(ltr[&RED].2, 250.0);
assert_eq!(ltr[&GREEN].2, 300.0, "the reference row spans the grid");
let (l, r) = span(<r);
assert_eq!((l, r), (ltr[&GREEN].0, ltr[&GREEN].0 + 300.0));
assert_eq!(
ltr[&RED].0 - l,
50.0,
"without the flag the skipped 1000-twip column is on the left"
);
let (_, rr) = span(&rtl);
assert_eq!(
rr - (rtl[&RED].0 + rtl[&RED].2),
50.0,
"with it the same column is skipped on the right"
);
assert_mirrored(<r, &rtl);
}
#[test]
fn a_vertical_merge_mirrors_with_its_column() {
let rows = format!(
"<w:tr>{}{}{}</w:tr><w:tr>{}{}{}</w:tr>",
cell("FF0000", r#"<w:vMerge w:val="restart"/>"#),
cell("00FF00", ""),
cell("0000FF", ""),
cell("FF0000", "<w:vMerge/>"),
cell("00FFFF", ""),
cell("FF00FF", ""),
);
let ltr = boxes(&layout(&table(false, "", &rows), None));
let rtl = boxes(&layout(&table(true, "", &rows), None));
assert_mirrored(<r, &rtl);
assert_eq!(
rtl[&RED].3, ltr[&RED].3,
"the merged cell keeps the height of its span"
);
assert!(
rtl[&RED].3 > rtl[&GREEN].3,
"and that height really is more than one row: {rtl:?}"
);
}
#[test]
fn a_cells_start_border_paints_on_its_visual_right() {
let row = format!(
"<w:tr>{}</w:tr>",
cell(
"FF0000",
r#"<w:tcBorders>
<w:left w:val="single" w:sz="24" w:space="0" w:color="0000FF"/>
<w:top w:val="nil"/><w:bottom w:val="nil"/><w:right w:val="nil"/>
</w:tcBorders>"#
)
);
let gaps = |bidi: bool| -> (f32, f32) {
let pages = layout(&table(bidi, "", &row), None);
let rects = boxes(&pages);
let (cx, _, cw, _) = rects[&RED];
let (bx, _, bw, _) = rects[&BLUE];
assert_eq!(bw, 3.0, "w:sz=24 is 3pt");
(bx - cx, (cx + cw) - (bx + bw))
};
let (before, after) = gaps(false);
assert_eq!(before, -1.5, "without the flag, straddling the left edge");
assert!(after > 0.0, "…and the rest of the cell is to its right");
assert_eq!(
gaps(true),
(after, before),
"the logical start border moves to the visual right of its cell"
);
}
#[test]
fn a_cells_margins_mirror_with_it() {
let row = format!(
"<w:tr>{}</w:tr>",
cell(
"FF0000",
r#"<w:tcMar>
<w:left w:w="0" w:type="dxa"/><w:right w:w="800" w:type="dxa"/>
<w:top w:w="0" w:type="dxa"/><w:bottom w:w="0" w:type="dxa"/>
</w:tcMar>"#
)
);
let inset = |bidi: bool| -> f32 {
let pages = layout(&table(bidi, "", &row), None);
let (cx, _, _, _) = boxes(&pages)[&RED];
pages
.iter()
.flat_map(|p| &p.commands)
.find_map(|c| match c {
DrawCommand::Text { text, position, .. } if &**text == "x" => {
Some(position.x.raw() - cx)
}
_ => None,
})
.expect("the cell's own text")
};
assert_eq!(inset(false), 0.0, "w:left = 0 leads without the flag");
assert_eq!(inset(true), 40.0, "w:right = 800 twips leads with it");
}
#[test]
fn a_table_level_start_border_paints_on_the_visual_right() {
let row = format!("<w:tr>{}</w:tr>", cell("FF0000", ""));
let borders = r#"<w:tblBorders>
<w:left w:val="single" w:sz="24" w:space="0" w:color="0000FF"/>
<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>"#;
let side = |bidi: bool| -> (f32, f32) {
let rects = boxes(&layout(&table(bidi, borders, &row), None));
let (cx, _, cw, _) = rects[&RED];
let (bx, _, bw, _) = rects[&BLUE];
(bx - cx, (cx + cw) - (bx + bw))
};
let (before, after) = side(false);
assert_eq!(before, -1.5, "the table's start edge is on the left");
assert!(after > 0.0);
assert_eq!(
side(true),
(after, before),
"and on the right once the columns reverse"
);
}
#[test]
fn a_row_level_border_override_mirrors_too() {
let rows = format!(
"<w:tr><w:tblPrEx><w:tblBorders>\
<w:left w:val=\"single\" w:sz=\"24\" w:space=\"0\" w:color=\"0000FF\"/>\
<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:tblPrEx>{}</w:tr><w:tr>{}</w:tr>",
cell("FF0000", ""),
cell("00FF00", ""),
);
let side = |bidi: bool| -> (f32, f32) {
let rects = boxes(&layout(&table(bidi, "", &rows), None));
let (cx, _, cw, _) = rects[&RED];
let (bx, _, bw, _) = rects[&BLUE];
let centre = bx + bw / 2.0;
assert!(
(centre - cx).abs() < 0.01 || (centre - (cx + cw)).abs() < 0.01,
"the override stands on an edge of the row that declares it"
);
(bx - cx, (cx + cw) - (bx + bw))
};
let (before, after) = side(false);
assert_eq!(before, -1.5);
assert!(after > 0.0);
assert_eq!(side(true), (after, before));
}
#[test]
fn a_row_that_overruns_its_grid_does_not_panic() {
let rows = format!(
"<w:tr><w:trPr><w:gridBefore w:val=\"9\"/></w:trPr>{}</w:tr>",
cell("FF0000", r#"<w:gridSpan w:val="4"/>"#)
);
let pages = layout(&table(true, "", &rows), None);
assert!(
pages
.iter()
.flat_map(|p| &p.commands)
.any(|c| matches!(c, DrawCommand::Text { text, .. } if &**text == "x")),
"the overrun row's content still reaches the page"
);
}
#[test]
fn the_first_column_region_stays_the_logical_first_column() {
let styles = 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:tblStylePr w:type="firstCol">
<w:tcPr><w:shd w:val="clear" w:color="auto" w:fill="00FF00"/></w:tcPr>
</w:tblStylePr>
</w:style>
</w:styles>"#;
let plain = r#"<w:tc><w:tcPr/><w:p><w:r><w:t>x</w:t></w:r></w:p></w:tc>"#;
let row = format!("<w:tr>{plain}{plain}{plain}</w:tr>");
let extra = r#"<w:tblStyle w:val="TestTbl"/>
<w:tblLook w:firstRow="0" w:lastRow="0" w:firstColumn="1"
w:lastColumn="0" w:noHBand="1" w:noVBand="1"/>"#;
let ltr = boxes(&layout(&table(false, extra, &row), Some(styles)));
let rtl = boxes(&layout(&table(true, extra, &row), Some(styles)));
let (l, r) = (ltr[&GREEN].0, ltr[&GREEN].0 + ltr[&GREEN].2);
assert_eq!(
ltr[&GREEN].2, 50.0,
"the logical first column is 1000 twips"
);
assert_eq!(rtl[&GREEN].2, 50.0, "…and still is, on the other side");
assert!(
rtl[&GREEN].0 > l,
"the shaded column must move right, from {l}..{r} to {:?}",
rtl[&GREEN]
);
}
#[test]
fn the_committed_fixture_mirrors_its_own_control() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/test-files/bidi-visual-table.docx"
);
let bytes = std::fs::read(path).expect("read the fixture");
let doc = dxpdf::docx::parse(&bytes).expect("parse the fixture");
let pages = dxpdf::render::resolve_and_layout(doc).1;
let mut by_colour: HashMap<(u8, u8, u8), Vec<Rect>> = HashMap::new();
for c in pages.iter().flat_map(|p| &p.commands) {
if let DrawCommand::Rect { rect, color } = c {
by_colour
.entry((color.r, color.g, color.b))
.or_default()
.push((
rect.origin.x.raw(),
rect.origin.y.raw(),
rect.size.width.raw(),
rect.size.height.raw(),
));
}
}
const FILLS: [(u8, u8, u8); 11] = [
(0xF8, 0xCB, 0xAD), (0xC6, 0xE0, 0xB4), (0xBD, 0xD7, 0xEE), (0xFF, 0xE6, 0x99), (0xD9, 0xD2, 0xE9), (0xF4, 0xCC, 0xCC), (0xD0, 0xE0, 0xE3), (0xEA, 0xD1, 0xDC), (0xFF, 0xF2, 0xCC), (0xD9, 0xEA, 0xD3), (0xCF, 0xE2, 0xF3), ];
let mut cells: Vec<((u8, u8, u8), Rect, Rect)> = Vec::new();
for colour in FILLS {
let mut rects = by_colour
.get(&colour)
.unwrap_or_else(|| panic!("fixture no longer paints {colour:?}"))
.clone();
assert_eq!(rects.len(), 2, "{colour:?} must appear once per table");
rects.sort_by(|a, b| a.1.total_cmp(&b.1));
cells.push((colour, rects[0], rects[1]));
}
assert_eq!(cells.len(), 11);
let (ctrl_left, ctrl_right) = extent(cells.iter().map(|c| c.1));
let (mirror_left, mirror_right) = extent(cells.iter().map(|c| c.2));
const CONTENT_LEFT: f32 = 72.0;
const CONTENT_RIGHT: f32 = 72.0 + 468.0;
assert_eq!(ctrl_left, CONTENT_LEFT, "the control is flush left");
assert_eq!(
mirror_right, CONTENT_RIGHT,
"the w:bidiVisual table is flush right"
);
assert_eq!(
mirror_right - mirror_left,
ctrl_right - ctrl_left,
"the two tables are the same width, so only their placement differs"
);
let mut moved = 0;
for (colour, ctrl, mirror) in &cells {
assert_eq!(mirror.2, ctrl.2, "{colour:?} keeps its width");
assert_eq!(
mirror.0 - mirror_left,
ctrl_right - (ctrl.0 + ctrl.2),
"{colour:?}: {ctrl:?} must reflect about its own table's edges"
);
if mirror.0 - mirror_left != ctrl.0 - ctrl_left {
moved += 1;
}
}
assert!(
moved >= 8,
"only {moved} of {} cells moved — the fixture stopped discriminating",
cells.len()
);
}