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