use anyhow::{Context, Result};
use chrono::{DateTime, NaiveDateTime, Utc};
use log::warn;
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Seek};
use std::path::Path;
use super::types::{AstroMetadata, AttachmentInfo, ColorManagement, DisplayFunction, XisfMetadata};
pub fn extract_metadata<R: Read + Seek>(reader: &mut R) -> Result<AstroMetadata> {
let mut metadata = AstroMetadata::default();
let mut raw_headers = HashMap::new();
let mut xisf_metadata = XisfMetadata {
version: "1.0".to_string(),
creator: None,
creation_time: None,
block_alignment: None,
};
let mut signature = [0u8; 8];
reader
.read_exact(&mut signature)
.context("Failed to read XISF signature")?;
if &signature != b"XISF0100" {
return Err(anyhow::anyhow!("Invalid XISF signature"));
}
let mut header_size_bytes = [0u8; 4];
reader
.read_exact(&mut header_size_bytes)
.context("Failed to read header size")?;
let header_size = u32::from_le_bytes(header_size_bytes) as usize;
if let Ok(xml_content) = extract_xml_content(reader, header_size) {
extract_fits_keywords(&xml_content, &mut metadata, &mut raw_headers);
extract_xml_attributes(&xml_content, &mut metadata);
extract_xisf_metadata(&xml_content, &mut metadata, &mut xisf_metadata);
extract_color_management(&xml_content, &mut metadata);
extract_attachments(&xml_content, &mut metadata);
}
metadata.raw_headers = raw_headers;
metadata.xisf = Some(xisf_metadata);
metadata.calculate_session_date();
Ok(metadata)
}
pub fn extract_metadata_from_path(path: &Path) -> Result<AstroMetadata> {
let mut file = File::open(path).context("Failed to open XISF file")?;
extract_metadata(&mut file)
}
fn extract_xml_content<R: Read>(reader: &mut R, header_size: usize) -> Result<String> {
let mut header_data = vec![0u8; header_size];
reader
.read_exact(&mut header_data)
.context("Failed to read XML header")?;
let mut xml_start = 0;
for i in 0..header_data.len() {
if i + 5 < header_data.len() && &header_data[i..i + 5] == b"<?xml" {
xml_start = i;
break;
}
}
let actual_size = header_data[xml_start..]
.iter()
.position(|&b| b == 0)
.map(|pos| xml_start + pos)
.unwrap_or(header_data.len());
let xml_content = String::from_utf8_lossy(&header_data[xml_start..actual_size]).to_string();
Ok(xml_content)
}
fn extract_fits_keywords(
xml: &str,
metadata: &mut AstroMetadata,
raw_headers: &mut HashMap<String, String>,
) {
let mut pos = 0;
while let Some(start_pos) = xml[pos..].find("<FITSKeyword ") {
let keyword_start = pos + start_pos;
if let Some(end_pos) = xml[keyword_start..].find("/>") {
let keyword_end = keyword_start + end_pos + 2;
let keyword_tag = &xml[keyword_start..keyword_end];
if let Some(name) = extract_attribute(keyword_tag, "name") {
if let Some(value) = extract_attribute(keyword_tag, "value") {
let clean_value = value.trim_matches('\'').to_string();
raw_headers.insert(name.clone(), clean_value.clone());
process_fits_keyword(metadata, &name, &clean_value);
}
}
pos = keyword_end;
} else {
break;
}
}
}
fn extract_xml_attributes(xml: &str, metadata: &mut AstroMetadata) {
if let Some(geometry) = extract_attribute(xml, "geometry") {
let parts: Vec<&str> = geometry.split(':').collect();
if parts.len() >= 2 {
metadata.detector.width = parts[0].parse().unwrap_or(0);
metadata.detector.height = parts[1].parse().unwrap_or(0);
}
}
if let Some(color_space) = extract_attribute(xml, "colorSpace") {
if color_space == "Gray" {
}
}
if let Some(sample_format) = extract_attribute(xml, "sampleFormat") {
if sample_format == "UInt16" {
}
}
if let Some(creation_time) = extract_property_value(xml, "XISF:CreationTime") {
metadata.exposure.date_obs = parse_date_time(&creation_time);
}
if let Some(creator_app) = extract_property_value(xml, "XISF:CreatorApplication") {
if let Some(ref mut env) = metadata.environment {
env.software_version = Some(creator_app);
} else {
metadata.environment = Some(super::types::Environment {
software_version: Some(creator_app),
..Default::default()
});
}
}
}
fn extract_xisf_metadata(
xml: &str,
_metadata: &mut AstroMetadata,
xisf_metadata: &mut XisfMetadata,
) {
if let Some(version) = extract_attribute(xml, "version") {
xisf_metadata.version = version;
}
if let Some(creator_app) = extract_property_value(xml, "XISF:CreatorApplication") {
xisf_metadata.creator = Some(creator_app);
}
if let Some(creation_time) = extract_property_value(xml, "XISF:CreationTime") {
xisf_metadata.creation_time = parse_date_time(&creation_time);
}
if let Some(block_alignment) = extract_attribute(xml, "blockAlignment") {
xisf_metadata.block_alignment = block_alignment.parse::<usize>().ok();
}
}
fn extract_color_management(xml: &str, metadata: &mut AstroMetadata) {
let mut color_management = ColorManagement::default();
let mut has_color_info = false;
if let Some(color_space) = extract_attribute(xml, "colorSpace") {
color_management.color_space = Some(color_space);
has_color_info = true;
}
if let Some(_icc_profile) = extract_property_value(xml, "ICCProfile") {
color_management.icc_profile = Some(Vec::new());
has_color_info = true;
}
if let Some(display_function_type) = extract_attribute(xml, "displayFunction") {
let mut display_function = DisplayFunction {
function_type: Some(display_function_type),
..Default::default()
};
if let Some(params) = extract_attribute(xml, "displayParameters") {
let param_pairs: Vec<&str> = params.split(';').collect();
let mut parameters = HashMap::new();
for pair in param_pairs {
let kv: Vec<&str> = pair.split('=').collect();
if kv.len() == 2 {
if let Ok(value) = kv[1].parse::<f64>() {
parameters.insert(kv[0].to_string(), value);
}
}
}
display_function.parameters = parameters;
}
color_management.display_function = Some(display_function);
has_color_info = true;
}
if has_color_info {
metadata.color_management = Some(color_management);
}
}
fn extract_attachments(xml: &str, metadata: &mut AstroMetadata) {
let mut attachments = Vec::new();
let mut pos = 0;
while let Some(start_pos) = xml[pos..].find("<Image ") {
let image_start = pos + start_pos;
if let Some(end_pos) = xml[image_start..].find(">") {
let image_end = image_start + end_pos + 1;
let image_tag = &xml[image_start..image_end];
let mut attachment = AttachmentInfo::default();
if let Some(id) = extract_attribute(image_tag, "id") {
attachment.id = id;
} else {
attachment.id = format!("image{}", attachments.len());
}
if let Some(geometry) = extract_attribute(image_tag, "geometry") {
attachment.geometry = geometry;
}
if let Some(sample_format) = extract_attribute(image_tag, "sampleFormat") {
attachment.sample_format = sample_format;
} else {
attachment.sample_format = "UInt16".to_string(); }
if let Some(bits_per_sample) = extract_attribute(image_tag, "bitsPerSample") {
attachment.bits_per_sample = bits_per_sample.parse().unwrap_or(16);
} else {
attachment.bits_per_sample = 16; }
if let Some(compression) = extract_attribute(image_tag, "compression") {
attachment.compression = Some(compression);
if let Some(params) = extract_attribute(image_tag, "compressionParameters") {
let param_pairs: Vec<&str> = params.split(';').collect();
let mut parameters = HashMap::new();
for pair in param_pairs {
let kv: Vec<&str> = pair.split('=').collect();
if kv.len() == 2 {
parameters.insert(kv[0].to_string(), kv[1].to_string());
}
}
attachment.compression_parameters = parameters;
}
}
if let Some(checksum_type) = extract_attribute(image_tag, "checksumType") {
attachment.checksum_type = Some(checksum_type);
if let Some(checksum) = extract_attribute(image_tag, "checksum") {
attachment.checksum = Some(checksum);
}
}
if let Some(resolution_x) = extract_attribute(image_tag, "xResolution") {
attachment.resolution_x = resolution_x.parse::<f64>().ok();
if let Some(resolution_y) = extract_attribute(image_tag, "yResolution") {
attachment.resolution_y = resolution_y.parse::<f64>().ok();
}
if let Some(resolution_unit) = extract_attribute(image_tag, "resolutionUnit") {
attachment.resolution_unit = Some(resolution_unit);
}
}
attachments.push(attachment);
pos = image_end;
} else {
break;
}
}
if !attachments.is_empty() {
metadata.attachments = attachments;
}
}
fn process_fits_keyword(metadata: &mut AstroMetadata, name: &str, value: &str) {
match name {
"TELESCOP" => metadata.equipment.telescope_name = Some(value.to_string()),
"FOCALLEN" => metadata.equipment.focal_length = value.parse().ok(),
"APERTURE" => metadata.equipment.aperture = value.parse().ok(),
"FOCRATIO" => metadata.equipment.focal_ratio = value.parse().ok(),
"INSTRUME" | "CAMERA" => metadata.detector.camera_name = Some(value.to_string()),
"XPIXSZ" | "PIXSIZE" => metadata.detector.pixel_size = value.parse().ok(),
"XBINNING" => metadata.detector.binning_x = value.parse().unwrap_or(1),
"YBINNING" => metadata.detector.binning_y = value.parse().unwrap_or(1),
"GAIN" | "EGAIN" => metadata.detector.gain = value.parse().ok(),
"RDNOISE" => metadata.detector.read_noise = value.parse().ok(),
"CCD-TEMP" | "CCDTEMP" => metadata.detector.temperature = value.parse().ok(),
"SET-TEMP" => metadata.detector.temp_setpoint = value.parse().ok(),
"FILTER" => metadata.filter.name = Some(value.to_string()),
"OBJECT" => metadata.exposure.object_name = Some(value.to_string()),
"RA" | "OBJCTRA" => {
if let Ok(ra) = value.parse::<f32>() {
metadata.exposure.ra = Some(ra as f64);
} else {
if let Some(ra_deg) = parse_sexagesimal(value) {
metadata.exposure.ra = Some(ra_deg * 15.0); }
}
}
"DEC" | "OBJCTDEC" => {
if let Ok(dec) = value.parse::<f32>() {
metadata.exposure.dec = Some(dec as f64);
} else {
if let Some(dec_deg) = parse_sexagesimal(value) {
metadata.exposure.dec = Some(dec_deg);
}
}
}
"DATE-OBS" => metadata.exposure.date_obs = parse_date_time(value),
"EXPTIME" | "EXPOSURE" => metadata.exposure.exposure_time = value.parse().ok(),
"IMAGETYP" | "FRAME" => metadata.exposure.frame_type = Some(value.to_string()),
"PIERSIDE" => {
if let Some(ref mut mount) = metadata.mount {
mount.pier_side = Some(value.to_string());
} else {
metadata.mount = Some(super::types::Mount {
pier_side: Some(value.to_string()),
..Default::default()
});
}
}
"AMB_TEMP" | "AMBTEMP" => {
if let Some(ref mut env) = metadata.environment {
env.ambient_temp = value.parse().ok();
} else {
metadata.environment = Some(super::types::Environment {
ambient_temp: value.parse().ok(),
..Default::default()
});
}
}
"HUMIDITY" => {
if let Some(ref mut env) = metadata.environment {
env.humidity = value.parse().ok();
} else {
metadata.environment = Some(super::types::Environment {
humidity: value.parse().ok(),
..Default::default()
});
}
}
"CRPIX1" => {
if let Some(ref mut wcs) = metadata.wcs {
wcs.crpix1 = value.parse().ok();
} else {
metadata.wcs = Some(super::types::WcsData {
crpix1: value.parse().ok(),
..Default::default()
});
}
}
"CRPIX2" => {
if let Some(ref mut wcs) = metadata.wcs {
wcs.crpix2 = value.parse().ok();
} else {
metadata.wcs = Some(super::types::WcsData {
crpix2: value.parse().ok(),
..Default::default()
});
}
}
"SITELAT" | "OBSLAT" => {
if let Some(ref mut mount) = metadata.mount {
mount.latitude = value.parse().ok();
} else {
metadata.mount = Some(super::types::Mount {
latitude: value.parse().ok(),
..Default::default()
});
}
}
"SITELONG" | "OBSLONG" => {
if let Some(ref mut mount) = metadata.mount {
mount.longitude = value.parse().ok();
} else {
metadata.mount = Some(super::types::Mount {
longitude: value.parse().ok(),
..Default::default()
});
}
}
"SITEELEV" | "OBSELEV" => {
if let Some(ref mut mount) = metadata.mount {
mount.height = value.parse().ok();
} else {
metadata.mount = Some(super::types::Mount {
height: value.parse().ok(),
..Default::default()
});
}
}
"OFFSET" | "CCDOFFST" => metadata.detector.offset = value.parse().ok(),
"READOUT" | "READOUTM" => metadata.detector.readout_mode = Some(value.to_string()),
"USBLIMIT" | "USBTRFC" => metadata.detector.usb_limit = Some(value.to_string()),
"ROTANG" | "ROTPA" | "ROTATANG" => metadata.detector.rotator_angle = value.parse().ok(),
"FOCPOS" | "FOCUSPOS" => metadata.equipment.focuser_position = value.parse().ok(),
"FOCTEMP" | "FOCUSTEMP" => metadata.equipment.focuser_temperature = value.parse().ok(),
"PEAKRA" | "PEAKRAER" => {
if let Some(ref mut mount) = metadata.mount {
mount.peak_ra_error = value.parse().ok();
} else {
metadata.mount = Some(super::types::Mount {
peak_ra_error: value.parse().ok(),
..Default::default()
});
}
}
"PEAKDEC" | "PEAKDCER" => {
if let Some(ref mut mount) = metadata.mount {
mount.peak_dec_error = value.parse().ok();
} else {
metadata.mount = Some(super::types::Mount {
peak_dec_error: value.parse().ok(),
..Default::default()
});
}
}
"SQM" | "SQMMAG" | "SKYQUAL" => {
if let Some(ref mut env) = metadata.environment {
env.sqm = value.parse().ok();
} else {
metadata.environment = Some(super::types::Environment {
sqm: value.parse().ok(),
..Default::default()
});
}
}
"PROJECT" | "PROJNAME" => metadata.exposure.project_name = Some(value.to_string()),
"SESSIONID" | "SESSID" => metadata.exposure.session_id = Some(value.to_string()),
_ => {}
}
}
fn extract_attribute(xml: &str, attr_name: &str) -> Option<String> {
let search_pattern = format!("{}=\"", attr_name);
if let Some(start_pos) = xml.find(&search_pattern) {
let start = start_pos + search_pattern.len();
if let Some(end_pos) = xml[start..].find('"') {
return Some(xml[start..start + end_pos].to_string());
}
}
None
}
fn extract_property_value(xml: &str, property_id: &str) -> Option<String> {
let search_pattern = format!("id=\"{}\" type=\"", property_id);
if let Some(start_pos) = xml.find(&search_pattern) {
if let Some(tag_end) = xml[start_pos..].find(">") {
let tag_end_pos = start_pos + tag_end + 1;
if let Some(end_tag_pos) = xml[tag_end_pos..].find("</Property>") {
let value_end = tag_end_pos + end_tag_pos;
return Some(xml[tag_end_pos..value_end].trim().to_string());
}
}
}
None
}
fn parse_sexagesimal(value: &str) -> Option<f64> {
let parts: Vec<&str> = value.split_whitespace().collect();
if parts.len() >= 3 {
if let (Ok(h), Ok(m), Ok(s)) = (
parts[0].parse::<f64>(),
parts[1].parse::<f64>(),
parts[2].parse::<f64>(),
) {
let sign = if h < 0.0 || value.starts_with('-') {
-1.0
} else {
1.0
};
return Some(sign * (h.abs() + m / 60.0 + s / 3600.0));
}
}
None
}
fn parse_date_time(date_str: &str) -> Option<DateTime<Utc>> {
let formats = [
"%Y-%m-%dT%H:%M:%S%.fZ", "%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%S%.f", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S%.f", "%Y-%m-%d %H:%M:%S", ];
for format in &formats {
if let Ok(dt) = NaiveDateTime::parse_from_str(date_str, format) {
return Some(DateTime::from_naive_utc_and_offset(dt, Utc));
}
}
warn!("Failed to parse date string: {}", date_str);
None
}