Source code for src.xmlChecks.ttXmlCheck

# SPDX-FileCopyrightText: Copyright © 2026 BBC
#
# SPDX-License-Identifier: BSD-3-Clause

from src.validationLogging.validationCodes import ValidationCode
from src.validationLogging.validationResult import ValidationResult, \
    ERROR, WARN
from src.validationLogging.validationLogger import ValidationLogger
from xml.etree.ElementTree import Element
from src.xmlUtils import get_namespace, get_unqualified_name, make_qname
from .xmlCheck import XmlCheck
from .ttmlUtils import ns_ttml
import re


[docs] class ttTagAndNamespaceCheck(XmlCheck):
[docs] def run( self, input: Element, context: dict, validation_results: ValidationLogger) -> bool: ns = get_namespace(input.tag) unq_name = get_unqualified_name(input.tag) ttml_root_el_name = 'tt' valid = True if ns != ns_ttml: valid = False validation_results.error( location=input.tag, message='Root element has unexpected namespace "{}"' .format(ns), code=ValidationCode.xml_tt_namespace ) else: validation_results.good( location=input.tag, message='Root element has expected namespace "{}"'.format(ns), code=ValidationCode.xml_tt_namespace ) if unq_name != ttml_root_el_name: valid = False validation_results.error( location=input.tag, message='Root element has unexpected tag <{}>' .format(unq_name), code=ValidationCode.xml_root_element ) else: validation_results.good( location=input.tag, message='Root element has expected tag <{}>'.format(unq_name), code=ValidationCode.xml_root_element ) context['root_ns'] = ns return valid
[docs] class timeBaseCheck(XmlCheck): default_timeBase = 'media' def __init__(self, timeBase_acceptlist: list[str] = ['media'], timeBase_required: bool = False): super().__init__() self._timeBase_acceptlist = timeBase_acceptlist self._timeBase_required = timeBase_required
[docs] def run( self, input: Element, context: dict, validation_results: ValidationLogger) -> bool: ttp_ns = \ context.get('root_ns', ns_ttml) \ + '#parameter' timeBase_attr_key = make_qname(ttp_ns, 'timeBase') valid = True if self._timeBase_required and timeBase_attr_key not in input.attrib: valid = False validation_results.error( location='{} {} attribute'.format( input.tag, timeBase_attr_key), message='Required timeBase attribute absent', code=ValidationCode.ebuttd_parameter_timeBase ) timeBase_attr_val = \ input.get(timeBase_attr_key, self.default_timeBase) if timeBase_attr_val not in self._timeBase_acceptlist: valid = False validation_results.error( location='{} {} attribute'.format( input.tag, timeBase_attr_key), message='timeBase {} not in the allowed set {}'.format( timeBase_attr_val, self._timeBase_acceptlist), code=ValidationCode.ebuttd_parameter_timeBase ) if valid: validation_results.good( location='{} {} attribute'.format( input.tag, timeBase_attr_key), message='timeBase checked', code=ValidationCode.ebuttd_parameter_timeBase ) return valid
[docs] class activeAreaCheck(XmlCheck): activeArea_re = re.compile( r'^(?P<leftOffset>[\d]+(\.[\d]+)?)%[\s]+' r'(?P<topOffset>[\d]+(\.[\d]+)?)%[\s]+' r'(?P<width>[\d]+(\.[\d]+)?)%[\s]+' r'(?P<height>[\d]+(\.[\d]+)?)%$') def __init__(self, activeArea_required: bool = False): super().__init__() self._activeArea_required = activeArea_required
[docs] def run( self, input: Element, context: dict, validation_results: ValidationLogger) -> bool: ittp_ns = 'http://www.w3.org/ns/ttml/profile/imsc1#parameter' ittp_attr_key = make_qname(ittp_ns, 'activeArea') valid = True warn = False if ittp_attr_key not in input.attrib: valid = warn = not self._activeArea_required validation_results.append( ValidationResult( status=ERROR if self._activeArea_required else WARN, location='{} {} attribute'.format( input.tag, ittp_attr_key), message='{}activeArea attribute absent'.format( 'Required ' if self._activeArea_required else '' ), code=ValidationCode.imsc_parameter_activeArea ) ) else: ittp_attr_val = input.get(ittp_attr_key) matches = self.activeArea_re.match(ittp_attr_val) if matches: # check validity for g in ['leftOffset', 'topOffset', 'width', 'height']: if float(matches.group(g)) > 100: valid = False if not valid: validation_results.error( location='{} {} attribute'.format( input.tag, ittp_attr_key), message='activeArea {} has ' 'at least one component >100%'.format( ittp_attr_val), code=ValidationCode.imsc_parameter_activeArea ) else: valid = False validation_results.error( location='{} {} attribute'.format( input.tag, ittp_attr_key), message='activeArea {} does not ' 'match syntax requirements'.format( ittp_attr_val), code=ValidationCode.imsc_parameter_activeArea ) if valid and not warn: validation_results.good( location='{} {} attribute'.format( input.tag, ittp_attr_key), message='activeArea checked', code=ValidationCode.imsc_parameter_activeArea ) return valid
[docs] class cellResolutionCheck(XmlCheck): cellResolution_re = re.compile( r'^(?P<horizontal>[\d]+)[\s]+' r'(?P<vertical>[\d]+)$') default_cellResolution = '32 15' # defined by TTML spec def __init__(self, cellResolution_required: bool = False): super().__init__() self._cellResolution_required = cellResolution_required
[docs] def run( self, input: Element, context: dict, validation_results: ValidationLogger) -> bool: ttp_ns = \ context.get('root_ns', ns_ttml) \ + '#parameter' cellResolution_attr_key = make_qname(ttp_ns, 'cellResolution') valid = True if cellResolution_attr_key not in input.attrib: valid = not self._cellResolution_required validation_results.append( ValidationResult( status=ERROR if self._cellResolution_required else WARN, location='{} {} attribute'.format( input.tag, cellResolution_attr_key), message='{}cellResolution attribute absent'.format( 'Required ' if self._cellResolution_required else ''), code=ValidationCode.ttml_parameter_cellResolution ) ) cellResolution_attr_val = self.default_cellResolution validation_results.info( location='{} {} attribute'.format( input.tag, cellResolution_attr_key), message='using default cellResolution value {}'.format( cellResolution_attr_val), code=ValidationCode.ttml_parameter_cellResolution ) else: cellResolution_attr_val = input.get(cellResolution_attr_key) matches = self.cellResolution_re.match(cellResolution_attr_val) if matches: # check validity for g in ['horizontal', 'vertical']: if int(matches.group(g)) == 0: valid = False if not valid: validation_results.error( location='{} {} attribute'.format( input.tag, cellResolution_attr_key), message='cellResolution {} has ' 'at least one component == 0'.format( cellResolution_attr_val), code=ValidationCode.ttml_parameter_cellResolution ) else: valid = False validation_results.error( location='{} {} attribute'.format( input.tag, cellResolution_attr_key), message='cellResolution {} does not ' 'match syntax requirements'.format( cellResolution_attr_val), code=ValidationCode.ttml_parameter_cellResolution ) if valid: validation_results.good( location='{} {} attribute'.format( input.tag, cellResolution_attr_key), message='cellResolution checked', code=ValidationCode.ttml_parameter_cellResolution ) context['cellResolution'] = cellResolution_attr_val else: context['cellResolution'] = self.default_cellResolution return valid
[docs] class contentProfilesCheck(XmlCheck): def __init__(self, contentProfiles_atleastonelist: list[str] = [], contentProfiles_denylist: list[str] = [], contentProfiles_required: bool = False): super().__init__() self._contentProfiles_atleastonelist = contentProfiles_atleastonelist self._contentProfiles_denylist = contentProfiles_denylist self._contentProfiles_required = contentProfiles_required
[docs] def run( self, input: Element, context: dict, validation_results: ValidationLogger) -> bool: ttp_ns = \ context.get('root_ns', ns_ttml) \ + '#parameter' contentProfiles_attr_key = make_qname(ttp_ns, 'contentProfiles') valid = True if self._contentProfiles_required \ and contentProfiles_attr_key not in input.attrib: valid = False validation_results.error( location='{} {} attribute'.format( input.tag, contentProfiles_attr_key), message='Required contentProfiles attribute absent', code=ValidationCode.ttml_parameter_contentProfiles ) contentProfiles_attr_val = \ input.get(contentProfiles_attr_key, '') contentProfiles = contentProfiles_attr_val.split() at_least_one_found = False for contentProfile in contentProfiles: if contentProfile in self._contentProfiles_denylist: valid = False validation_results.error( location='{} {} attribute'.format( input.tag, contentProfiles_attr_key), message='contentProfile {} present but ' 'in the prohibited set {}' .format( contentProfile, self._contentProfiles_denylist), code=ValidationCode.ttml_parameter_contentProfiles ) if contentProfile in self._contentProfiles_atleastonelist: at_least_one_found = True if at_least_one_found is False \ and len(self._contentProfiles_atleastonelist) > 0: valid = False validation_results.error( location='{} {} attribute'.format( input.tag, contentProfiles_attr_key), message='At least one of the contentProfiles {} ' 'must be present, all are missing from the ' 'contentProfile attribute {}'.format( self._contentProfiles_atleastonelist, contentProfiles_attr_val), code=ValidationCode.ttml_parameter_contentProfiles ) if valid: validation_results.good( location='{} {} attribute'.format( input.tag, contentProfiles_attr_key), message='contentProfiles checked', code=ValidationCode.ttml_parameter_contentProfiles ) return valid