diff --git a/src/main/python/ttconv/scc/caption_line.py b/src/main/python/ttconv/scc/caption_line.py index 20e89991..45c88ce1 100644 --- a/src/main/python/ttconv/scc/caption_line.py +++ b/src/main/python/ttconv/scc/caption_line.py @@ -28,7 +28,7 @@ from __future__ import annotations import logging -from typing import Optional, List, Union +from typing import List, Union from ttconv.scc.caption_text import SccCaptionText @@ -38,13 +38,18 @@ class SccCaptionLine: """Caption paragraph line""" + @staticmethod + def default(): + """Initializes a default caption paragraph line""" + return SccCaptionLine(0, 0) + def __init__(self, row: int, indent: int): - self._texts: List[SccCaptionText] = [] self._row: int = row # Row in the active area self._indent: int = indent # Indentation in the active area self._cursor: int = 0 # Position of the cursor on the line - self._current_text: Optional[SccCaptionText] = None # Text content where the cursor is + self._current_text: SccCaptionText = SccCaptionText() # Text content where the cursor is + self._texts: List[SccCaptionText] = [self._current_text] def add_text(self, text: Union[SccCaptionText, str]): """Add text to line""" @@ -55,31 +60,23 @@ def add_text(self, text: Union[SccCaptionText, str]): self._cursor = self.get_length() elif isinstance(text, str): + remaining_text = text - if self._current_text is None: - # Initialize a new text element if necessary - self._texts.append(SccCaptionText()) - self._current_text = self._texts[-1] - self._append_text(text) - - else: - remaining_text = text - - # While the cursor is not on the last text element, and some text remains - while self._current_text is not self._texts[-1] and len(remaining_text) > 0: - available = self._current_text.get_length() - self._current_text.get_cursor() - text_to_write = remaining_text[:available] + # While the cursor is not on the last text element, and some text remains + while self._current_text is not self._texts[-1] and len(remaining_text) > 0: + available = self._current_text.get_length() - self._current_text.get_cursor() + text_to_write = remaining_text[:available] - # Replace current text element content - self._append_text(text_to_write) - remaining_text = remaining_text[available:] + # Replace current text element content + self._append_text(text_to_write) + remaining_text = remaining_text[available:] - # If some text remains on the last text element - if len(remaining_text) > 0: - assert self._current_text is self._texts[-1] + # If some text remains on the last text element + if len(remaining_text) > 0: + assert self._current_text is self._texts[-1] - # Replace and append to current text element content - self._append_text(remaining_text) + # Replace and append to current text element content + self._append_text(remaining_text) else: raise ValueError("Unsupported text type for SCC caption line") @@ -96,7 +93,7 @@ def indent(self, indent: int): """Indent current line""" self._indent += indent - def get_current_text(self) -> Optional[SccCaptionText]: + def get_current_text(self) -> SccCaptionText: """Returns current text content""" return self._current_text @@ -145,13 +142,13 @@ def get_indent(self) -> int: def clear(self): """Clears the line text contents""" self._texts.clear() - self._current_text = None + self._current_text = SccCaptionText() + self._texts = [self._current_text] self.set_cursor(0) def is_empty(self) -> bool: """Returns whether the line text is empty or not""" - # no caption texts or an empty text - return len(self._texts) == 0 or (len(self._texts) == 1 and self._texts[-1].get_text() == "") + return self.get_length() == 0 def get_leading_spaces(self) -> int: """Returns the number of leading space characters of the line""" diff --git a/src/main/python/ttconv/scc/caption_paragraph.py b/src/main/python/ttconv/scc/caption_paragraph.py index 84b971c7..e4be5d42 100644 --- a/src/main/python/ttconv/scc/caption_paragraph.py +++ b/src/main/python/ttconv/scc/caption_paragraph.py @@ -52,6 +52,11 @@ class SccCaptionParagraph: """Caption paragraph""" + @staticmethod + def default(caption_style: SccCaptionStyle = SccCaptionStyle.Unknown): + """Initializes a default caption paragraph""" + return SccCaptionParagraph(caption_style=caption_style) + def __init__(self, safe_area_x_offset: int = 0, safe_area_y_offset: int = 0, caption_style: SccCaptionStyle = SccCaptionStyle.Unknown): self._caption_id: str = "" @@ -70,6 +75,8 @@ def __init__(self, safe_area_x_offset: int = 0, safe_area_y_offset: int = 0, self._current_line: Optional[SccCaptionLine] = None # Lines per row in the active area (will be separated by line-breaks) self._caption_lines: Dict[int, SccCaptionLine] = {} + # Initialize first default line + self.new_caption_line() self._caption_style: SccCaptionStyle = caption_style self._style_properties = {} @@ -114,14 +121,12 @@ def get_caption_style(self) -> SccCaptionStyle: """Returns the caption style""" return self._caption_style - def get_current_line(self) -> Optional[SccCaptionLine]: + def get_current_line(self) -> SccCaptionLine: """Returns the current caption line""" return self._current_line - def get_current_text(self) -> Optional[SccCaptionText]: + def get_current_text(self) -> SccCaptionText: """Returns the current caption text""" - if self._current_line is None: - return None return self._current_line.get_current_text() def append_text(self, text: str): @@ -155,9 +160,14 @@ def get_style_property(self, style_property) -> Optional: def set_cursor_at(self, row: int, indent: Optional[int] = None): """Set cursor position and initialize a new line if necessary""" - # Remove current line if empty (useless) - if self._current_line is not None and self._current_line.is_empty(): - del self._caption_lines[self._current_line.get_row()] + if self._caption_lines.get(self._current_line.get_row()) is not None: + # Set current line if necessary + if self._caption_lines.get(self._current_line.get_row()) is not self._current_line: + self._current_line = self._caption_lines.get(self._current_line.get_row()) + + # Remove current line if empty (i.e. useless) + if self._current_line.is_empty(): + del self._caption_lines[self._current_line.get_row()] self._cursor = (row, indent if indent is not None else 0) @@ -198,7 +208,11 @@ def get_lines(self) -> Dict[int, SccCaptionLine]: def is_empty(self) -> bool: """Returns whether the paragraph has no content""" - return not self._caption_lines + return self._get_length() == 0 + + def _get_length(self) -> int: + """Returns the total length of contained text""" + return sum([line.get_length() for line in self._caption_lines.values()]) def copy_lines(self) -> Dict[int, SccCaptionLine]: """Copy paragraph lines (without time attributes)""" @@ -217,10 +231,6 @@ def copy_lines(self) -> Dict[int, SccCaptionLine]: def new_caption_text(self): """Appends a new caption text content, and keeps reference on it""" - if self._current_line is None: - LOGGER.warning("Add a new caption line to add new caption text") - self.new_caption_line() - self._current_line.add_text(SccCaptionText()) def new_caption_line(self): @@ -245,7 +255,7 @@ def roll_up(self): def get_origin(self) -> CoordinateType: """Computes and returns the current paragraph origin, based on its content""" - if len(self._caption_lines) > 0: + if not self.is_empty(): x_offsets = [text.get_indent() for text in self._caption_lines.values()] y_offsets = [text.get_row() - 1 for text in self._caption_lines.values()] @@ -255,7 +265,7 @@ def get_origin(self) -> CoordinateType: def get_extent(self) -> ExtentType: """Computes and returns the current paragraph extent, based on its content""" - if len(self._caption_lines) == 0: + if self.is_empty(): return get_extent_from_dimensions(0, 0) paragraph_rows = self._caption_lines.keys() diff --git a/src/main/python/ttconv/scc/caption_style.py b/src/main/python/ttconv/scc/caption_style.py index 9243188d..544073be 100644 --- a/src/main/python/ttconv/scc/caption_style.py +++ b/src/main/python/ttconv/scc/caption_style.py @@ -58,3 +58,8 @@ class SccCaptionStyle(Enum): # - EDM (to erase the displayed caption, optional) # - EOC (to display the current caption) PopOn = 3 + + @staticmethod + def default(): + """Returns the default caption style""" + return SccCaptionStyle.PopOn diff --git a/src/main/python/ttconv/scc/context.py b/src/main/python/ttconv/scc/context.py index d2db5237..051790b3 100644 --- a/src/main/python/ttconv/scc/context.py +++ b/src/main/python/ttconv/scc/context.py @@ -66,12 +66,12 @@ def __init__(self, safe_area_x_offset: int, safe_area_y_offset: int, config: Opt self.previous_word: Optional[SccWord] = None self.previous_word_type: Optional[Type] = None + # Caption style (Pop-on, Roll-up, Paint-on) currently processed + self.current_style = SccCaptionStyle.default() # Buffered caption being built self.buffered_caption = None # Captions being displayed self.active_caption: Optional[SccCaptionParagraph] = None - # Caption style (Pop-on, Roll-up, Paint-on) currently processed - self.current_style = SccCaptionStyle.Unknown # Roll-up caption number of lines self.roll_up_depth: int = 0 @@ -97,7 +97,7 @@ def new_active_caption(self, begin_time_code: SmpteTimeCode, caption_style: SccC def new_buffered_caption(self): """Resets buffered caption""" - self.buffered_caption = SccCaptionParagraph(self.safe_area_x_offset, self.safe_area_y_offset) + self.buffered_caption = SccCaptionParagraph(self.safe_area_x_offset, self.safe_area_y_offset, SccCaptionStyle.PopOn) def get_caption_to_process(self) -> Optional[SccCaptionParagraph]: """Returns the caption currently being processed""" @@ -204,9 +204,6 @@ def process_preamble_address_code(self, pac: SccPreambleAddressCode, time_code: self.active_caption.set_cursor_at(pac_row, pac_indent) - if self.active_caption.get_current_text() is None: - self.active_caption.new_caption_text() - elif self.current_style is SccCaptionStyle.RollUp: if not self.has_active_caption(): @@ -299,11 +296,11 @@ def process_attribute_code(self, attribute_code: SccAttributeCode): processed_caption = self.get_caption_to_process() - if processed_caption is None or processed_caption.get_current_text() is None: + if processed_caption is None: LOGGER.warning("No current SCC caption nor content initialized") return - if processed_caption.get_current_text() is not None and processed_caption.get_current_text().get_text(): + if processed_caption.get_current_text().get_text(): processed_caption.new_caption_text() if attribute_code.is_background(): @@ -321,11 +318,6 @@ def process_control_code(self, control_code: SccControlCode, time_code: SmpteTim # Start a new Pop-On caption self.current_style = SccCaptionStyle.PopOn - if self.buffered_caption.get_caption_style() is SccCaptionStyle.Unknown: - self.buffered_caption.set_caption_style(self.current_style) - self.buffered_caption.new_caption_line() - self.buffered_caption.new_caption_text() - elif control_code is SccControlCode.RDC: # Start a new Paint-On caption self.current_style = SccCaptionStyle.PaintOn @@ -432,6 +424,10 @@ def process_control_code(self, control_code: SccControlCode, time_code: SmpteTim def process_text(self, word: str, time_code: SmpteTimeCode): """Processes SCC text words""" if self.current_style is SccCaptionStyle.PaintOn: + if not self.has_active_caption(): + LOGGER.warning("Initialize active caption buffer to handle paint-on text at %s", time_code) + self.paint_on_active_caption(time_code) + if word.startswith(" "): if self.active_caption.get_caption_style() is not SccCaptionStyle.PaintOn: @@ -464,6 +460,10 @@ def process_text(self, word: str, time_code: SmpteTimeCode): self.active_caption.get_current_text().add_style_property(StyleProperties.TextDecoration, self.current_text_decoration) elif self.current_style is SccCaptionStyle.RollUp: + if not self.has_active_caption(): + LOGGER.warning("Initialize active caption buffer to handle roll-up text at %s", time_code) + self.new_active_caption(time_code, self.current_style) + self.active_caption.append_text(word) self.active_caption.get_current_text().add_style_property(StyleProperties.Color, self.current_color) diff --git a/src/main/python/ttconv/scc/disassembly.py b/src/main/python/ttconv/scc/disassembly.py index b4c788ad..d4bf88fa 100644 --- a/src/main/python/ttconv/scc/disassembly.py +++ b/src/main/python/ttconv/scc/disassembly.py @@ -26,10 +26,18 @@ """SCC disassembly functions""" import logging +from ttconv.scc.codes.attribute_codes import SccAttributeCode +from ttconv.scc.codes.control_codes import SccControlCode +from ttconv.scc.codes.extended_characters import SccExtendedCharacter +from ttconv.scc.codes.mid_row_codes import SccMidRowCode +from ttconv.scc.codes.preambles_address_codes import SccPreambleAddressCode +from ttconv.scc.codes.special_characters import SccSpecialCharacter +from ttconv.scc.word import SccWord from ttconv.style_properties import ColorType, NamedColors, FontStyleType, TextDecorationType LOGGER = logging.getLogger(__name__) + def get_color_disassembly(color: ColorType) -> str: """Get color disassembly code""" disassembly = "" @@ -81,3 +89,63 @@ def get_text_decoration_disassembly(text_decoration: TextDecorationType) -> str: if text_decoration is not None and text_decoration.underline is True: return "U" return "" + + +def get_scc_word_disassembly(scc_word: SccWord) -> str: + """Returns the disassembly code for specified SCC word""" + if scc_word.value == 0x0000: + return "{}" + + if scc_word.byte_1 < 0x20: + + attribute_code = SccAttributeCode.find(scc_word.value) + control_code = SccControlCode.find(scc_word.value) + mid_row_code = SccMidRowCode.find(scc_word.value) + pac = SccPreambleAddressCode.find(scc_word.byte_1, scc_word.byte_2) + spec_char = SccSpecialCharacter.find(scc_word.value) + extended_char = SccExtendedCharacter.find(scc_word.value) + + if pac is not None: + disassembly_code = f"{{{pac.get_row():02}" + color = pac.get_color() + indent = pac.get_indent() + if indent is not None and indent > 0: + disassembly_code += f"{indent :02}" + elif color is not None: + disassembly_code += get_color_disassembly(color) + disassembly_code += get_font_style_disassembly(pac.get_font_style()) + disassembly_code += get_text_decoration_disassembly(pac.get_text_decoration()) + else: + disassembly_code += "00" + disassembly_code += "}" + return disassembly_code + + if attribute_code is not None: + disassembly_code = "{" + disassembly_code += "B" if attribute_code.is_background() else "" + disassembly_code += get_color_disassembly(attribute_code.get_color()) + disassembly_code += get_text_decoration_disassembly(attribute_code.get_text_decoration()) + disassembly_code += "}" + return disassembly_code + + if mid_row_code is not None: + disassembly_code = "{" + disassembly_code += get_color_disassembly(mid_row_code.get_color()) + disassembly_code += get_font_style_disassembly(mid_row_code.get_font_style()) + disassembly_code += get_text_decoration_disassembly(mid_row_code.get_text_decoration()) + disassembly_code += "}" + return disassembly_code + + if control_code is not None: + return "{" + control_code.get_name() + "}" + + if spec_char is not None: + return spec_char.get_unicode_value() + + if extended_char is not None: + return extended_char.get_unicode_value() + + LOGGER.warning("Unsupported SCC word: %s", hex(scc_word.value)) + return "{??}" + + return scc_word.to_text() diff --git a/src/main/python/ttconv/scc/line.py b/src/main/python/ttconv/scc/line.py index 0fc6f933..b86d4261 100644 --- a/src/main/python/ttconv/scc/line.py +++ b/src/main/python/ttconv/scc/line.py @@ -39,7 +39,7 @@ from ttconv.scc.codes.preambles_address_codes import SccPreambleAddressCode from ttconv.scc.codes.special_characters import SccSpecialCharacter from ttconv.scc.context import SccContext -from ttconv.scc.disassembly import get_color_disassembly, get_font_style_disassembly, get_text_decoration_disassembly +from ttconv.scc.disassembly import get_scc_word_disassembly from ttconv.scc.word import SccWord from ttconv.time_code import SmpteTimeCode, FPS_30 @@ -99,63 +99,7 @@ def to_disassembly(self) -> str: disassembly_line = str(self.time_code) + "\t" for scc_word in self.scc_words: - - if scc_word.value == 0x0000: - disassembly_line += "{}" - continue - - if scc_word.byte_1 < 0x20: - - attribute_code = SccAttributeCode.find(scc_word.value) - control_code = SccControlCode.find(scc_word.value) - mid_row_code = SccMidRowCode.find(scc_word.value) - pac = SccPreambleAddressCode.find(scc_word.byte_1, scc_word.byte_2) - spec_char = SccSpecialCharacter.find(scc_word.value) - extended_char = SccExtendedCharacter.find(scc_word.value) - - if pac is not None: - disassembly_line += f"{{{pac.get_row():02}" - color = pac.get_color() - indent = pac.get_indent() - if indent is not None and indent > 0: - disassembly_line += f"{indent :02}" - elif color is not None: - disassembly_line += get_color_disassembly(color) - disassembly_line += get_font_style_disassembly(pac.get_font_style()) - disassembly_line += get_text_decoration_disassembly(pac.get_text_decoration()) - else: - disassembly_line += "00" - disassembly_line += "}" - - elif attribute_code is not None: - disassembly_line += "{" - disassembly_line += "B" if attribute_code.is_background() else "" - disassembly_line += get_color_disassembly(attribute_code.get_color()) - disassembly_line += get_text_decoration_disassembly(attribute_code.get_text_decoration()) - disassembly_line += "}" - - elif mid_row_code is not None: - disassembly_line += "{" - disassembly_line += get_color_disassembly(mid_row_code.get_color()) - disassembly_line += get_font_style_disassembly(mid_row_code.get_font_style()) - disassembly_line += get_text_decoration_disassembly(mid_row_code.get_text_decoration()) - disassembly_line += "}" - - elif control_code is not None: - disassembly_line += "{" + control_code.get_name() + "}" - - elif spec_char is not None: - disassembly_line += spec_char.get_unicode_value() - - elif extended_char is not None: - disassembly_line += extended_char.get_unicode_value() - - else: - disassembly_line += "{??}" - LOGGER.warning("Unsupported SCC word: %s", hex(scc_word.value)) - - else: - disassembly_line += scc_word.to_text() + disassembly_line += get_scc_word_disassembly(scc_word) return disassembly_line diff --git a/src/test/python/test_scc_content.py b/src/test/python/test_scc_content.py index 2de0cd93..1e099fb9 100644 --- a/src/test/python/test_scc_content.py +++ b/src/test/python/test_scc_content.py @@ -50,11 +50,11 @@ def test_line(self): self.assertEqual(7, caption_line.get_row()) self.assertEqual(4, caption_line.get_indent()) - self.assertIsNone(caption_line.get_current_text()) + self.assertTrue(caption_line.is_empty()) self.assertEqual(0, caption_line.get_cursor()) self.assertEqual(0, caption_line.get_length()) - self.assertTrue(caption_line.is_empty()) - self.assertListEqual([], caption_line.get_texts()) + self.assertEqual(1, len(caption_line.get_texts())) + self.assertTrue(caption_line.get_current_text().is_empty()) caption_line.set_cursor(10) self.assertEqual(0, caption_line.get_cursor()) @@ -134,7 +134,7 @@ def test_line(self): caption_line.clear() self.assertEqual(0, caption_line.get_cursor()) self.assertEqual(0, caption_line.get_length()) - self.assertListEqual([], caption_line.get_texts()) + self.assertTrue(caption_line.is_empty()) def test_line_text_leading_and_trailing_spaces(self): line = SccCaptionLine(0, 0) diff --git a/src/test/python/test_scc_paragraph.py b/src/test/python/test_scc_paragraph.py index f3e51f3c..ae489fdd 100644 --- a/src/test/python/test_scc_paragraph.py +++ b/src/test/python/test_scc_paragraph.py @@ -49,12 +49,8 @@ def test_content(self): self.assertEqual(4, caption_paragraph._safe_area_x_offset) self.assertEqual(2, caption_paragraph._safe_area_y_offset) - self.assertIsNone(caption_paragraph.get_current_text()) - self.assertEqual(0, len(caption_paragraph._caption_lines)) - - caption_paragraph.new_caption_text() self.assertEqual(caption_paragraph.get_current_line(), caption_paragraph.get_lines()[0]) - self.assertIsNotNone(caption_paragraph.get_current_text()) + self.assertTrue(caption_paragraph.get_current_text().is_empty()) self.assertEqual(1, len(caption_paragraph._caption_lines)) caption_paragraph.set_cursor_at(4, 4) diff --git a/src/test/python/test_scc_reader.py b/src/test/python/test_scc_reader.py index 6563b905..46470f05 100644 --- a/src/test/python/test_scc_reader.py +++ b/src/test/python/test_scc_reader.py @@ -1016,7 +1016,7 @@ def test_scc_mid_row_erase_displayed_memory_control_code(self): self.check_caption(p_list[5], "caption6", "00:00:03:19", None, "sagittis.") self.assertEqual(region_4, p_list[5].get_region()) - def test_scc_content_parsing_from_text(self): + def test_scc_content_starting_with_text(self): scc_content = """\ Scenarist_SCC V1.0 @@ -1040,11 +1040,18 @@ def test_scc_content_parsing_from_text(self): region_1 = doc.get_region("pop1") self.assertIsNotNone(region_1) - self.check_region_origin(region_1, 4, 15, doc.get_cell_resolution()) - self.check_region_extent(region_1, 28, 2, doc.get_cell_resolution()) + self.check_region_origin(region_1, 4, 1, doc.get_cell_resolution()) + self.check_region_extent(region_1, 5, 16, doc.get_cell_resolution()) self.check_element_style(region_1, StyleProperties.DisplayAlign, DisplayAlignType.before) self.check_element_style(region_1, StyleProperties.ShowBackground, ShowBackgroundType.whenActive) + region_2 = doc.get_region("pop2") + self.assertIsNotNone(region_2) + self.check_region_origin(region_2, 4, 15, doc.get_cell_resolution()) + self.check_region_extent(region_2, 28, 2, doc.get_cell_resolution()) + self.check_element_style(region_2, StyleProperties.DisplayAlign, DisplayAlignType.before) + self.check_element_style(region_2, StyleProperties.ShowBackground, ShowBackgroundType.whenActive) + body = doc.get_body() self.assertIsNotNone(body) @@ -1054,13 +1061,16 @@ def test_scc_content_parsing_from_text(self): self.assertIsNotNone(div) p_list = list(div) - self.assertEqual(1, len(p_list)) + self.assertEqual(2, len(p_list)) - self.check_caption(p_list[0], "caption2", "00:00:08:26", "00:00:09:09", "dolor sit amet,", Br, - "consectetur adipiscing elit.") + self.check_caption(p_list[0], "caption1", "00:00:03:06", "00:00:08:26", "ipsum") self.assertEqual(region_1, p_list[0].get_region()) - def test_scc_content_parsing_from_mid_row_code(self): + self.check_caption(p_list[1], "caption2", "00:00:08:26", "00:00:09:09", "dolor sit amet,", Br, + "consectetur adipiscing elit.") + self.assertEqual(region_2, p_list[1].get_region()) + + def test_scc_content_starting_with_mid_row_code(self): scc_content = """\ Scenarist_SCC V1.0 @@ -1084,11 +1094,18 @@ def test_scc_content_parsing_from_mid_row_code(self): region_1 = doc.get_region("pop1") self.assertIsNotNone(region_1) - self.check_region_origin(region_1, 4, 15, doc.get_cell_resolution()) - self.check_region_extent(region_1, 28, 2, doc.get_cell_resolution()) + self.check_region_origin(region_1, 4, 1, doc.get_cell_resolution()) + self.check_region_extent(region_1, 6, 16, doc.get_cell_resolution()) self.check_element_style(region_1, StyleProperties.DisplayAlign, DisplayAlignType.before) self.check_element_style(region_1, StyleProperties.ShowBackground, ShowBackgroundType.whenActive) + region_2 = doc.get_region("pop2") + self.assertIsNotNone(region_2) + self.check_region_origin(region_2, 4, 15, doc.get_cell_resolution()) + self.check_region_extent(region_2, 28, 2, doc.get_cell_resolution()) + self.check_element_style(region_2, StyleProperties.DisplayAlign, DisplayAlignType.before) + self.check_element_style(region_2, StyleProperties.ShowBackground, ShowBackgroundType.whenActive) + body = doc.get_body() self.assertIsNotNone(body) @@ -1098,13 +1115,16 @@ def test_scc_content_parsing_from_mid_row_code(self): self.assertIsNotNone(div) p_list = list(div) - self.assertEqual(1, len(p_list)) + self.assertEqual(2, len(p_list)) - self.check_caption(p_list[0], "caption2", "00:00:08:26", "00:00:09:09", "dolor sit amet,", Br, - "consectetur adipiscing elit.") + self.check_caption(p_list[0], "caption1", "00:00:03:07", "00:00:08:26", " ipsum") self.assertEqual(region_1, p_list[0].get_region()) - def test_scc_content_parsing_from_control_code(self): + self.check_caption(p_list[1], "caption2", "00:00:08:26", "00:00:09:09", "dolor sit amet,", Br, + "consectetur adipiscing elit.") + self.assertEqual(region_2, p_list[1].get_region()) + + def test_scc_content_starting_with_control_code(self): scc_content = """\ Scenarist_SCC V1.0 @@ -1128,11 +1148,18 @@ def test_scc_content_parsing_from_control_code(self): region_1 = doc.get_region("pop1") self.assertIsNotNone(region_1) - self.check_region_origin(region_1, 4, 15, doc.get_cell_resolution()) - self.check_region_extent(region_1, 28, 2, doc.get_cell_resolution()) + self.check_region_origin(region_1, 4, 1, doc.get_cell_resolution()) + self.check_region_extent(region_1, 5, 16, doc.get_cell_resolution()) self.check_element_style(region_1, StyleProperties.DisplayAlign, DisplayAlignType.before) self.check_element_style(region_1, StyleProperties.ShowBackground, ShowBackgroundType.whenActive) + region_2 = doc.get_region("pop2") + self.assertIsNotNone(region_2) + self.check_region_origin(region_2, 4, 15, doc.get_cell_resolution()) + self.check_region_extent(region_2, 28, 2, doc.get_cell_resolution()) + self.check_element_style(region_2, StyleProperties.DisplayAlign, DisplayAlignType.before) + self.check_element_style(region_2, StyleProperties.ShowBackground, ShowBackgroundType.whenActive) + body = doc.get_body() self.assertIsNotNone(body) @@ -1142,13 +1169,16 @@ def test_scc_content_parsing_from_control_code(self): self.assertIsNotNone(div) p_list = list(div) - self.assertEqual(1, len(p_list)) + self.assertEqual(2, len(p_list)) - self.check_caption(p_list[0], "caption2", "00:00:08:26", "00:00:09:09", "dolor sit amet,", Br, - "consectetur adipiscing elit.") + self.check_caption(p_list[0], "caption1", "00:00:03:07", "00:00:08:26", "ipsum") self.assertEqual(region_1, p_list[0].get_region()) - def test_scc_content_parsing_from_preamble_address_code(self): + self.check_caption(p_list[1], "caption2", "00:00:08:26", "00:00:09:09", "dolor sit amet,", Br, + "consectetur adipiscing elit.") + self.assertEqual(region_2, p_list[1].get_region()) + + def test_scc_content_starting_with_preamble_address_code(self): scc_content = """\ Scenarist_SCC V1.0 @@ -1172,11 +1202,18 @@ def test_scc_content_parsing_from_preamble_address_code(self): region_1 = doc.get_region("pop1") self.assertIsNotNone(region_1) - self.check_region_origin(region_1, 4, 15, doc.get_cell_resolution()) - self.check_region_extent(region_1, 28, 2, doc.get_cell_resolution()) + self.check_region_origin(region_1, 4, 14, doc.get_cell_resolution()) + self.check_region_extent(region_1, 5, 3, doc.get_cell_resolution()) self.check_element_style(region_1, StyleProperties.DisplayAlign, DisplayAlignType.before) self.check_element_style(region_1, StyleProperties.ShowBackground, ShowBackgroundType.whenActive) + region_2 = doc.get_region("pop2") + self.assertIsNotNone(region_2) + self.check_region_origin(region_2, 4, 15, doc.get_cell_resolution()) + self.check_region_extent(region_2, 28, 2, doc.get_cell_resolution()) + self.check_element_style(region_2, StyleProperties.DisplayAlign, DisplayAlignType.before) + self.check_element_style(region_2, StyleProperties.ShowBackground, ShowBackgroundType.whenActive) + body = doc.get_body() self.assertIsNotNone(body) @@ -1186,12 +1223,15 @@ def test_scc_content_parsing_from_preamble_address_code(self): self.assertIsNotNone(div) p_list = list(div) - self.assertEqual(1, len(p_list)) + self.assertEqual(2, len(p_list)) - self.check_caption(p_list[0], "caption2", "00:00:08:26", "00:00:09:09", "dolor sit amet,", Br, - "consectetur adipiscing elit.") + self.check_caption(p_list[0], "caption1", "00:00:03:07", "00:00:08:26", "ipsum") self.assertEqual(region_1, p_list[0].get_region()) + self.check_caption(p_list[1], "caption2", "00:00:08:26", "00:00:09:09", "dolor sit amet,", Br, + "consectetur adipiscing elit.") + self.assertEqual(region_2, p_list[1].get_region()) + def test_scc_with_negative_cursor(self): scc_content = """Scenarist_SCC V1.0 00:00:01:00 94AE 94AE 9420 9420 94F8 94F8 45E5 E5E3 68A1 94F4 94F4 D3E3 61F2 79A1 942C 942C 942F 942F @@ -1229,6 +1269,112 @@ def test_scc_with_negative_cursor(self): self.check_caption(p_list[0], "caption1", "00:00:01:12", "00:00:02:01", "Scary! Eeech!") self.assertEqual(region_1, p_list[0].get_region()) + def test_scc_content_starting_with_backspace(self): + scc_content = """Scenarist_SCC V1.0 +10:01:44;17 94AE 9420 9470 9723 946E 80C1 92B0 20ec 6120 e6e9 6e20 64e5 7320 616e 6edc e573 2031 38b0 b02c 942C 8080 8080 942F +""" + + expected_scc_disassembly = """\ +10:01:44;17 {ENM}{RCL}{1500}{TO3}{15WhI}{??}À la fin des années 1800,{EDM}{}{}{EOC} +""" + + scc_disassembly = to_disassembly(scc_content) + self.assertEqual(expected_scc_disassembly, scc_disassembly) + + doc = to_model(scc_content) + self.assertIsNotNone(doc) + + region_1 = doc.get_region("pop1") + self.assertIsNotNone(region_1) + self.check_region_origin(region_1, 4, 16, doc.get_cell_resolution()) + self.check_region_extent(region_1, 25, 1, doc.get_cell_resolution()) + self.check_element_style(region_1, StyleProperties.DisplayAlign, DisplayAlignType.before) + self.check_element_style(region_1, StyleProperties.ShowBackground, ShowBackgroundType.whenActive) + + body = doc.get_body() + self.assertIsNotNone(body) + + div_list = list(body) + self.assertEqual(1, len(div_list)) + div = div_list[0] + self.assertIsNotNone(div) + + p_list = list(div) + self.assertEqual(1, len(p_list)) + + self.check_caption(p_list[0], "caption1", "10:01:45;10", None, "À la fin des années 1800,") + self.assertEqual(region_1, p_list[0].get_region()) + + + def test_scc_content_roll_up_empty_caption(self): + scc_content = """Scenarist_SCC V1.0 +10:03:20:16 94ad 94ad 9426 9426 92d0 92d0 a880 9138 9138 2064 942c 942c e575 f820 76ef e9f8 2c20 e56e 2061 6e67 ec61 e973 29ba +""" + expected_scc_disassembly = """\ +10:03:20:16 {CR}{CR}{RU3}{RU3}{0300}{0300}(àà d{EDM}{EDM}eux voix, en anglais): +""" + + scc_disassembly = to_disassembly(scc_content) + self.assertEqual(expected_scc_disassembly, scc_disassembly) + + doc = to_model(scc_content) + self.assertIsNotNone(doc) + + region_1 = doc.get_region("rollup1") + self.assertIsNotNone(region_1) + self.check_region_origin(region_1, 4, 2, doc.get_cell_resolution()) + self.check_region_extent(region_1, 4, 15, doc.get_cell_resolution()) + self.check_element_style(region_1, StyleProperties.DisplayAlign, DisplayAlignType.after) + self.check_element_style(region_1, StyleProperties.ShowBackground, ShowBackgroundType.whenActive) + + region_2 = doc.get_region("rollup2") + self.assertIsNotNone(region_2) + self.check_region_origin(region_2, 4, 2, doc.get_cell_resolution()) + self.check_region_extent(region_2, 22, 15, doc.get_cell_resolution()) + self.check_element_style(region_2, StyleProperties.DisplayAlign, DisplayAlignType.after) + self.check_element_style(region_2, StyleProperties.ShowBackground, ShowBackgroundType.whenActive) + + body = doc.get_body() + self.assertIsNotNone(body) + + div_list = list(body) + self.assertEqual(1, len(div_list)) + div = div_list[0] + self.assertIsNotNone(div) + + p_list = list(div) + self.assertEqual(2, len(p_list)) + + self.check_caption(p_list[0], "caption1", "10:03:20:18", "10:03:20:24", "(à d") + self.assertEqual(region_1, p_list[0].get_region()) + + self.check_caption(p_list[1], "caption2", "10:03:20:24", None, "eux voix, en anglais):") + self.assertEqual(region_2, p_list[1].get_region()) + + + def test_scc_text_without_style_nor_position(self): + scc_content = """Scenarist_SCC V1.0 +10:55:31:29 2080 3280 2046 3180 +""" + expected_scc_disassembly = """\ +10:55:31:29 2 F1 +""" + + scc_disassembly = to_disassembly(scc_content) + self.assertEqual(expected_scc_disassembly, scc_disassembly) + + doc = to_model(scc_content) + self.assertIsNotNone(doc) + + body = doc.get_body() + self.assertIsNotNone(body) + + div_list = list(body) + self.assertEqual(1, len(div_list)) + div = div_list[0] + self.assertIsNotNone(div) + self.assertEqual(0, len(list(div))) + if __name__ == '__main__': unittest.main()