1use crate::error::IffError;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub enum Rotation {
25 None,
27 Ccw90,
29 Rot180,
31 Cw90,
33}
34
35#[derive(Debug, Clone, PartialEq)]
37#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
38pub struct PageInfo {
39 pub width: u16,
41 pub height: u16,
43 pub dpi: u16,
45 pub gamma: f32,
47 pub rotation: Rotation,
49}
50
51impl PageInfo {
52 pub fn parse(data: &[u8]) -> Result<Self, IffError> {
58 if data.len() < 10 {
59 return Err(IffError::Truncated);
60 }
61
62 let width = u16::from_be_bytes(data[0..2].try_into().map_err(|_| IffError::Truncated)?);
64 let height = u16::from_be_bytes(data[2..4].try_into().map_err(|_| IffError::Truncated)?);
65
66 let dpi = u16::from_le_bytes(data[6..8].try_into().map_err(|_| IffError::Truncated)?);
68
69 let gamma_byte = data[8];
71 let gamma = if gamma_byte == 0 {
72 2.2_f32 } else {
74 gamma_byte as f32 / 10.0
75 };
76
77 let flags = data[9];
83 let rotation = match flags & 0x07 {
84 5 => Rotation::Cw90,
85 2 => Rotation::Rot180,
86 6 => Rotation::Ccw90,
87 _ => Rotation::None,
88 };
89
90 Ok(PageInfo {
91 width,
92 height,
93 dpi,
94 gamma,
95 rotation,
96 })
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 fn chicken_info_bytes() -> [u8; 10] {
106 [
107 0x00, 0xB5, 0x00, 0xF0, 0x18, 0x00, 0x64, 0x00, 0x16, 0x00, ]
115 }
116
117 #[test]
118 fn parse_chicken_info() {
119 let info = PageInfo::parse(&chicken_info_bytes()).expect("should parse");
120 assert_eq!(info.width, 181);
121 assert_eq!(info.height, 240);
122 assert_eq!(info.dpi, 100);
123 assert!((info.gamma - 2.2).abs() < 0.01, "gamma should be 2.2");
124 assert_eq!(info.rotation, Rotation::None);
125 }
126
127 #[test]
128 fn too_short_is_error() {
129 let data = [0u8; 9]; assert_eq!(PageInfo::parse(&data).unwrap_err(), IffError::Truncated);
131 }
132
133 #[test]
134 fn empty_is_error() {
135 assert_eq!(PageInfo::parse(&[]).unwrap_err(), IffError::Truncated);
136 }
137
138 #[test]
139 fn rotation_none() {
140 let mut bytes = chicken_info_bytes();
141 bytes[9] = 0x00; let info = PageInfo::parse(&bytes).unwrap();
143 assert_eq!(info.rotation, Rotation::None);
144 }
145
146 #[test]
147 fn rotation_flag1_is_none() {
148 let mut bytes = chicken_info_bytes();
149 bytes[9] = 0x01;
150 let info = PageInfo::parse(&bytes).unwrap();
151 assert_eq!(info.rotation, Rotation::None);
152 }
153
154 #[test]
155 fn rotation_flag2_is_180() {
156 let mut bytes = chicken_info_bytes();
157 bytes[9] = 0x02;
158 let info = PageInfo::parse(&bytes).unwrap();
159 assert_eq!(info.rotation, Rotation::Rot180);
160 }
161
162 #[test]
163 fn rotation_flag5_is_cw90() {
164 let mut bytes = chicken_info_bytes();
165 bytes[9] = 0x05;
166 let info = PageInfo::parse(&bytes).unwrap();
167 assert_eq!(info.rotation, Rotation::Cw90);
168 }
169
170 #[test]
171 fn rotation_flag6_is_ccw90() {
172 let mut bytes = chicken_info_bytes();
173 bytes[9] = 0x06;
174 let info = PageInfo::parse(&bytes).unwrap();
175 assert_eq!(info.rotation, Rotation::Ccw90);
176 }
177
178 #[test]
179 fn gamma_zero_defaults_to_2_2() {
180 let mut bytes = chicken_info_bytes();
181 bytes[8] = 0x00; let info = PageInfo::parse(&bytes).unwrap();
183 assert!(
184 (info.gamma - 2.2).abs() < 0.01,
185 "default gamma should be 2.2"
186 );
187 }
188
189 #[test]
190 fn parse_real_chicken_info_from_iff() {
191 let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
193 .join("references/djvujs/library/assets/chicken.djvu");
194 let data = std::fs::read(&path).expect("chicken.djvu must exist");
195 let form = crate::iff::parse_form(&data).expect("IFF parse failed");
196
197 let info_chunk = form
198 .chunks
199 .iter()
200 .find(|c| &c.id == b"INFO")
201 .expect("INFO chunk must be present");
202
203 let info = PageInfo::parse(info_chunk.data).expect("INFO parse failed");
204 assert_eq!(info.width, 181);
205 assert_eq!(info.height, 240);
206 assert_eq!(info.dpi, 100);
207 }
208}