From 4d358d4fa4df894d7de6f63a0cecf16d8a1bdd6a Mon Sep 17 00:00:00 2001 From: pauliusiv123-rgb <235694617+pauliusiv123-rgb@users.noreply.github.com> Date: Thu, 15 Oct 2026 09:27:02 +0300 Subject: [PATCH] M2: a /Rotate that isn't a quarter turn counts as 0, as the page is drawn (#1863) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Page::rotation rounded a /Rotate that isn't a multiple of 90 down to a quarter turn (135 became 90), while the renderer draws such a page upright. Display size and the display-to-user-space matrix (header and footer, watermarks, measuring, printing, image signatures) then disagreed with the page on screen. ISO 32000-2 §7.7.3.3 requires a multiple of 90; read any other value as 0, as the renderer does. Page import and image stamps, which read /Rotate themselves, already treat such a value as 0. Test: rotate_off_a_quarter_turn_counts_as_upright (/Rotate 135, 30, -45, 359). --- crates/model/README.md | 3 ++- crates/model/src/lib.rs | 21 +++++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/crates/model/README.md b/crates/model/README.md index 06c30b039..5872b2566 100644 --- a/crates/model/README.md +++ b/crates/model/README.md @@ -9,7 +9,8 @@ L3/L4 crates need: geometry, and the matrix from "display space" (points, origin at the bottom-left of the page as shown, after `/UserUnit` and `/Rotate`) to user space, so content can be placed the way the reader sees the page. `/UserUnit` is read as Acrobat reads it: the page's own entry, a number - from 1 up, at most 75,000; anything else counts as 1. + from 1 up, at most 75,000; anything else counts as 1. A `/Rotate` that isn't a multiple of 90 + counts as 0, as the renderer draws it. `view_matrix_for(rotation, rect)` is the same matrix for any rectangle, e.g. to draw a comment's picture upright inside its user-space `/Rect` on a turned page. diff --git a/crates/model/src/lib.rs b/crates/model/src/lib.rs index 4e4f3f53b..211b44492 100644 --- a/crates/model/src/lib.rs +++ b/crates/model/src/lib.rs @@ -37,9 +37,11 @@ impl Page { } } - /// Clockwise rotation when displayed: 0, 90, 180 or 270. + /// Clockwise rotation when displayed: 0, 90, 180 or 270. A `/Rotate` that isn't a multiple + /// of 90 (ISO 32000-2 §7.7.3.3 requires one) counts as 0, as the renderer draws that page. pub fn rotation(&self, doc: &Document) -> i64 { - self.dict.get(b"Rotate").and_then(|o| doc.resolve(o).as_int()).unwrap_or(0).rem_euclid(360) / 90 * 90 + let r = self.dict.get(b"Rotate").and_then(|o| doc.resolve(o).as_int()).unwrap_or(0).rem_euclid(360); + if r % 90 == 0 { r } else { 0 } } /// The size of the page's user-space unit in points (`/UserUnit`; it isn't inherited), read @@ -184,6 +186,21 @@ mod tests { } } + /// A `/Rotate` that isn't a quarter turn counts as 0, as the renderer draws the page + /// upright, so display space matches what's on screen. + #[test] + fn rotate_off_a_quarter_turn_counts_as_upright() { + for rotate in [135, 30, -45, 359] { + let d = doc(rotate); + let p = &pages(&d)[0]; + assert_eq!(p.rotation(&d), 0, "{rotate}"); + assert_eq!(p.display_size(&d), (200.0, 300.0), "{rotate}"); + assert_eq!(p.view_matrix(&d), [1.0, 0.0, 0.0, 1.0, 10.0, 20.0], "{rotate}"); + } + let d = doc(-90); + assert_eq!(pages(&d)[0].rotation(&d), 270); + } + /// Display space is in points: `/UserUnit 2` doubles it. As in Acrobat, the value isn't /// inherited, values below 1 or not numbers count as 1, and values over 75,000 as 75,000. #[test]