use crate::tiles::TileError;
pub const COVERAGE_EXTENSION_NAME: &str = "gpkg_2d_gridded_coverage";
pub const COVERAGE_EXTENSION_DEFINITION: &str =
"http://docs.opengeospatial.org/is/17-066r1/17-066r1.html";
pub const COVERAGE_EXTENSION_SCOPE: &str = "read-write";
pub const COVERAGE_DATA_TYPE: &str = "2d-gridded-coverage";
pub const COVERAGE_ANCILLARY_TABLE: &str = "gpkg_2d_gridded_coverage_ancillary";
pub const TILE_ANCILLARY_TABLE: &str = "gpkg_2d_gridded_tile_ancillary";
mod tag {
pub const IMAGE_WIDTH: u16 = 256;
pub const IMAGE_LENGTH: u16 = 257;
pub const BITS_PER_SAMPLE: u16 = 258;
pub const COMPRESSION: u16 = 259;
pub const SAMPLES_PER_PIXEL: u16 = 277;
pub const TILE_WIDTH: u16 = 322;
pub const TILE_LENGTH: u16 = 323;
pub const TILE_OFFSETS: u16 = 324;
pub const TILE_BYTE_COUNTS: u16 = 325;
pub const SUB_IFDS: u16 = 330;
pub const SAMPLE_FORMAT: u16 = 339;
pub fn name(tag: u16) -> &'static str {
match tag {
TILE_WIDTH => "TileWidth",
TILE_LENGTH => "TileLength",
TILE_OFFSETS => "TileOffsets",
TILE_BYTE_COUNTS => "TileByteCounts",
_ => "tag",
}
}
}
const ENTRY_LEN: usize = 12;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SampleType {
Float32,
Unsigned(u8),
Signed(u8),
}
impl SampleType {
pub fn bits(self) -> u8 {
match self {
Self::Float32 => 32,
Self::Unsigned(bits) | Self::Signed(bits) => bits,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TiffCompression {
None,
Lzw,
Deflate,
}
impl TiffCompression {
pub fn is_baseline(self) -> bool {
matches!(self, Self::None | Self::Lzw)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct CoverageTiff {
pub sample_type: SampleType,
pub compression: TiffCompression,
pub width: i64,
pub height: i64,
}
pub fn coverage_tiff(bytes: &[u8]) -> Result<CoverageTiff, TileError> {
if let Some((_, 43)) = header(bytes) {
return Err(violation(
15,
"BigTIFF (version 43) is not the baseline TIFF the profile requires",
));
}
let ifd = FirstIfd::read(bytes)?;
if ifd.more_ifds {
return Err(violation(
19,
"the first IFD points to a second image, and multiple images are not allowed",
));
}
if ifd.sub_ifds {
return Err(violation(
19,
"SubIFDs (tag 330) declares further images, and multiple images are not allowed",
));
}
if let Some(tile_tag) = ifd.tile_tag {
return Err(violation(
20,
format!(
"{} (tag {tile_tag}) organises the image into internal tiles",
tag::name(tile_tag)
),
));
}
match ifd.samples_per_pixel.value(1) {
Some(1) => {}
Some(samples) => {
return Err(violation(
16,
format!(
"SamplesPerPixel is {samples}, and a coverage tile has one sample per grid cell"
),
));
}
None => return Err(violation(16, "SamplesPerPixel is not a single number")),
}
let format = ifd
.sample_format
.value(1)
.ok_or_else(|| violation(17, "SampleFormat is not a single number"))?;
let bits = ifd
.bits_per_sample
.value(1)
.ok_or_else(|| violation(17, "BitsPerSample is not a single number"))?;
let sample_type = match (format, bits) {
(3, 32) => SampleType::Float32,
(3, other) => {
return Err(violation(
17,
format!("SampleFormat 3 (IEEE floating point) needs BitsPerSample 32, not {other}"),
));
}
(1, 8) => SampleType::Unsigned(8),
(1, 16) => SampleType::Unsigned(16),
(1, 32) => SampleType::Unsigned(32),
(2, 8) => SampleType::Signed(8),
(2, 16) => SampleType::Signed(16),
(2, 32) => SampleType::Signed(32),
(1 | 2, other) => {
return Err(violation(
17,
format!(
"BitsPerSample is {other}, and an integer coverage sample is 8, 16 or 32 bits"
),
));
}
(other, _) => {
return Err(violation(
17,
format!(
"SampleFormat is {other}, and a coverage sample is 1 (unsigned integer), 2 (signed integer) or 3 (IEEE floating point)"
),
));
}
};
let compression = match ifd.compression.value(1) {
Some(1) => TiffCompression::None,
Some(5) => TiffCompression::Lzw,
Some(8 | 0x80B2) => TiffCompression::Deflate,
Some(other) => {
return Err(violation(
18,
format!(
"Compression is {other}, and a coverage tile is uncompressed or LZW compressed"
),
));
}
None => return Err(violation(18, "Compression is not a single number")),
};
let (width, height) = ifd.dimensions()?;
Ok(CoverageTiff {
sample_type,
compression,
width,
height,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct CoveragePng {
pub width: i64,
pub height: i64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CoveragePayload {
Tiff(CoverageTiff),
Png(CoveragePng),
}
impl CoveragePayload {
pub fn sample_type(self) -> SampleType {
match self {
Self::Tiff(tiff) => tiff.sample_type,
Self::Png(_) => SampleType::Unsigned(16),
}
}
pub fn width(self) -> i64 {
match self {
Self::Tiff(tiff) => tiff.width,
Self::Png(png) => png.width,
}
}
pub fn height(self) -> i64 {
match self {
Self::Tiff(tiff) => tiff.height,
Self::Png(png) => png.height,
}
}
pub fn mime_type(self) -> &'static str {
match self {
Self::Tiff(_) => "image/tiff",
Self::Png(_) => "image/png",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CoverageDatatype {
Integer,
Float,
}
impl CoverageDatatype {
pub fn parse(value: &str) -> Option<Self> {
match value {
"integer" => Some(Self::Integer),
"float" => Some(Self::Float),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Integer => "integer",
Self::Float => "float",
}
}
pub fn check_payload(self, payload: &CoveragePayload) -> Result<(), TileError> {
match (self, payload.sample_type()) {
(Self::Float, SampleType::Float32) if matches!(payload, CoveragePayload::Tiff(_)) => {
Ok(())
}
(Self::Float, _) => Err(violation(
14,
format!(
"the coverage declares datatype float, and the payload is {} with {:?} samples",
payload.mime_type(),
payload.sample_type()
),
)),
(Self::Integer, SampleType::Unsigned(_) | SampleType::Signed(_)) => Ok(()),
(Self::Integer, sample_type) => Err(violation(
13,
format!(
"the coverage declares datatype integer, and the payload has {sample_type:?} samples"
),
)),
}
}
}
pub fn coverage_payload(bytes: &[u8]) -> Result<CoveragePayload, TileError> {
if bytes.starts_with(&PNG_SIGNATURE) {
return Ok(CoveragePayload::Png(coverage_png(bytes)?));
}
Ok(CoveragePayload::Tiff(coverage_tiff(bytes)?))
}
const PNG_SIGNATURE: [u8; 8] = [0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
pub fn coverage_png(bytes: &[u8]) -> Result<CoveragePng, TileError> {
if !bytes.starts_with(&PNG_SIGNATURE) {
return Err(unreadable("no PNG signature"));
}
let Some(ihdr) = bytes.get(12..).and_then(|rest| rest.first_chunk::<14>()) else {
return Err(unreadable("truncated before the IHDR header ends"));
};
let [
type0,
type1,
type2,
type3,
width0,
width1,
width2,
width3,
height0,
height1,
height2,
height3,
bit_depth,
colour_type,
] = *ihdr;
if [type0, type1, type2, type3] != *b"IHDR" {
return Err(unreadable("the first PNG chunk is not IHDR"));
}
if bit_depth != 16 {
return Err(violation(
13,
format!("PNG bit depth is {bit_depth}, and a coverage tile is 16-bit"),
));
}
if colour_type != 0 {
return Err(violation(
13,
format!(
"PNG colour type is {colour_type}, and a coverage tile is single channel (greyscale, colour type 0)"
),
));
}
Ok(CoveragePng {
width: i64::from(u32::from_be_bytes([width0, width1, width2, width3])),
height: i64::from(u32::from_be_bytes([height0, height1, height2, height3])),
})
}
pub(crate) fn dimensions(bytes: &[u8]) -> Result<(i64, i64), TileError> {
FirstIfd::read(bytes)?.dimensions()
}
#[derive(Debug, Default, Clone, Copy)]
struct FirstIfd {
image_width: Tag,
image_length: Tag,
bits_per_sample: Tag,
sample_format: Tag,
samples_per_pixel: Tag,
compression: Tag,
tile_tag: Option<u16>,
sub_ifds: bool,
more_ifds: bool,
}
#[derive(Debug, Default, Clone, Copy)]
enum Tag {
#[default]
Absent,
Present(Option<u32>),
}
impl Tag {
fn value(self, default: u32) -> Option<u32> {
match self {
Self::Absent => Some(default),
Self::Present(value) => value,
}
}
}
impl FirstIfd {
fn read(bytes: &[u8]) -> Result<Self, TileError> {
let Some((endian, version)) = header(bytes) else {
return Err(unreadable(if bytes.len() < 4 {
"shorter than a TIFF header"
} else {
"no TIFF byte order mark"
}));
};
if version != 42 {
return Err(unreadable(format!("TIFF version is {version}, not 42")));
}
let Some(offset_bytes) = bytes.get(4..).and_then(|rest| rest.first_chunk::<4>()) else {
return Err(unreadable("truncated before the first IFD pointer"));
};
let Ok(offset) = usize::try_from(endian.u32(*offset_bytes)) else {
return Err(unreadable("the first IFD lies beyond this address space"));
};
let Some(count) = bytes
.get(offset..)
.and_then(|rest| rest.first_chunk::<2>())
.map(|count| endian.u16(*count))
else {
return Err(unreadable("truncated before the first IFD"));
};
let entries_start = offset
.checked_add(2)
.ok_or_else(|| unreadable("the first IFD lies beyond this address space"))?;
let entries_end = entries_start
.checked_add(usize::from(count) * ENTRY_LEN)
.ok_or_else(|| unreadable("the first IFD extends beyond this address space"))?;
let Some(entries) = bytes.get(entries_start..entries_end) else {
return Err(unreadable("truncated inside the first IFD"));
};
let Some(next) = bytes
.get(entries_end..)
.and_then(|rest| rest.first_chunk::<4>())
.map(|next| endian.u32(*next))
else {
return Err(unreadable("truncated before the next-IFD pointer"));
};
let mut ifd = Self {
more_ifds: next != 0,
..Self::default()
};
let (chunks, _) = entries.as_chunks::<ENTRY_LEN>();
for entry in chunks {
ifd.read_entry(*entry, endian);
}
Ok(ifd)
}
fn read_entry(&mut self, entry: [u8; ENTRY_LEN], endian: Endian) {
let [
tag0,
tag1,
type0,
type1,
count0,
count1,
count2,
count3,
field @ ..,
] = entry;
let tag = endian.u16([tag0, tag1]);
let field_type = endian.u16([type0, type1]);
let count = endian.u32([count0, count1, count2, count3]);
let slot = match tag {
tag::IMAGE_WIDTH => &mut self.image_width,
tag::IMAGE_LENGTH => &mut self.image_length,
tag::BITS_PER_SAMPLE => &mut self.bits_per_sample,
tag::SAMPLE_FORMAT => &mut self.sample_format,
tag::SAMPLES_PER_PIXEL => &mut self.samples_per_pixel,
tag::COMPRESSION => &mut self.compression,
tag::TILE_WIDTH | tag::TILE_LENGTH | tag::TILE_OFFSETS | tag::TILE_BYTE_COUNTS => {
if self.tile_tag.is_none() {
self.tile_tag = Some(tag);
}
return;
}
tag::SUB_IFDS => {
self.sub_ifds = true;
return;
}
_ => return,
};
if matches!(slot, Tag::Absent) {
*slot = Tag::Present(scalar(field, field_type, count, endian));
}
}
fn dimensions(self) -> Result<(i64, i64), TileError> {
let read = |tag: Tag, name: &str| match tag {
Tag::Absent => Err(unreadable(format!("no {name} tag"))),
Tag::Present(None) => Err(unreadable(format!("{name} is not a single number"))),
Tag::Present(Some(value)) => Ok(i64::from(value)),
};
Ok((
read(self.image_width, "ImageWidth")?,
read(self.image_length, "ImageLength")?,
))
}
}
fn header(bytes: &[u8]) -> Option<(Endian, u16)> {
let [order0, order1, version0, version1] = *bytes.first_chunk::<4>()?;
let endian = match (order0, order1) {
(b'I', b'I') => Endian::Little,
(b'M', b'M') => Endian::Big,
_ => return None,
};
Some((endian, endian.u16([version0, version1])))
}
fn scalar(field: [u8; 4], field_type: u16, count: u32, endian: Endian) -> Option<u32> {
if count != 1 {
return None;
}
let [byte0, byte1, byte2, byte3] = field;
match field_type {
1 | 2 | 6 => Some(u32::from(byte0)),
3 | 8 => Some(u32::from(endian.u16([byte0, byte1]))),
4 | 9 => Some(endian.u32([byte0, byte1, byte2, byte3])),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Endian {
Little,
Big,
}
impl Endian {
fn u16(self, bytes: [u8; 2]) -> u16 {
match self {
Self::Little => u16::from_le_bytes(bytes),
Self::Big => u16::from_be_bytes(bytes),
}
}
fn u32(self, bytes: [u8; 4]) -> u32 {
match self {
Self::Little => u32::from_le_bytes(bytes),
Self::Big => u32::from_be_bytes(bytes),
}
}
}
fn unreadable(reason: impl Into<String>) -> TileError {
TileError::UnreadablePayload {
reason: reason.into(),
}
}
fn violation(requirement: u8, detail: impl Into<String>) -> TileError {
TileError::CoverageProfileViolation {
requirement,
detail: detail.into(),
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
#[derive(Debug, Clone)]
pub(crate) struct Tiff {
endian: Endian,
version: u16,
entries: Vec<(u16, u16, u32, u32)>,
next_ifd: u32,
}
const SHORT: u16 = 3;
const LONG: u16 = 4;
pub(crate) fn conformant() -> Tiff {
Tiff {
endian: Endian::Little,
version: 42,
entries: vec![
(tag::IMAGE_WIDTH, SHORT, 1, 256),
(tag::IMAGE_LENGTH, SHORT, 1, 256),
(tag::BITS_PER_SAMPLE, SHORT, 1, 32),
(tag::COMPRESSION, SHORT, 1, 5),
(262, SHORT, 1, 1), (273, LONG, 1, 1024), (tag::SAMPLES_PER_PIXEL, SHORT, 1, 1),
(278, SHORT, 1, 256), (279, LONG, 1, 262144), (tag::SAMPLE_FORMAT, SHORT, 1, 3),
],
next_ifd: 0,
}
}
impl Tiff {
fn set(mut self, tag: u16, value: u32) -> Self {
for entry in &mut self.entries {
if entry.0 == tag {
entry.3 = value;
}
}
self
}
fn without(mut self, tag: u16) -> Self {
self.entries.retain(|entry| entry.0 != tag);
self
}
fn with(mut self, tag: u16, field_type: u16, count: u32, value: u32) -> Self {
self.entries.push((tag, field_type, count, value));
self.entries.sort_by_key(|entry| entry.0);
self
}
fn retype(mut self, tag: u16, field_type: u16, count: u32) -> Self {
for entry in &mut self.entries {
if entry.0 == tag {
entry.1 = field_type;
entry.2 = count;
}
}
self
}
fn big_endian(mut self) -> Self {
self.endian = Endian::Big;
self
}
fn version(mut self, version: u16) -> Self {
self.version = version;
self
}
fn next_ifd(mut self, offset: u32) -> Self {
self.next_ifd = offset;
self
}
pub(crate) fn build(&self) -> Vec<u8> {
let u16_bytes = |value: u16| match self.endian {
Endian::Little => value.to_le_bytes(),
Endian::Big => value.to_be_bytes(),
};
let u32_bytes = |value: u32| match self.endian {
Endian::Little => value.to_le_bytes(),
Endian::Big => value.to_be_bytes(),
};
let mut bytes = Vec::new();
bytes.extend_from_slice(match self.endian {
Endian::Little => b"II",
Endian::Big => b"MM",
});
bytes.extend_from_slice(&u16_bytes(self.version));
bytes.extend_from_slice(&u32_bytes(8));
bytes.extend_from_slice(&u16_bytes(u16::try_from(self.entries.len()).unwrap()));
for &(tag, field_type, count, value) in &self.entries {
bytes.extend_from_slice(&u16_bytes(tag));
bytes.extend_from_slice(&u16_bytes(field_type));
bytes.extend_from_slice(&u32_bytes(count));
if field_type == SHORT {
bytes.extend_from_slice(&u16_bytes(u16::try_from(value).unwrap()));
bytes.extend_from_slice(&[0, 0]);
} else {
bytes.extend_from_slice(&u32_bytes(value));
}
}
bytes.extend_from_slice(&u32_bytes(self.next_ifd));
bytes
}
}
fn violated(tiff: &Tiff) -> u8 {
match coverage_tiff(&tiff.build()) {
Err(TileError::CoverageProfileViolation { requirement, .. }) => requirement,
other => panic!("expected a profile violation, got {other:?}"),
}
}
fn accepted(tiff: &Tiff) -> CoverageTiff {
coverage_tiff(&tiff.build()).unwrap()
}
#[test]
fn conforming_float32_tile_is_accepted() {
for tiff in [conformant(), conformant().big_endian()] {
let coverage = accepted(&tiff);
assert_eq!(coverage.sample_type, SampleType::Float32);
assert_eq!(coverage.sample_type.bits(), 32);
assert_eq!(coverage.compression, TiffCompression::Lzw);
assert!(coverage.compression.is_baseline());
assert_eq!((coverage.width, coverage.height), (256, 256));
}
}
#[test]
fn integer_samples_are_accepted() {
for (format, bits, expected) in [
(1, 8, SampleType::Unsigned(8)),
(1, 16, SampleType::Unsigned(16)),
(1, 32, SampleType::Unsigned(32)),
(2, 8, SampleType::Signed(8)),
(2, 16, SampleType::Signed(16)),
(2, 32, SampleType::Signed(32)),
] {
let tiff = conformant()
.set(tag::SAMPLE_FORMAT, format)
.set(tag::BITS_PER_SAMPLE, bits);
assert_eq!(accepted(&tiff).sample_type, expected);
assert_eq!(
accepted(&tiff).sample_type.bits(),
u8::try_from(bits).unwrap()
);
}
}
#[test]
fn dimensions_are_read_in_both_byte_orders_and_widths() {
for tiff in [conformant(), conformant().big_endian()] {
let rectangular = tiff
.clone()
.set(tag::IMAGE_WIDTH, 640)
.set(tag::IMAGE_LENGTH, 480);
let coverage = accepted(&rectangular);
assert_eq!((coverage.width, coverage.height), (640, 480));
let wide = tiff
.retype(tag::IMAGE_WIDTH, LONG, 1)
.set(tag::IMAGE_WIDTH, 70_000);
assert_eq!(accepted(&wide).width, 70_000);
}
}
#[test]
fn bigtiff_is_not_baseline() {
assert_eq!(violated(&conformant().version(43)), 15);
}
#[test]
fn a_second_image_is_refused() {
assert_eq!(violated(&conformant().next_ifd(8)), 19);
assert_eq!(
violated(&conformant().with(tag::SUB_IFDS, LONG, 1, 4096)),
19
);
}
#[test]
fn internal_tiles_are_refused() {
for tag in [
tag::TILE_WIDTH,
tag::TILE_LENGTH,
tag::TILE_OFFSETS,
tag::TILE_BYTE_COUNTS,
] {
assert_eq!(violated(&conformant().with(tag, LONG, 1, 256)), 20);
}
}
#[test]
fn more_than_one_sample_per_cell_is_refused() {
assert_eq!(violated(&conformant().set(tag::SAMPLES_PER_PIXEL, 3)), 16);
assert_eq!(
violated(&conformant().retype(tag::SAMPLES_PER_PIXEL, SHORT, 2)),
16
);
}
#[test]
fn absent_samples_per_pixel_means_one() {
let tiff = conformant().without(tag::SAMPLES_PER_PIXEL);
assert_eq!(accepted(&tiff).sample_type, SampleType::Float32);
}
#[test]
fn absent_bits_per_sample_is_a_bilevel_image() {
assert_eq!(violated(&conformant().without(tag::BITS_PER_SAMPLE)), 17);
}
#[test]
fn float_samples_must_be_32_bits() {
assert_eq!(violated(&conformant().set(tag::BITS_PER_SAMPLE, 16)), 17);
assert_eq!(violated(&conformant().set(tag::BITS_PER_SAMPLE, 64)), 17);
}
#[test]
fn integer_samples_must_be_8_16_or_32_bits() {
let tiff = conformant().set(tag::SAMPLE_FORMAT, 1);
assert_eq!(violated(&tiff.clone().set(tag::BITS_PER_SAMPLE, 4)), 17);
assert_eq!(violated(&tiff.set(tag::BITS_PER_SAMPLE, 64)), 17);
}
#[test]
fn unknown_sample_format_is_refused() {
assert_eq!(violated(&conformant().set(tag::SAMPLE_FORMAT, 4)), 17);
}
#[test]
fn absent_sample_format_means_unsigned_integer() {
let tiff = conformant()
.without(tag::SAMPLE_FORMAT)
.set(tag::BITS_PER_SAMPLE, 16);
assert_eq!(accepted(&tiff).sample_type, SampleType::Unsigned(16));
}
#[test]
fn picture_compression_is_refused() {
for compression in [6, 7, 32773] {
assert_eq!(
violated(&conformant().set(tag::COMPRESSION, compression)),
18
);
}
}
#[test]
fn deflate_is_read_but_is_not_baseline() {
for code in [8, 0x80B2] {
let coverage = accepted(&conformant().set(tag::COMPRESSION, code));
assert_eq!(coverage.compression, TiffCompression::Deflate);
assert!(!coverage.compression.is_baseline());
}
}
#[test]
fn absent_compression_means_uncompressed() {
let tiff = conformant().without(tag::COMPRESSION);
assert_eq!(accepted(&tiff).compression, TiffCompression::None);
}
#[test]
fn a_repeated_tag_keeps_its_first_value() {
let mut tiff = conformant();
tiff.entries.push((tag::COMPRESSION, SHORT, 1, 7));
assert_eq!(accepted(&tiff).compression, TiffCompression::Lzw);
}
#[test]
fn payloads_that_are_not_tiffs_are_unreadable() {
for bytes in [
b"\x89PNG\r\n\x1a\n".as_slice(),
b"II".as_slice(),
b"".as_slice(),
b"XX\x2a\x00\x08\x00\x00\x00".as_slice(),
] {
assert!(matches!(
coverage_tiff(bytes),
Err(TileError::UnreadablePayload { .. })
));
}
}
#[test]
fn an_unknown_tiff_version_is_unreadable() {
assert!(matches!(
coverage_tiff(&conformant().version(7).build()),
Err(TileError::UnreadablePayload { .. })
));
}
#[test]
fn the_probe_reads_a_tiff_through_this_walker() {
let bytes = conformant()
.set(tag::IMAGE_WIDTH, 512)
.set(tag::IMAGE_LENGTH, 128)
.build();
let payload = crate::tiles::probe(&bytes).unwrap();
assert_eq!(payload.format, crate::tiles::TileFormat::Tiff);
assert_eq!((payload.width, payload.height), (512, 128));
assert!(matches!(
crate::tiles::probe(&conformant().version(43).build()),
Err(TileError::UnreadablePayload { .. })
));
}
#[test]
fn an_ifd_outside_the_payload_is_unreadable() {
let mut bytes = conformant().build();
bytes.splice(4..8, u32::to_le_bytes(0xFFFF));
assert!(matches!(
coverage_tiff(&bytes),
Err(TileError::UnreadablePayload { .. })
));
}
#[test]
fn a_missing_dimension_is_unreadable() {
assert!(matches!(
coverage_tiff(&conformant().without(tag::IMAGE_LENGTH).build()),
Err(TileError::UnreadablePayload { .. })
));
}
#[test]
fn every_truncation_is_an_error_and_never_a_panic() {
let bytes = conformant().build();
for length in 0..bytes.len() {
let prefix = &bytes[..length];
assert!(
coverage_tiff(prefix).is_err(),
"a payload truncated to {length} bytes was accepted"
);
dimensions(prefix).unwrap_err();
}
coverage_tiff(&bytes).unwrap();
}
fn png(width: u32, height: u32, bit_depth: u8, colour_type: u8) -> Vec<u8> {
let mut bytes = PNG_SIGNATURE.to_vec();
bytes.extend_from_slice(&13u32.to_be_bytes());
bytes.extend_from_slice(b"IHDR");
bytes.extend_from_slice(&width.to_be_bytes());
bytes.extend_from_slice(&height.to_be_bytes());
bytes.extend_from_slice(&[bit_depth, colour_type, 0, 0, 0]);
bytes.extend_from_slice(&[0, 0, 0, 0]);
bytes
}
#[test]
fn a_16_bit_greyscale_png_conforms() {
let payload = coverage_png(&png(256, 128, 16, 0)).unwrap();
assert_eq!((payload.width, payload.height), (256, 128));
}
#[test]
fn a_png_that_is_not_16_bit_greyscale_is_refused() {
for (bit_depth, colour_type) in [(8, 0), (16, 2), (8, 6), (16, 3)] {
match coverage_png(&png(64, 64, bit_depth, colour_type)) {
Err(TileError::CoverageProfileViolation { requirement, .. }) => {
assert_eq!(requirement, 13);
}
other => panic!("expected a Requirement 13 violation, got {other:?}"),
}
}
}
#[test]
fn a_truncated_png_is_unreadable() {
let bytes = png(64, 64, 16, 0);
const HEADER_LEN: usize = 26;
for length in 0..HEADER_LEN {
assert!(
coverage_png(&bytes[..length]).is_err(),
"a PNG truncated to {length} bytes was accepted"
);
}
for length in HEADER_LEN..=bytes.len() {
coverage_png(&bytes[..length]).unwrap();
}
let mut wrong_chunk = bytes.clone();
wrong_chunk.splice(12..16, *b"sRGB");
assert!(matches!(
coverage_png(&wrong_chunk),
Err(TileError::UnreadablePayload { .. })
));
}
#[test]
fn a_payload_is_read_in_either_encoding() {
let tiff = coverage_payload(&conformant().build()).unwrap();
assert_eq!(tiff.sample_type(), SampleType::Float32);
assert_eq!(tiff.mime_type(), "image/tiff");
assert_eq!((tiff.width(), tiff.height()), (256, 256));
let png = coverage_payload(&png(256, 256, 16, 0)).unwrap();
assert_eq!(png.sample_type(), SampleType::Unsigned(16));
assert_eq!(png.mime_type(), "image/png");
assert_eq!((png.width(), png.height()), (256, 256));
assert!(matches!(
coverage_payload(b"\xff\xd8\xff\xe0 a JPEG"),
Err(TileError::UnreadablePayload { .. })
));
}
#[test]
fn a_float_coverage_takes_float32_tiff_and_nothing_else() {
let float32 = coverage_payload(&conformant().build()).unwrap();
CoverageDatatype::Float.check_payload(&float32).unwrap();
let int16 = coverage_payload(
&conformant()
.set(tag::SAMPLE_FORMAT, 2)
.set(tag::BITS_PER_SAMPLE, 16)
.build(),
)
.unwrap();
let png16 = coverage_payload(&png(64, 64, 16, 0)).unwrap();
for payload in [int16, png16] {
match CoverageDatatype::Float.check_payload(&payload) {
Err(TileError::CoverageProfileViolation { requirement, .. }) => {
assert_eq!(requirement, 14);
}
other => panic!("expected a Requirement 14 violation, got {other:?}"),
}
}
}
#[test]
fn an_integer_coverage_takes_either_encoding_but_not_float_samples() {
for payload in [
coverage_payload(&png(64, 64, 16, 0)).unwrap(),
coverage_payload(
&conformant()
.set(tag::SAMPLE_FORMAT, 1)
.set(tag::BITS_PER_SAMPLE, 8)
.build(),
)
.unwrap(),
] {
CoverageDatatype::Integer.check_payload(&payload).unwrap();
}
let float32 = coverage_payload(&conformant().build()).unwrap();
match CoverageDatatype::Integer.check_payload(&float32) {
Err(TileError::CoverageProfileViolation { requirement, .. }) => {
assert_eq!(requirement, 13);
}
other => panic!("expected a Requirement 13 violation, got {other:?}"),
}
}
#[test]
fn the_datatype_column_round_trips() {
for datatype in [CoverageDatatype::Integer, CoverageDatatype::Float] {
assert_eq!(CoverageDatatype::parse(datatype.as_str()), Some(datatype));
}
assert_eq!(CoverageDatatype::parse("elevation"), None);
}
#[test]
fn dimensions_ignore_the_profile() {
let tiff = conformant()
.set(tag::SAMPLES_PER_PIXEL, 3)
.set(tag::IMAGE_WIDTH, 512);
assert_eq!(dimensions(&tiff.build()).unwrap(), (512, 256));
}
}