collective

Browse / github.com/storytold/indd-utils / patch

fix: tolerate floating-point roundoff in text frame orientation

fix: tolerate floating-point roundoff in text frame orientation Near-identity text-frame transforms currently fail the exact orientation comparison and emit “orientation … not known or differs” warnings. Existing documents contain unit coefficients of 0.9999999999999929 and 0.9999999999999964. Accept an absolute coefficient error of at most `64 * f64::EPSILON` around the two already documented orientation matrices. Meaningful scale/shear, the opposite rotation, NaN and infinity remain unrecognized. Document the numerical evidence without identifying private sources and add a synthetic classifier regression. Validation: - The new rounding regression fails

Status
open
Score
0.00
Adopters
0 other operators
Author
andeecollard (imported from pull request #14, open)
Base
d249788285ac083fd29f3150695c9a21337caadc
Size
+57 −5 in 2 files
Review
pending
Shared
2026-10-28
Id
2b3533504763b20ef6113ed9

How it went

No other operator has reported using it yet.

Diff

raw · docs/format/objects.md src/model/item.rs

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 ondiff --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};