diff --git a/docs/format/objects.md b/docs/format/objects.md
--- a/docs/format/objects.md
+++ b/docs/format/objects.md
@@ -4418,6 +4418,18 @@ and otherwise `Horizontal`, the value every IDML has. A story whose
 frames disagree or have another matrix gets a warning. Two frames in
 the InDesign 3.0 file have no chunk 0x2DE; they do not count.
 
+Numerical tolerance (2026-10-22): in two privately held, pre-existing
+documents, `indd dump` found four matrices `1 0 0 0.9999999999999929`
+and three matrices `1 0 0 0.9999999999999964`; the other fifteen frames
+had exact identity matrices. Exact equality unnecessarily warned on
+those seven near-identity frames. The orientation classifier now accepts
+an absolute coefficient error of at most `64 * f64::EPSILON` (about
+1.42e-14) around either documented matrix. This is a rounding policy,
+not a newly inferred orientation mapping or a visual-fidelity claim.
+Synthetic tests also cover the quarter-turn matrix with rounding,
+meaningful scaling/shear, the opposite rotation, NaN and infinity.
+Private source paths and the dump logs remain outside the repository.
+
 **First baseline offset.** IDML writes `FirstBaselineOffset` on a frame's
 `TextFramePreference` only when it differs from the frame's object style.
 Over the frames of the corpus pairs, compared with the value written on
diff --git a/src/model/item.rs b/src/model/item.rs
--- a/src/model/item.rs
+++ b/src/model/item.rs
@@ -679,6 +679,24 @@ fn oval_anchors(path: &Path) -> bool {
     }
 }
 
+fn frame_text_orientation(matrix: [f64; 4]) -> Option<Orientation> {
+    // See docs/format/objects.md: unit coefficients can differ from 1 by
+    // floating-point roundoff. Do not accept meaningful scale or shear.
+    for (expected, orientation) in [
+        ([1.0, 0.0, 0.0, 1.0], Orientation::Horizontal),
+        ([0.0, 1.0, -1.0, 0.0], Orientation::Vertical),
+    ] {
+        if matrix
+            .iter()
+            .zip(expected)
+            .all(|(a, b)| (*a - b).abs() <= 64.0 * f64::EPSILON)
+        {
+            return Some(orientation);
+        }
+    }
+    None
+}
+
 impl<'a> Reader<'a> {
     /// Text orientation of a multi-column frame (chunk 0x2DE): the
     /// identity gives horizontal text, the rotation 0 1 −1 0 vertical
@@ -692,11 +710,7 @@ impl<'a> Reader<'a> {
             return Ok(Some(None));
         }
         let Matrix([a, b, c, dd, _, _]) = Matrix::read(&mut self.cursor(&d))?;
-        Ok(Some(match [a, b, c, dd] {
-            [1.0, 0.0, 0.0, 1.0] => Some(Orientation::Horizontal),
-            [0.0, 1.0, -1.0, 0.0] => Some(Orientation::Vertical),
-            _ => None,
-        }))
+        Ok(Some(frame_text_orientation([a, b, c, dd])))
     }
 
     /// Text frame settings of frame `frame`, from its multi-column frame
@@ -2099,6 +2113,32 @@ mod tests {
     use super::*;
     use crate::object::Encoding;
 
+    #[test]
+    fn frame_orientation_tolerates_rounding_without_accepting_other_transforms() {
+        assert_eq!(
+            frame_text_orientation([1.0, 0.0, 0.0, 0.9999999999999929]),
+            Some(Orientation::Horizontal)
+        );
+        assert_eq!(
+            frame_text_orientation([1.0, 0.0, 0.0, 0.9999999999999964]),
+            Some(Orientation::Horizontal)
+        );
+        assert_eq!(
+            frame_text_orientation([0.0, 0.9999999999999964, -1.0, 0.0]),
+            Some(Orientation::Vertical)
+        );
+        for matrix in [
+            [1.0, 0.0, 0.0, 0.999999],
+            [1.0, 0.0, 0.0, 1.0 + 1e-12],
+            [1.0, 0.001, 0.0, 1.0],
+            [0.0, -1.0, 1.0, 0.0],
+            [f64::NAN, 0.0, 0.0, 1.0],
+            [1.0, 0.0, 0.0, f64::INFINITY],
+        ] {
+            assert_eq!(frame_text_orientation(matrix), None);
+        }
+    }
+
     #[test]
     fn reads_the_first_alpha_channel() {
         use crate::header::{ByteOrder, Version};
