use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
pub mod tiff;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("not a recognised container")]
UnknownFormat,
#[error("malformed {container}: {detail}")]
Malformed {
container: &'static str,
detail: String,
},
#[error("unsupported: {0}")]
Unsupported(String),
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum PixelLayout {
Chunky,
Cfa,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ImageSpec {
pub width: u32,
pub height: u32,
pub bits_per_sample: u16,
pub samples_per_pixel: u16,
pub layout: PixelLayout,
pub little_endian: bool,
}
impl ImageSpec {
pub fn byte_len(&self) -> u64 {
u64::from(self.width)
* u64::from(self.height)
* u64::from(self.samples_per_pixel)
* u64::from(self.bits_per_sample / 8)
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum SegmentKind {
Verbatim,
Image(ImageSpec),
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Segment {
pub src_offset: u64,
pub len: u64,
pub kind: SegmentKind,
}
fn orientation_default() -> u16 {
1
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Layout {
pub container: String,
pub total_len: u64,
#[serde(default = "orientation_default")]
pub orientation: u16,
pub segments: Vec<Segment>,
}
impl Layout {
pub fn validate(&self) -> Result<()> {
let mut cursor = 0u64;
for (i, s) in self.segments.iter().enumerate() {
if s.src_offset != cursor {
return Err(Error::Malformed {
container: "layout",
detail: format!(
"segment {i} starts at {} but previous coverage ended at {cursor}",
s.src_offset
),
});
}
if let SegmentKind::Image(spec) = &s.kind {
if spec.byte_len() != s.len {
return Err(Error::Malformed {
container: "layout",
detail: format!(
"segment {i}: spec implies {} bytes, segment claims {}",
spec.byte_len(),
s.len
),
});
}
}
cursor = cursor.checked_add(s.len).ok_or_else(|| Error::Malformed {
container: "layout",
detail: "segment length overflow".into(),
})?;
}
if cursor != self.total_len {
return Err(Error::Malformed {
container: "layout",
detail: format!("segments cover {cursor} bytes, file is {}", self.total_len),
});
}
Ok(())
}
pub fn image_segments(&self) -> impl Iterator<Item = (usize, &Segment, &ImageSpec)> {
self.segments
.iter()
.enumerate()
.filter_map(|(i, s)| match &s.kind {
SegmentKind::Image(spec) => Some((i, s, spec)),
SegmentKind::Verbatim => None,
})
}
pub fn payload_len(&self) -> u64 {
self.image_segments().map(|(_, s, _)| s.len).sum()
}
pub fn skeleton_len(&self) -> u64 {
self.total_len - self.payload_len()
}
}
pub fn analyze(path: &Path) -> Result<Layout> {
let mut f = File::open(path)?;
let total_len = f.metadata()?.len();
let mut magic = [0u8; 4];
f.seek(SeekFrom::Start(0))?;
f.read_exact(&mut magic)?;
f.seek(SeekFrom::Start(0))?;
match &magic {
[b'I', b'I', 42, 0] | [b'M', b'M', 0, 42] => tiff::analyze(&mut f, total_len),
_ => Err(Error::UnknownFormat),
}
}
pub(crate) fn tile(
container: &str,
total_len: u64,
orientation: u16,
mut regions: Vec<(u64, u64, ImageSpec)>,
) -> Result<Layout> {
regions.sort_by_key(|(off, _, _)| *off);
let mut segments = Vec::with_capacity(regions.len() * 2 + 1);
let mut cursor = 0u64;
for (off, len, spec) in regions {
if off < cursor {
return Err(Error::Malformed {
container: "layout",
detail: format!("image region at {off} overlaps previous coverage ending {cursor}"),
});
}
if off > cursor {
segments.push(Segment {
src_offset: cursor,
len: off - cursor,
kind: SegmentKind::Verbatim,
});
}
segments.push(Segment {
src_offset: off,
len,
kind: SegmentKind::Image(spec),
});
cursor = off + len;
}
if cursor < total_len {
segments.push(Segment {
src_offset: cursor,
len: total_len - cursor,
kind: SegmentKind::Verbatim,
});
}
let layout = Layout {
container: container.to_string(),
total_len,
orientation,
segments,
};
layout.validate()?;
Ok(layout)
}
#[cfg(test)]
mod tests {
use super::*;
fn spec(w: u32, h: u32) -> ImageSpec {
ImageSpec {
width: w,
height: h,
bits_per_sample: 16,
samples_per_pixel: 1,
layout: PixelLayout::Cfa,
little_endian: true,
}
}
#[test]
fn tile_fills_gaps_and_validates() {
let l = tile("test", 100, 1, vec![(20, 40, spec(4, 5))]).unwrap();
assert_eq!(l.segments.len(), 3);
assert_eq!(l.segments[0].kind, SegmentKind::Verbatim);
assert_eq!(l.segments[0].len, 20);
assert!(matches!(l.segments[1].kind, SegmentKind::Image(_)));
assert_eq!(l.segments[2].src_offset, 60);
assert_eq!(l.segments[2].len, 40);
assert_eq!(l.payload_len(), 40);
assert_eq!(l.skeleton_len(), 60);
}
#[test]
fn tile_sorts_out_of_order_regions() {
let l = tile(
"test",
100,
1,
vec![(60, 20, spec(2, 5)), (10, 20, spec(2, 5))],
)
.unwrap();
l.validate().unwrap();
let offsets: Vec<u64> = l.segments.iter().map(|s| s.src_offset).collect();
assert_eq!(offsets, vec![0, 10, 30, 60, 80]);
}
#[test]
fn image_region_at_file_start_and_end_needs_no_padding() {
let l = tile("test", 40, 1, vec![(0, 40, spec(4, 5))]).unwrap();
assert_eq!(l.segments.len(), 1);
assert_eq!(l.skeleton_len(), 0);
}
#[test]
fn overlapping_regions_are_rejected() {
let e = tile(
"test",
100,
1,
vec![(10, 30, spec(2, 5)), (20, 20, spec(2, 5))],
);
assert!(e.is_err());
}
#[test]
fn spec_mismatch_is_rejected() {
let bad = tile("test", 100, 1, vec![(0, 39, spec(4, 5))]);
assert!(bad.is_err());
}
#[test]
fn validate_rejects_short_coverage() {
let l = Layout {
container: "test".into(),
total_len: 100,
orientation: 1,
segments: vec![Segment {
src_offset: 0,
len: 50,
kind: SegmentKind::Verbatim,
}],
};
assert!(l.validate().is_err());
}
}