diff --git a/crates/geom/src/homography.rs b/crates/geom/src/homography.rs index b27ecef2..daa11b05 100644 --- a/crates/geom/src/homography.rs +++ b/crates/geom/src/homography.rs @@ -51,11 +51,11 @@ impl Homography { let c01 = m[5] * m[6] - m[3] * m[8]; let c02 = m[3] * m[7] - m[4] * m[6]; let det = m[0] * c00 + m[1] * c01 + m[2] * c02; - if det.abs() < 1e-300 { + if !det.is_finite() || det.abs() < 1e-300 { return None; } let inv = 1.0 / det; - Some(Homography([ + let coefficients = [ c00 * inv, (m[2] * m[7] - m[1] * m[8]) * inv, (m[1] * m[5] - m[2] * m[4]) * inv, @@ -65,11 +65,17 @@ impl Homography { c02 * inv, (m[1] * m[6] - m[0] * m[7]) * inv, (m[0] * m[4] - m[1] * m[3]) * inv, - ])) + ]; + coefficients.iter().all(|v| v.is_finite()).then_some(Homography(coefficients)) } /// The homography mapping `src[i]` → `dst[i]` (4 correspondences, DLT with h33 = 1). pub fn from_quads(src: &[Point; 4], dst: &[Point; 4]) -> Option { + // Failed feature matches and invalid perspective parameters can contain + // non-finite coordinates. Do not return a transform that poisons rendering. + if src.iter().chain(dst.iter()).any(|p| !p.x.is_finite() || !p.y.is_finite()) { + return None; + } let mut a = [[0.0f64; 9]; 8]; for i in 0..4 { let (x, y) = (src[i].x, src[i].y); @@ -80,7 +86,7 @@ impl Homography { // Gaussian elimination with partial pivoting on the 8×8 system (last column = rhs). for col in 0..8 { let piv = (col..8).max_by(|&i, &j| a[i][col].abs().total_cmp(&a[j][col].abs()))?; - if a[piv][col].abs() < 1e-12 { + if !a[piv][col].is_finite() || a[piv][col].abs() < 1e-12 { return None; } a.swap(col, piv); @@ -94,7 +100,7 @@ impl Homography { } } let h: Vec = (0..8).map(|i| a[i][8] / a[i][i]).collect(); - Some(Homography([h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7], 1.0])) + h.iter().all(|v| v.is_finite()).then_some(Homography([h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7], 1.0])) } } @@ -121,4 +127,19 @@ mod tests { let src = [Point::new(0.0, 0.0); 4]; assert!(Homography::from_quads(&src, &src).is_none()); } + + #[test] + fn nonfinite_projective_and_affine_transforms_are_rejected() { + let src = [Point::new(0.0, 0.0), Point::new(1.0, 0.0), Point::new(1.0, 1.0), Point::new(0.0, 1.0)]; + let mut invalid = src; + invalid[2].x = f64::NAN; + assert!(Homography::from_quads(&invalid, &src).is_none()); + invalid[2].x = f64::INFINITY; + assert!(Homography::from_quads(&src, &invalid).is_none()); + assert!(Homography([f64::NAN, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]).inverse().is_none()); + assert!(Homography([f64::INFINITY, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]).inverse().is_none()); + assert!(Affine([f64::NAN, 0.0, 0.0, 1.0, 0.0, 0.0]).inverse().is_none()); + assert!(Affine([f64::INFINITY, 0.0, 0.0, 1.0, 0.0, 0.0]).inverse().is_none()); + assert_eq!(Affine::IDENTITY.inverse(), Some(Affine::IDENTITY)); + } } diff --git a/crates/geom/src/lib.rs b/crates/geom/src/lib.rs index 1d81279d..4b8f623e 100644 --- a/crates/geom/src/lib.rs +++ b/crates/geom/src/lib.rs @@ -234,12 +234,13 @@ impl Affine { } pub fn inverse(&self) -> Option { let det = self.determinant(); - if det.abs() < 1e-300 { + if !det.is_finite() || det.abs() < 1e-300 { return None; } let [a, b, c, d, e, f] = self.0; let inv = 1.0 / det; - Some(Affine([d * inv, -b * inv, -c * inv, a * inv, (c * f - d * e) * inv, (b * e - a * f) * inv])) + let coefficients = [d * inv, -b * inv, -c * inv, a * inv, (c * f - d * e) * inv, (b * e - a * f) * inv]; + coefficients.iter().all(|v| v.is_finite()).then_some(Affine(coefficients)) } /// Bounding box of a transformed rectangle. pub fn transform_rect_bbox(&self, r: &Rect) -> Rect {