@@ -425,10 +425,22 @@ def set(ttml_element, frame_rate: Fraction):
425425 f"{ fps_multiplier .numerator :g} { fps_multiplier .denominator :g} "
426426 )
427427
428+ class TimeBase (Enum ):
429+ clock = "clock"
430+ media = "media"
431+ smpte = "smpte"
432+
433+ class DropMode (Enum ):
434+ nonDrop = "nonDrop"
435+ dropNTSC = "dropNTSC"
436+ dropPAL = "dropPAL"
437+
428438@dataclass
429439class TemporalAttributeParsingContext :
430440 frame_rate : Fraction = Fraction (30 , 1 )
431441 tick_rate : int = 1
442+ time_base : TimeBase = TimeBase .media
443+ drop_mode : DropMode = DropMode .nonDrop
432444
433445class TimeExpressionSyntaxEnum (Enum ):
434446 """IMSC time expression configuration values"""
@@ -450,6 +462,81 @@ def to_time_format(context: TemporalAttributeWritingContext, time: Fraction) ->
450462
451463 return f"{ SmpteTimeCode .from_seconds (time , context .frame_rate )} "
452464
465+ _CLOCK_TIME_FRACTION_RE = re .compile (r"^(\d{2,}):(\d\d):(\d\d(?:\.\d+)?)$" )
466+ _CLOCK_TIME_FRAMES_RE = re .compile (r"^(\d{2,}):(\d\d):(\d\d):(\d{2,})$" )
467+ _OFFSET_FRAME_RE = re .compile (r"^(\d+(?:\.\d+)?)f" )
468+ _OFFSET_TICK_RE = re .compile (r"^(\d+(?:\.\d+)?)t$" )
469+ _OFFSET_MS_RE = re .compile (r"^(\d+(?:\.\d+)?)ms$" )
470+ _OFFSET_S_RE = re .compile (r"^(\d+(?:\.\d+)?)s$" )
471+ _OFFSET_H_RE = re .compile (r"^(\d+(?:\.\d+)?)h$" )
472+ _OFFSET_M_RE = re .compile (r"^(\d+(?:\.\d+)?)m$" )
473+
474+
475+ def parse_time_expression (ctx : TemporalAttributeParsingContext , time_expr : str , strict : bool = True ) -> Fraction :
476+ '''Parse a TTML time expression in a fractional number in seconds
477+ '''
478+
479+ m = _OFFSET_FRAME_RE .match (time_expr )
480+
481+ if m and ctx .frame_rate is not None :
482+ return Fraction (m .group (1 )) / ctx .frame_rate
483+
484+ m = _OFFSET_TICK_RE .match (time_expr )
485+
486+ if m and ctx .tick_rate is not None :
487+ return Fraction (m .group (1 )) / ctx .tick_rate
488+
489+ m = _OFFSET_MS_RE .match (time_expr )
490+
491+ if m :
492+ return Fraction (m .group (1 )) / 1000
493+
494+ m = _OFFSET_S_RE .match (time_expr )
495+
496+ if m :
497+ return Fraction (m .group (1 ))
498+
499+ m = _OFFSET_M_RE .match (time_expr )
500+
501+ if m :
502+ return Fraction (m .group (1 )) * 60
503+
504+ m = _OFFSET_H_RE .match (time_expr )
505+
506+ if m :
507+ return Fraction (m .group (1 )) * 3600
508+
509+ m = _CLOCK_TIME_FRACTION_RE .match (time_expr )
510+
511+ if m :
512+ return Fraction (m .group (1 )) * 3600 + \
513+ Fraction (m .group (2 )) * 60 + \
514+ Fraction (m .group (3 ))
515+
516+ m = _CLOCK_TIME_FRAMES_RE .match (time_expr )
517+
518+ if m and ctx .frame_rate is not None :
519+ frames = int (m .group (4 )) if m .group (4 ) else 0
520+
521+ if frames >= ctx .frame_rate :
522+ if strict :
523+ raise ValueError ("Frame count exceeds frame rate" )
524+ else :
525+ LOGGER .error ("Frame count %s exceeds frame rate %s, rounding to frame rate minus 1" , frames , ctx .frame_rate )
526+ frames = round (ctx .frame_rate ) - 1
527+
528+ hh = int (m .group (1 ))
529+ mm = int (m .group (2 ))
530+ ss = int (m .group (3 ))
531+
532+ if ctx .time_base is TimeBase .smpte :
533+ tc = SmpteTimeCode (hh , mm , ss , frames , ctx .frame_rate , ctx .drop_mode != DropMode .nonDrop )
534+ return tc .to_temporal_offset ()
535+ else :
536+ return Fraction (hh * 3600 + mm * 60 + ss ) + Fraction (frames ) / ctx .frame_rate
537+
538+ raise ValueError ("Syntax error" )
539+
453540class BeginAttribute :
454541 '''begin attribute
455542 '''
@@ -463,7 +550,7 @@ def extract(context: TemporalAttributeParsingContext, xml_element) -> typing.Opt
463550 begin_raw = xml_element .attrib .get (BeginAttribute .qn )
464551
465552 try :
466- return utils . parse_time_expression (context . tick_rate , context . frame_rate , begin_raw , False ) if begin_raw is not None else None
553+ return parse_time_expression (context , begin_raw , False ) if begin_raw is not None else None
467554 except ValueError :
468555 LOGGER .error ("Bad begin attribute value: %s" , begin_raw )
469556 return None
@@ -486,7 +573,7 @@ def extract(context: TemporalAttributeParsingContext, xml_element) -> typing.Opt
486573 end_raw = xml_element .attrib .get (EndAttribute .qn )
487574
488575 try :
489- return utils . parse_time_expression (context . tick_rate , context . frame_rate , end_raw , False ) if end_raw is not None else None
576+ return parse_time_expression (context , end_raw , False ) if end_raw is not None else None
490577 except ValueError :
491578 LOGGER .error ("Bad end attribute value: %s" , end_raw )
492579 return None
@@ -507,7 +594,7 @@ def extract(context: TemporalAttributeParsingContext, xml_element) -> typing.Opt
507594 dur_raw = xml_element .attrib .get (DurAttribute .qn )
508595
509596 try :
510- return utils . parse_time_expression (context . tick_rate , context . frame_rate , dur_raw , False ) if dur_raw is not None else None
597+ return parse_time_expression (context , dur_raw , False ) if dur_raw is not None else None
511598 except ValueError :
512599 LOGGER .error ("Bad dur attribute value: %s" , dur_raw )
513600 return None
@@ -559,3 +646,57 @@ def extract(xml_element) -> typing.List[str]:
559646 raw_value = xml_element .attrib .get (StyleAttribute .qn )
560647
561648 return raw_value .split (" " ) if raw_value is not None else []
649+
650+ class TimeBaseAttribute :
651+ '''ttp:timeBase attribute
652+ '''
653+
654+ qn = f"{{{ ns .TTP } }}timeBase"
655+
656+ @staticmethod
657+ def extract (ttml_element ) -> TimeBase :
658+
659+ cr = ttml_element .attrib .get (TimeBaseAttribute .qn )
660+
661+ if cr is None :
662+ return TimeBase .media
663+
664+ try :
665+ tb = TimeBase [cr ]
666+ except KeyError :
667+ LOGGER .error (f"Bad ttp:timeBase value '{ cr } ', using 'media' instead" )
668+ return TimeBase .media
669+
670+ if tb is TimeBase .clock :
671+ raise ValueError ("Clock timebase not supported" )
672+
673+ return tb
674+
675+ # We do not support writing ttp:timeBase since all model values are always in media timebase
676+
677+ class DropModeAttribute :
678+ '''ttp:dropMode attribute
679+ '''
680+
681+ qn = f"{{{ ns .TTP } }}dropMode"
682+
683+ @staticmethod
684+ def extract (ttml_element ) -> DropMode :
685+
686+ cr = ttml_element .attrib .get (DropModeAttribute .qn )
687+
688+ if cr is None :
689+ return DropMode .nonDrop
690+
691+ try :
692+ dm = DropMode [cr ]
693+ except KeyError :
694+ LOGGER .error (f"Bad ttp:dropMode value '{ cr } ', using 'nonDrop' instead" )
695+ return DropMode .nonDrop
696+
697+ if dm is DropMode .dropPAL :
698+ raise ValueError ("PAL drop frame timecode not supported" )
699+
700+ return dm
701+
702+ # We do not support writing ttp:dropMode since all model values are always in media timebase
0 commit comments