use std::io::Write;
use dxpdf::render::layout::draw_command::{DrawCommand, LayoutedPage};
const SZ: &str = "96";
const WIDTH: f32 = 12.0;
const EPS: f32 = 1e-4;
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 junction_table(v_colour: &str, h_colour: &str, style: &str) -> String {
weighted_table(v_colour, h_colour, style, SZ, SZ)
}
fn weighted_table(v_colour: &str, h_colour: &str, style: &str, v_sz: &str, h_sz: &str) -> String {
let cells: String = (0..3)
.map(|_| {
r#"<w:tc><w:tcPr><w:tcW w:w="2000" w:type="dxa"/></w:tcPr><w:p/></w:tc>"#.to_string()
})
.collect();
let row = format!(
r#"<w:tr><w:trPr><w:trHeight w:val="1200" w:hRule="atLeast"/></w:trPr>{cells}</w:tr>"#
);
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:tblW w:w="6000" w:type="dxa"/>
<w:tblBorders>
<w:insideV w:val="{style}" w:sz="{v_sz}" w:space="0" w:color="{v_colour}"/>
<w:insideH w:val="{style}" w:sz="{h_sz}" w:space="0" w:color="{h_colour}"/>
</w:tblBorders>
<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>
{row}{row}{row}
</w:tbl>
<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(body)).expect("parse");
dxpdf::render::resolve_and_layout(doc).1
}
type Colour = (u8, u8, u8);
const BLACK: Colour = (0x00, 0x00, 0x00);
const GREY: Colour = (0xBF, 0xBF, 0xBF);
fn ink_at(pages: &[LayoutedPage], (px, py): (f32, f32)) -> Option<Colour> {
pages
.iter()
.flat_map(|p| &p.commands)
.filter_map(|c| match c {
DrawCommand::Rect { rect, color } => {
let (x, y) = (rect.origin.x.raw(), rect.origin.y.raw());
let (w, h) = (rect.size.width.raw(), rect.size.height.raw());
(x < px && px < x + w && y < py && py < y + h)
.then_some((color.r, color.g, color.b))
}
_ => None,
})
.next_back()
}
fn crossings(pages: &[LayoutedPage]) -> Vec<((f32, f32), (f32, f32))> {
let mut vertical: Vec<(f32, f32)> = Vec::new();
let mut horizontal: Vec<(f32, f32)> = Vec::new();
for c in pages.iter().flat_map(|p| &p.commands) {
let DrawCommand::Rect { rect, .. } = c else {
continue;
};
let (w, h) = (rect.size.width.raw(), rect.size.height.raw());
let (into, lo) = if h > w {
(&mut vertical, rect.origin.x.raw())
} else {
(&mut horizontal, rect.origin.y.raw())
};
let span = (lo, lo + w.min(h));
if !into.iter().any(|&b| (b.0 - span.0).abs() < EPS) {
into.push(span);
}
}
let merge = |mut v: Vec<(f32, f32)>| -> Vec<(f32, f32)> {
v.sort_by(|a, b| a.0.total_cmp(&b.0));
let mut out: Vec<(f32, f32)> = Vec::new();
for (lo, hi) in v {
match out.last_mut() {
Some(last) if lo - last.1 <= (last.1 - last.0).max(hi - lo) + EPS => {
last.1 = last.1.max(hi)
}
_ => out.push((lo, hi)),
}
}
out
};
let (vertical, horizontal) = (merge(vertical), merge(horizontal));
vertical
.iter()
.flat_map(|&vx| horizontal.iter().map(move |&hy| (vx, hy)))
.collect()
}
fn centre(b: ((f32, f32), (f32, f32))) -> (f32, f32) {
((b.0 .0 + b.0 .1) * 0.5, (b.1 .0 + b.1 .1) * 0.5)
}
#[test]
fn at_equal_weight_the_square_takes_the_horizontals_colour() {
let v_dark = layout(&junction_table("000000", "BFBFBF", "single"));
let h_dark = layout(&junction_table("BFBFBF", "000000", "single"));
let nodes = crossings(&v_dark);
assert_eq!(
nodes.len(),
4,
"two interior grid lines crossing two interior row boundaries: {nodes:?}"
);
assert_eq!(
nodes,
crossings(&h_dark),
"exchanging two colours must not move a grid line"
);
assert!(
nodes
.iter()
.all(|b| (b.0 .1 - b.0 .0 - WIDTH).abs() < EPS
&& (b.1 .1 - b.1 .0 - WIDTH).abs() < EPS),
"a crossing of two 12pt singles is 12pt square: {nodes:?}"
);
for node in nodes {
let at = centre(node);
assert_eq!(
ink_at(&v_dark, at),
Some(GREY),
"the horizontal is the pale line here, so the crossing at {at:?} should be pale"
);
assert_eq!(
ink_at(&h_dark, at),
Some(BLACK),
"and dark once the colours are exchanged, at {at:?}"
);
}
}
#[test]
fn the_heavier_of_the_two_borders_takes_the_square() {
let heavy_v = layout(&weighted_table("000000", "BFBFBF", "single", SZ, "24"));
let heavy_h = layout(&weighted_table("BFBFBF", "000000", "single", "24", SZ));
for (pages, want, which) in [
(&heavy_v, BLACK, "the vertical is the heavy line here"),
(
&heavy_h,
BLACK,
"and the horizontal is, once they are exchanged",
),
] {
let nodes = crossings(pages);
assert_eq!(nodes.len(), 4, "{which}: crossings {nodes:?}");
for node in nodes {
let at = centre(node);
assert_eq!(
ink_at(pages, at),
Some(want),
"{which}, so the crossing at {at:?} should be its colour"
);
}
}
}
#[test]
fn two_doubles_cross_as_a_lattice_with_both_gaps_running_through() {
let single = layout(&junction_table("000000", "000000", "single"));
let double = layout(&junction_table("000000", "000000", "double"));
let control = crossings(&single);
let nodes = crossings(&double);
assert_eq!(control.len(), 4, "the control's crossings: {control:?}");
assert_eq!(nodes.len(), 4, "and the double's: {nodes:?}");
for (node, control) in nodes.iter().zip(&control) {
let at = centre(*node);
assert_eq!(
ink_at(&single, centre(*control)),
Some(BLACK),
"the control fills its crossing whole"
);
assert!(
(at.0 - centre(*control).0).abs() < EPS,
"the crossing did not move along the grid line: {node:?} vs {control:?}"
);
for (side, control_side) in [
(node.0 .1 - node.0 .0, control.0 .1 - control.0 .0),
(node.1 .1 - node.1 .0, control.1 .1 - control.1 .0),
] {
assert!(
(side - control_side * 3.0).abs() < EPS,
"a double crossing is three times the control's: {side} vs {control_side}"
);
}
let third = (node.0 .1 - node.0 .0) / 3.0;
for (i, j) in [0usize, 1, 2]
.into_iter()
.flat_map(|i| [0, 1, 2].map(|j| (i, j)))
{
let p = (
node.0 .0 + third * (i as f32 + 0.5),
node.1 .0 + third * (j as f32 + 0.5),
);
let corner = i != 1 && j != 1;
assert_eq!(
ink_at(&double, p),
corner.then_some(BLACK),
"at {p:?} — third ({i}, {j}) of the crossing {node:?}; the corners \
are ink and everything else is the two gaps crossing"
);
}
}
}