extern crate alloc;
use alloc::format;
use alloc::string::String;
use alloc::string::ToString;
use alloc::vec::Vec;
use crate::document::{self, *};
use crate::error::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ImscVersion {
V1_0,
V1_1,
}
impl ImscVersion {
pub fn name(&self) -> &'static str {
match self {
ImscVersion::V1_0 => "1.0",
ImscVersion::V1_1 => "1.1",
}
}
}
broadcast_common::impl_spec_display!(ImscVersion);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Profile {
Text,
Image,
}
impl Profile {
pub fn name(&self) -> &'static str {
match self {
Profile::Text => "text",
Profile::Image => "image",
}
}
}
broadcast_common::impl_spec_display!(Profile);
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ValidationError {
pub constraint: String,
pub detail: String,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<ValidationError>,
}
#[derive(Debug, Clone)]
pub struct Validator {
profile: Profile,
version: ImscVersion,
errors: Vec<ValidationError>,
}
impl Validator {
pub fn new(profile: Profile, version: ImscVersion) -> Self {
Self {
profile,
version,
errors: Vec::new(),
}
}
pub fn validate(mut self, doc: &Document) -> ValidationResult {
self.validate_document(doc);
ValidationResult {
valid: self.errors.is_empty(),
errors: self.errors,
}
}
pub fn validate_to_result(self, doc: &Document) -> Result<(), Error> {
let result = self.validate(doc);
if result.valid {
Ok(())
} else {
let messages: Vec<String> = result
.errors
.iter()
.map(|e| format!("{}: {}", e.constraint, e.detail))
.collect();
Err(Error::Validation(messages.join("; ")))
}
}
fn err(&mut self, constraint: &str, detail: String) {
self.errors.push(ValidationError {
constraint: constraint.to_string(),
detail,
});
}
fn validate_document(&mut self, doc: &Document) {
self.validate_tt(&doc.tt);
if let Some(ref head) = doc.tt.head {
self.validate_head(head);
}
if let Some(ref body) = doc.tt.body {
self.validate_body(body);
}
}
fn validate_tt(&mut self, tt: &TtElement) {
let claimed_text = self.claims_text_profile(tt);
let claimed_image = self.claims_image_profile(tt);
if self.profile == Profile::Text && !claimed_text {
self.err(
"IMSC §7.9",
"Document does not claim Text Profile via ttp:contentProfiles or ttp:profile"
.into(),
);
}
if self.profile == Profile::Image && !claimed_image {
self.err(
"IMSC §7.9",
"Document does not claim Image Profile via ttp:contentProfiles or ttp:profile"
.into(),
);
}
if tt.ittp_aspect_ratio.is_some() && tt.ttp_display_aspect_ratio.is_some() {
self.err(
"IMSC §7.12.4/§7.12.5",
"ittp:aspectRatio and ttp:displayAspectRatio are mutually exclusive".into(),
);
}
if self.version == ImscVersion::V1_1 {
if self.has_frame_usage(tt) && tt.ttp_frame_rate.is_none() {
self.err(
"IMSC §7.12.7",
"ttp:frameRate must be present when frame terms are used".into(),
);
}
}
if self.profile == Profile::Image {
}
}
fn claims_text_profile(&self, tt: &TtElement) -> bool {
let text_designators = [document::IMSC11_TEXT_PROFILE, document::IMSC1_TEXT_PROFILE];
if let Some(ref cp) = tt.ttp_content_profiles {
for d in &text_designators {
if cp.contains(d) {
return true;
}
}
}
if let Some(ref p) = tt.ttp_profile {
for d in &text_designators {
if p == *d {
return true;
}
}
}
false
}
fn claims_image_profile(&self, tt: &TtElement) -> bool {
let image_designators = [
document::IMSC11_IMAGE_PROFILE,
document::IMSC1_IMAGE_PROFILE,
];
if let Some(ref cp) = tt.ttp_content_profiles {
for d in &image_designators {
if cp.contains(d) {
return true;
}
}
}
if let Some(ref p) = tt.ttp_profile {
for d in &image_designators {
if p == *d {
return true;
}
}
}
false
}
fn has_frame_usage(&self, tt: &TtElement) -> bool {
let body = match tt.body {
Some(ref b) => b,
None => return false,
};
Self::body_has_frame_usage(body)
}
fn body_has_frame_usage(body: &BodyElement) -> bool {
for div in &body.divs {
if Self::time_expr_has_frame(body.begin.as_deref())
|| Self::time_expr_has_frame(body.dur.as_deref())
|| Self::time_expr_has_frame(body.end.as_deref())
{
return true;
}
for p in &div.paragraphs {
if Self::time_expr_has_frame(p.begin.as_deref())
|| Self::time_expr_has_frame(p.dur.as_deref())
|| Self::time_expr_has_frame(p.end.as_deref())
{
return true;
}
}
for img in &div.images {
if Self::time_expr_has_frame(img.begin.as_deref())
|| Self::time_expr_has_frame(img.dur.as_deref())
|| Self::time_expr_has_frame(img.end.as_deref())
{
return true;
}
}
}
false
}
fn time_expr_has_frame(expr: Option<&str>) -> bool {
let expr = match expr {
Some(e) => e,
None => return false,
};
if expr.ends_with('f') && expr.len() > 1 {
return true;
}
let colon_count = expr.chars().filter(|&c| c == ':').count();
colon_count == 3
}
fn validate_head(&mut self, head: &HeadElement) {
if let Some(ref layout) = head.layout {
self.validate_layout(layout);
}
}
fn validate_layout(&mut self, layout: &LayoutElement) {
if layout.regions.len() > 4 {
self.err(
"IMSC §7.12.1.3",
format!(
"Document has {} regions; maximum 4 presented regions allowed in any ISD",
layout.regions.len()
),
);
}
for region in &layout.regions {
self.validate_region(region);
}
}
fn validate_region(&mut self, _region: &RegionElement) {
}
fn validate_body(&mut self, body: &BodyElement) {
if self.profile == Profile::Image {
for div in &body.divs {
self.validate_div_image_constraints(div);
}
}
if self.profile == Profile::Text {
for div in &body.divs {
self.validate_div_text_constraints(div);
}
}
}
fn validate_div_image_constraints(&mut self, div: &DivElement) {
if !div.paragraphs.is_empty() {
self.err(
"IMSC §9.4.1",
format!(
"Image Profile div contains {} <p> element(s) — p/span/br SHALL NOT be present in Image Profile",
div.paragraphs.len()
),
);
}
}
fn validate_div_text_constraints(&mut self, div: &DivElement) {
for p in &div.paragraphs {
if let Some(ref ts) = p.style_attributes.tts_text_shadow {
let count: usize = ts.split(',').count();
if count > 4 {
self.err(
"IMSC §8.4.11",
format!("tts:textShadow has {} shadow values (max 4)", count),
);
}
}
}
}
}