Skip to content

Commit 9b5bbc6

Browse files
authored
SCC: caption management refactoring (#395)
1 parent b2cc10a commit 9b5bbc6

25 files changed

+2455
-1386
lines changed

src/main/python/ttconv/scc/content.py renamed to src/main/python/ttconv/scc/caption_line.py

Lines changed: 41 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -23,31 +23,33 @@
2323
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
2424
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2525

26-
"""SCC caption content"""
26+
"""SCC caption line"""
2727

2828
from __future__ import annotations
2929

30-
import copy
3130
import logging
32-
from typing import Optional, List, Union
31+
from typing import List, Union
3332

34-
from ttconv.time_code import SmpteTimeCode
33+
from ttconv.scc.caption_text import SccCaptionText
3534

3635
LOGGER = logging.getLogger(__name__)
3736

38-
ROLL_UP_BASE_ROW = 15
39-
4037

4138
class SccCaptionLine:
4239
"""Caption paragraph line"""
4340

41+
@staticmethod
42+
def default():
43+
"""Initializes a default caption paragraph line"""
44+
return SccCaptionLine(0, 0)
45+
4446
def __init__(self, row: int, indent: int):
45-
self._texts: List[SccCaptionText] = []
4647
self._row: int = row # Row in the active area
4748
self._indent: int = indent # Indentation in the active area
4849

4950
self._cursor: int = 0 # Position of the cursor on the line
50-
self._current_text: Optional[SccCaptionText] = None # Text content where the cursor is
51+
self._current_text: SccCaptionText = SccCaptionText() # Text content where the cursor is
52+
self._texts: List[SccCaptionText] = [self._current_text]
5153

5254
def add_text(self, text: Union[SccCaptionText, str]):
5355
"""Add text to line"""
@@ -58,42 +60,40 @@ def add_text(self, text: Union[SccCaptionText, str]):
5860
self._cursor = self.get_length()
5961

6062
elif isinstance(text, str):
63+
remaining_text = text
6164

62-
if self._current_text is None:
63-
# Initialize a new text element if necessary
64-
self._texts.append(SccCaptionText(text))
65-
self._current_text = self._texts[-1]
66-
self._cursor = self._current_text.get_length()
67-
68-
else:
69-
remaining_text = text
70-
71-
# While the cursor is not on the last text element, and some text remains
72-
while self._current_text is not self._texts[-1] and len(remaining_text) > 0:
73-
available = self._current_text.get_length() - self.get_current_text().get_cursor()
74-
text_to_write = remaining_text[:available]
65+
# While the cursor is not on the last text element, and some text remains
66+
while self._current_text is not self._texts[-1] and len(remaining_text) > 0:
67+
available = self._current_text.get_length() - self._current_text.get_cursor()
68+
text_to_write = remaining_text[:available]
7569

76-
# Replace current text element content
77-
self._current_text.append(text_to_write)
78-
self.set_cursor(self._cursor + len(text_to_write))
79-
remaining_text = remaining_text[available:]
70+
# Replace current text element content
71+
self._append_text(text_to_write)
72+
remaining_text = remaining_text[available:]
8073

81-
# If some text remains on the last text element
82-
if len(remaining_text) > 0:
83-
assert self._current_text is self._texts[-1]
74+
# If some text remains on the last text element
75+
if len(remaining_text) > 0:
76+
assert self._current_text is self._texts[-1]
8477

85-
# Replace and append to current text element content
86-
self._current_text.append(remaining_text)
87-
self.set_cursor(self._cursor + len(remaining_text))
78+
# Replace and append to current text element content
79+
self._append_text(remaining_text)
8880

8981
else:
9082
raise ValueError("Unsupported text type for SCC caption line")
9183

84+
def _append_text(self, text: str):
85+
"""Appends text and update cursor position"""
86+
self._current_text.append(text)
87+
if self._cursor < 0:
88+
self._cursor = 0
89+
90+
self.set_cursor(self._cursor + len(text))
91+
9292
def indent(self, indent: int):
9393
"""Indent current line"""
9494
self._indent += indent
9595

96-
def get_current_text(self) -> Optional[SccCaptionText]:
96+
def get_current_text(self) -> SccCaptionText:
9797
"""Returns current text content"""
9898
return self._current_text
9999

@@ -142,24 +142,26 @@ def get_indent(self) -> int:
142142
def clear(self):
143143
"""Clears the line text contents"""
144144
self._texts.clear()
145-
self._current_text = None
145+
self._current_text = SccCaptionText()
146+
self._texts = [self._current_text]
146147
self.set_cursor(0)
147148

148149
def is_empty(self) -> bool:
149150
"""Returns whether the line text is empty or not"""
150-
# no caption texts or an empty text
151-
return len(self._texts) == 0 or (len(self._texts) == 1 and self._texts[-1].get_text() == "")
151+
return self.get_length() == 0
152152

153153
def get_leading_spaces(self) -> int:
154154
"""Returns the number of leading space characters of the line"""
155155
index = 0
156156
leading_spaces = 0
157-
first_text = self.get_texts()[index].get_text()
158157

159-
while first_text.isspace() and index < len(self.get_texts()):
160-
leading_spaces += len(first_text)
161-
index += 1
158+
while index < len(self.get_texts()):
162159
first_text = self.get_texts()[index].get_text()
160+
if first_text.isspace():
161+
leading_spaces += len(first_text)
162+
index += 1
163+
else:
164+
break
163165

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

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

179181
def __repr__(self):
180182
return "<" + self.__class__.__name__ + " " + str(self.__dict__) + ">"
181-
182-
183-
class SccCaptionText:
184-
"""Caption text content with specific positional, temporal and styling attributes"""
185-
186-
def __init__(self, text: Optional[str] = ""):
187-
self._begin: Optional[SmpteTimeCode] = None
188-
self._end: Optional[SmpteTimeCode] = None
189-
self._style_properties = {}
190-
self._text: str = ""
191-
self._cursor = 0 # Cursor in the text
192-
193-
if text is not None and text != "":
194-
self.append(text)
195-
196-
def set_begin(self, time_code: SmpteTimeCode):
197-
"""Sets begin time code"""
198-
self._begin = copy.copy(time_code)
199-
200-
def get_begin(self) -> SmpteTimeCode:
201-
"""Returns the begin time code"""
202-
return self._begin
203-
204-
def set_end(self, time_code: SmpteTimeCode):
205-
"""Sets end time code"""
206-
self._end = copy.copy(time_code)
207-
208-
def get_end(self) -> SmpteTimeCode:
209-
"""Returns the end time code"""
210-
return self._end
211-
212-
def get_text(self) -> str:
213-
"""Returns the text"""
214-
return self._text
215-
216-
def get_length(self) -> int:
217-
"""Returns text length"""
218-
return len(self._text)
219-
220-
def is_empty(self) -> bool:
221-
"""Returns whether the text is empty or not"""
222-
return self.get_length() == 0
223-
224-
def append(self, text: str):
225-
"""Add or replace text content at cursor position"""
226-
# print("Append text: ", text, "to", self._text, "at", self._cursor)
227-
self._text = self._text[:self._cursor] + text + self._text[(self._cursor + len(text)):]
228-
self._cursor += len(text)
229-
# print("\t=>", self._text, ", cursor:", self._cursor)
230-
231-
def set_cursor_at(self, position: int):
232-
"""Set text cursor position"""
233-
self._cursor = position
234-
235-
def get_cursor(self) -> int:
236-
"""Returns the cursor position"""
237-
return self._cursor
238-
239-
def backspace(self):
240-
"""Remove last character"""
241-
self._text = self._text[:-1]
242-
243-
def get_style_properties(self) -> dict:
244-
"""Sets the style properties map"""
245-
return self._style_properties
246-
247-
def add_style_property(self, style_property, value):
248-
"""Adds a style property"""
249-
if value is None:
250-
return
251-
self._style_properties[style_property] = value
252-
253-
def has_same_style_properties(self, other):
254-
"""Returns whether the current text has the same style properties as the other text"""
255-
return self._style_properties == other.get_style_properties()
256-
257-
def __repr__(self):
258-
return "<" + self.__class__.__name__ + " " + str(self.__dict__) + ">"

src/main/python/ttconv/scc/paragraph.py renamed to src/main/python/ttconv/scc/caption_paragraph.py

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@
3232
from typing import Optional, List, Dict, Union
3333

3434
from ttconv.model import Region, ContentDocument, P, Br, Span, Text
35-
from ttconv.scc.content import SccCaptionText, SccCaptionLine
36-
from ttconv.scc.style import SccCaptionStyle
35+
from ttconv.scc.caption_line import SccCaptionLine
36+
from ttconv.scc.caption_style import SccCaptionStyle
37+
from ttconv.scc.caption_text import SccCaptionText
3738
from ttconv.scc.utils import get_position_from_offsets, get_extent_from_dimensions, convert_cells_to_percentages
3839
from ttconv.style_properties import CoordinateType, ExtentType, StyleProperties, LengthType, DisplayAlignType, ShowBackgroundType, \
3940
TextAlignType, NamedColors
@@ -51,6 +52,11 @@
5152
class SccCaptionParagraph:
5253
"""Caption paragraph"""
5354

55+
@staticmethod
56+
def default(caption_style: SccCaptionStyle = SccCaptionStyle.Unknown):
57+
"""Initializes a default caption paragraph"""
58+
return SccCaptionParagraph(caption_style=caption_style)
59+
5460
def __init__(self, safe_area_x_offset: int = 0, safe_area_y_offset: int = 0,
5561
caption_style: SccCaptionStyle = SccCaptionStyle.Unknown):
5662
self._caption_id: str = ""
@@ -69,6 +75,8 @@ def __init__(self, safe_area_x_offset: int = 0, safe_area_y_offset: int = 0,
6975
self._current_line: Optional[SccCaptionLine] = None
7076
# Lines per row in the active area (will be separated by line-breaks)
7177
self._caption_lines: Dict[int, SccCaptionLine] = {}
78+
# Initialize first default line
79+
self.new_caption_line()
7280

7381
self._caption_style: SccCaptionStyle = caption_style
7482
self._style_properties = {}
@@ -85,15 +93,15 @@ def set_begin(self, time_code):
8593
"""Sets caption begin time code"""
8694
self._begin = copy.copy(time_code)
8795

88-
def get_begin(self) -> SmpteTimeCode:
96+
def get_begin(self) -> Optional[SmpteTimeCode]:
8997
"""Returns the caption begin time code"""
9098
return self._begin
9199

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

96-
def get_end(self) -> SmpteTimeCode:
104+
def get_end(self) -> Optional[SmpteTimeCode]:
97105
"""Returns the caption end time code"""
98106
return self._end
99107

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

116+
def set_caption_style(self, caption_style: SccCaptionStyle):
117+
"""Sets the caption style"""
118+
self._caption_style = caption_style
119+
108120
def get_caption_style(self) -> SccCaptionStyle:
109121
"""Returns the caption style"""
110122
return self._caption_style
111123

112-
def get_current_line(self) -> Optional[SccCaptionLine]:
124+
def get_current_line(self) -> SccCaptionLine:
113125
"""Returns the current caption line"""
114126
return self._current_line
115127

116-
def get_current_text(self) -> Optional[SccCaptionText]:
128+
def get_current_text(self) -> SccCaptionText:
117129
"""Returns the current caption text"""
118-
if self._current_line is None:
119-
return None
120130
return self._current_line.get_current_text()
121131

122132
def append_text(self, text: str):
@@ -150,9 +160,14 @@ def get_style_property(self, style_property) -> Optional:
150160
def set_cursor_at(self, row: int, indent: Optional[int] = None):
151161
"""Set cursor position and initialize a new line if necessary"""
152162

153-
# Remove current line if empty (useless)
154-
if self._current_line is not None and self._current_line.is_empty():
155-
del self._caption_lines[self._current_line.get_row()]
163+
if self._caption_lines.get(self._current_line.get_row()) is not None:
164+
# Set current line if necessary
165+
if self._caption_lines.get(self._current_line.get_row()) is not self._current_line:
166+
self._current_line = self._caption_lines.get(self._current_line.get_row())
167+
168+
# Remove current line if empty (i.e. useless)
169+
if self._current_line.is_empty():
170+
del self._caption_lines[self._current_line.get_row()]
156171

157172
self._cursor = (row, indent if indent is not None else 0)
158173

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

164179
if indent is not None:
165-
self._current_line.set_cursor(self._cursor[1] - self._current_line.get_indent())
180+
self._update_current_line_cursor()
166181

167182
def get_cursor(self) -> (int, int):
168183
"""Returns cursor coordinates"""
@@ -176,12 +191,29 @@ def indent_cursor(self, indent: int):
176191
# If the current line is empty, set cursor indent as a line tabulation
177192
self._current_line.indent(indent)
178193
else:
179-
self._current_line.set_cursor(self._cursor[1] - self._current_line.get_indent())
194+
self._update_current_line_cursor()
195+
196+
def _update_current_line_cursor(self):
197+
"""Updates cursor position on current line"""
198+
new_cursor_position = self._cursor[1] - self._current_line.get_indent()
199+
200+
if new_cursor_position < 0:
201+
self._current_line.indent(new_cursor_position)
202+
203+
self._current_line.set_cursor(new_cursor_position)
180204

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

209+
def is_empty(self) -> bool:
210+
"""Returns whether the paragraph has no content"""
211+
return self._get_length() == 0
212+
213+
def _get_length(self) -> int:
214+
"""Returns the total length of contained text"""
215+
return sum([line.get_length() for line in self._caption_lines.values()])
216+
185217
def copy_lines(self) -> Dict[int, SccCaptionLine]:
186218
"""Copy paragraph lines (without time attributes)"""
187219
lines_copy = {}
@@ -199,10 +231,6 @@ def copy_lines(self) -> Dict[int, SccCaptionLine]:
199231

200232
def new_caption_text(self):
201233
"""Appends a new caption text content, and keeps reference on it"""
202-
if self._current_line is None:
203-
LOGGER.warning("Add a new caption line to add new caption text")
204-
self.new_caption_line()
205-
206234
self._current_line.add_text(SccCaptionText())
207235

208236
def new_caption_line(self):
@@ -227,7 +255,7 @@ def roll_up(self):
227255

228256
def get_origin(self) -> CoordinateType:
229257
"""Computes and returns the current paragraph origin, based on its content"""
230-
if len(self._caption_lines) > 0:
258+
if not self.is_empty():
231259
x_offsets = [text.get_indent() for text in self._caption_lines.values()]
232260
y_offsets = [text.get_row() - 1 for text in self._caption_lines.values()]
233261

@@ -237,7 +265,7 @@ def get_origin(self) -> CoordinateType:
237265

238266
def get_extent(self) -> ExtentType:
239267
"""Computes and returns the current paragraph extent, based on its content"""
240-
if len(self._caption_lines) == 0:
268+
if self.is_empty():
241269
return get_extent_from_dimensions(0, 0)
242270

243271
paragraph_rows = self._caption_lines.keys()
@@ -260,6 +288,9 @@ def guess_text_alignment(self) -> TextAlignType:
260288
def get_line_right_offset(line: SccCaptionLine) -> int:
261289
return SCC_ROOT_CELL_RESOLUTION_COLUMNS - (line.get_indent() + line.get_length())
262290

291+
if self.is_empty():
292+
return TextAlignType.start
293+
263294
# look for longest line
264295
longest_line = max(self._caption_lines.values(), key=lambda line: line.get_length())
265296

0 commit comments

Comments
 (0)