Annual Report"), "{h}");
assert!(h.contains(""), "{h}");
let r = rtf(&[p.clone()]);
assert!(r.contains("{\\colortbl;\\red13\\green59\\blue46;\\red0\\green255\\blue0;}"), "{r}");
assert!(r.contains("\\cf1 Annual Report"), "{r}");
let ppt = pptx(&[p], "Colour");
let pxml = part(&ppt, "ppt/slides/slide1.xml");
assert!(pxml.contains(""), "{pxml}");
assert!(pxml.contains(""), "out-of-range values clamp: {pxml}");
// A table cell keeps its colour too.
let mut t = table_page();
t.blocks[0].color = [1.0, 0.0, 0.0];
let xml = part(&docx(&[t.clone()], "Grid"), "word/document.xml");
assert!(xml.contains(""), "{xml}");
assert!(html(&[t], "Grid").contains("style=\"color:#FF0000\""));
}
#[test]
fn rtf_escapes_and_sizes() {
let mut p = page();
p.blocks.push(Block {
text: "Café {x}".into(),
rect: [72.0, 100.0, 200.0, 110.0],
size: 10.0,
bold: false,
italic: true,
color: [0.0; 3],
rotation: 0.0,
});
let r = rtf(&[p]);
assert!(r.starts_with("{\\rtf1") && r.ends_with('}'));
assert!(r.contains("\\fs48\\b Annual Report"));
assert!(r.contains("\\fs20\\i Caf\\u233? \\{x\\}"), "{r}");
}
/// A 3-column table with a spanning header row and a hole in the last row.
fn table_page() -> Page {
let cell = |t: &str, x: f64, y: f64, w: f64| Block {
text: t.into(),
rect: [x, y, x + w, y + 12.0],
size: 11.0,
bold: false,
italic: false,
color: [0.0; 3],
rotation: 0.0,
};
Page {
width: 612.0,
height: 792.0,
blocks: vec![
cell("Meter", 72.0, 620.0, 120.0),
cell("Unit", 232.0, 620.0, 80.0),
cell("Reading", 392.0, 620.0, 100.0),
cell("A1", 72.0, 590.0, 120.0),
cell("kWh", 232.0, 590.0, 80.0),
cell("37414.00", 392.0, 590.0, 100.0),
cell("A2", 72.0, 560.0, 120.0),
cell("kW", 232.0, 560.0, 80.0),
// A2's row is missing its third cell (an empty cell in the grid).
cell("Totals", 72.0, 530.0, 240.0),
Block {
text: "Notes follow the table and are long enough to be the body text size of the page here.".into(),
rect: [72.0, 460.0, 500.0, 471.0],
size: 11.0,
bold: false,
italic: false,
color: [0.0; 3],
rotation: 0.0,
},
],
images: Vec::new(),
}
}
#[test]
fn grid_text_becomes_a_real_table_in_every_format() {
let p = table_page();
let h = html(std::slice::from_ref(&p), "Bill");
assert!(h.contains(""), "{h}");
assert!(h.contains("| Totals | "), "spanning cell: {h}");
assert_eq!(h.matches("").count(), 4, "{h}");
assert!(h.contains(" | "), "the hole is an empty cell: {h}");
assert!(h.contains("Notes follow"), "the paragraph after the table stays: {h}");
let d = docx(std::slice::from_ref(&p), "Bill");
assert!(d.starts_with(b"PK"));
assert!(d.windows(b"word/document.xml".len()).any(|w| w == b"word/document.xml"));
let (tables, _) = tables(&p.blocks);
assert_eq!(tables.len(), 1);
let table_xml = docx_table(&tables[0]);
assert!(table_xml.contains(""));
assert!(table_xml.contains(""), "{table_xml}");
assert_eq!(table_xml.matches("").count(), 4);
let r = rtf(std::slice::from_ref(&p));
assert!(r.contains("\\trowd"), "{r}");
assert_eq!(r.matches("\\row").count(), 4);
// Boundaries precede the cells; "Totals" spans two columns, so it ends at the third edge.
assert!(r.contains("\\trowd\\trgaph108\\cellx4640\\cellx7840\\cellx9920{"), "{r}");
assert!(r.contains("\\trowd\\trgaph108\\cellx7840{\\intbl\\fs22 Totals}\\cell\\row"), "{r}");
let pt = pptx(std::slice::from_ref(&p), "Bill");
assert!(pt.starts_with(b"PK"));
assert!(pt.windows(b"ppt/slides/slide1.xml".len()).any(|w| w == b"ppt/slides/slide1.xml"));
let slide_xml = part(&pt, "ppt/slides/slide1.xml");
assert!(slide_xml.contains(""));
assert!(slide_xml.contains("gridSpan=\"2\""));
assert_eq!(slide_xml.matches(" = (0..n)
.map(|i| {
let (row, col) = (i / 2, i / 2 + i % 2);
let (x, y) = (10.0 + col as f64 * 20.0, 10_000.0 - row as f64 * 14.0);
Block { text: "x".into(), rect: [x, y, x + 8.0, y + 12.0], size: 11.0, bold: false, italic: false, color: [0.0; 3], rotation: 0.0 }
})
.collect();
let started = std::time::Instant::now();
let (tables, _) = tables(&blocks);
assert!(tables.iter().all(|t| t.cols.len() <= MAX_COLS), "{} columns", tables.iter().map(|t| t.cols.len()).max().unwrap_or(0));
assert!(started.elapsed() < std::time::Duration::from_secs(5), "took {:?}", started.elapsed());
}
#[test]
fn word_opens_tables_up_to_its_column_limit() {
// Rows of short numbers on a regular grid, n columns wide.
let grid = |n: usize| Page {
width: 1500.0,
height: 792.0,
blocks: (0..3)
.flat_map(|row| {
(0..n).map(move |col| {
let (x, y) = (20.0 + col as f64 * 22.0, 700.0 - row as f64 * 14.0);
Block {
text: (col + 1).to_string(),
rect: [x, y, x + 8.0, y + 8.0],
size: 8.0,
bold: false,
italic: false,
color: [0.0; 3],
rotation: 0.0,
}
})
})
.collect(),
images: Vec::new(),
};
let grid_cols = |p: &Page| part(&docx(std::slice::from_ref(p), "Grid"), "word/document.xml").matches(""), "tall two-column text must stay paragraphs: {h}");
}
}