use std::fs::File;
use std::io::{BufWriter, Read as _, Seek as _, SeekFrom};
use std::path::Path;
use martin_tile_utils::{Format, TileCoord, TileData};
use tiff::ColorType;
use tiff::decoder::Decoder;
use tiff::tags::CompressionMethod;
use crate::tiles::cog::CogError;
pub const COMPRESSION_WEBP: u16 = 50001;
#[derive(Clone, Debug)]
pub struct Image {
ifd_index: usize,
zoom_level: u8,
tiles_origin: (u32, u32),
tiles_across: u32,
tiles_down: u32,
tile_size: u32,
compression: u16,
}
impl Image {
pub fn new(
ifd_index: usize,
zoom_level: u8,
tiles_origin: (u32, u32),
tiles_across: u32,
tiles_down: u32,
tile_size: u32,
compression: u16,
) -> Self {
Self {
ifd_index,
zoom_level,
tiles_origin,
tiles_across,
tiles_down,
tile_size,
compression,
}
}
pub fn output_format(&self) -> Option<Format> {
if self.compression == COMPRESSION_WEBP {
return Some(Format::Webp);
}
match CompressionMethod::from_u16(self.compression) {
Some(CompressionMethod::ModernJPEG) => Some(Format::Jpeg),
Some(CompressionMethod::Deflate | CompressionMethod::LZW | CompressionMethod::None) => {
Some(Format::Png)
}
_ => None,
}
}
fn is_passthrough_compression(&self) -> bool {
if self.compression == COMPRESSION_WEBP {
return true;
}
matches!(
CompressionMethod::from_u16(self.compression),
Some(CompressionMethod::ModernJPEG)
)
}
pub fn get_tile(
&self,
decoder: &mut Decoder<File>,
xyz: TileCoord,
path: &Path,
) -> Result<TileData, CogError> {
decoder
.seek_to_image(self.ifd_index)
.map_err(|e| CogError::IfdSeekFailed(e, self.ifd_index, path.to_path_buf()))?;
let Some(idx) = self.get_chunk_index(xyz) else {
return Ok(Vec::new());
};
if self.is_passthrough_compression() {
return self.read_raw_tile_bytes(decoder, idx, path);
}
let color_type = decoder
.colortype()
.map_err(|e| CogError::InvalidTiffFile(e, path.to_path_buf()))?;
let mut pixels = vec![
0;
(self.tile_size * self.tile_size * u32::from(color_type.num_samples()))
as usize
];
if decoder.read_chunk_bytes(idx, &mut pixels).is_err() {
return Ok(Vec::new());
}
let png = encode_as_png(self.tile_size(), &pixels, path, color_type)?;
Ok(png)
}
fn read_raw_tile_bytes(
&self,
decoder: &mut Decoder<File>,
chunk_index: u32,
path: &Path,
) -> Result<TileData, CogError> {
use tiff::tags::Tag;
let jpeg_tables = if CompressionMethod::from_u16(self.compression)
== Some(CompressionMethod::ModernJPEG)
{
decoder.get_tag_u8_vec(Tag::JPEGTables).ok()
} else {
None
};
let tile_offsets = decoder
.get_tag_u64_vec(Tag::TileOffsets)
.map_err(|e| CogError::InvalidTiffFile(e, path.to_path_buf()))?;
let tile_byte_counts = decoder
.get_tag_u64_vec(Tag::TileByteCounts)
.map_err(|e| CogError::InvalidTiffFile(e, path.to_path_buf()))?;
let idx = chunk_index as usize;
if idx >= tile_offsets.len() || idx >= tile_byte_counts.len() {
return Ok(Vec::new());
}
let offset = tile_offsets[idx];
let byte_count = usize::try_from(tile_byte_counts[idx]).unwrap_or(0);
if byte_count == 0 {
return Ok(Vec::new());
}
let file = decoder.inner();
file.seek(SeekFrom::Start(offset))
.map_err(|e| CogError::IoError(e, path.to_path_buf()))?;
let mut tile_data = vec![0u8; byte_count];
file.read_exact(&mut tile_data)
.map_err(|e| CogError::IoError(e, path.to_path_buf()))?;
if let Some(tables) = jpeg_tables {
return Ok(merge_jpeg_tables_with_tile(&tables, &tile_data));
}
Ok(tile_data)
}
pub fn compression(&self) -> u16 {
self.compression
}
pub fn tile_size(&self) -> u32 {
self.tile_size
}
pub fn zoom_level(&self) -> u8 {
self.zoom_level
}
fn get_chunk_index(&self, xyz: TileCoord) -> Option<u32> {
if xyz.z != self.zoom_level {
return None;
}
let x = i64::from(xyz.x) - i64::from(self.tiles_origin.0);
let y = i64::from(xyz.y) - i64::from(self.tiles_origin.1);
if 0 > x || x >= i64::from(self.tiles_across) || 0 > y || y >= i64::from(self.tiles_down) {
return None;
}
let idx = y * i64::from(self.tiles_across) + x;
u32::try_from(idx).ok()
}
}
const JPEG_SOI: [u8; 2] = [0xFF, 0xD8]; const JPEG_EOI: [u8; 2] = [0xFF, 0xD9];
fn merge_jpeg_tables_with_tile(jpeg_tables: &[u8], tile_data: &[u8]) -> Vec<u8> {
if jpeg_tables.len() < 4 || tile_data.len() < 4 {
return tile_data.to_vec();
}
if jpeg_tables[0..2] != JPEG_SOI || tile_data[0..2] != JPEG_SOI {
return tile_data.to_vec();
}
let tables_end = if jpeg_tables.len() >= 2 && jpeg_tables[jpeg_tables.len() - 2..] == JPEG_EOI {
jpeg_tables.len() - 2
} else {
jpeg_tables.len()
};
let tables_content = &jpeg_tables[2..tables_end];
let mut result = Vec::with_capacity(2 + tables_content.len() + tile_data.len() - 2);
result.extend_from_slice(&JPEG_SOI);
result.extend_from_slice(tables_content);
result.extend_from_slice(&tile_data[2..]);
result
}
fn encode_as_png(
tile_size: u32,
pixels: &[u8],
path: &Path,
color_type: ColorType,
) -> Result<Vec<u8>, CogError> {
let mut result_file_buffer = Vec::new();
let png_color_type = match color_type {
ColorType::RGB(8) => Ok(png::ColorType::Rgb),
ColorType::RGBA(8) => Ok(png::ColorType::Rgba),
c => Err(CogError::NotSupportedColorTypeAndBitDepth(
c,
path.to_path_buf(),
)),
}?;
{
let mut encoder = png::Encoder::new(
BufWriter::new(&mut result_file_buffer),
tile_size,
tile_size,
);
encoder.set_color(png_color_type);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder
.write_header()
.map_err(|e| CogError::WritePngHeaderFailed(path.to_path_buf(), e))?;
writer
.write_image_data(pixels)
.map_err(|e| CogError::WriteToPngFailed(path.to_path_buf(), e))?;
}
Ok(result_file_buffer)
}
#[cfg(test)]
mod tests {
use martin_tile_utils::TileCoord;
use crate::tiles::cog::image::{Image, merge_jpeg_tables_with_tile};
#[test]
fn can_calculate_correct_chunk_index() {
let image = Image {
ifd_index: 0,
zoom_level: 0,
tiles_origin: (0, 0),
tiles_across: 3,
tiles_down: 3,
tile_size: 256,
compression: 1, };
assert_eq!(
Some(0),
image.get_chunk_index(TileCoord { z: 0, x: 0, y: 0 })
);
assert_eq!(None, image.get_chunk_index(TileCoord { z: 2, x: 2, y: 2 }));
assert_eq!(None, image.get_chunk_index(TileCoord { z: 0, x: 3, y: 0 }));
assert_eq!(None, image.get_chunk_index(TileCoord { z: 0, x: 1, y: 9 }));
}
#[test]
fn can_merge_jpeg_tables_with_tile() {
let jpeg_tables = vec![
0xFF, 0xD8, 0xFF, 0xDB, 0x00, 0x05, 0x00, 0x10, 0x20, 0xFF, 0xD9, ];
let tile_data = vec![
0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x04, 0x08, 0x10, 0xFF, 0xDA, 0x00, 0x02, 0x12, 0x34, 0x56, 0xFF, 0xD9, ];
let merged = merge_jpeg_tables_with_tile(&jpeg_tables, &tile_data);
let expected = vec![
0xFF, 0xD8, 0xFF, 0xDB, 0x00, 0x05, 0x00, 0x10, 0x20, 0xFF, 0xC0, 0x00, 0x04, 0x08, 0x10, 0xFF, 0xDA, 0x00, 0x02, 0x12, 0x34, 0x56, 0xFF, 0xD9, ];
assert_eq!(merged, expected);
}
#[test]
fn merge_returns_tile_data_when_tables_too_short() {
let jpeg_tables = vec![0xFF, 0xD8]; let tile_data = vec![0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x02, 0xFF, 0xD9];
let merged = merge_jpeg_tables_with_tile(&jpeg_tables, &tile_data);
assert_eq!(merged, tile_data);
}
#[test]
fn merge_returns_tile_data_when_invalid_markers() {
let jpeg_tables = vec![0x00, 0x00, 0x00, 0x00]; let tile_data = vec![0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x02, 0xFF, 0xD9];
let merged = merge_jpeg_tables_with_tile(&jpeg_tables, &tile_data);
assert_eq!(merged, tile_data);
}
}