def test_compute_extent_em(self): doc = model.ContentDocument() r1 = model.Region("r1", doc) r1.set_style(styles.StyleProperties.ShowBackground, styles.ShowBackgroundType.always) r1.set_style( styles.StyleProperties.Extent, styles.ExtentType( width=styles.LengthType(20, styles.LengthType.Units.em), height=styles.LengthType(3, styles.LengthType.Units.em) ) ) doc.put_region(r1) isd = ISD.from_model(doc, 0) region = list(isd.iter_regions())[0] extent: styles.ExtentType = region.get_style(styles.StyleProperties.Extent) self.assertAlmostEqual(extent.width.value, 100*20/doc.get_cell_resolution().rows) self.assertEqual(extent.width.units, styles.LengthType.Units.rh) self.assertAlmostEqual(extent.height.value, 100*3/doc.get_cell_resolution().rows) self.assertEqual(extent.height.units, styles.LengthType.Units.rh)
def extract(cls, context: StyleParsingContext, xml_attrib: str): (h_edge, h_offset, v_edge, v_offset) = utils.parse_position(xml_attrib) if h_edge == "right": if h_offset.units is styles.LengthType.Units.px: h_offset = styles.LengthType( context.doc.get_px_resolution().width - h_offset.value, h_offset.units) elif h_offset.units is styles.LengthType.Units.pct or h_offset.units is styles.LengthType.Units.rw: h_offset = styles.LengthType(100 - h_offset.value, h_offset.units) else: raise ValueError( "Units other than px, pct, rh, rw used in tts:position" ) if v_edge == "bottom": if v_offset.units is styles.LengthType.Units.px: v_offset = styles.LengthType( context.doc.get_px_resolution().height - v_offset.value, v_offset.units) elif v_offset.units is styles.LengthType.Units.pct or v_offset.units is styles.LengthType.Units.rh: v_offset = styles.LengthType(100 - v_offset.value, v_offset.units) else: raise ValueError( "Units other than px, pct, rh, rw used in tts:position" ) return styles.PositionType(x=h_offset, y=v_offset)
def test_compute_extent_pct(self): doc = model.ContentDocument() r1 = model.Region("r1", doc) r1.set_style(styles.StyleProperties.ShowBackground, styles.ShowBackgroundType.always) r1.set_style( styles.StyleProperties.Extent, styles.ExtentType( width=styles.LengthType(50, styles.LengthType.Units.pct), height=styles.LengthType(25, styles.LengthType.Units.pct) ) ) doc.put_region(r1) isd = ISD.from_model(doc, 0) region = list(isd.iter_regions())[0] extent: styles.ExtentType = region.get_style(styles.StyleProperties.Extent) self.assertEqual(extent.height.value, 25) self.assertEqual(extent.height.units, styles.LengthType.Units.rh) self.assertEqual(extent.width.value, 50) self.assertEqual(extent.width.units, styles.LengthType.Units.rw)
def position(h_edge, h_offval, h_offunit, v_edge, v_offval, v_offunit): return (h_edge, styles.LengthType(value=h_offval, units=styles.LengthType.Units(h_offunit)), v_edge, styles.LengthType(value=v_offval, units=styles.LengthType.Units(v_offunit)))
def test_compute_style_property(self): doc = model.ContentDocument() r1 = model.Region("r1", doc) r1.set_style(styles.StyleProperties.FontSize, styles.LengthType(value=50, units=styles.LengthType.Units.pct)) doc.put_region(r1) b = model.Body(doc) b.set_style(styles.StyleProperties.FontSize, styles.LengthType(value=50, units=styles.LengthType.Units.pct)) b.set_region(r1) doc.set_body(b) div1 = model.Div(doc) b.push_child(div1) p1 = model.P(doc) div1.push_child(p1) span1 = model.Span(doc) p1.push_child(span1) t1 = model.Text(doc, "hello") span1.push_child(t1) isd = ISD.from_model(doc, 0) region = list(isd.iter_regions())[0] span = region[0][0][0][0] fs: styles.LengthType = span.get_style(styles.StyleProperties.FontSize) self.assertAlmostEqual(fs.value, 25 / doc.get_cell_resolution().rows) self.assertEqual(fs.units, styles.LengthType.Units.rh)
def test_style_property_disparity_has_px(self): prop = styles.LengthType(1, styles.LengthType.units.em) self.assertEqual(imsc_styles.StyleProperties.Disparity.has_px(prop), False) prop = styles.LengthType(1, styles.LengthType.units.px) self.assertEqual(imsc_styles.StyleProperties.Disparity.has_px(prop), True)
def test_style_property_font_size_has_px(self): prop = styles.LengthType(1, styles.LengthType.units.em) self.assertEqual(imsc_styles.StyleProperties.FontSize.has_px(prop), False) prop = styles.LengthType(1, styles.LengthType.units.px) self.assertEqual(imsc_styles.StyleProperties.FontSize.has_px(prop), True)
def test_tts_padding(self): padding = styles.PaddingType(before=styles.LengthType(10.1), end=styles.LengthType(20.2), after=styles.LengthType(30.3), start=styles.LengthType(40.4)) self.assertEqual( _get_set_style(imsc_styles.StyleProperties.Padding, padding), r"10.1% 20.2% 30.3% 40.4%")
def _get_region_from_model(doc: model.ContentDocument, x_origin: Number, y_origin: Number, width: Number, height: Number, display_align: styles.DisplayAlignType): """Returns a matching region from `doc` or creates one """ found_region = None regions = list(doc.iter_regions()) for r in regions: r_origin: styles.CoordinateType = r.get_style( styles.StyleProperties.Origin) assert r_origin is not None assert r_origin.x.units is styles.LengthType.Units.pct assert r_origin.y.units is styles.LengthType.Units.pct if r_origin.x.value != x_origin or r_origin.y.value != y_origin: continue r_extent: styles.ExtentType = r.get_style( styles.StyleProperties.Extent) assert r_extent is not None assert r_extent.height.units is styles.LengthType.Units.pct assert r_extent.width.units is styles.LengthType.Units.pct if r_extent.height.value != height or r_extent.width.value != width: continue r_display_align: styles.DisplayAlignType = r.get_style( styles.StyleProperties.DisplayAlign) assert r_display_align is not None if r_display_align != display_align: continue found_region = r break if found_region is None: found_region = model.Region(f"r{len(regions)}", doc) found_region.set_style( styles.StyleProperties.Extent, styles.ExtentType( height=styles.LengthType(height, styles.LengthType.Units.pct), width=styles.LengthType(width, styles.LengthType.Units.pct), )) found_region.set_style( styles.StyleProperties.Origin, styles.CoordinateType( x=styles.LengthType(x_origin, styles.LengthType.Units.pct), y=styles.LengthType(y_origin, styles.LengthType.Units.pct))) found_region.set_style(styles.StyleProperties.DisplayAlign, display_align) doc.put_region(found_region) return found_region
def test_compute_extent_em(self): doc = model.ContentDocument() r1 = model.Region("r1", doc) r1.set_style(styles.StyleProperties.ShowBackground, styles.ShowBackgroundType.always) with self.assertRaises(ValueError) as _context: r1.set_style( styles.StyleProperties.Extent, styles.ExtentType( width=styles.LengthType(20, styles.LengthType.Units.em), height=styles.LengthType(3, styles.LengthType.Units.em)))
def test_style_property_line_height_has_px(self): prop = styles.SpecialValues.normal self.assertEqual(imsc_styles.StyleProperties.LineHeight.has_px(prop), False) prop = styles.LengthType(1, styles.LengthType.units.em) self.assertEqual(imsc_styles.StyleProperties.LineHeight.has_px(prop), False) prop = styles.LengthType(1, styles.LengthType.units.px) self.assertEqual(imsc_styles.StyleProperties.LineHeight.has_px(prop), True)
def test_style_property_ruby_reserve_has_px(self): prop = styles.RubyReserveType( position=styles.RubyReserveType.Position.both, length=styles.LengthType(value=1.2, units=styles.LengthType.Units.px)) self.assertEqual(imsc_styles.StyleProperties.RubyReserve.has_px(prop), True) prop = styles.RubyReserveType( position=styles.RubyReserveType.Position.both, length=styles.LengthType(value=1.2, units=styles.LengthType.Units.em)) self.assertEqual(imsc_styles.StyleProperties.RubyReserve.has_px(prop), False)
def extract(cls, context: StyleParsingContext, xml_attrib: str): if xml_attrib == "auto": return styles.ExtentType( height=styles.LengthType(1, styles.LengthType.Units.rh), width=styles.LengthType(1, styles.LengthType.Units.rw)) s = xml_attrib.split(" ") if len(s) != 2: raise ValueError("Bad tts:extent syntax") return styles.ExtentType( height=StyleProperties.ttml_length_to_model(context, s[1]), width=StyleProperties.ttml_length_to_model(context, s[0]))
def test_style_property_text_outline_has_px(self): prop = styles.SpecialValues.none self.assertEqual(imsc_styles.StyleProperties.TextOutline.has_px(prop), False) prop = styles.TextOutlineType(color=styles.NamedColors.red.value, thickness=styles.LengthType( value=5, units=styles.LengthType.Units.em)) self.assertEqual(imsc_styles.StyleProperties.TextOutline.has_px(prop), False) prop = styles.TextOutlineType(color=styles.NamedColors.red.value, thickness=styles.LengthType( value=5, units=styles.LengthType.Units.px)) self.assertEqual(imsc_styles.StyleProperties.TextOutline.has_px(prop), True)
def extract(cls, context: StyleParsingContext, xml_attrib: str): if xml_attrib == "auto": r = styles.PositionType( x=styles.LengthType(0, styles.LengthType.Units.pct), y=styles.LengthType(0, styles.LengthType.Units.pct)) else: s = xml_attrib.split(" ") if len(s) != 2: raise ValueError("tts:origin has not two components") r = styles.PositionType( x=StyleProperties.ttml_length_to_model(context, s[0]), y=StyleProperties.ttml_length_to_model(context, s[1])) return r
def test_tts_text_outline(self): to1 = styles.SpecialValues.none self.assertEqual( _get_set_style(imsc_styles.StyleProperties.TextOutline, to1), "none") to2 = styles.TextOutlineType(color=styles.NamedColors.red.value, thickness=styles.LengthType(value=5)) self.assertEqual( _get_set_style(imsc_styles.StyleProperties.TextOutline, to2), "#ff0000 5%")
def test_compute_padding(self): doc = model.ContentDocument() r1 = model.Region("r1", doc) r1.set_style(styles.StyleProperties.ShowBackground, styles.ShowBackgroundType.always) r1.set_style( styles.StyleProperties.Extent, styles.ExtentType( width=styles.LengthType(50, styles.LengthType.Units.pct), height=styles.LengthType(25, styles.LengthType.Units.pct) ) ) r1.set_style( styles.StyleProperties.Padding, styles.PaddingType( before=styles.LengthType(5, styles.LengthType.Units.pct), after=styles.LengthType(10, styles.LengthType.Units.pct), start=styles.LengthType(15, styles.LengthType.Units.pct), end=styles.LengthType(20, styles.LengthType.Units.pct) ) ) doc.put_region(r1) isd = ISD.from_model(doc, 0) region = list(isd.iter_regions())[0] padding: styles.PaddingType = region.get_style(styles.StyleProperties.Padding) self.assertAlmostEqual(padding.before.value, 25 * 0.05) self.assertAlmostEqual(padding.after.value, 25 * 0.10) self.assertAlmostEqual(padding.start.value, 50 * 0.15) self.assertAlmostEqual(padding.end.value, 50 * 0.2)
def test_tts_text_shadow(self): ts1 = styles.SpecialValues.none self.assertEqual( _get_set_style(imsc_styles.StyleProperties.TextShadow, ts1), "none") ts2 = styles.TextShadowType( (styles.TextShadowType.Shadow( x_offset=styles.LengthType(value=1, units=styles.LengthType.Units.em), y_offset=styles.LengthType(value=1.2, units=styles.LengthType.Units.em)), styles.TextShadowType.Shadow( x_offset=styles.LengthType(value=0.5, units=styles.LengthType.Units.em), y_offset=styles.LengthType(value=0.7, units=styles.LengthType.Units.em), blur_radius=styles.LengthType( value=1, units=styles.LengthType.Units.em), color=styles.NamedColors.red.value))) self.assertEqual( _get_set_style(imsc_styles.StyleProperties.TextShadow, ts2), "1em 1.2em, 0.5em 0.7em 1em #ff0000")
def test_style_property_origin_has_px(self): prop = styles.CoordinateType( styles.LengthType(1, styles.LengthType.units.px), styles.LengthType(1, styles.LengthType.units.em)) self.assertEqual(imsc_styles.StyleProperties.Origin.has_px(prop), True) prop = styles.CoordinateType( styles.LengthType(1, styles.LengthType.units.px), styles.LengthType(1, styles.LengthType.units.px)) self.assertEqual(imsc_styles.StyleProperties.Origin.has_px(prop), True) prop = styles.CoordinateType( styles.LengthType(1, styles.LengthType.units.em), styles.LengthType(1, styles.LengthType.units.px)) self.assertEqual(imsc_styles.StyleProperties.Origin.has_px(prop), True) prop = styles.CoordinateType( styles.LengthType(1, styles.LengthType.units.em), styles.LengthType(1, styles.LengthType.units.em)) self.assertEqual(imsc_styles.StyleProperties.Origin.has_px(prop), False)
def test_tts_ruby_reserve(self): rr1 = styles.SpecialValues.none self.assertEqual( _get_set_style(imsc_styles.StyleProperties.RubyReserve, rr1), "none") rr2 = styles.RubyReserveType( position=styles.RubyReserveType.Position.both, length=styles.LengthType(value=1.2, units=styles.LengthType.Units.em)) self.assertEqual( _get_set_style(imsc_styles.StyleProperties.RubyReserve, rr2), "both 1.2em") rr3 = styles.RubyReserveType( position=styles.RubyReserveType.Position.outside, ) self.assertEqual( _get_set_style(imsc_styles.StyleProperties.RubyReserve, rr3), "outside")
def test_style_property_extent_has_px(self): prop = styles.ExtentType( styles.LengthType(1, styles.LengthType.units.px), styles.LengthType(1, styles.LengthType.units.em)) self.assertEqual(imsc_styles.StyleProperties.Extent.has_px(prop), True) prop = styles.ExtentType( styles.LengthType(1, styles.LengthType.units.px), styles.LengthType(1, styles.LengthType.units.px)) self.assertEqual(imsc_styles.StyleProperties.Extent.has_px(prop), True) prop = styles.ExtentType( styles.LengthType(1, styles.LengthType.units.em), styles.LengthType(1, styles.LengthType.units.px)) self.assertEqual(imsc_styles.StyleProperties.Extent.has_px(prop), True) prop = styles.ExtentType( styles.LengthType(1, styles.LengthType.units.em), styles.LengthType(1, styles.LengthType.units.em)) self.assertEqual(imsc_styles.StyleProperties.Extent.has_px(prop), False)
def ttml_length_to_model(cls, _context: StyleParsingContext, xml_attrib: str): (value, units) = utils.parse_length(xml_attrib) return styles.LengthType(value, styles.LengthType.Units(units))
def parse_position( attr_value: str ) -> typing.Tuple[str, styles.LengthType, str, styles.LengthType]: '''Parse a TTML \\<position\\> value into offsets from a horizontal and vertical edge ''' length_50pct = styles.LengthType(value=50, units=styles.LengthType.Units.pct) length_0pct = styles.LengthType(value=0, units=styles.LengthType.Units.pct) h_edges = {"left", "right"} v_edges = {"top", "bottom"} h_edge: str = None h_offset: styles.LengthType = None v_edge: str = None v_offset: styles.LengthType = None items = attr_value.split() if len(items) in (1, 2): # begin processing 1 and 2 components while len(items) > 0: cur_item = items.pop(0) if cur_item in h_edges: h_edge = cur_item h_offset = length_0pct elif cur_item in v_edges: v_edge = cur_item v_offset = length_0pct elif cur_item == "center": if h_edge is None: h_edge = "left" h_offset = length_50pct elif v_edge is None: v_edge = "top" v_offset = length_50pct else: (value, units) = parse_length(cur_item) if h_edge is None: h_edge = "left" h_offset = styles.LengthType( value, styles.LengthType.Units(units)) elif v_edge is None: v_edge = "top" v_offset = styles.LengthType( value, styles.LengthType.Units(units)) # end processing 1 and 2 components else: # begin processing 3 and 4 components while len(items) > 0: cur_item = items.pop(0) if cur_item in h_edges: h_edge = cur_item if v_edge is not None and v_offset is None: v_offset = length_0pct elif cur_item in v_edges: v_edge = cur_item if h_edge is not None and h_offset is None: h_offset = length_0pct elif cur_item == "center": pass else: (value, units) = parse_length(cur_item) if h_edge is not None and h_offset is None: h_offset = styles.LengthType( value, styles.LengthType.Units(units)) if v_edge is not None and v_offset is None: v_offset = styles.LengthType( value, styles.LengthType.Units(units)) # end processing 3 and 4 components # fill-in missing components, if any if h_offset is None: if h_edge is None: h_edge = "left" h_offset = length_50pct else: h_offset = length_0pct if v_offset is None: if v_edge is None: v_edge = "top" v_offset = length_50pct else: v_offset = length_0pct return (h_edge, h_offset, v_edge, v_offset)
def __init__(self, gsi_block: bytes, disable_fill_line_gap: bool = False, disable_line_padding: bool = False, start_tc: typing.Optional[str] = None, font_stack: typing.Tuple[typing.Union[ str, styles.GenericFontFamilyType]] = None, max_row_count: typing.Optional[typing.Union[int, str]] = None): self.gsi = _GSIBlock._make( struct.unpack( '3s8sc2s2s32s32s32s32s32s32s16s6s6s2s5s5s3s2s2s1s8s8s1s1s3s32s32s32s75x576s', gsi_block)) self.doc = model.ContentDocument() self.doc.set_cell_resolution( model.CellResolutionType( columns=round(100 * DEFAULT_TELETEXT_COLS / (100 - 2 * DEFAULT_HORIZONTAL_SAFE_MARGIN_PCT)), rows=round(100 * DEFAULT_TELETEXT_ROWS / (100 - 2 * DEFAULT_VERTICAL_SAFE_MARGIN_PCT)))) self.doc.set_active_area( model.ActiveAreaType( left_offset=DEFAULT_HORIZONTAL_SAFE_MARGIN_PCT / 100, top_offset=DEFAULT_VERTICAL_SAFE_MARGIN_PCT / 100, width=1 - 2 * DEFAULT_HORIZONTAL_SAFE_MARGIN_PCT / 100, height=1 - 2 * DEFAULT_VERTICAL_SAFE_MARGIN_PCT / 100)) self.body = model.Body(self.doc) if not disable_fill_line_gap: self.body.set_style(styles.StyleProperties.FillLineGap, True) if not disable_line_padding: self.body.set_style( styles.StyleProperties.LinePadding, styles.LengthType(LINE_PADDING_LENGTH_C, styles.LengthType.Units.c)) if font_stack is not None: self.body.set_style(styles.StyleProperties.FontFamily, font_stack) else: self.body.set_style(styles.StyleProperties.FontFamily, DEFAULT_FONT_STACK) self.doc.set_body(self.body) self.sgn_to_div_map = {} self.last_sn = None self.is_in_extension = False self.tti_tf = None self.fps = _DFC_FRACTION_MAP.get(self.gsi.DFC) if self.fps is None: LOGGER.error("Unknown GSI DFC value %s, defaulting to 25 fps", self.gsi.DFC) self.fps = Fraction(25) else: LOGGER.debug("GSI DFC: %s", self.gsi.DFC) self.cct = self.gsi.CCT LOGGER.debug("GSI CCT: %s", self.gsi.CCT) try: self.tti_count = int(self.gsi.TNB) LOGGER.debug("GSI TNB: %s", self.gsi.TNB) except ValueError: LOGGER.error("Invalid TNB field value: %s", self.gsi.TNB) self.tti_count = sys.maxsize self.language = _LC_BCP47_MAP.get(self.gsi.LC) if self.language is None: LOGGER.warning( "Unknown LC value: %s, defaulting to 'unspecified''", self.gsi.LC) self.language = "" else: LOGGER.debug("GSI LC: %s", self.gsi.LC) self.doc.set_lang(self.language) if start_tc is None: self.start_offset = 0 elif start_tc == "TCP": try: self.start_offset = SmpteTimeCode( int(self.gsi.TCP[0:2]), int(self.gsi.TCP[2:4]), int(self.gsi.TCP[4:6]), int(self.gsi.TCP[6:8]), self.get_fps()).to_temporal_offset() LOGGER.debug("GSI TCP: %s", self.gsi.TCP) except ValueError: LOGGER.error("Invalid TCP value: %s", self.gsi.tcp) self.start_offset = 0 else: try: self.start_offset = SmpteTimeCode.parse( start_tc, self.get_fps()).to_temporal_offset() except ValueError: LOGGER.error("Invalid start_tc value") raise if max_row_count is None or self.is_teletext(): self.max_row_count = DEFAULT_TELETEXT_ROWS elif isinstance(max_row_count, str) and max_row_count == "MNR": try: self.max_row_count = int(self.gsi.MNR) LOGGER.debug("GSI MNR: %s", self.gsi.MNR) except ValueError: LOGGER.error("Invalid MNR value: %s", self.gsi.MNR) self.start_offset = DEFAULT_TELETEXT_ROWS else: self.max_row_count = max_row_count # p_element for use across cumulative subtitles self.cur_p_element = None
def extract(cls, context: StyleParsingContext, xml_attrib: str): return styles.PositionType(x=styles.LengthType(), y=styles.LengthType())
def process_tti_block(self, tti_block: bytes): """Processes a single TTI block """ if tti_block is None: raise ValueError("tti_block should not be None") tti = _TTIBlock._make(struct.unpack('<BHBBBBBBBBBBBBB112s', tti_block)) LOGGER.debug("Subtitle SN: %s", tti.SN) LOGGER.debug(" EBN: %s", tti.EBN) LOGGER.debug(" CS: %s", tti.CS) LOGGER.debug(" SGN: %s", tti.SGN) LOGGER.debug(" JC: %s", tti.JC) LOGGER.debug(" VP: %s", tti.VP) if 0xEF < tti.EBN < 0xFF: # skip user data and reserved blocks return if not self.is_in_extension: self.tti_tf = b'' self.tti_tf += tti.TF.strip(b'\x8f') is_double_height_characters = tf.has_double_height_char(self.tti_tf) # continue accumulating if we have an extension block if tti.EBN != 0xFF: self.is_in_extension = True return self.is_in_extension = False # apply program offset try: tci = SmpteTimeCode(tti.TCIh, tti.TCIm, tti.TCIs, tti.TCIf, self.get_fps()) tco = SmpteTimeCode(tti.TCOh, tti.TCOm, tti.TCOs, tti.TCOf, self.get_fps()) except ValueError: LOGGER.error("Invalid TTI timecode") return begin_time = tci.to_temporal_offset() - self.start_offset if begin_time < 0: LOGGER.debug( "Skipping subtitle because TCI is less than start time") return LOGGER.debug(" Time in: %s", tci) end_time = tco.to_temporal_offset() - self.start_offset if end_time < begin_time: LOGGER.error("Subtitle TCO is less than TCI") return LOGGER.debug(" Time out: %s", tco) # create a new subtitle if SN changes and we are not in cumulative mode if tti.SN is not self.last_sn and tti.CS in (0x00, 0x01): self.last_sn = tti.SN # find the div to which the subtitle belongs, based on SGN div_element = self.sgn_to_div_map.get(tti.SGN) # create the div if it does not exist if div_element is None: div_element = model.Div(self.doc) self.body.push_child(div_element) self.sgn_to_div_map[tti.SGN] = div_element # create the p that will hold the subtitle self.cur_p_element = model.P(self.doc) if tti.JC == 0x01: self.cur_p_element.set_style(styles.StyleProperties.TextAlign, styles.TextAlignType.start) elif tti.JC == 0x03: self.cur_p_element.set_style(styles.StyleProperties.TextAlign, styles.TextAlignType.end) else: self.cur_p_element.set_style(styles.StyleProperties.TextAlign, styles.TextAlignType.center) self.cur_p_element.set_style( styles.StyleProperties.LineHeight, styles.LengthType(DEFAULT_LINE_HEIGHT_PCT, styles.LengthType.Units.pct)) if self.is_teletext() and not is_double_height_characters: font_size = DEFAULT_SINGLE_HEIGHT_FONT_SIZE_PCT else: font_size = DEFAULT_DOUBLE_HEIGHT_FONT_SIZE_PCT self.cur_p_element.set_style( styles.StyleProperties.FontSize, styles.LengthType(font_size, styles.LengthType.Units.pct)) safe_area_height = round(100 - DEFAULT_VERTICAL_SAFE_MARGIN_PCT * 2) safe_area_width = round(100 - DEFAULT_HORIZONTAL_SAFE_MARGIN_PCT * 2) # assume that VP < max number of rows/2 means bottom-aligned and otherwise top-aligned # probably should offer an option to override this if tti.VP < self.get_max_row_count() // 2: # top-aligned large region r_y = DEFAULT_VERTICAL_SAFE_MARGIN_PCT + ( (tti.VP - 1) / self.get_max_row_count()) * safe_area_height r_height = 100 - DEFAULT_VERTICAL_SAFE_MARGIN_PCT - r_y region = _get_region_from_model( self.doc, round(DEFAULT_HORIZONTAL_SAFE_MARGIN_PCT), r_y, safe_area_width, r_height, styles.DisplayAlignType.before) else: line_count = tf.line_count(self.tti_tf, is_double_height_characters) vp = tti.VP line_height = 2 if is_double_height_characters else 1 r_y = DEFAULT_VERTICAL_SAFE_MARGIN_PCT r_height = ((vp + line_count * line_height - 1) / self.get_max_row_count()) * safe_area_height region = _get_region_from_model( self.doc, round(DEFAULT_HORIZONTAL_SAFE_MARGIN_PCT), r_y, safe_area_width, r_height, styles.DisplayAlignType.after) self.cur_p_element.set_region(region) div_element.push_child(self.cur_p_element) if tti.CS in (0x01, 0x02, 0x03): # create a nested span if we are in cumulative mode sub_element = model.Span(self.doc) self.cur_p_element.push_child(sub_element) else: sub_element = self.cur_p_element sub_element.set_begin(begin_time) sub_element.set_end(end_time) LOGGER.debug(" TF: %s", self.tti_tf) tf.to_model(sub_element, self.is_teletext(), self.get_cct(), self.tti_tf) if tti.CS in (0x01, 0x02): sub_element.push_child(model.Br(self.doc))
def to_model(data_file: typing.IO, _config = None, progress_callback=lambda _: None): """Converts an SRT document to the data model""" doc = model.ContentDocument() region = model.Region(_DEFAULT_REGION_ID, doc) region.set_style( styles.StyleProperties.Origin, styles.CoordinateType( x=styles.LengthType(5, styles.LengthType.Units.pct), y=styles.LengthType(5, styles.LengthType.Units.pct) ) ) region.set_style( styles.StyleProperties.Extent, styles.ExtentType( height=styles.LengthType(90, styles.LengthType.Units.pct), width=styles.LengthType(90, styles.LengthType.Units.pct) ) ) region.set_style( styles.StyleProperties.DisplayAlign, styles.DisplayAlignType.after ) region.set_style( styles.StyleProperties.TextAlign, styles.TextAlignType.center ) region.set_style( styles.StyleProperties.LineHeight, _DEFAULT_LINE_HEIGHT ) region.set_style( styles.StyleProperties.FontFamily, _DEFAULT_FONT_STACK ) region.set_style( styles.StyleProperties.FontSize, _DEFAULT_FONT_SIZE ) region.set_style( styles.StyleProperties.Color, _DEFAULT_TEXT_COLOR ) region.set_style( styles.StyleProperties.TextOutline, styles.TextOutlineType( _DEFAULT_OUTLINE_THICKNESS, _DEFAULT_OUTLINE_COLOR ) ) doc.put_region(region) body = model.Body(doc) body.set_region(region) doc.set_body(body) div = model.Div(doc) body.push_child(div) lines : str = data_file.readlines() state = _State.COUNTER current_p = None for line_index, line in enumerate(_none_terminated(lines)): if state is _State.COUNTER: if line is None: break if _EMPTY_RE.fullmatch(line): continue if _COUNTER_RE.search(line) is None: LOGGER.fatal("Missing subtitle counter at line %s", line_index) return None progress_callback(line_index/len(lines)) state = _State.TC continue if state is _State.TC: if line is None: break m = _TIMECODE_RE.search(line) if m is None: LOGGER.fatal("Missing timecode at line %s", line_index) return None current_p = model.P(doc) current_p.set_begin( int(m.group('begin_h')) * 3600 + int(m.group('begin_m')) * 60 + int(m.group('begin_s')) + int(m.group('begin_ms')) / 1000 ) current_p.set_end( int(m.group('end_h')) * 3600 + int(m.group('end_m')) * 60 + int(m.group('end_s')) + int(m.group('end_ms')) / 1000 ) state = _State.TEXT continue if state in (_State.TEXT, _State.TEXT_MORE): if line is None or _EMPTY_RE.fullmatch(line): subtitle_text = subtitle_text.strip('\r\n')\ .replace(r"\n\r", "\n")\ .replace(r"{bold}", r"<bold>")\ .replace(r"{/bold}", r"</bold>")\ .replace(r"{italic}", r"<italic>")\ .replace(r"{/italic}", r"</italic>")\ .replace(r"{underline}", r"<underline>")\ .replace(r"{/underline}", r"</underline>") parser = _TextParser(current_p, line_index) parser.feed(subtitle_text) parser.close() state = _State.COUNTER continue if state is _State.TEXT: div.push_child(current_p) subtitle_text = "" if state is _State.TEXT_MORE: current_p.push_child(model.Br(current_p.get_doc())) subtitle_text += line state = _State.TEXT_MORE continue return doc
span = model.Span(self.parent.get_doc()) span.push_child(model.Text(self.parent.get_doc(), line)) self.parent.push_child(span) class _State(Enum): COUNTER = 1 TC = 2 TEXT = 3 TEXT_MORE = 4 _EMPTY_RE = re.compile(r"\s+") _COUNTER_RE = re.compile(r"\d+") _TIMECODE_RE = re.compile(r"(?P<begin_h>[0-9]{2,3}):(?P<begin_m>[0-9]{2}):(?P<begin_s>[0-9]{2}),(?P<begin_ms>[0-9]{3})\s+-->\s+(?P<end_h>[0-9]{2,3}):(?P<end_m>[0-9]{2}):(?P<end_s>[0-9]{2}),(?P<end_ms>[0-9]{3})") _DEFAULT_REGION_ID = "r1" _DEFAULT_FONT_STACK = ("Verdana", "Arial", "Tiresias", styles.GenericFontFamilyType.sansSerif) _DEFAULT_FONT_SIZE = styles.LengthType(80, styles.LengthType.Units.pct) _DEFAULT_OUTLINE_THICKNESS = styles.LengthType(5, styles.LengthType.Units.pct) _DEFAULT_TEXT_COLOR = styles.NamedColors.white.value _DEFAULT_OUTLINE_COLOR = styles.NamedColors.black.value _DEFAULT_LINE_HEIGHT = styles.LengthType(125, styles.LengthType.Units.pct) def to_model(data_file: typing.IO, _config = None, progress_callback=lambda _: None): """Converts an SRT document to the data model""" doc = model.ContentDocument() region = model.Region(_DEFAULT_REGION_ID, doc) region.set_style( styles.StyleProperties.Origin, styles.CoordinateType( x=styles.LengthType(5, styles.LengthType.Units.pct),
def test_style_property_text_shadow_has_px(self): prop = styles.TextShadowType( (styles.TextShadowType.Shadow( x_offset=styles.LengthType(value=1, units=styles.LengthType.Units.em), y_offset=styles.LengthType(value=1, units=styles.LengthType.Units.em)), styles.TextShadowType.Shadow( x_offset=styles.LengthType(value=0.5, units=styles.LengthType.Units.em), y_offset=styles.LengthType(value=0.7, units=styles.LengthType.Units.em), blur_radius=styles.LengthType( value=1, units=styles.LengthType.Units.em), color=styles.NamedColors.red.value))) self.assertEqual(imsc_styles.StyleProperties.TextShadow.has_px(prop), False) prop = styles.TextShadowType( (styles.TextShadowType.Shadow( x_offset=styles.LengthType(value=1, units=styles.LengthType.Units.px), y_offset=styles.LengthType(value=1.2, units=styles.LengthType.Units.em)), styles.TextShadowType.Shadow( x_offset=styles.LengthType(value=0.5, units=styles.LengthType.Units.em), y_offset=styles.LengthType(value=0.7, units=styles.LengthType.Units.em), blur_radius=styles.LengthType( value=1, units=styles.LengthType.Units.em), color=styles.NamedColors.red.value))) self.assertEqual(imsc_styles.StyleProperties.TextShadow.has_px(prop), True) prop = styles.TextShadowType( (styles.TextShadowType.Shadow( x_offset=styles.LengthType(value=1, units=styles.LengthType.Units.em), y_offset=styles.LengthType(value=1.2, units=styles.LengthType.Units.em)), styles.TextShadowType.Shadow( x_offset=styles.LengthType(value=0.5, units=styles.LengthType.Units.px), y_offset=styles.LengthType(value=0.7, units=styles.LengthType.Units.em), blur_radius=styles.LengthType( value=1, units=styles.LengthType.Units.em), color=styles.NamedColors.red.value))) self.assertEqual(imsc_styles.StyleProperties.TextShadow.has_px(prop), True)
def test_style_property_padding_has_px(self): prop = styles.PaddingType( before=styles.LengthType(10.1, styles.LengthType.units.px), end=styles.LengthType(20.2, styles.LengthType.units.em), after=styles.LengthType(30.3, styles.LengthType.units.em), start=styles.LengthType(40.4, styles.LengthType.units.em)) self.assertEqual(imsc_styles.StyleProperties.Padding.has_px(prop), True) prop = styles.PaddingType( before=styles.LengthType(10.1, styles.LengthType.units.em), end=styles.LengthType(20.2, styles.LengthType.units.px), after=styles.LengthType(30.3, styles.LengthType.units.em), start=styles.LengthType(40.4, styles.LengthType.units.em)) self.assertEqual(imsc_styles.StyleProperties.Padding.has_px(prop), True) prop = styles.PaddingType( before=styles.LengthType(10.1, styles.LengthType.units.em), end=styles.LengthType(20.2, styles.LengthType.units.em), after=styles.LengthType(30.3, styles.LengthType.units.px), start=styles.LengthType(40.4, styles.LengthType.units.em)) self.assertEqual(imsc_styles.StyleProperties.Padding.has_px(prop), True) prop = styles.PaddingType( before=styles.LengthType(10.1, styles.LengthType.units.em), end=styles.LengthType(20.2, styles.LengthType.units.em), after=styles.LengthType(30.3, styles.LengthType.units.em), start=styles.LengthType(40.4, styles.LengthType.units.px)) self.assertEqual(imsc_styles.StyleProperties.Padding.has_px(prop), True) prop = styles.PaddingType( before=styles.LengthType(10.1, styles.LengthType.units.em), end=styles.LengthType(20.2, styles.LengthType.units.em), after=styles.LengthType(30.3, styles.LengthType.units.em), start=styles.LengthType(40.4, styles.LengthType.units.em)) self.assertEqual(imsc_styles.StyleProperties.Padding.has_px(prop), False)