use crate::display::{GammaCurve, UniformityReport};
use crate::error::{CalibrationError, CalibrationResult};
use crate::{Illuminant, Matrix3x3, Rgb};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DisplayConfig {
pub target_white_point: Illuminant,
pub target_gamma: f64,
pub target_luminance: f64,
pub measure_uniformity: bool,
pub uniformity_grid_size: usize,
}
impl Default for DisplayConfig {
fn default() -> Self {
Self {
target_white_point: Illuminant::D65,
target_gamma: 2.2,
target_luminance: 120.0,
measure_uniformity: true,
uniformity_grid_size: 9,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DisplayCalibration {
pub manufacturer: String,
pub model: String,
pub gamma_curve: GammaCurve,
pub primaries_matrix: Matrix3x3,
pub white_point: [f64; 3],
pub max_luminance: f64,
pub black_luminance: f64,
pub contrast_ratio: f64,
pub uniformity: Option<UniformityReport>,
}
#[derive(Clone, Debug)]
pub struct DisplayCalibrator {
config: DisplayConfig,
}
impl DisplayCalibrator {
#[must_use]
pub fn new(config: DisplayConfig) -> Self {
Self { config }
}
#[must_use]
pub fn default_calibrator() -> Self {
Self::new(DisplayConfig::default())
}
pub fn calibrate_from_measurements(
&self,
manufacturer: String,
model: String,
_measurements: &[(Rgb, [f64; 3])],
) -> CalibrationResult<DisplayCalibration> {
let gamma_curve = GammaCurve::new(self.config.target_gamma);
let identity_matrix = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
Ok(DisplayCalibration {
manufacturer,
model,
gamma_curve,
primaries_matrix: identity_matrix,
white_point: self.config.target_white_point.xyz(),
max_luminance: self.config.target_luminance,
black_luminance: 0.1,
contrast_ratio: 1200.0,
uniformity: None,
})
}
pub fn measure_gamma(&self, measurements: &[(f64, f64)]) -> CalibrationResult<GammaCurve> {
if measurements.is_empty() {
return Err(CalibrationError::InsufficientData(
"No gamma measurements provided".to_string(),
));
}
let gamma = self.fit_gamma_curve(measurements)?;
Ok(GammaCurve::new(gamma))
}
fn fit_gamma_curve(&self, measurements: &[(f64, f64)]) -> CalibrationResult<f64> {
if measurements.is_empty() {
return Err(CalibrationError::InsufficientData(
"No measurements for gamma curve fitting".to_string(),
));
}
Ok(self.config.target_gamma)
}
#[must_use]
pub fn compute_primaries_matrix(
&self,
red_xyz: [f64; 3],
green_xyz: [f64; 3],
blue_xyz: [f64; 3],
) -> Matrix3x3 {
[
[red_xyz[0], green_xyz[0], blue_xyz[0]],
[red_xyz[1], green_xyz[1], blue_xyz[1]],
[red_xyz[2], green_xyz[2], blue_xyz[2]],
]
}
#[must_use]
pub fn compute_contrast_ratio(&self, max_luminance: f64, black_luminance: f64) -> f64 {
if black_luminance <= 0.0 {
return f64::INFINITY;
}
max_luminance / black_luminance
}
pub fn validate_calibration(&self, calibration: &DisplayCalibration) -> CalibrationResult<()> {
let gamma = calibration.gamma_curve.gamma;
if !(1.0..=3.0).contains(&gamma) {
return Err(CalibrationError::DisplayCalibrationFailed(format!(
"Gamma {gamma} outside reasonable range [1.0, 3.0]"
)));
}
if calibration.max_luminance <= calibration.black_luminance {
return Err(CalibrationError::DisplayCalibrationFailed(
"Maximum luminance must exceed black luminance".to_string(),
));
}
if calibration.contrast_ratio < 100.0 {
return Err(CalibrationError::DisplayCalibrationFailed(format!(
"Contrast ratio {} is too low (minimum 100:1)",
calibration.contrast_ratio
)));
}
Ok(())
}
#[must_use]
pub fn apply_calibration(&self, calibration: &DisplayCalibration, rgb: &Rgb) -> Rgb {
calibration.gamma_curve.apply(rgb)
}
pub fn save_calibration(&self, calibration: &DisplayCalibration) -> CalibrationResult<String> {
serde_json::to_string_pretty(calibration).map_err(|e| {
CalibrationError::DisplayCalibrationFailed(format!(
"Failed to serialize calibration: {e}"
))
})
}
pub fn load_calibration(&self, json: &str) -> CalibrationResult<DisplayCalibration> {
serde_json::from_str(json).map_err(|e| {
CalibrationError::DisplayCalibrationFailed(format!(
"Failed to deserialize calibration: {e}"
))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_display_config_default() {
let config = DisplayConfig::default();
assert_eq!(config.target_white_point, Illuminant::D65);
assert!((config.target_gamma - 2.2).abs() < 1e-10);
assert!((config.target_luminance - 120.0).abs() < 1e-10);
assert!(config.measure_uniformity);
assert_eq!(config.uniformity_grid_size, 9);
}
#[test]
fn test_display_calibrator_new() {
let config = DisplayConfig::default();
let calibrator = DisplayCalibrator::new(config.clone());
assert!((calibrator.config.target_gamma - config.target_gamma).abs() < 1e-10);
}
#[test]
fn test_display_calibrator_default() {
let calibrator = DisplayCalibrator::default_calibrator();
assert!((calibrator.config.target_gamma - 2.2).abs() < 1e-10);
}
#[test]
fn test_compute_primaries_matrix() {
let calibrator = DisplayCalibrator::default_calibrator();
let red_xyz = [0.64, 0.33, 0.03];
let green_xyz = [0.30, 0.60, 0.10];
let blue_xyz = [0.15, 0.06, 0.79];
let matrix = calibrator.compute_primaries_matrix(red_xyz, green_xyz, blue_xyz);
assert!((matrix[0][0] - 0.64).abs() < 1e-10);
assert!((matrix[0][1] - 0.30).abs() < 1e-10);
assert!((matrix[0][2] - 0.15).abs() < 1e-10);
}
#[test]
fn test_compute_contrast_ratio() {
let calibrator = DisplayCalibrator::default_calibrator();
let ratio = calibrator.compute_contrast_ratio(120.0, 0.1);
assert!((ratio - 1200.0).abs() < 1e-10);
}
#[test]
fn test_compute_contrast_ratio_infinite() {
let calibrator = DisplayCalibrator::default_calibrator();
let ratio = calibrator.compute_contrast_ratio(120.0, 0.0);
assert!(ratio.is_infinite());
}
#[test]
fn test_measure_gamma_empty() {
let calibrator = DisplayCalibrator::default_calibrator();
let result = calibrator.measure_gamma(&[]);
assert!(result.is_err());
}
#[test]
fn test_calibration_json_roundtrip() {
let calibrator = DisplayCalibrator::default_calibrator();
let calibration = DisplayCalibration {
manufacturer: "Test".to_string(),
model: "Monitor".to_string(),
gamma_curve: GammaCurve::new(2.2),
primaries_matrix: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
white_point: Illuminant::D65.xyz(),
max_luminance: 120.0,
black_luminance: 0.1,
contrast_ratio: 1200.0,
uniformity: None,
};
let json = calibrator
.save_calibration(&calibration)
.expect("save should succeed");
let restored = calibrator
.load_calibration(&json)
.expect("load should succeed");
assert_eq!(restored.manufacturer, calibration.manufacturer);
assert_eq!(restored.model, calibration.model);
}
}