astro-metadata 0.4.0

Metadata handling for astronomical images
# astro-metadata

Metadata extraction and handling for astronomical images.

## Overview

`astro-metadata` provides functionality for extracting and processing metadata from astronomical image formats, including FITS and XISF. It handles parsing of headers, extraction of equipment information, exposure details, and more.

## Features

- Comprehensive metadata type definitions
- FITS header parsing
- XISF header parsing
- Equipment information (telescope, camera, etc.)
- Exposure details
- Filter information
- Environmental data
- Coordinate and timing utilities

## Windows FITS Path-Length Note

On Windows, FITS file access in AstroMuninn and the ravensky-astro FITS APIs depends on CFITSIO (via `fitsio` / `fitsio-sys`). CFITSIO currently opens disk files using its `fopen`-based path handling (`file_openfile`), which in this environment follows the classic Windows path-length boundary.

Use full FITS paths shorter than 260 characters (`< 260`). At 260 or more, FITS open calls may fail.

This limitation is specific to FITS access through CFITSIO. XISF handling is not affected.

## Installation

Add to your `Cargo.toml`:

```toml
[dependencies]
astro-metadata = "0.4.0"
```

## API Reference

### Core Types

#### AstroMetadata

```rust
pub struct AstroMetadata {
    pub equipment: Equipment,
    pub detector: Detector,
    pub filter: Filter,
    pub exposure: Exposure,
    pub mount: Option<Mount>,
    pub environment: Option<Environment>,
    pub wcs: Option<WcsData>,
    pub xisf: Option<XisfMetadata>,
    pub color_management: Option<ColorManagement>,
    pub attachments: Vec<AttachmentInfo>,
    pub raw_header_cards: Vec<FitsHeaderCard>,
    pub raw_headers: HashMap<String, String>,
}
```

Key methods:
```rust
impl AstroMetadata {
    /// Check if we have enough information to calculate plate scale
    pub fn can_calculate_plate_scale(&self) -> bool

    /// Calculate plate scale in arcsec/pixel
    pub fn plate_scale(&self) -> Option<f32>
    
    /// Calculate field of view in arcminutes
    pub fn field_of_view(&self) -> Option<(f32, f32)>
    
    /// Calculate the session date using location information if available
    pub fn calculate_session_date(&mut self)
}
```

#### Equipment

```rust
pub struct Equipment {
    pub telescope_name: Option<String>,
    pub focal_length: Option<f32>,
    pub aperture: Option<f32>,
    pub focal_ratio: Option<f32>,
    pub reducer_flattener: Option<String>,
    pub mount_model: Option<String>,
    pub focuser_position: Option<i32>,
    pub focuser_temperature: Option<f32>,
}
```

#### Detector

```rust
pub struct Detector {
    pub camera_name: Option<String>,
    pub pixel_size: Option<f32>,
    pub width: usize,
    pub height: usize,
    pub binning_x: usize,
    pub binning_y: usize,
    pub gain: Option<f32>,
    pub offset: Option<i32>,
    pub readout_mode: Option<String>,
    pub usb_limit: Option<String>,
    pub read_noise: Option<f32>,
    pub full_well: Option<f32>,
    pub temperature: Option<f32>,
    pub temp_setpoint: Option<f32>,
    pub cooler_power: Option<f32>,
    pub cooler_status: Option<String>,
    pub rotator_angle: Option<f32>,
}
```

#### Filter

```rust
pub struct Filter {
    pub name: Option<String>,
    pub position: Option<usize>,
    pub wavelength: Option<f32>,
}
```

#### Exposure

```rust
pub struct Exposure {
    pub object_name: Option<String>,
    pub ra: Option<f64>,
    pub dec: Option<f64>,
    pub date_obs: Option<DateTime<Utc>>,
    pub session_date: Option<DateTime<Utc>>,
    pub exposure_time: Option<f32>,
    pub frame_type: Option<String>,
    pub sequence_id: Option<String>,
    pub frame_number: Option<usize>,
    pub dither_offset_x: Option<f32>,
    pub dither_offset_y: Option<f32>,
    pub project_name: Option<String>,
    pub session_id: Option<String>,
}
```

### FITS Parser

```rust
/// Extract metadata from a FITS file path
pub fn extract_metadata_from_path(path: &Path) -> Result<AstroMetadata>

/// Extract metadata from a FITS file
pub fn extract_metadata(fits_file: &mut FitsFile) -> Result<AstroMetadata>

/// Parse sexagesimal format (HH MM SS or DD MM SS) to decimal degrees
pub fn parse_sexagesimal(value: &str) -> Option<f64>
```

- **Parameters**:
  - `path`: Path to the FITS file
  - `fits_file`: Open FITS file handle
  - `value`: String in sexagesimal format (e.g., "12 34 56" for RA or "-45 12 34" for DEC)
- **Returns**:
  - `AstroMetadata`: Extracted metadata structure
  - `f64`: Decimal degrees (for parse_sexagesimal)
- **Errors**:
  - If the file cannot be opened
  - If required headers cannot be read
  - On Windows, FITS open may fail when the full pathname is 260 characters or longer due to CFITSIO `fopen` path handling.

### XISF Parser

```rust
/// Extract metadata from an XISF file path
pub fn extract_metadata_from_path(path: &Path) -> Result<AstroMetadata>

/// Extract metadata from an XISF file
pub fn extract_metadata<R: Read + Seek>(reader: &mut R) -> Result<AstroMetadata>
```

- **Parameters**:
  - `path`: Path to the XISF file
  - `reader`: Reader for the XISF file
- **Returns**:
  - `AstroMetadata`: Extracted metadata structure
- **Errors**:
  - If the file cannot be opened
  - If the XISF signature is invalid
  - If the XML header cannot be parsed

## Usage Examples

### Extracting metadata from a FITS file

```rust
use astro_metadata::fits_parser;
use std::path::Path;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = Path::new("/path/to/image.fits");
    let metadata = fits_parser::extract_metadata_from_path(path)?;
    
    // Access equipment information
    if let Some(focal_length) = metadata.equipment.focal_length {
        println!("Focal length: {} mm", focal_length);
    }
    
    // Access exposure information
    if let Some(exp_time) = metadata.exposure.exposure_time {
        println!("Exposure time: {} seconds", exp_time);
    }
    
    // Calculate plate scale
    if let Some(plate_scale) = metadata.plate_scale() {
        println!("Plate scale: {} arcsec/pixel", plate_scale);
    }
    
    // Calculate field of view
    if let Some((width, height)) = metadata.field_of_view() {
        println!("Field of view: {:.2}' × {:.2}'", width, height);
    }
    
    Ok(())
}
```

### Parsing sexagesimal coordinates

```rust
use astro_metadata::fits_parser::parse_sexagesimal;

fn main() {
    // Parse right ascension: "12 34 56" (12h 34m 56s)
    let ra_deg = parse_sexagesimal("12 34 56").map(|ra| ra * 15.0); // Convert hours to degrees
    println!("RA: {:?} degrees", ra_deg);
    
    // Parse declination: "-45 12 34" (-45° 12' 34")
    let dec_deg = parse_sexagesimal("-45 12 34");
    println!("DEC: {:?} degrees", dec_deg);
}
```

## License

This project is dual-licensed under the MIT License or the Apache License, Version 2.0.