diff --git a/src/main/python/ttconv/imsc/style_properties.py b/src/main/python/ttconv/imsc/style_properties.py index d6045305..671df698 100644 --- a/src/main/python/ttconv/imsc/style_properties.py +++ b/src/main/python/ttconv/imsc/style_properties.py @@ -1114,8 +1114,11 @@ def extract(cls, context: StyleParsingContext, xml_attrib: str): if xml_attrib == "rl": return styles.WritingModeType.rltb - if xml_attrib == "tb": - return styles.WritingModeType.tbrl + if xml_attrib.startswith("tb"): + if xml_attrib == "tbrl": + return styles.WritingModeType.tbrl + elif xml_attrib == "tbrl" + return styles.WritingModeType.tblr return styles.WritingModeType[xml_attrib] diff --git a/src/main/python/ttconv/isd.py b/src/main/python/ttconv/isd.py index 77bde2c1..32ec09b6 100644 --- a/src/main/python/ttconv/isd.py +++ b/src/main/python/ttconv/isd.py @@ -394,7 +394,8 @@ def generate_isd_sequence( styles.StyleProperties.TextOutline, styles.StyleProperties.TextShadow, styles.StyleProperties.TextEmphasis, - styles.StyleProperties.Padding + styles.StyleProperties.Padding, + styles.StyleProperties.WritingMode ) @staticmethod diff --git a/src/main/python/ttconv/style_properties.py b/src/main/python/ttconv/style_properties.py index b1758076..eea5e48d 100644 --- a/src/main/python/ttconv/style_properties.py +++ b/src/main/python/ttconv/style_properties.py @@ -963,7 +963,7 @@ class WritingMode(StyleProperty): '''Corresponds to tts:writingMode ''' - is_inherited = False + is_inherited = True is_animatable = True @staticmethod diff --git a/src/main/python/ttconv/vtt/config.py b/src/main/python/ttconv/vtt/config.py index 6878d885..87594f1f 100644 --- a/src/main/python/ttconv/vtt/config.py +++ b/src/main/python/ttconv/vtt/config.py @@ -46,3 +46,6 @@ def name(cls): # outputs cue identifier cue_id: bool = field(default=True, metadata={"decoder": bool}) + + # vertical positioning + vertical_position: bool = field(default=False, metadata={"decoder": bool}) diff --git a/src/main/python/ttconv/vtt/cue.py b/src/main/python/ttconv/vtt/cue.py index b2fd38f9..edc9f347 100644 --- a/src/main/python/ttconv/vtt/cue.py +++ b/src/main/python/ttconv/vtt/cue.py @@ -47,6 +47,13 @@ class TextAlignment(Enum): left = "left" center = "center" right = "right" + start = "start" + end = "end" + + class Vertical(Enum): + """WebVTT vertical positioning cue setting""" + lr = "lr" # left to right + rl = "rl" # right to left _EOL_SEQ_RE = re.compile(r"\n{2,}") @@ -55,9 +62,12 @@ def __init__(self, identifier: Optional[int] = None): self._begin: Optional[ClockTime] = None self._end: Optional[ClockTime] = None self._text: str = "" + self._position: int = None + self._size: int = None self._line: int = None self._linealign: VttCue.LineAlignment = None self._textalign: VttCue.TextAlignment = None + self._vertical: VttCue.Vertical = None def set_begin(self, offset: Fraction): """Sets the paragraph begin time code""" @@ -95,6 +105,30 @@ def set_align(self, align: LineAlignment): def get_align(self) -> Optional[LineAlignment]: """Return the WebVTT line alignment cue setting""" return self._linealign + + def set_vertical(self, vertical: Vertical): + """Sets the WebVTT vertical positioning cue setting""" + self._vertical = vertical + + def get_vertical(self) -> Optional[Vertical]: + """Returns the WebVTT vertical positioning cue setting""" + return self._vertical + + def set_position(self, position: int): + """Sets the WebVTT position cue setting (in whole percent)""" + self._position = position + + def get_position(self) -> Optional[int]: + """Returns the WebVTT position cue setting (in whole percent)""" + return self._position + + def set_size(self, size: int): + """Sets the WebVTT size cue setting (in whole percent)""" + self._size = size + + def get_size(self) -> Optional[int]: + """Returns the WebVTT size cue setting (in whole percent)""" + return self._size def set_textalign(self, textalign: TextAlignment): """Sets the WebVTT text alignment cue setting""" @@ -134,6 +168,10 @@ def __str__(self) -> str: # cue timing t += f"{self._begin} --> {self._end}" + # cue vertical positioning + if self._vertical is not None: + t += f" vertical:{self._vertical.value}" + # cue text position if self._textalign is not None: t += f" align:{self._textalign.value}" @@ -144,6 +182,13 @@ def __str__(self) -> str: if self._linealign is not None: t += f",{self._linealign.value}" + + # cue position + if self._position is not None: + t += f" position:{self._position}%" + + if self._size is not None: + t += f" size:{self._size}%" t += "\n" diff --git a/src/main/python/ttconv/vtt/writer.py b/src/main/python/ttconv/vtt/writer.py index 2a079e61..1e448bee 100644 --- a/src/main/python/ttconv/vtt/writer.py +++ b/src/main/python/ttconv/vtt/writer.py @@ -40,7 +40,7 @@ from ttconv.vtt.cue import VttCue from ttconv.vtt.css_class import CssClass from ttconv.style_properties import DirectionType, ExtentType, PositionType, StyleProperties, FontStyleType, NamedColors, \ - FontWeightType, TextDecorationType, DisplayAlignType, TextAlignType + FontWeightType, TextDecorationType, DisplayAlignType, TextAlignType, WritingModeType LOGGER = logging.getLogger(__name__) @@ -92,6 +92,11 @@ def __init__(self, config: VTTWriterConfiguration): StyleProperties.Direction: [], }) + if self._config.vertical_position: + supported_styles.update({ + StyleProperties.WritingMode: [] + }) + self._filters.append(SupportedStylePropertiesISDFilter(supported_styles)) self._filters.append( @@ -167,7 +172,74 @@ def process_p(self, region: ISD.Region, element: model.P, begin: Fraction, end: cue.set_begin(begin) cue.set_end(end) - if self._config.line_position: + # add vertical position config + if self._config.vertical_position: + vertical = region.get_style(StyleProperties.WritingMode) + display_align = region.get_style(StyleProperties.DisplayAlign) + position: PositionType = region.get_style(StyleProperties.Position) + extent: ExtentType = region.get_style(StyleProperties.Extent) + text_align = element.get_style(StyleProperties.TextAlign) + + if vertical == WritingModeType.tbrl: + cue.set_vertical(VttCue.Vertical.rl) + if display_align == DisplayAlignType.after: + # vertical subtitle should be on the left side + cue.set_line(round(position.v_offset.value)) + if text_align == TextAlignType.start: + # vertical subtitle should be on the top + cue.set_textalign(VttCue.TextAlignment.start) + cue.set_position(round(position.h_offset.value)) + elif text_align == TextAlignType.end: + # vertical subtitle should be on the bottom + cue.set_textalign(VttCue.TextAlignment.end) + cue.set_position(round(position.h_offset.value + extent.height.value)) + else: + cue.set_position(round(position.h_offset.value)) + cue.set_textalign(VttCue.TextAlignment.start) + elif display_align == DisplayAlignType.before: + # vertical subtitle should be on the right side + cue.set_line(round(position.v_offset.value + extent.height.value)) + if text_align == TextAlignType.start: + # vertical subtitle should be on the top + cue.set_textalign(VttCue.TextAlignment.start) + cue.set_position(round(position.h_offset.value)) + elif text_align == TextAlignType.end: + # vertical subtitle should be on the bottom + cue.set_textalign(VttCue.TextAlignment.end) + cue.set_position(round(position.h_offset.value + extent.height.value)) + else: + cue.set_position(round(position.h_offset.value)) + cue.set_textalign(VttCue.TextAlignment.start) + cue.set_size(round(extent.width.value - position.v_offset.value - position.h_offset.value)) + elif vertical == WritingModeType.tblr: + cue.set_vertical(VttCue.Vertical.lr) + if display_align == DisplayAlignType.after: + # vertical subtitle should be on the right side + cue.set_line(round(position.v_offset.value)) + if text_align == TextAlignType.start: + # vertical subtitle should be on the top + cue.set_textalign(VttCue.TextAlignment.start) + cue.set_position(round(position.h_offset.value)) + elif text_align == TextAlignType.end: + # vertical subtitle should be on the bottom + cue.set_textalign(VttCue.TextAlignment.end) + cue.set_position(round(position.h_offset.value + extent.height.value)) + elif display_align == DisplayAlignType.before: + # vertical subtitle should be on the left side + cue.set_line(round(position.v_offset.value + extent.width.value)) + if text_align == TextAlignType.start: + # vertical subtitle should be on the start + cue.set_textalign(VttCue.TextAlignment.start) + cue.set_position(round(position.h_offset.value)) + elif text_align == TextAlignType.end: + # vertical subtitle should be on the top + cue.set_textalign(VttCue.TextAlignment.end) + cue.set_position(round(position.h_offset.value + extent.height.value)) + cue.set_size(round(extent.width.value - position.v_offset.value - position.h_offset.value)) + else: + cue.set_vertical(None) + + if self._config.line_position and cue.get_vertical() is None: display_align = region.get_style(StyleProperties.DisplayAlign) position: PositionType = region.get_style(StyleProperties.Position) extent: ExtentType = region.get_style(StyleProperties.Extent) @@ -182,7 +254,7 @@ def process_p(self, region: ISD.Region, element: model.P, begin: Fraction, end: cue.set_line(round(position.v_offset.value + extent.height.value / 2)) cue.set_align(VttCue.LineAlignment.center) - if self._config.text_align: + if self._config.text_align and cue.get_vertical() is None: direction = element.get_style(StyleProperties.Direction) text_align = element.get_style(StyleProperties.TextAlign) if text_align == TextAlignType.center: