use std::fs::File;
use std::io::{self, BufReader, Read, Write, BufWriter, BufRead};
use std::path::Path;
use std::collections::HashMap;
use log::{debug, info, warn, error};
use crate::tiff::errors::{TiffError, TiffResult};
use crate::tiff::ifd::{IFD, IFDEntry};
use crate::tiff::constants::{tags, photometric, field_types};
use crate::io::byte_order::ByteOrderHandler;
use crate::io::seekable::SeekableReader;
use crate::tiff::TiffReader;
use crate::tiff::TiffBuilder;
use crate::utils::logger::Logger;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RgbColor {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl RgbColor {
pub fn new(r: u8, g: u8, b: u8) -> Self {
RgbColor { r, g, b }
}
pub fn to_hex(&self) -> String {
format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
}
pub fn from_hex(hex: &str) -> TiffResult<Self> {
let hex = hex.trim_start_matches('#');
if hex.len() != 6 {
return Err(TiffError::GenericError(
format!("Invalid hex color code: {} - must be 6 hexadecimal digits", hex)
));
}
let r = parse_hex_component(&hex[0..2], hex)?;
let g = parse_hex_component(&hex[2..4], hex)?;
let b = parse_hex_component(&hex[4..6], hex)?;
Ok(RgbColor { r, g, b })
}
}
fn parse_hex_component(hex_part: &str, full_hex: &str) -> TiffResult<u8> {
u8::from_str_radix(hex_part, 16)
.map_err(|_| TiffError::GenericError(format!("Invalid hex color: {}", full_hex)))
}
#[derive(Debug, Clone)]
pub struct ColorMapEntry {
pub value: u16,
pub label: Option<String>,
pub color: RgbColor,
}
impl ColorMapEntry {
pub fn new(value: u16, color: RgbColor) -> Self {
ColorMapEntry {
value,
label: None,
color
}
}
pub fn with_label(value: u16, color: RgbColor, label: String) -> Self {
ColorMapEntry {
value,
label: Some(label),
color,
}
}
pub fn to_hex_color(&self) -> String {
self.color.to_hex()
}
pub fn from_hex_color(value: u16, hex: &str, label: Option<String>) -> TiffResult<Self> {
let color = RgbColor::from_hex(hex)?;
Ok(ColorMapEntry {
value,
label,
color,
})
}
}
#[derive(Debug, Clone)]
pub struct ColorMap {
pub entries: Vec<ColorMapEntry>,
pub map_type: String,
}
impl ColorMap {
pub fn new() -> Self {
ColorMap {
entries: Vec::new(),
map_type: "ramp".to_string(), }
}
pub fn add_entry(&mut self, entry: ColorMapEntry) {
self.entries.push(entry);
self.entries.sort_by_key(|e| e.value);
}
pub fn set_type(&mut self, map_type: &str) {
self.map_type = map_type.to_string();
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn from_tiff_ifd<R: SeekableReader>(
ifd: &IFD,
reader: &mut R,
byte_order_handler: &Box<dyn ByteOrderHandler>
) -> TiffResult<Self> {
debug!("Reading color map from TIFF IFD");
let photometric_interp = ifd.get_tag_value(tags::PHOTOMETRIC_INTERPRETATION)
.unwrap_or(0);
if photometric_interp != photometric::PALETTE as u64 {
return Err(TiffError::GenericError(
"IFD does not contain a color map (not a palette image)".to_string()
));
}
let bits_per_sample = ifd.get_tag_value(tags::BITS_PER_SAMPLE)
.unwrap_or(8) as u16;
let num_entries = 1 << bits_per_sample; debug!("Color map should have {} entries ({}-bit)", num_entries, bits_per_sample);
let colormap_entry = ifd.get_entry(tags::COLOR_MAP)
.ok_or_else(|| TiffError::GenericError("No ColorMap tag found in IFD".to_string()))?;
if colormap_entry.count != (3 * num_entries as u64) {
return Err(TiffError::GenericError(
format!("ColorMap has wrong size: {} (expected {})",
colormap_entry.count, 3 * num_entries)
));
}
reader.seek(std::io::SeekFrom::Start(colormap_entry.value_offset))?;
let (r_values, g_values, b_values) = read_colormap_data(
reader,
byte_order_handler,
num_entries
)?;
let mut colormap = ColorMap::new();
for i in 0..num_entries {
let r = (r_values[i as usize] / 257) as u8;
let g = (g_values[i as usize] / 257) as u8;
let b = (b_values[i as usize] / 257) as u8;
colormap.add_entry(ColorMapEntry::new(i, RgbColor::new(r, g, b)));
}
colormap.remove_empty_entries();
colormap.simplify_if_needed();
Ok(colormap)
}
fn remove_empty_entries(&mut self) {
while !self.entries.is_empty() &&
self.entries[0].color.r == 0 &&
self.entries[0].color.g == 0 &&
self.entries[0].color.b == 0 {
self.entries.remove(0);
}
}
fn simplify_if_needed(&mut self) {
if self.entries.len() <= 256 {
return;
}
info!("Simplifying large color map with {} entries", self.entries.len());
let mut simplified = Vec::new();
let mut seen_colors = HashMap::new();
for entry in &self.entries {
let color_key = (entry.color.r, entry.color.g, entry.color.b);
if !seen_colors.contains_key(&color_key) {
seen_colors.insert(color_key, true);
simplified.push(entry.clone());
}
}
self.entries = simplified;
info!("Simplified to {} unique colors", self.entries.len());
}
pub fn from_sld_file<P: AsRef<Path>>(file_path: P) -> TiffResult<Self> {
debug!("Reading color map from SLD file: {:?}", file_path.as_ref());
let file = File::open(file_path)?;
let reader = BufReader::new(file);
Self::from_sld_reader(reader)
}
pub fn from_sld_reader<R: Read>(mut reader: R) -> TiffResult<Self> {
let mut content = String::new();
reader.read_to_string(&mut content)?;
let mut colormap = ColorMap::new();
colormap.set_type("ramp");
if let Some(map_type) = extract_colormap_type(&content) {
colormap.set_type(&map_type);
}
for line in content.lines() {
if line.contains("sld:ColorMapEntry") || line.contains("ColorMapEntry") {
parse_sld_entry_attributes(&mut colormap, line);
}
}
if colormap.is_empty() {
return Err(TiffError::GenericError("No color map entries found in SLD file".to_string()));
}
debug!("Read {} entries from SLD", colormap.len());
Ok(colormap)
}
pub fn from_csv_file<P: AsRef<Path>>(file_path: P) -> TiffResult<Self> {
debug!("Reading color map from CSV file: {:?}", file_path.as_ref());
let file = File::open(file_path)?;
let reader = BufReader::new(file);
Self::from_csv_reader(reader)
}
pub fn from_csv_reader<R: Read>(mut reader: R) -> TiffResult<Self> {
let mut content = String::new();
reader.read_to_string(&mut content)?;
let mut colormap = ColorMap::new();
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
if let Some(entry) = parse_csv_line(&parts) {
colormap.add_entry(entry);
} else {
warn!("Ignoring invalid CSV line: {}", line);
}
}
if colormap.is_empty() {
return Err(TiffError::GenericError("No valid color map entries found in CSV".to_string()));
}
debug!("Read {} entries from CSV", colormap.len());
Ok(colormap)
}
pub fn to_tiff_colormap(&self) -> (u16, Vec<u16>) {
let max_value = self.entries.iter()
.map(|e| e.value)
.max()
.unwrap_or(0);
let bits_needed = ((max_value as f32).log2().ceil() as u32).max(1);
let num_entries = 1 << bits_needed;
debug!("Creating TIFF colormap with {} entries (using {} bits)",
num_entries, bits_needed);
let mut r_values = vec![0u16; num_entries as usize];
let mut g_values = vec![0u16; num_entries as usize];
let mut b_values = vec![0u16; num_entries as usize];
for entry in &self.entries {
let idx = entry.value as usize;
if idx < num_entries as usize {
r_values[idx] = entry.color.r as u16 * 257;
g_values[idx] = entry.color.g as u16 * 257;
b_values[idx] = entry.color.b as u16 * 257;
}
}
if self.map_type == "ramp" && self.entries.len() > 1 {
self.interpolate_ramp_values(&mut r_values, &mut g_values, &mut b_values, num_entries);
}
let mut result = Vec::with_capacity(3 * num_entries as usize);
result.extend_from_slice(&r_values);
result.extend_from_slice(&g_values);
result.extend_from_slice(&b_values);
(num_entries as u16, result)
}
fn interpolate_ramp_values(
&self,
r_values: &mut [u16],
g_values: &mut [u16],
b_values: &mut [u16],
num_entries: u32
) {
debug!("Interpolating color ramp for missing values");
let mut sorted_entries = self.entries.clone();
sorted_entries.sort_by_key(|e| e.value);
for i in 1..sorted_entries.len() {
let prev = &sorted_entries[i-1];
let curr = &sorted_entries[i];
if curr.value <= prev.value + 1 {
continue; }
let gap_size = (curr.value - prev.value) as f32;
for j in 1..curr.value - prev.value {
let t = j as f32 / gap_size;
let idx = (prev.value + j) as usize;
if idx >= num_entries as usize {
continue; }
r_values[idx] = interpolate_color_component(prev.color.r, curr.color.r, t);
g_values[idx] = interpolate_color_component(prev.color.g, curr.color.g, t);
b_values[idx] = interpolate_color_component(prev.color.b, curr.color.b, t);
}
}
}
pub fn to_sld_file<P: AsRef<Path>>(&self, file_path: P, layer_name: &str) -> TiffResult<()> {
debug!("Writing color map to SLD file: {:?}", file_path.as_ref());
let file = File::create(file_path)?;
let mut writer = BufWriter::new(file);
writeln!(writer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
writeln!(writer, "<StyledLayerDescriptor xmlns=\"http://www.opengis.net/sld\" version=\"1.0.0\" xmlns:gml=\"http://www.opengis.net/gml\" xmlns:sld=\"http://www.opengis.net/sld\" xmlns:ogc=\"http://www.opengis.net/ogc\">")?;
writeln!(writer, " <UserLayer>")?;
writeln!(writer, " <sld:LayerFeatureConstraints>")?;
writeln!(writer, " <sld:FeatureTypeConstraint/>")?;
writeln!(writer, " </sld:LayerFeatureConstraints>")?;
writeln!(writer, " <sld:UserStyle>")?;
writeln!(writer, " <sld:Name>{}</sld:Name>", escape_xml(layer_name))?;
writeln!(writer, " <sld:FeatureTypeStyle>")?;
writeln!(writer, " <sld:Rule>")?;
writeln!(writer, " <sld:RasterSymbolizer>")?;
writeln!(writer, " <sld:ChannelSelection>")?;
writeln!(writer, " <sld:GrayChannel>")?;
writeln!(writer, " <sld:SourceChannelName>1</sld:SourceChannelName>")?;
writeln!(writer, " </sld:GrayChannel>")?;
writeln!(writer, " </sld:ChannelSelection>")?;
writeln!(writer, " <sld:ColorMap type=\"{}\">", self.map_type)?;
for entry in &self.entries {
let label = entry.label.as_ref().map_or_else(
|| format!("{:.4}", entry.value),
|s| s.clone()
);
writeln!(writer, " <sld:ColorMapEntry quantity=\"{}\" label=\"{}\" color=\"{}\"/>",
entry.value, escape_xml(&label), entry.to_hex_color())?;
}
writeln!(writer, " </sld:ColorMap>")?;
writeln!(writer, " </sld:RasterSymbolizer>")?;
writeln!(writer, " </sld:Rule>")?;
writeln!(writer, " </sld:FeatureTypeStyle>")?;
writeln!(writer, " </sld:UserStyle>")?;
writeln!(writer, " </UserLayer>")?;
writeln!(writer, "</StyledLayerDescriptor>")?;
Ok(())
}
pub fn print(&self) {
println!("Color Map with {} entries (type: {}):", self.entries.len(), self.map_type);
println!("{:^8} {:^20} {:^10}", "Value", "Color (RGB)", "Label");
println!("{:-^8} {:-^20} {:-^10}", "", "", "");
for entry in &self.entries {
println!("{:^8} {:^20} {:^10}",
entry.value,
format!("({},{},{}) {}",
entry.color.r,
entry.color.g,
entry.color.b,
entry.to_hex_color()
),
entry.label.as_deref().unwrap_or("")
);
}
}
pub fn apply_to_builder(&self, builder: &mut TiffBuilder, ifd_index: usize) -> TiffResult<()> {
if ifd_index >= builder.ifds.len() {
return Err(TiffError::GenericError(format!(
"Invalid IFD index {}, only have {} IFDs", ifd_index, builder.ifds.len())));
}
let (num_entries, colormap_data) = self.to_tiff_colormap();
let bits_needed = match num_entries {
0..=2 => 1,
3..=4 => 2,
5..=16 => 4,
17..=256 => 8,
_ => 16,
};
debug!("Setting up colormap with {} entries, {} bits per pixel", num_entries, bits_needed);
let mut bytes = Vec::with_capacity(colormap_data.len() * 2);
for value in &colormap_data {
bytes.extend_from_slice(&value.to_le_bytes());
}
builder.set_external_data(ifd_index, tags::COLOR_MAP, bytes);
builder.ifds[ifd_index].add_entry(IFDEntry::new(
tags::PHOTOMETRIC_INTERPRETATION,
field_types::SHORT,
1,
photometric::PALETTE as u64
));
builder.ifds[ifd_index].add_entry(IFDEntry::new(
tags::BITS_PER_SAMPLE,
field_types::SHORT,
1,
bits_needed as u64
));
builder.ifds[ifd_index].add_entry(IFDEntry::new(
tags::SAMPLES_PER_PIXEL,
field_types::SHORT,
1,
1
));
Ok(())
}
}
pub struct ColorMapReader<'a> {
logger: &'a Logger,
}
impl<'a> ColorMapReader<'a> {
pub fn new(logger: &'a Logger) -> Self {
ColorMapReader {
logger
}
}
pub fn read_file(&self, file_path: &str) -> TiffResult<ColorMap> {
info!("Reading color map from file: {}", file_path);
let extension = match std::path::Path::new(file_path).extension() {
Some(ext) => ext.to_string_lossy().to_lowercase(),
None => "".to_string()
};
match extension.as_str() {
"sld" => {
debug!("Detected SLD format");
ColorMap::from_sld_file(file_path)
},
"csv" | "txt" => {
debug!("Detected CSV format");
ColorMap::from_csv_file(file_path)
},
"tif" | "tiff" => {
debug!("Detected TIFF format");
self.read_from_tiff(file_path)
},
_ => {
self.guess_format(file_path)
}
}
}
pub fn read_from_tiff(&self, file_path: &str) -> TiffResult<ColorMap> {
info!("Reading color map from TIFF file: {}", file_path);
let mut reader = TiffReader::new(self.logger);
let tiff = reader.load(file_path)?;
if tiff.ifds.is_empty() {
return Err(TiffError::GenericError("No IFDs found in TIFF file".to_string()));
}
let ifd = &tiff.ifds[0];
let mut file_reader = reader.create_reader()?;
let byte_order_handler = reader.get_byte_order_handler()
.ok_or_else(|| TiffError::GenericError("No byte order handler available".to_string()))?;
let colormap = ColorMap::from_tiff_ifd(ifd, &mut file_reader, byte_order_handler)?;
info!("Successfully read color map with {} entries from TIFF", colormap.len());
self.logger.log(&format!("Read color map with {} entries from {}", colormap.len(), file_path))?;
Ok(colormap)
}
fn guess_format(&self, file_path: &str) -> TiffResult<ColorMap> {
info!("Attempting to guess color map format for: {}", file_path);
let file = File::open(file_path)?;
let reader = BufReader::new(file);
let mut lines = Vec::new();
for line in reader.lines().take(10) {
if let Ok(line) = line {
lines.push(line);
}
}
let looks_like_xml = lines.iter()
.any(|line| line.contains("<?xml") || line.contains("<StyledLayerDescriptor"));
if looks_like_xml {
debug!("Content appears to be XML/SLD format");
return ColorMap::from_sld_file(file_path);
}
let looks_like_csv = lines.iter()
.any(|line| line.contains(',') && !line.contains('<') && !line.contains('>'));
if looks_like_csv {
debug!("Content appears to be CSV format");
return ColorMap::from_csv_file(file_path);
}
warn!("Could not determine format, trying CSV as fallback");
ColorMap::from_csv_file(file_path)
}
}
fn read_colormap_data<R: SeekableReader>(
reader: &mut R,
byte_order_handler: &Box<dyn ByteOrderHandler>,
num_entries: u16
) -> TiffResult<(Vec<u16>, Vec<u16>, Vec<u16>)> {
let mut r_values = Vec::with_capacity(num_entries as usize);
let mut g_values = Vec::with_capacity(num_entries as usize);
let mut b_values = Vec::with_capacity(num_entries as usize);
for _ in 0..num_entries {
r_values.push(byte_order_handler.read_u16(reader)?);
}
for _ in 0..num_entries {
g_values.push(byte_order_handler.read_u16(reader)?);
}
for _ in 0..num_entries {
b_values.push(byte_order_handler.read_u16(reader)?);
}
Ok((r_values, g_values, b_values))
}
fn parse_sld_entry_attributes(colormap: &mut ColorMap, line: &str) {
let quantity = match extract_attribute(line, "quantity") {
Some(qty) => qty,
None => return, };
let color_hex = match extract_attribute(line, "color") {
Some(clr) => clr,
None => return, };
let value = match quantity.parse::<f64>() {
Ok(val) => val as u16,
Err(_) => return, };
let rgb_color = match RgbColor::from_hex(&color_hex) {
Ok(clr) => clr,
Err(_) => return, };
let label = extract_attribute(line, "label");
let entry = ColorMapEntry {
value,
label,
color: rgb_color
};
colormap.add_entry(entry);
}
fn parse_csv_line(parts: &[&str]) -> Option<ColorMapEntry> {
match parts.len() {
2 => parse_csv_value_hex(parts),
3 => parse_csv_three_parts(parts),
4 => parse_csv_value_rgb(parts),
5 => parse_csv_value_rgb_label(parts),
_ => None,
}
}
fn parse_csv_value_hex(parts: &[&str]) -> Option<ColorMapEntry> {
let value = parts[0].parse::<f64>().ok()?;
let color = RgbColor::from_hex(parts[1]).ok()?;
Some(ColorMapEntry::new(value as u16, color))
}
fn parse_csv_three_parts(parts: &[&str]) -> Option<ColorMapEntry> {
let value = parts[0].parse::<f64>().ok()?;
if let Ok(color) = RgbColor::from_hex(parts[1]) {
return Some(ColorMapEntry::with_label(
value as u16, color, parts[2].to_string()
));
}
let r = parts[1].parse::<u8>().ok()?;
let g = parts[2].parse::<u8>().ok()?;
Some(ColorMapEntry::new(value as u16, RgbColor::new(r, g, 0)))
}
fn parse_csv_value_rgb(parts: &[&str]) -> Option<ColorMapEntry> {
let value = parts[0].parse::<f64>().ok()?;
let r = parts[1].parse::<u8>().ok()?;
let g = parts[2].parse::<u8>().ok()?;
let b = parts[3].parse::<u8>().ok()?;
Some(ColorMapEntry::new(value as u16, RgbColor::new(r, g, b)))
}
fn parse_csv_value_rgb_label(parts: &[&str]) -> Option<ColorMapEntry> {
let value = parts[0].parse::<f64>().ok()?;
let r = parts[1].parse::<u8>().ok()?;
let g = parts[2].parse::<u8>().ok()?;
let b = parts[3].parse::<u8>().ok()?;
Some(ColorMapEntry::with_label(
value as u16, RgbColor::new(r, g, b), parts[4].to_string()
))
}
fn interpolate_color_component(start: u8, end: u8, t: f32) -> u16 {
((start as f32 * (1.0 - t) + end as f32 * t) as u16 * 257)
}
fn extract_attribute(line: &str, attr_name: &str) -> Option<String> {
let attr_pattern = format!("{}=\"", attr_name);
if let Some(start_pos) = line.find(&attr_pattern) {
let start_val = start_pos + attr_pattern.len();
if let Some(end_pos) = line[start_val..].find('"') {
return Some(line[start_val..(start_val + end_pos)].to_string());
}
}
None
}
fn extract_colormap_type(content: &str) -> Option<String> {
for line in content.lines() {
if line.contains("sld:ColorMap") || line.contains("ColorMap") {
return extract_attribute(line, "type");
}
}
None
}
fn escape_xml(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('\'', "'")
.replace('"', """)
}