1use std::fs::File;
16use std::io::{Read, Seek, SeekFrom};
17use std::path::Path;
18
19pub mod tiff;
20
21#[derive(Debug, thiserror::Error)]
22pub enum Error {
23 #[error("io: {0}")]
24 Io(#[from] std::io::Error),
25 #[error("not a recognised container")]
26 UnknownFormat,
27 #[error("malformed {container}: {detail}")]
28 Malformed {
29 container: &'static str,
30 detail: String,
31 },
32 #[error("unsupported: {0}")]
33 Unsupported(String),
34}
35
36pub type Result<T> = std::result::Result<T, Error>;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
40pub enum PixelLayout {
41 Chunky,
43 Cfa,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
49pub struct ImageSpec {
50 pub width: u32,
51 pub height: u32,
52 pub bits_per_sample: u16,
53 pub samples_per_pixel: u16,
54 pub layout: PixelLayout,
55 pub little_endian: bool,
57}
58
59impl ImageSpec {
60 pub fn byte_len(&self) -> u64 {
62 u64::from(self.width)
63 * u64::from(self.height)
64 * u64::from(self.samples_per_pixel)
65 * u64::from(self.bits_per_sample / 8)
66 }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
70pub enum SegmentKind {
71 Verbatim,
73 Image(ImageSpec),
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
78pub struct Segment {
79 pub src_offset: u64,
80 pub len: u64,
81 pub kind: SegmentKind,
82}
83
84fn orientation_default() -> u16 {
86 1
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
90pub struct Layout {
91 pub container: String,
92 pub total_len: u64,
93 #[serde(default = "orientation_default")]
99 pub orientation: u16,
100 pub segments: Vec<Segment>,
101}
102
103impl Layout {
104 pub fn validate(&self) -> Result<()> {
111 let mut cursor = 0u64;
112 for (i, s) in self.segments.iter().enumerate() {
113 if s.src_offset != cursor {
114 return Err(Error::Malformed {
115 container: "layout",
116 detail: format!(
117 "segment {i} starts at {} but previous coverage ended at {cursor}",
118 s.src_offset
119 ),
120 });
121 }
122 if let SegmentKind::Image(spec) = &s.kind {
123 if spec.byte_len() != s.len {
124 return Err(Error::Malformed {
125 container: "layout",
126 detail: format!(
127 "segment {i}: spec implies {} bytes, segment claims {}",
128 spec.byte_len(),
129 s.len
130 ),
131 });
132 }
133 }
134 cursor = cursor.checked_add(s.len).ok_or_else(|| Error::Malformed {
135 container: "layout",
136 detail: "segment length overflow".into(),
137 })?;
138 }
139 if cursor != self.total_len {
140 return Err(Error::Malformed {
141 container: "layout",
142 detail: format!("segments cover {cursor} bytes, file is {}", self.total_len),
143 });
144 }
145 Ok(())
146 }
147
148 pub fn image_segments(&self) -> impl Iterator<Item = (usize, &Segment, &ImageSpec)> {
149 self.segments
150 .iter()
151 .enumerate()
152 .filter_map(|(i, s)| match &s.kind {
153 SegmentKind::Image(spec) => Some((i, s, spec)),
154 SegmentKind::Verbatim => None,
155 })
156 }
157
158 pub fn payload_len(&self) -> u64 {
160 self.image_segments().map(|(_, s, _)| s.len).sum()
161 }
162
163 pub fn skeleton_len(&self) -> u64 {
165 self.total_len - self.payload_len()
166 }
167}
168
169pub fn analyze(path: &Path) -> Result<Layout> {
171 let mut f = File::open(path)?;
172 let total_len = f.metadata()?.len();
173 let mut magic = [0u8; 4];
174 f.seek(SeekFrom::Start(0))?;
175 f.read_exact(&mut magic)?;
176 f.seek(SeekFrom::Start(0))?;
177
178 match &magic {
179 [b'I', b'I', 42, 0] | [b'M', b'M', 0, 42] => tiff::analyze(&mut f, total_len),
181 _ => Err(Error::UnknownFormat),
182 }
183}
184
185pub(crate) fn tile(
191 container: &str,
192 total_len: u64,
193 orientation: u16,
194 mut regions: Vec<(u64, u64, ImageSpec)>,
195) -> Result<Layout> {
196 regions.sort_by_key(|(off, _, _)| *off);
197
198 let mut segments = Vec::with_capacity(regions.len() * 2 + 1);
199 let mut cursor = 0u64;
200 for (off, len, spec) in regions {
201 if off < cursor {
202 return Err(Error::Malformed {
203 container: "layout",
204 detail: format!("image region at {off} overlaps previous coverage ending {cursor}"),
205 });
206 }
207 if off > cursor {
208 segments.push(Segment {
209 src_offset: cursor,
210 len: off - cursor,
211 kind: SegmentKind::Verbatim,
212 });
213 }
214 segments.push(Segment {
215 src_offset: off,
216 len,
217 kind: SegmentKind::Image(spec),
218 });
219 cursor = off + len;
220 }
221 if cursor < total_len {
222 segments.push(Segment {
223 src_offset: cursor,
224 len: total_len - cursor,
225 kind: SegmentKind::Verbatim,
226 });
227 }
228
229 let layout = Layout {
230 container: container.to_string(),
231 total_len,
232 orientation,
233 segments,
234 };
235 layout.validate()?;
236 Ok(layout)
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 fn spec(w: u32, h: u32) -> ImageSpec {
244 ImageSpec {
245 width: w,
246 height: h,
247 bits_per_sample: 16,
248 samples_per_pixel: 1,
249 layout: PixelLayout::Cfa,
250 little_endian: true,
251 }
252 }
253
254 #[test]
255 fn tile_fills_gaps_and_validates() {
256 let l = tile("test", 100, 1, vec![(20, 40, spec(4, 5))]).unwrap();
258 assert_eq!(l.segments.len(), 3);
259 assert_eq!(l.segments[0].kind, SegmentKind::Verbatim);
260 assert_eq!(l.segments[0].len, 20);
261 assert!(matches!(l.segments[1].kind, SegmentKind::Image(_)));
262 assert_eq!(l.segments[2].src_offset, 60);
263 assert_eq!(l.segments[2].len, 40);
264 assert_eq!(l.payload_len(), 40);
265 assert_eq!(l.skeleton_len(), 60);
266 }
267
268 #[test]
269 fn tile_sorts_out_of_order_regions() {
270 let l = tile(
271 "test",
272 100,
273 1,
274 vec![(60, 20, spec(2, 5)), (10, 20, spec(2, 5))],
275 )
276 .unwrap();
277 l.validate().unwrap();
278 let offsets: Vec<u64> = l.segments.iter().map(|s| s.src_offset).collect();
279 assert_eq!(offsets, vec![0, 10, 30, 60, 80]);
280 }
281
282 #[test]
283 fn image_region_at_file_start_and_end_needs_no_padding() {
284 let l = tile("test", 40, 1, vec![(0, 40, spec(4, 5))]).unwrap();
285 assert_eq!(l.segments.len(), 1);
286 assert_eq!(l.skeleton_len(), 0);
287 }
288
289 #[test]
290 fn overlapping_regions_are_rejected() {
291 let e = tile(
292 "test",
293 100,
294 1,
295 vec![(10, 30, spec(2, 5)), (20, 20, spec(2, 5))],
296 );
297 assert!(e.is_err());
298 }
299
300 #[test]
301 fn spec_mismatch_is_rejected() {
302 let bad = tile("test", 100, 1, vec![(0, 39, spec(4, 5))]);
304 assert!(bad.is_err());
305 }
306
307 #[test]
308 fn validate_rejects_short_coverage() {
309 let l = Layout {
310 container: "test".into(),
311 total_len: 100,
312 orientation: 1,
313 segments: vec![Segment {
314 src_offset: 0,
315 len: 50,
316 kind: SegmentKind::Verbatim,
317 }],
318 };
319 assert!(l.validate().is_err());
320 }
321}