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