#![allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum IssueSeverity {
Warning,
Error,
Critical,
}
impl IssueSeverity {
#[must_use]
pub fn priority(&self) -> u32 {
match self {
Self::Warning => 1,
Self::Error => 2,
Self::Critical => 3,
}
}
}
#[derive(Debug, Clone)]
pub struct ConformIssue {
pub field: String,
pub expected: String,
pub found: String,
pub severity: IssueSeverity,
}
impl ConformIssue {
#[must_use]
pub fn new(
field: impl Into<String>,
expected: impl Into<String>,
found: impl Into<String>,
severity: IssueSeverity,
) -> Self {
Self {
field: field.into(),
expected: expected.into(),
found: found.into(),
severity,
}
}
#[must_use]
pub fn is_blocking(&self) -> bool {
matches!(
self.severity,
IssueSeverity::Error | IssueSeverity::Critical
)
}
}
#[derive(Debug, Clone)]
pub struct FormatSpec {
pub width: u32,
pub height: u32,
pub frame_rate: f64,
pub color_space: String,
pub bit_depth: u8,
pub codec: String,
}
impl FormatSpec {
#[must_use]
pub fn is_hd(&self) -> bool {
(self.width == 1280 && self.height == 720) || (self.width == 1920 && self.height == 1080)
}
#[must_use]
pub fn is_4k(&self) -> bool {
self.width == 3840 && self.height == 2160
}
#[must_use]
pub fn aspect_ratio(&self) -> f64 {
if self.height == 0 {
return 0.0;
}
f64::from(self.width) / f64::from(self.height)
}
}
#[derive(Debug, Clone)]
pub struct FormatTolerance {
pub frame_rate_tolerance: f64,
pub allow_upscale: bool,
}
impl FormatTolerance {
#[must_use]
pub fn strict() -> Self {
Self {
frame_rate_tolerance: 0.001,
allow_upscale: false,
}
}
#[must_use]
pub fn loose() -> Self {
Self {
frame_rate_tolerance: 0.5,
allow_upscale: true,
}
}
}
#[derive(Debug, Default)]
pub struct FormatConformReport {
pub issues: Vec<ConformIssue>,
}
impl FormatConformReport {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn is_conformant(&self) -> bool {
!self.issues.iter().any(ConformIssue::is_blocking)
}
#[must_use]
pub fn critical_issues(&self) -> Vec<&ConformIssue> {
self.issues
.iter()
.filter(|i| i.severity == IssueSeverity::Critical)
.collect()
}
#[must_use]
pub fn warnings(&self) -> Vec<&ConformIssue> {
self.issues
.iter()
.filter(|i| i.severity == IssueSeverity::Warning)
.collect()
}
}
#[must_use]
pub fn check_format(
target: &FormatSpec,
actual: &FormatSpec,
tolerance: &FormatTolerance,
) -> FormatConformReport {
let mut report = FormatConformReport::new();
if actual.width != target.width {
let severity = if !tolerance.allow_upscale || actual.width < target.width {
IssueSeverity::Error
} else {
IssueSeverity::Warning
};
report.issues.push(ConformIssue::new(
"width",
target.width.to_string(),
actual.width.to_string(),
severity,
));
}
if actual.height != target.height {
let severity = if !tolerance.allow_upscale || actual.height < target.height {
IssueSeverity::Error
} else {
IssueSeverity::Warning
};
report.issues.push(ConformIssue::new(
"height",
target.height.to_string(),
actual.height.to_string(),
severity,
));
}
if (actual.frame_rate - target.frame_rate).abs() > tolerance.frame_rate_tolerance {
report.issues.push(ConformIssue::new(
"frame_rate",
format!("{:.3}", target.frame_rate),
format!("{:.3}", actual.frame_rate),
IssueSeverity::Error,
));
}
if actual.color_space != target.color_space {
report.issues.push(ConformIssue::new(
"color_space",
target.color_space.clone(),
actual.color_space.clone(),
IssueSeverity::Warning,
));
}
if actual.bit_depth != target.bit_depth {
let severity = if actual.bit_depth < target.bit_depth {
IssueSeverity::Error
} else {
IssueSeverity::Warning
};
report.issues.push(ConformIssue::new(
"bit_depth",
target.bit_depth.to_string(),
actual.bit_depth.to_string(),
severity,
));
}
if actual.codec != target.codec {
report.issues.push(ConformIssue::new(
"codec",
target.codec.clone(),
actual.codec.clone(),
IssueSeverity::Critical,
));
}
report
}
#[cfg(test)]
mod tests {
use super::*;
fn hd_spec() -> FormatSpec {
FormatSpec {
width: 1920,
height: 1080,
frame_rate: 25.0,
color_space: "BT.709".to_string(),
bit_depth: 8,
codec: "H.264".to_string(),
}
}
#[test]
fn test_is_hd_1080() {
let spec = hd_spec();
assert!(spec.is_hd());
}
#[test]
fn test_is_hd_720() {
let spec = FormatSpec {
width: 1280,
height: 720,
frame_rate: 25.0,
color_space: "BT.709".to_string(),
bit_depth: 8,
codec: "H.264".to_string(),
};
assert!(spec.is_hd());
}
#[test]
fn test_is_not_hd() {
let spec = FormatSpec {
width: 720,
height: 576,
frame_rate: 25.0,
color_space: "BT.601".to_string(),
bit_depth: 8,
codec: "MPEG-2".to_string(),
};
assert!(!spec.is_hd());
}
#[test]
fn test_is_4k() {
let spec = FormatSpec {
width: 3840,
height: 2160,
frame_rate: 24.0,
color_space: "BT.2020".to_string(),
bit_depth: 10,
codec: "H.265".to_string(),
};
assert!(spec.is_4k());
}
#[test]
fn test_aspect_ratio() {
let spec = hd_spec();
let ratio = spec.aspect_ratio();
assert!((ratio - 16.0 / 9.0).abs() < 0.001, "ratio: {ratio}");
}
#[test]
fn test_aspect_ratio_zero_height() {
let spec = FormatSpec {
width: 1920,
height: 0,
frame_rate: 25.0,
color_space: "BT.709".to_string(),
bit_depth: 8,
codec: "H.264".to_string(),
};
assert_eq!(spec.aspect_ratio(), 0.0);
}
#[test]
fn test_issue_severity_priority_order() {
assert!(IssueSeverity::Critical.priority() > IssueSeverity::Error.priority());
assert!(IssueSeverity::Error.priority() > IssueSeverity::Warning.priority());
}
#[test]
fn test_conform_issue_blocking() {
let err = ConformIssue::new("codec", "H.264", "H.265", IssueSeverity::Critical);
assert!(err.is_blocking());
let warn = ConformIssue::new("color_space", "BT.709", "BT.601", IssueSeverity::Warning);
assert!(!warn.is_blocking());
}
#[test]
fn test_format_tolerance_strict() {
let t = FormatTolerance::strict();
assert!(!t.allow_upscale);
assert!(t.frame_rate_tolerance < 0.01);
}
#[test]
fn test_format_tolerance_loose() {
let t = FormatTolerance::loose();
assert!(t.allow_upscale);
assert!(t.frame_rate_tolerance >= 0.5);
}
#[test]
fn test_conformant_when_identical() {
let spec = hd_spec();
let report = check_format(&spec, &spec, &FormatTolerance::strict());
assert!(report.is_conformant());
assert!(report.issues.is_empty());
}
#[test]
fn test_codec_mismatch_is_critical() {
let target = hd_spec();
let actual = FormatSpec {
codec: "H.265".to_string(),
..hd_spec()
};
let report = check_format(&target, &actual, &FormatTolerance::strict());
assert!(!report.is_conformant());
assert!(!report.critical_issues().is_empty());
}
#[test]
fn test_frame_rate_mismatch_strict() {
let target = hd_spec();
let actual = FormatSpec {
frame_rate: 29.97,
..hd_spec()
};
let report = check_format(&target, &actual, &FormatTolerance::strict());
let fps_issue = report.issues.iter().find(|i| i.field == "frame_rate");
assert!(fps_issue.is_some());
}
#[test]
fn test_frame_rate_within_loose_tolerance() {
let target = hd_spec();
let actual = FormatSpec {
frame_rate: 25.1,
..hd_spec()
};
let report = check_format(&target, &actual, &FormatTolerance::loose());
let fps_issue = report.issues.iter().find(|i| i.field == "frame_rate");
assert!(fps_issue.is_none());
}
#[test]
fn test_warnings_vs_critical_split() {
let target = hd_spec();
let actual = FormatSpec {
color_space: "BT.601".to_string(),
codec: "H.265".to_string(),
..hd_spec()
};
let report = check_format(&target, &actual, &FormatTolerance::strict());
assert!(!report.warnings().is_empty());
assert!(!report.critical_issues().is_empty());
}
}