use odox_core::draw::Geometry;
use odox_core::xml;
fn geometry(inner: &str) -> Geometry {
let document = format!(
r#"<draw:enhanced-geometry
xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"
xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"
{inner}"#
);
let element = xml::parse(document.as_bytes(), "test").expect("well-formed XML");
Geometry::read(&element).expect("a readable geometry")
}
#[test]
fn a_full_ellipse_is_the_commonest_shape_there_is() {
let shape = geometry(
r#"svg:viewBox="0 0 21600 21600"
draw:enhanced-path="U 10800 10800 10800 10800 0 360 Z N"/>"#,
);
assert_eq!(shape.paths.len(), 1);
let path = &shape.paths[0];
assert!(path.closed && path.fill && path.stroke);
assert!(path.points.len() > 30, "{} points", path.points.len());
for (x, y) in &path.points {
let radius = ((x - 10800.0).powi(2) + (y - 10800.0).powi(2)).sqrt();
assert!(
(radius - 10800.0).abs() < 1.0,
"({x}, {y}) is off the circle"
);
}
}
#[test]
fn a_rectangle_is_four_lines_written_once() {
let shape = geometry(
r#"svg:viewBox="0 0 21600 21600"
draw:enhanced-path="M 0 0 L 21600 0 21600 21600 0 21600 0 0 Z N"/>"#,
);
assert_eq!(shape.paths.len(), 1);
assert!(shape.paths[0].closed);
assert_eq!(
shape.paths[0].points,
vec![
(0.0, 0.0),
(21600.0, 0.0),
(21600.0, 21600.0),
(0.0, 21600.0),
(0.0, 0.0),
]
);
}
#[test]
fn a_rounded_rectangle_resolves_its_formulas_and_rounds_its_corners() {
let shape = geometry(
r#"svg:viewBox="0 0 21600 21600" draw:type="round-rectangle"
draw:modifiers="10800"
draw:enhanced-path="M ?f7 0 X 0 ?f8 L 0 ?f9 Y ?f7 21600 L ?f10 21600 X 21600 ?f9 L 21600 ?f8 Y ?f10 0 Z N">
<draw:equation draw:name="f0" draw:formula="45"/>
<draw:equation draw:name="f1" draw:formula="$0 *sin(?f0 *(pi/180))"/>
<draw:equation draw:name="f2" draw:formula="?f1 *3163/7636"/>
<draw:equation draw:name="f7" draw:formula="left+$0 "/>
<draw:equation draw:name="f8" draw:formula="top+$0 "/>
<draw:equation draw:name="f9" draw:formula="bottom-$0 "/>
<draw:equation draw:name="f10" draw:formula="right-$0 "/>
</draw:enhanced-geometry>"#,
);
assert_eq!(shape.paths.len(), 1);
let path = &shape.paths[0];
assert!(path.closed);
assert_eq!(path.points[0], (10800.0, 0.0));
let (mut left, mut right, mut top, mut bottom) = (f32::MAX, f32::MIN, f32::MAX, f32::MIN);
for (x, y) in &path.points {
left = left.min(*x);
right = right.max(*x);
top = top.min(*y);
bottom = bottom.max(*y);
}
assert!(left.abs() < 1.0 && top.abs() < 1.0, "{left}, {top}");
assert!(
(right - 21600.0).abs() < 1.0 && (bottom - 21600.0).abs() < 1.0,
"{right}, {bottom}"
);
assert!(path.points.len() > 40, "the corners are not arcs");
}
#[test]
fn a_formula_may_name_another_and_the_chain_is_followed() {
let shape = geometry(
r#"svg:viewBox="0 0 21600 21600" draw:modifiers="10800"
draw:enhanced-path="M ?f2 0 L 21600 0 Z N">
<draw:equation draw:name="f0" draw:formula="45"/>
<draw:equation draw:name="f1" draw:formula="$0 *sin(?f0 *(pi/180))"/>
<draw:equation draw:name="f2" draw:formula="?f1 *3163/7636"/>
</draw:enhanced-geometry>"#,
);
let x = shape.paths[0].points[0].0;
assert!((x - 3163.5).abs() < 1.0, "{x}");
}
#[test]
fn a_formula_that_refers_to_itself_gives_up_rather_than_hanging() {
let shape = geometry(
r#"svg:viewBox="0 0 100 100"
draw:enhanced-path="M ?f0 0 L 100 100 Z N">
<draw:equation draw:name="f0" draw:formula="?f1 + 1"/>
<draw:equation draw:name="f1" draw:formula="?f0 + 1"/>
</draw:enhanced-geometry>"#,
);
assert!(shape.paths[0].points[0].0.is_finite());
}
#[test]
fn a_shape_may_be_several_strokes_and_say_which_are_filled() {
let shape = geometry(
r#"svg:viewBox="0 0 100 100"
draw:enhanced-path="M 0 0 L 100 0 100 100 Z N F M 10 10 L 90 10 Z N"/>"#,
);
assert_eq!(shape.paths.len(), 2);
assert!(shape.paths[0].fill, "the body should be filled");
assert!(
!shape.paths[1].fill,
"F turns the fill off for what follows"
);
}
#[test]
fn a_mirrored_shape_is_flipped_within_its_own_space() {
let plain = geometry(r#"svg:viewBox="0 0 100 50" draw:enhanced-path="M 10 5 L 90 5 Z N"/>"#);
let flipped = geometry(
r#"svg:viewBox="0 0 100 50" draw:mirror-horizontal="true"
draw:enhanced-path="M 10 5 L 90 5 Z N"/>"#,
);
assert_eq!(plain.paths[0].points[0], (10.0, 5.0));
assert_eq!(flipped.paths[0].points[0], (90.0, 5.0));
}
#[test]
fn every_custom_shape_in_the_corpus_yields_an_outline() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/libreoffice");
let Ok(entries) = std::fs::read_dir(&root) else {
return;
};
let mut shapes = 0;
for entry in entries.filter_map(Result::ok) {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("odp") {
continue;
}
let bytes = std::fs::read(&path).expect("the fixture");
let package = odox_core::Package::read(&bytes).expect("a readable package");
for part in ["content.xml", "styles.xml"] {
let Ok(Some(tree)) = package.optional_xml(part) else {
continue;
};
for element in descendants(&tree) {
if !element.is(&odox_core::Ns::Draw, "enhanced-geometry") {
continue;
}
shapes += 1;
let geometry = Geometry::read(element)
.unwrap_or_else(|| panic!("{} in {part}", path.display()));
assert!(
!geometry.paths.is_empty(),
"{} in {part} drew nothing",
path.display()
);
for stroke in &geometry.paths {
for (x, y) in &stroke.points {
assert!(
x.is_finite() && y.is_finite(),
"{} gave ({x}, {y})",
path.display()
);
}
}
}
}
}
assert!(shapes > 0, "the corpus has no custom shapes to read");
}
fn descendants(element: &odox_core::Element) -> Vec<&odox_core::Element> {
let mut found = vec![element];
for child in element.elements() {
found.extend(descendants(child));
}
found
}
mod svg {
use odox_core::draw::Geometry;
use odox_core::xml;
fn path(attrs: &str) -> Geometry {
let document = format!(
r#"<draw:path
xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"
xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"
{attrs}/>"#
);
let element = xml::parse(document.as_bytes(), "test").expect("well-formed XML");
Geometry::read_path(&element).expect("a readable path")
}
#[test]
fn a_sign_separates_two_numbers_with_no_space_between_them() {
let shape = path(r#"svg:viewBox="0 0 100 100" svg:d="M0 0h10v10-5-5z""#);
assert_eq!(
shape.paths[0].points,
vec![
(0.0, 0.0),
(10.0, 0.0),
(10.0, 10.0),
(10.0, 5.0),
(10.0, 0.0)
]
);
assert!(shape.paths[0].closed);
}
#[test]
fn lower_case_is_relative_and_upper_case_is_not() {
let relative = path(r#"svg:viewBox="0 0 100 100" svg:d="M10 10 l5 5 l5 5""#);
assert_eq!(
relative.paths[0].points,
vec![(10.0, 10.0), (15.0, 15.0), (20.0, 20.0)]
);
let absolute = path(r#"svg:viewBox="0 0 100 100" svg:d="M10 10 L5 5 L5 5""#);
assert_eq!(
absolute.paths[0].points,
vec![(10.0, 10.0), (5.0, 5.0), (5.0, 5.0)]
);
}
#[test]
fn the_pairs_after_a_moveto_are_lines() {
let shape = path(r#"svg:viewBox="0 0 100 100" svg:d="M0 0 10 0 10 10z""#);
assert_eq!(shape.paths.len(), 1);
assert_eq!(
shape.paths[0].points,
vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)]
);
}
#[test]
fn a_relative_cubic_is_flattened_and_ends_where_it_should() {
let shape = path(r#"svg:viewBox="0 0 100 100" svg:d="M0 0c10 0 20 10 20 20""#);
let points = &shape.paths[0].points;
assert_eq!(points[0], (0.0, 0.0));
let (x, y) = *points.last().expect("an end");
assert!(
(x - 20.0).abs() < 0.01 && (y - 20.0).abs() < 0.01,
"({x}, {y})"
);
assert!(points.len() > 8, "a curve is more than its ends");
}
#[test]
fn a_smooth_curve_continues_the_one_before_it() {
let shape = path(r#"svg:viewBox="0 0 100 100" svg:d="M0 0c0 10 10 10 10 10s10 0 10-10""#);
let points = &shape.paths[0].points;
let join = points.len() / 2;
let before = (
points[join].0 - points[join - 1].0,
points[join].1 - points[join - 1].1,
);
let after = (
points[join + 1].0 - points[join].0,
points[join + 1].1 - points[join].1,
);
let angle = |v: (f32, f32)| v.1.atan2(v.0);
assert!(
(angle(before) - angle(after)).abs() < 0.3,
"the join turns a corner: {before:?} then {after:?}"
);
}
#[test]
fn every_path_in_the_corpus_yields_an_outline() {
let root =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/libreoffice");
let Ok(entries) = std::fs::read_dir(&root) else {
return;
};
let mut found = 0;
for entry in entries.filter_map(Result::ok) {
let file = entry.path();
if file.extension().and_then(|e| e.to_str()) != Some("odp") {
continue;
}
let bytes = std::fs::read(&file).expect("the fixture");
let package = odox_core::Package::read(&bytes).expect("a readable package");
for part in ["content.xml", "styles.xml"] {
let Ok(Some(tree)) = package.optional_xml(part) else {
continue;
};
for element in super::descendants(&tree) {
if !element.is(&odox_core::Ns::Draw, "path") {
continue;
}
found += 1;
let geometry = Geometry::read_path(element)
.unwrap_or_else(|| panic!("{} in {part}", file.display()));
assert!(
!geometry.paths.is_empty(),
"{} drew nothing",
file.display()
);
for stroke in &geometry.paths {
for (x, y) in &stroke.points {
assert!(x.is_finite() && y.is_finite(), "({x}, {y})");
}
}
}
}
}
assert!(found > 0, "the corpus has no draw:path to read");
}
#[test]
fn a_refit_box_holds_the_outline_it_was_measured_from() {
let mut shape =
path(r#"svg:viewBox="0 0 3947 2706" svg:d="M5280 8808c2959 0 987-2705 3946-2705""#);
shape.refit();
assert!((shape.view.x - 5280.0).abs() < 1.0, "{:?}", shape.view);
assert!((shape.view.y - 6103.0).abs() < 1.0, "{:?}", shape.view);
assert!((shape.view.width - 3946.0).abs() < 1.0, "{:?}", shape.view);
assert!((shape.view.height - 2705.0).abs() < 1.0, "{:?}", shape.view);
for (x, y) in shape.paths.iter().flat_map(|p| &p.points) {
assert!(
*x >= shape.view.x && *x <= shape.view.x + shape.view.width,
"({x}, {y}) is outside {:?}",
shape.view
);
}
}
#[test]
fn a_straight_run_still_refits_to_a_box_with_a_size() {
let mut shape = path(r#"svg:viewBox="0 0 10 10" svg:d="M0 40H100""#);
shape.refit();
assert!(shape.view.height > 0.0, "{:?}", shape.view);
assert!((shape.view.width - 100.0).abs() < 0.01, "{:?}", shape.view);
}
#[test]
fn every_connector_in_the_corpus_yields_a_route() {
let root =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/libreoffice");
let Ok(entries) = std::fs::read_dir(&root) else {
return;
};
let mut found = 0;
for entry in entries.filter_map(Result::ok) {
let file = entry.path();
if file.extension().and_then(|e| e.to_str()) != Some("odp") {
continue;
}
let bytes = std::fs::read(&file).expect("the fixture");
let package = odox_core::Package::read(&bytes).expect("a readable package");
let Ok(Some(tree)) = package.optional_xml("content.xml") else {
continue;
};
for element in super::descendants(&tree) {
if !element.is(&odox_core::Ns::Draw, "connector") {
continue;
}
found += 1;
let mut geometry =
Geometry::read_path(element).expect("a connector states its route");
geometry.refit();
assert!(geometry.view.width > 0.0 && geometry.view.height > 0.0);
assert!(
geometry.paths.iter().any(|p| p.points.len() > 2),
"{} routed a bare line",
file.display()
);
}
}
assert!(found > 0, "the corpus has no draw:connector to read");
}
}