Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SCC: caption management refactoring #395

Merged
merged 13 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,31 +23,33 @@
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""SCC caption content"""
"""SCC caption line"""

from __future__ import annotations

import copy
import logging
from typing import Optional, List, Union
from typing import List, Union

from ttconv.time_code import SmpteTimeCode
from ttconv.scc.caption_text import SccCaptionText

LOGGER = logging.getLogger(__name__)

ROLL_UP_BASE_ROW = 15


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"""
Expand All @@ -58,42 +60,40 @@ 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(text))
self._current_text = self._texts[-1]
self._cursor = self._current_text.get_length()

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.get_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._current_text.append(text_to_write)
self.set_cursor(self._cursor + len(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._current_text.append(remaining_text)
self.set_cursor(self._cursor + len(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")

def _append_text(self, text: str):
"""Appends text and update cursor position"""
self._current_text.append(text)
if self._cursor < 0:
self._cursor = 0

self.set_cursor(self._cursor + len(text))

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

Expand Down Expand Up @@ -142,24 +142,26 @@ 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"""
index = 0
leading_spaces = 0
first_text = self.get_texts()[index].get_text()

while first_text.isspace() and index < len(self.get_texts()):
leading_spaces += len(first_text)
index += 1
while index < len(self.get_texts()):
first_text = self.get_texts()[index].get_text()
if first_text.isspace():
leading_spaces += len(first_text)
index += 1
else:
break

return leading_spaces + len(first_text) - len(first_text.lstrip())

Expand All @@ -178,81 +180,3 @@ def get_trailing_spaces(self) -> int:

def __repr__(self):
return "<" + self.__class__.__name__ + " " + str(self.__dict__) + ">"


class SccCaptionText:
"""Caption text content with specific positional, temporal and styling attributes"""

def __init__(self, text: Optional[str] = ""):
self._begin: Optional[SmpteTimeCode] = None
self._end: Optional[SmpteTimeCode] = None
self._style_properties = {}
self._text: str = ""
self._cursor = 0 # Cursor in the text

if text is not None and text != "":
self.append(text)

def set_begin(self, time_code: SmpteTimeCode):
"""Sets begin time code"""
self._begin = copy.copy(time_code)

def get_begin(self) -> SmpteTimeCode:
"""Returns the begin time code"""
return self._begin

def set_end(self, time_code: SmpteTimeCode):
"""Sets end time code"""
self._end = copy.copy(time_code)

def get_end(self) -> SmpteTimeCode:
"""Returns the end time code"""
return self._end

def get_text(self) -> str:
"""Returns the text"""
return self._text

def get_length(self) -> int:
"""Returns text length"""
return len(self._text)

def is_empty(self) -> bool:
"""Returns whether the text is empty or not"""
return self.get_length() == 0

def append(self, text: str):
"""Add or replace text content at cursor position"""
# print("Append text: ", text, "to", self._text, "at", self._cursor)
self._text = self._text[:self._cursor] + text + self._text[(self._cursor + len(text)):]
self._cursor += len(text)
# print("\t=>", self._text, ", cursor:", self._cursor)

def set_cursor_at(self, position: int):
"""Set text cursor position"""
self._cursor = position

def get_cursor(self) -> int:
"""Returns the cursor position"""
return self._cursor

def backspace(self):
"""Remove last character"""
self._text = self._text[:-1]

def get_style_properties(self) -> dict:
"""Sets the style properties map"""
return self._style_properties

def add_style_property(self, style_property, value):
"""Adds a style property"""
if value is None:
return
self._style_properties[style_property] = value

def has_same_style_properties(self, other):
"""Returns whether the current text has the same style properties as the other text"""
return self._style_properties == other.get_style_properties()

def __repr__(self):
return "<" + self.__class__.__name__ + " " + str(self.__dict__) + ">"
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@
from typing import Optional, List, Dict, Union

from ttconv.model import Region, ContentDocument, P, Br, Span, Text
from ttconv.scc.content import SccCaptionText, SccCaptionLine
from ttconv.scc.style import SccCaptionStyle
from ttconv.scc.caption_line import SccCaptionLine
from ttconv.scc.caption_style import SccCaptionStyle
from ttconv.scc.caption_text import SccCaptionText
from ttconv.scc.utils import get_position_from_offsets, get_extent_from_dimensions, convert_cells_to_percentages
from ttconv.style_properties import CoordinateType, ExtentType, StyleProperties, LengthType, DisplayAlignType, ShowBackgroundType, \
TextAlignType, NamedColors
Expand All @@ -51,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 = ""
Expand All @@ -69,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 = {}
Expand All @@ -85,15 +93,15 @@ def set_begin(self, time_code):
"""Sets caption begin time code"""
self._begin = copy.copy(time_code)

def get_begin(self) -> SmpteTimeCode:
def get_begin(self) -> Optional[SmpteTimeCode]:
"""Returns the caption begin time code"""
return self._begin

def set_end(self, time_code):
"""Sets caption end time code"""
self._end = copy.copy(time_code)

def get_end(self) -> SmpteTimeCode:
def get_end(self) -> Optional[SmpteTimeCode]:
"""Returns the caption end time code"""
return self._end

Expand All @@ -105,18 +113,20 @@ def get_safe_area_y_offset(self):
"""Returns the safe area y offset"""
return self._safe_area_y_offset

def set_caption_style(self, caption_style: SccCaptionStyle):
"""Sets the caption style"""
self._caption_style = caption_style

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):
Expand Down Expand Up @@ -150,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)

Expand All @@ -162,7 +177,7 @@ def set_cursor_at(self, row: int, indent: Optional[int] = None):
self._current_line = self._caption_lines.get(row)

if indent is not None:
self._current_line.set_cursor(self._cursor[1] - self._current_line.get_indent())
self._update_current_line_cursor()

def get_cursor(self) -> (int, int):
"""Returns cursor coordinates"""
Expand All @@ -176,12 +191,29 @@ def indent_cursor(self, indent: int):
# If the current line is empty, set cursor indent as a line tabulation
self._current_line.indent(indent)
else:
self._current_line.set_cursor(self._cursor[1] - self._current_line.get_indent())
self._update_current_line_cursor()

def _update_current_line_cursor(self):
"""Updates cursor position on current line"""
new_cursor_position = self._cursor[1] - self._current_line.get_indent()

if new_cursor_position < 0:
self._current_line.indent(new_cursor_position)

self._current_line.set_cursor(new_cursor_position)

def get_lines(self) -> Dict[int, SccCaptionLine]:
"""Returns the paragraph lines per row"""
return self._caption_lines

def is_empty(self) -> bool:
"""Returns whether the paragraph has no content"""
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)"""
lines_copy = {}
Expand All @@ -199,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):
Expand All @@ -227,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()]

Expand All @@ -237,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()
Expand All @@ -260,6 +288,9 @@ def guess_text_alignment(self) -> TextAlignType:
def get_line_right_offset(line: SccCaptionLine) -> int:
return SCC_ROOT_CELL_RESOLUTION_COLUMNS - (line.get_indent() + line.get_length())

if self.is_empty():
return TextAlignType.start

# look for longest line
longest_line = max(self._caption_lines.values(), key=lambda line: line.get_length())

Expand Down
Loading
Loading