# -*- coding: utf-8 -*-
from raw._ebuttdt import *
from raw import _ebuttdt as ebuttdt_raw
from datetime import timedelta
from decimal import Decimal
import re, logging
import six
from pyxb.exceptions_ import SimpleTypeValueError, SimpleFacetValueError
from ebu_tt_live.errors import TimeFormatOverflowError, ExtentMissingError
from ebu_tt_live.strings import ERR_TIME_FORMAT_OVERFLOW, ERR_SEMANTIC_VALIDATION_TIMING_TYPE, ERR_1DIM_ONLY, \
ERR_2DIM_ONLY
from .pyxb_utils import get_xml_parsing_context
from .validation.base import SemanticValidationMixin
from .validation.presentation import SizingValidationMixin
log = logging.getLogger(__name__)
def _get_time_members(checked_time):
hours, seconds = divmod(checked_time.seconds, 3600)
hours += checked_time.days * 24
minutes, seconds = divmod(seconds, 60)
milliseconds, _ = divmod(checked_time.microseconds, 1000)
return hours, minutes, seconds, milliseconds
class _TimedeltaBindingMixin(object):
"""
Wiring in timedelta assignment and conversion operators
"""
# For each timing attribute a list of timeBases is specified, which represents the valid timeBase, timing attribute
# and timing type semantic constraint.
_compatible_timebases = {
'begin': [],
'dur': [],
'end': []
}
@classmethod
def compatible_timebases(cls):
return cls._compatible_timebases
@classmethod
def _ConvertArguments_vx(cls, args, kw):
"""
This hook is called before the type in question is instantiated. This is meant to do some normalization
of input parameters and convert them to tuple. In this function we check the timeBase and the attribute name
against our compatible_timebases mapping inside the timing type class. If an invalid scenario is encountered
SimpleTypeValueError is raised, which effectively prevents the timingType union to instantiate the type.
:raises pyxb.SimpleTypeValueError:
:param args:
:param kw:
:return: tuple of converted input parameters.
"""
result = []
# In parsing mode check timebase compatibility at instantiation time. This prevents pyxb instantiating
# the wrong type given 2 types having overlapping values in a union as it happens in full and limited
# clock timing types.
context = get_xml_parsing_context()
if context is not None:
# This means we are in XML parsing context. There should be a timeBase and a timing_attribute_name in the
# context object.
time_base = context['timeBase']
# It is possible for a timing type to exist as the value of an element not an attribute,
# in which case no timing_attribute_name is in the context; in that case don't attempt
# to validate the data against a timebase. At the moment this only affects the
# documentStartOfProgramme metadata element.
if 'timing_attribute_name' in context:
timing_att_name = context['timing_attribute_name']
if time_base not in cls._compatible_timebases[timing_att_name]:
log.debug(ERR_SEMANTIC_VALIDATION_TIMING_TYPE.format(
attr_name=timing_att_name,
attr_type=cls,
attr_value=args,
time_base=time_base
))
raise pyxb.SimpleTypeValueError(ERR_SEMANTIC_VALIDATION_TIMING_TYPE.format(
attr_name=timing_att_name,
attr_type=cls,
attr_value=args,
time_base=time_base
))
for item in args:
if isinstance(item, timedelta):
result.append(cls.from_timedelta(item))
else:
result.append(item)
return tuple(result)
@property
def timedelta(self):
return self.as_timedelta(self)
[docs]def cells_to_pixels(cells_in, root_extent, cell_resolution):
if not isinstance(root_extent, PixelExtentType):
raise Exception()
if cells_in.horizontal is not None:
# 2dimensional
return cells_in.horizontal * root_extent.horizontal / cell_resolution.horizontal, \
cells_in.vertical * root_extent.vertical / cell_resolution.vertical
else:
return cells_in.vertical * root_extent.vertical / cell_resolution.vertical,
[docs]def pixels_to_cells(pixels_in, root_extent, cell_resolution):
if not isinstance(root_extent, PixelExtentType):
raise Exception()
if pixels_in.horizontal is not None:
return pixels_in.horizontal * cell_resolution.horizontal / root_extent.horizontal, \
pixels_in.vertical * cell_resolution.vertical / root_extent.vertical
else:
return pixels_in.vertical * cell_resolution.vertical / root_extent.vertical,
[docs]def named_color_to_rgba(named_color):
color_map = {
"transparent": "00000000",
"black": "000000ff",
"silver": "c0c0c0ff",
"gray": "808080ff",
"white": "ffffffff",
"maroon": "800000ff",
"red": "ff0000ff",
"purple": "800080ff",
"fuchsia": "ff00ffff",
"magenta": "ff00ffff",
"green": "008000ff",
"lime": "00ff00ff",
"olive": "808000ff",
"yellow": "ffff00ff",
"navy": "000080ff",
"blue": "0000ffff",
"teal": "008080ff",
"aqua": "00ffffff",
"cyan": "00ffffff"
}
return '#{}'.format(color_map[named_color])
[docs]def convert_cell_region_to_percentage(cells_in, cell_resolution):
return '{}% {}%'.format(
(float(cells_in.horizontal) / float(cell_resolution.horizontal)) * 100,
(float(cells_in.vertical) / float(cell_resolution.vertical)) * 100
)
[docs]class TwoDimSizingMixin(object):
_groups_regex = None
_1dim_format = None
_2dim_format = None
[docs] @classmethod
def as_tuple(cls, instance):
if cls._2dim_format is None:
first, second = cls._groups_regex.match(instance).groups()[0], None
else:
first, second = cls._groups_regex.match(instance).groups()
if second is not None:
second = float(second)
return float(first), second
[docs] @classmethod
def from_tuple(cls, instance):
if len(instance) > 1:
if cls._2dim_format is None:
raise SimpleTypeValueError(cls, ERR_1DIM_ONLY.format(
type=cls
))
return cls._2dim_format.format(*instance)
else:
if cls._1dim_format is None:
raise SimpleTypeValueError(cls, ERR_2DIM_ONLY.format(
type=cls
))
return cls._1dim_format.format(*instance)
@property
def horizontal(self):
# TODO: Caching of tuple
tup_value = self.as_tuple(self)
if tup_value[1] is not None:
return tup_value[0]
else:
return None
@property
def vertical(self):
tup_value = self.as_tuple(self)
if tup_value[1] is not None:
return tup_value[1]
else:
return tup_value[0]
@classmethod
def _ConvertArguments_vx(cls, args, kw):
result = []
current_pair = []
for item in args:
if isinstance(item, int) or isinstance(item, float):
current_pair.append(item)
if len(current_pair) > 1:
result.append(cls.from_tuple(tuple(current_pair)))
current_pair = []
else:
result.append(item)
if len(current_pair) > 0:
result.append(cls.from_tuple(tuple(current_pair)))
return tuple(result)
def __eq__(self, other):
if type(self) == type(other) and self.horizontal == other.horizontal and self.vertical == other.vertical:
return True
elif isinstance(other, six.text_type):
return str(self) == str(other)
else:
return NotImplemented
[docs]class TimecountTimingType(_TimedeltaBindingMixin, ebuttdt_raw.timecountTimingType):
"""
Extending the string type with conversions to and from timedelta
"""
# NOTE: Update this regex should the spec change about this type
_groups_regex = re.compile('(?P<numerator>[0-9]+(?:\\.[0-9]+)?)(?P<unit>h|ms|s|m)')
# TODO: Consult and restrict this in an intuitive way to avoid awkward timing type combinations on the timing attributes.
_compatible_timebases = {
'begin': ['clock', 'media'],
'dur': ['clock', 'media'],
'end': ['clock', 'media']
}
[docs] @classmethod
def as_timedelta(cls, instance):
"""
Group expression with regex than switch on unit to create timedelta.
:param instance:
:return:
"""
numerator, unit = cls._groups_regex.match(instance).groups()
numerator = float(numerator)
if unit == 's':
return timedelta(seconds=numerator)
elif unit == 'm':
return timedelta(minutes=numerator)
elif unit == 'h':
return timedelta(hours=numerator)
elif unit == 'ms':
return timedelta(milliseconds=numerator)
else:
raise SimpleTypeValueError()
[docs] @classmethod
def from_timedelta(cls, instance):
"""
Convert to one dimensional value.
Find the smallest unit and create value using that.
Consistency is ensured to the millisecond. Below that the number will be trimmed.
:param instance:
:return:
"""
# Get the edge case out of the way even though validation will catch a 0 duration later
if not instance:
return '0s'
hours, minutes, seconds, milliseconds = _get_time_members(instance)
multiplier = 1
numerator = 0
unit = None
if milliseconds:
unit = 'ms'
numerator += milliseconds
multiplier *= 1000 # For the next level values so that the algo does not need to look back
if unit or seconds:
if not unit:
unit = 's'
numerator += seconds * multiplier
multiplier *= 60
if unit or minutes:
if not unit:
unit = 'm'
numerator += minutes * multiplier
multiplier *= 60
if unit or hours:
if not unit:
unit = 'h'
numerator += hours * multiplier
return '{}{}'.format(numerator, unit)
ebuttdt_raw.timecountTimingType._SetSupersedingClass(TimecountTimingType)
[docs]class FullClockTimingType(SemanticValidationMixin, _TimedeltaBindingMixin, ebuttdt_raw.fullClockTimingType):
"""
Extending the string type with conversions to and from timedelta
"""
_compatible_timebases = {
'begin': ['media'],
'dur': ['media'],
'end': ['media']
}
_groups_regex = re.compile('([0-9][0-9]+):([0-5][0-9]):([0-5][0-9]|60)(?:\.([0-9]+))?')
[docs] @classmethod
def compatible_timebases(cls):
return cls._compatible_timebases
[docs] @classmethod
def as_timedelta(cls, instance):
"""
Using regex parse value and create timedelta
:param instance:
:return:
"""
hours_str, minutes_str, seconds_str, seconds_fraction_str = [x for x in cls._groups_regex.match(instance).groups()]
milliseconds = seconds_fraction_str and float('0.' + seconds_fraction_str) * 1000 or 0
return timedelta(hours=int(hours_str),
minutes=int(minutes_str),
seconds=int(seconds_str),
milliseconds=milliseconds)
[docs] @classmethod
def from_timedelta(cls, instance):
"""
Generate full clock type from timedelta
:param instance:
:return:
"""
hours, minutes, seconds, milliseconds = _get_time_members(instance)
if milliseconds:
return '{hours:02d}:{minutes:02d}:{seconds:02d}.{milliseconds:03d}'.format(
hours=hours,
minutes=minutes,
seconds=seconds,
milliseconds=milliseconds
)
else:
return '{hours:02d}:{minutes:02d}:{seconds:02d}'.format(
hours=hours,
minutes=minutes,
seconds=seconds
)
ebuttdt_raw.fullClockTimingType._SetSupersedingClass(FullClockTimingType)
[docs]class LimitedClockTimingType(_TimedeltaBindingMixin, ebuttdt_raw.limitedClockTimingType):
"""
Extending the string type with conversions to and from timedelta
"""
_compatible_timebases = {
'begin': ['clock'],
'dur': ['clock'],
'end': ['clock']
}
_groups_regex = re.compile('([0-9][0-9]):([0-5][0-9]):([0-5][0-9]|60)(?:\.([0-9]+))?')
[docs] @classmethod
def as_timedelta(cls, instance):
"""
Using regex parse value and create timedelta
:param instance:
:return:
"""
hours_str, minutes_str, seconds_str, seconds_fraction_str = [x for x in cls._groups_regex.match(instance).groups()]
milliseconds = seconds_fraction_str and float('0.' + seconds_fraction_str) * 1000 or 0
return timedelta(hours=int(hours_str),
minutes=int(minutes_str),
seconds=int(seconds_str),
milliseconds=milliseconds)
[docs] @classmethod
def from_timedelta(cls, instance):
"""
Generate limited clock type from timedelta
:param instance:
:return:
"""
hours, minutes, seconds, milliseconds = _get_time_members(instance)
# We have our most significant value. Time for range check
if hours > 99:
raise TimeFormatOverflowError(ERR_TIME_FORMAT_OVERFLOW)
if milliseconds:
return '{hours:02d}:{minutes:02d}:{seconds:02d}.{milliseconds:03d}'.format(
hours=hours,
minutes=minutes,
seconds=seconds,
milliseconds=milliseconds
)
else:
return '{hours:02d}:{minutes:02d}:{seconds:02d}'.format(
hours=hours,
minutes=minutes,
seconds=seconds
)
ebuttdt_raw.limitedClockTimingType._SetSupersedingClass(LimitedClockTimingType)
# NOTE: Some of the code below includes handling of SMPTE time base, which was removed from version 1.0 of the specification.
# Here comes the tricky one. The SMPTE requires knowledge about frames. The top level tt element knows the frameRate.
# Unfortunately the conversion methods run before the object gets created let alone inserted into a document structure.
# The conversion paradigm of storing data in the xml datatype does not work here. A deferred method is needed that
# will compute the appropriate value when the element is inserted into the document structure.
[docs]class SMPTETimingType(_TimedeltaBindingMixin, ebuttdt_raw.smpteTimingType):
"""
Extending the string type with conversions to and from timedelta
"""
_compatible_timebases = {
'begin': ['smpte'],
'dur': ['smpte'],
'end': ['smpte']
}
[docs] @classmethod
def as_timedelta(cls, instance):
# TODO: implement SMPTE
return timedelta()
[docs] @classmethod
def from_timedelta(cls, instance):
# TODO: implement SMPTE
return SMPTETimingType('00:00:00:00')
# TODO: SMPTE frameRate and frameRateMultiplier value from tt element.
ebuttdt_raw.smpteTimingType._SetSupersedingClass(SMPTETimingType)
[docs]class PixelOriginType(TwoDimSizingMixin, SizingValidationMixin, ebuttdt_raw.pixelOriginType):
_groups_regex = re.compile('(?:[+-]?(?P<first>\d*\.?\d+)(?:px))\s(?:[+-]?(?P<second>\d*\.?\d+)(?:px))')
_2dim_format = '{}px {}px'
def _semantic_validate_sizing_context(self, dataset):
extent = dataset['tt_element'].extent
if extent is None:
raise ExtentMissingError(self)
ebuttdt_raw.pixelOriginType._SetSupersedingClass(PixelOriginType)
[docs]class CellOriginType(TwoDimSizingMixin, ebuttdt_raw.cellOriginType):
_groups_regex = re.compile(r'(?:[+-]?(?P<first>\d*\.?\d+)(?:c))\s(?:[+-]?(?P<second>\d*\.?\d+)(?:c))')
_2dim_format = '{}c {}c'
ebuttdt_raw.cellOriginType._SetSupersedingClass(CellOriginType)
[docs]class PercentageOriginType(TwoDimSizingMixin, ebuttdt_raw.percentageOriginType):
_groups_regex = re.compile('(?:[+-]?(?P<first>\d*\.?\d+)(?:%))\s(?:[+-]?(?P<second>\d*\.?\d+)(?:%))')
_2dim_format = '{}% {}%'
ebuttdt_raw.percentageOriginType._SetSupersedingClass(PercentageOriginType)
[docs]class PixelExtentType(TwoDimSizingMixin, SizingValidationMixin, ebuttdt_raw.pixelExtentType):
_groups_regex = re.compile('(?:[+]?(?P<first>\d*\.?\d+)(?:px))\s(?:[+]?(?P<second>\d*\.?\d+)(?:px))')
_2dim_format = '{}px {}px'
def _semantic_validate_sizing_context(self, dataset):
extent = dataset['tt_element'].extent
if extent is None:
raise ExtentMissingError(self)
ebuttdt_raw.pixelExtentType._SetSupersedingClass(PixelExtentType)
[docs]class CellExtentType(TwoDimSizingMixin, ebuttdt_raw.cellExtentType):
_groups_regex = re.compile('(?:[+]?(?P<first>\d*\.?\d+)(?:c))\s(?:[+]?(?P<second>\d*\.?\d+)(?:c))')
_2dim_format = '{}c {}c'
ebuttdt_raw.cellExtentType._SetSupersedingClass(CellExtentType)
[docs]class PercentageExtentType(TwoDimSizingMixin, ebuttdt_raw.percentageExtentType):
_groups_regex = re.compile('(?:[+]?(?P<first>\d*\.?\d+)(?:%))\s(?:[+]?(?P<second>\d*\.?\d+)(?:%))')
_2dim_format = '{}% {}%'
ebuttdt_raw.percentageExtentType._SetSupersedingClass(PercentageExtentType)
[docs]class PixelLengthType(SizingValidationMixin, ebuttdt_raw.pixelLengthType):
def _semantic_validate_sizing_context(self, dataset):
extent = dataset['tt_element'].extent
if extent is None:
raise ExtentMissingError(self)
ebuttdt_raw.pixelLengthType._SetSupersedingClass(PixelLengthType)
[docs]class PercentageLengthType(ebuttdt_raw.percentageLengthType):
pass
ebuttdt_raw.percentageLengthType._SetSupersedingClass(PercentageLengthType)
[docs]class CellLengthType(ebuttdt_raw.cellLengthType):
pass
ebuttdt_raw.cellLengthType._SetSupersedingClass(CellLengthType)
[docs]class PixelFontSizeType(TwoDimSizingMixin, SizingValidationMixin, ebuttdt_raw.pixelFontSizeType):
_groups_regex = re.compile('(?:[+]?(?P<first>\d*\.?\d+)(?:px))(?:\s(?:[+]?(?P<second>\d*\.?\d+)(?:px)))?')
_1dim_format = '{}px'
_2dim_format = '{}px {}px'
def _semantic_validate_sizing_context(self, dataset):
extent = dataset['tt_element'].extent
if extent is None:
raise ExtentMissingError(self)
ebuttdt_raw.pixelFontSizeType._SetSupersedingClass(PixelFontSizeType)
[docs]class CellFontSizeType(TwoDimSizingMixin, ebuttdt_raw.cellFontSizeType):
_groups_regex = re.compile('(?:[+]?(?P<first>\d*\.?\d+)(?:c))(?:\s(?:[+]?(?P<second>\d*\.?\d+)(?:c)))?')
_1dim_format = '{}c'
_2dim_format = '{}c {}c'
def _do_div(self, other):
"""
:param other: CellFontSizeType
:return:
"""
if isinstance(other, CellFontSizeType):
result_list = []
if self.horizontal is not None and other.horizontal is not None:
result_list.append((float(self.horizontal) / float(other.horizontal)) * 100)
elif self.horizontal is None and other.horizontal is not None:
result_list.append((float(self.vertical) / float(other.horizontal)) * 100)
elif self.horizontal is not None and other.horizontal is None:
result_list.append((float(self.horizontal) / float(other.vertical)) * 100)
result_list.append((float(self.vertical) / float(other.vertical)) * 100)
return PercentageFontSizeType(*result_list)
else:
return NotImplemented
def __div__(self, other):
return self._do_div(other)
def _do_eq(self, other):
if isinstance(other, CellFontSizeType):
if self.horizontal is None and other.horizontal is None:
return self.vertical == other.vertical
elif self.horizontal is None:
return self.vertical == other.vertical and \
self.vertical == other.horizontal
elif other.horizontal is None:
return self.vertical == other.vertical and \
self.horizontal == other.vertical
else:
return self.vertical == other.vertical and \
self.horizontal == other.horizontal
elif isinstance(other, six.text_type):
return str(self) == str(other)
else:
return NotImplemented
def __eq__(self, other):
return self._do_eq(other)
ebuttdt_raw.cellFontSizeType._SetSupersedingClass(CellFontSizeType)
[docs]class PercentageFontSizeType(TwoDimSizingMixin, ebuttdt_raw.percentageFontSizeType):
_groups_regex = re.compile('(?:[+]?(?P<first>\d*\.?\d+)(?:%))(?:\s(?:[+]?(?P<second>\d*\.?\d+)(?:%)))?')
_1dim_format = '{}%'
_2dim_format = '{}% {}%'
[docs] def do_mul(self, other):
if isinstance(other, CellFontSizeType):
result_type = CellFontSizeType
elif isinstance(other, PixelFontSizeType):
result_type = PixelFontSizeType
elif isinstance(other, PercentageFontSizeType):
result_type = PercentageFontSizeType
else:
return NotImplemented
if self.horizontal is not None:
if other.horizontal is not None:
return result_type(
other.horizontal * self.horizontal / 100,
other.vertical * self.vertical / 100
)
else:
# This uses TTML's assumption of 1c => 1c 1c
return result_type(
other.vertical * self.horizontal / 100,
other.vertical * self.vertical / 100
)
else:
if other.horizontal is not None:
return result_type(
other.horizontal * self.vertical / 100,
other.vertical * self.vertical / 100
)
else:
return result_type(
other.vertical * self.vertical / 100
)
def __mul__(self, other):
return self.do_mul(other)
def __rmul__(self, other):
return self.do_mul(other)
ebuttdt_raw.percentageFontSizeType._SetSupersedingClass(PercentageFontSizeType)
[docs]class CellResolutionType(TwoDimSizingMixin, ebuttdt_raw.cellResolutionType):
_groups_regex = re.compile('(?P<first>[0]*[1-9][0-9]*)\s(?P<second>[0]*[1-9][0-9]*)')
_2dim_format = '{} {}'
ebuttdt_raw.cellResolutionType._SetSupersedingClass(CellResolutionType)
[docs]class CellLineHeightType(TwoDimSizingMixin, ebuttdt_raw.cellLineHeightType):
_groups_regex = re.compile('(?P<first>\d*\.?\d+)c')
_1dim_format = '{}c'
ebuttdt_raw.cellLineHeightType._SetSupersedingClass(CellLineHeightType)
[docs]class PercentageLineHeightType(TwoDimSizingMixin, ebuttdt_raw.percentageLineHeightType):
_groups_regex = re.compile('(?P<first>\d*\.?\d+)%')
_1dim_format = '{}%'
[docs] def do_mul(self, other):
if isinstance(other, CellFontSizeType):
return CellLineHeightType(self.vertical * other.vertical / 100)
else:
return NotImplemented
def __mul__(self, other):
return self.do_mul(other)
def __rmul__(self, other):
return self.do_mul(other)
ebuttdt_raw.percentageLineHeightType._SetSupersedingClass(PercentageLineHeightType)
[docs]class PixelLineHeightType(TwoDimSizingMixin, ebuttdt_raw.pixelLineHeightType):
_groups_regex = re.compile('(?P<first>\d*\.?\d+)px')
_1dim_format = '{}px'
ebuttdt_raw.pixelLineHeightType._SetSupersedingClass(PixelLineHeightType)