use std::fs::File;
use std::io::{BufWriter, Seek, Write};
use std::path::Path;
use tiff::encoder::colortype::{Gray32Float, RGB32Float, RGBA32Float};
use tiff::encoder::{Compression, TiffEncoder};
use tiff::tags::Tag;
use crate::geometry::projection::get_proj_string;
use crate::xyz_tile::ReprojectedRaster;
const GEOTIFF_MODELPIXELSCALE: u16 = 33550;
const GEOTIFF_MODELTIEPOINT: u16 = 33922;
const GEOTIFF_GEOKEYDIRECTORY: u16 = 34735;
#[allow(dead_code)]
const GEOTIFF_GEODOUBLEPARAMS: u16 = 34736;
const GEOTIFF_GEOASCIIPARAMS: u16 = 34737;
const GT_MODEL_TYPE_GEO_KEY: u16 = 1024;
const GT_RASTER_TYPE_GEO_KEY: u16 = 1025;
const GEOGRAPHIC_TYPE_GEO_KEY: u16 = 2048;
const PROJECTED_CS_TYPE_GEO_KEY: u16 = 3072;
const MODEL_TYPE_PROJECTED: u16 = 1;
const MODEL_TYPE_GEOGRAPHIC: u16 = 2;
const RASTER_PIXEL_IS_AREA: u16 = 1;
#[derive(Debug, Clone, Copy, Default)]
pub enum GeoTiffCompression {
#[default]
None,
Lzw,
Deflate,
}
#[derive(Debug)]
pub enum GeoTiffWriteError {
Io(std::io::Error),
TiffEncode(String),
InvalidData(String),
}
impl std::fmt::Display for GeoTiffWriteError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(e) => write!(f, "I/O error: {e}"),
Self::TiffEncode(e) => write!(f, "TIFF encoding error: {e}"),
Self::InvalidData(e) => write!(f, "Invalid data: {e}"),
}
}
}
impl std::error::Error for GeoTiffWriteError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(e) => Some(e),
_ => None,
}
}
}
impl From<std::io::Error> for GeoTiffWriteError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
impl From<tiff::TiffError> for GeoTiffWriteError {
fn from(e: tiff::TiffError) -> Self {
Self::TiffEncode(e.to_string())
}
}
pub struct GeoTiffWriter<'a> {
raster: &'a ReprojectedRaster,
compression: GeoTiffCompression,
}
impl<'a> GeoTiffWriter<'a> {
#[must_use]
pub fn new(raster: &'a ReprojectedRaster) -> Self {
Self {
raster,
compression: GeoTiffCompression::default(),
}
}
#[must_use]
pub fn compression(mut self, compression: GeoTiffCompression) -> Self {
self.compression = compression;
self
}
pub fn write<P: AsRef<Path>>(self, path: P) -> Result<(), GeoTiffWriteError> {
let file = File::create(path)?;
let writer = BufWriter::new(file);
self.write_to(writer)
}
pub fn write_to<W: Write + Seek>(self, writer: W) -> Result<(), GeoTiffWriteError> {
let raster = self.raster;
if raster.pixels.is_empty() {
return Err(GeoTiffWriteError::InvalidData(
"Raster has no pixel data".to_string(),
));
}
if raster.width == 0 || raster.height == 0 {
return Err(GeoTiffWriteError::InvalidData(
"Raster has zero dimensions".to_string(),
));
}
let width = raster.width as u32;
let height = raster.height as u32;
let compression = match self.compression {
GeoTiffCompression::None => Compression::Uncompressed,
GeoTiffCompression::Lzw => Compression::Lzw,
GeoTiffCompression::Deflate => Compression::Deflate(tiff::encoder::DeflateLevel::Fast),
};
let encoder = TiffEncoder::new(writer)?.with_compression(compression);
self.write_image(encoder, width, height)
}
fn write_image<W: Write + Seek>(
&self,
mut encoder: TiffEncoder<W>,
width: u32,
height: u32,
) -> Result<(), GeoTiffWriteError> {
let bands = self.raster.bands;
match bands {
1 => {
let mut image = encoder.new_image::<Gray32Float>(width, height)?;
self.write_geotiff_tags(image.encoder())?;
image.write_data(&self.raster.pixels)?;
}
3 => {
let mut image = encoder.new_image::<RGB32Float>(width, height)?;
self.write_geotiff_tags(image.encoder())?;
image.write_data(&self.raster.pixels)?;
}
4 => {
let mut image = encoder.new_image::<RGBA32Float>(width, height)?;
self.write_geotiff_tags(image.encoder())?;
image.write_data(&self.raster.pixels)?;
}
_ => {
self.write_multiband_image(encoder, width, height)?;
}
}
Ok(())
}
fn write_multiband_image<W: Write + Seek>(
&self,
mut encoder: TiffEncoder<W>,
width: u32,
height: u32,
) -> Result<(), GeoTiffWriteError> {
let bands = self.raster.bands;
let mut dir = encoder.image_directory()?;
dir.write_tag(Tag::ImageWidth, width)?;
dir.write_tag(Tag::ImageLength, height)?;
let bits_per_sample: Vec<u16> = vec![32; bands];
dir.write_tag(Tag::BitsPerSample, bits_per_sample.as_slice())?;
let compression_tag: u16 = match self.compression {
GeoTiffCompression::None => 1, GeoTiffCompression::Lzw => 5, GeoTiffCompression::Deflate => 8, };
dir.write_tag(Tag::Compression, compression_tag)?;
dir.write_tag(Tag::PhotometricInterpretation, 1u16)?;
dir.write_tag(Tag::SamplesPerPixel, bands as u16)?;
let sample_format: Vec<u16> = vec![3; bands];
dir.write_tag(Tag::SampleFormat, sample_format.as_slice())?;
dir.write_tag(Tag::PlanarConfiguration, 1u16)?;
dir.write_tag(Tag::RowsPerStrip, height)?;
if bands > 1 {
let extra_samples: Vec<u16> = vec![0; bands - 1];
dir.write_tag(Tag::ExtraSamples, extra_samples.as_slice())?;
}
self.write_geotiff_tags(&mut dir)?;
let pixel_bytes: Vec<u8> = self
.raster
.pixels
.iter()
.flat_map(|&f| f.to_le_bytes())
.collect();
let strip_offset = dir.write_data(pixel_bytes.as_slice())?;
dir.write_tag(Tag::StripOffsets, strip_offset)?;
let strip_byte_count = pixel_bytes.len() as u32;
dir.write_tag(Tag::StripByteCounts, strip_byte_count)?;
dir.finish()?;
Ok(())
}
fn write_geotiff_tags<W: Write + Seek, K: tiff::encoder::TiffKind>(
&self,
dir: &mut tiff::encoder::DirectoryEncoder<W, K>,
) -> Result<(), GeoTiffWriteError> {
let raster = self.raster;
let pixel_scale = [raster.resolution.0, raster.resolution.1, 0.0];
dir.write_tag(Tag::Unknown(GEOTIFF_MODELPIXELSCALE), pixel_scale.as_slice())?;
let tiepoint = [
0.0,
0.0,
0.0,
raster.bounds.minx,
raster.bounds.maxy,
0.0,
];
dir.write_tag(Tag::Unknown(GEOTIFF_MODELTIEPOINT), tiepoint.as_slice())?;
let geokeys = self.build_geokey_directory(raster);
dir.write_tag(Tag::Unknown(GEOTIFF_GEOKEYDIRECTORY), geokeys.as_slice())?;
if let Some(proj_string) = get_proj_string(raster.crs as i32) {
let ascii_params = format!("{proj_string}|");
dir.write_tag(Tag::Unknown(GEOTIFF_GEOASCIIPARAMS), ascii_params.as_bytes())?;
}
Ok(())
}
fn build_geokey_directory(&self, raster: &ReprojectedRaster) -> Vec<u16> {
let is_geographic = crate::geometry::projection::is_geographic_crs(raster.crs as i32);
let mut keys = vec![
1, 1, 0, 3, ];
keys.extend_from_slice(&[
GT_MODEL_TYPE_GEO_KEY,
0, 1, if is_geographic {
MODEL_TYPE_GEOGRAPHIC
} else {
MODEL_TYPE_PROJECTED
},
]);
keys.extend_from_slice(&[GT_RASTER_TYPE_GEO_KEY, 0, 1, RASTER_PIXEL_IS_AREA]);
if is_geographic {
keys.extend_from_slice(&[GEOGRAPHIC_TYPE_GEO_KEY, 0, 1, raster.crs as u16]);
} else {
keys.extend_from_slice(&[PROJECTED_CS_TYPE_GEO_KEY, 0, 1, raster.crs as u16]);
}
keys
}
}
impl ReprojectedRaster {
pub fn write_geotiff<P: AsRef<Path>>(&self, path: P) -> Result<(), GeoTiffWriteError> {
GeoTiffWriter::new(self).write(path)
}
pub fn write_geotiff_compressed<P: AsRef<Path>>(
&self,
path: P,
compression: GeoTiffCompression,
) -> Result<(), GeoTiffWriteError> {
GeoTiffWriter::new(self).compression(compression).write(path)
}
#[must_use]
pub fn geotiff_writer(&self) -> GeoTiffWriter<'_> {
GeoTiffWriter::new(self)
}
pub fn to_geotiff_bytes(&self) -> Result<Vec<u8>, GeoTiffWriteError> {
let mut buffer = std::io::Cursor::new(Vec::new());
GeoTiffWriter::new(self).write_to(&mut buffer)?;
Ok(buffer.into_inner())
}
pub fn to_geotiff_bytes_compressed(
&self,
compression: GeoTiffCompression,
) -> Result<Vec<u8>, GeoTiffWriteError> {
let mut buffer = std::io::Cursor::new(Vec::new());
GeoTiffWriter::new(self)
.compression(compression)
.write_to(&mut buffer)?;
Ok(buffer.into_inner())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::xyz_tile::BoundingBox;
fn create_test_raster(bands: usize, width: usize, height: usize) -> ReprojectedRaster {
let pixels: Vec<f32> = (0..width * height * bands)
.map(|i| (i % 256) as f32)
.collect();
ReprojectedRaster {
pixels,
bands,
width,
height,
crs: 32610, bounds: BoundingBox::new(500000.0, 4000000.0, 510000.0, 4010000.0),
resolution: (10.0, 10.0),
nodata: None,
}
}
#[test]
fn test_write_grayscale_geotiff() {
let raster = create_test_raster(1, 64, 64);
let bytes = raster.to_geotiff_bytes().unwrap();
assert!(bytes.len() > 8);
assert!(bytes[0] == b'I' && bytes[1] == b'I' || bytes[0] == b'M' && bytes[1] == b'M');
println!("Grayscale GeoTIFF: {} bytes", bytes.len());
}
#[test]
fn test_write_rgb_geotiff() {
let raster = create_test_raster(3, 64, 64);
let bytes = raster.to_geotiff_bytes().unwrap();
assert!(bytes.len() > 8);
println!("RGB GeoTIFF: {} bytes", bytes.len());
}
#[test]
fn test_write_rgba_geotiff() {
let raster = create_test_raster(4, 64, 64);
let bytes = raster.to_geotiff_bytes().unwrap();
assert!(bytes.len() > 8);
println!("RGBA GeoTIFF: {} bytes", bytes.len());
}
#[test]
fn test_write_compressed_lzw() {
let raster = create_test_raster(1, 128, 128);
let uncompressed = raster.to_geotiff_bytes().unwrap();
let compressed = raster.to_geotiff_bytes_compressed(GeoTiffCompression::Lzw).unwrap();
println!(
"Uncompressed: {} bytes, LZW: {} bytes",
uncompressed.len(),
compressed.len()
);
}
#[test]
fn test_write_compressed_deflate() {
let raster = create_test_raster(1, 128, 128);
let compressed = raster
.to_geotiff_bytes_compressed(GeoTiffCompression::Deflate)
.unwrap();
assert!(compressed.len() > 8);
println!("Deflate GeoTIFF: {} bytes", compressed.len());
}
#[test]
fn test_geokey_directory_projected() {
let raster = create_test_raster(1, 10, 10);
let writer = GeoTiffWriter::new(&raster);
let geokeys = writer.build_geokey_directory(&raster);
assert_eq!(geokeys[0], 1); assert_eq!(geokeys[1], 1); assert_eq!(geokeys[2], 0); assert_eq!(geokeys[3], 3);
assert_eq!(geokeys[4], GT_MODEL_TYPE_GEO_KEY);
assert_eq!(geokeys[7], MODEL_TYPE_PROJECTED);
assert_eq!(geokeys[12], PROJECTED_CS_TYPE_GEO_KEY);
assert_eq!(geokeys[15], 32610);
}
#[test]
fn test_geokey_directory_geographic() {
let mut raster = create_test_raster(1, 10, 10);
raster.crs = 4326;
raster.bounds = BoundingBox::new(-122.5, 37.0, -122.0, 37.5);
raster.resolution = (0.05, 0.05);
let writer = GeoTiffWriter::new(&raster);
let geokeys = writer.build_geokey_directory(&raster);
assert_eq!(geokeys[7], MODEL_TYPE_GEOGRAPHIC);
assert_eq!(geokeys[12], GEOGRAPHIC_TYPE_GEO_KEY);
assert_eq!(geokeys[15], 4326);
}
#[test]
fn test_empty_raster_error() {
let raster = ReprojectedRaster {
pixels: vec![],
bands: 1,
width: 0,
height: 0,
crs: 32610,
bounds: BoundingBox::new(0.0, 0.0, 0.0, 0.0),
resolution: (1.0, 1.0),
nodata: None,
};
let result = raster.to_geotiff_bytes();
assert!(result.is_err());
}
#[test]
fn test_multiband_geotiff() {
let raster = ReprojectedRaster {
pixels: vec![1.0; 100 * 5], bands: 5,
width: 10,
height: 10,
crs: 32610,
bounds: BoundingBox::new(0.0, 0.0, 100.0, 100.0),
resolution: (10.0, 10.0),
nodata: None,
};
let bytes = raster.to_geotiff_bytes().unwrap();
assert!(bytes.len() > 0);
println!("5-band GeoTIFF: {} bytes", bytes.len());
}
#[test]
fn test_two_band_geotiff() {
let raster = ReprojectedRaster {
pixels: vec![1.0; 64 * 2], bands: 2,
width: 8,
height: 8,
crs: 32610,
bounds: BoundingBox::new(0.0, 0.0, 80.0, 80.0),
resolution: (10.0, 10.0),
nodata: None,
};
let bytes = raster.to_geotiff_bytes().unwrap();
assert!(bytes.len() > 0);
println!("2-band GeoTIFF: {} bytes", bytes.len());
}
#[test]
fn test_many_band_geotiff() {
let bands = 16;
let raster = ReprojectedRaster {
pixels: vec![0.5; 100 * bands], bands,
width: 10,
height: 10,
crs: 32610,
bounds: BoundingBox::new(0.0, 0.0, 100.0, 100.0),
resolution: (10.0, 10.0),
nodata: None,
};
let bytes = raster.to_geotiff_bytes().unwrap();
assert!(bytes.len() > 0);
println!("16-band GeoTIFF: {} bytes", bytes.len());
let cursor = std::io::Cursor::new(bytes);
let mut decoder = tiff::decoder::Decoder::new(cursor).unwrap();
let (width, height) = decoder.dimensions().unwrap();
assert_eq!(width, 10);
assert_eq!(height, 10);
}
#[test]
fn test_write_to_file() {
let raster = create_test_raster(1, 64, 64);
let temp_path = "/tmp/test_geotiff_write.tif";
raster.write_geotiff(temp_path).unwrap();
let metadata = std::fs::metadata(temp_path).unwrap();
assert!(metadata.len() > 0);
std::fs::remove_file(temp_path).ok();
}
#[test]
fn test_roundtrip_verify_geotiff() {
use tiff::decoder::Decoder;
let raster = create_test_raster(1, 32, 32);
let bytes = raster.to_geotiff_bytes().unwrap();
let cursor = std::io::Cursor::new(bytes);
let mut decoder = Decoder::new(cursor).unwrap();
let (width, height) = decoder.dimensions().unwrap();
assert_eq!(width, 32);
assert_eq!(height, 32);
println!("Roundtrip GeoTIFF verified: {}x{}", width, height);
}
#[test]
fn test_various_utm_crs() {
let crs_list = [
(32610, "UTM 10N"),
(32618, "UTM 18N"),
(32633, "UTM 33N"),
(4326, "WGS84"),
(3857, "Web Mercator"),
];
for (crs, name) in crs_list {
let mut raster = create_test_raster(1, 10, 10);
raster.crs = crs;
let bytes = raster.to_geotiff_bytes().unwrap();
assert!(bytes.len() > 0, "Failed to write GeoTIFF for {name} (EPSG:{crs})");
println!("EPSG:{crs} ({name}): {} bytes", bytes.len());
}
}
}