Skip to content

Commit 6755bf4

Browse files
authored
Fix edge relative positioning (#208,#71)
1 parent afd86c0 commit 6755bf4

17 files changed

+199
-93
lines changed

doc/data_model.md

+1-5
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,6 @@ Writer module can express temporal offsets in units of ticks, frames, etc. as de
8686

8787
The `dur` timing attribute is not supported.
8888

89-
### Position
90-
91-
The `tts:position` style property is not supported and `tts:origin` is used instead.
92-
9389
### Lengths
9490

95-
The constraints against length units specified in IMSC are relaxed, and lengths can be expressed in `c`, `%`, `rh`, `rw`, `em` and `px` units.
91+
Extent, origin and position lengths can be expressed in `c`, `%`, `rh`, `rw` and `px` units.

doc/isd.md

+4
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,7 @@ is:
5353
```
5454

5555
An ISD contains no timing information, i.e. no `begin` or `end` properties, or animation steps.
56+
57+
Both the `Origin` and `Position` style properties are always equal.
58+
59+
All lengths are expressed in root-relative units `rh` and `rw`.

scripts/linter.sh

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/sh
2+
3+
pipenv run python -m pylint --exit-zero src/main/python/ttconv/ src/test/python/

scripts/unit_test.sh

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/sh
2+
3+
pipenv run python -m unittest discover -v -s src/test/python/ -t .

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
setup(
1212
name='ttconv',
13-
version='1.0.0.dev4',
13+
version='1.0.0rc1',
1414
description='Library for conversion of common timed text formats',
1515
long_description=long_description,
1616
long_description_content_type='text/markdown',

src/main/python/ttconv/imsc/style_properties.py

+16-31
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ class Origin(StyleProperty):
444444
model_prop = styles.StyleProperties.Origin
445445

446446
@classmethod
447-
def has_px(cls, attrib_value: styles.PositionType) -> bool:
447+
def has_px(cls, attrib_value: styles.CoordinateType) -> bool:
448448
return attrib_value.x.units == styles.LengthType.Units.px or \
449449
attrib_value.y.units == styles.LengthType.Units.px
450450

@@ -453,7 +453,7 @@ def extract(cls, context: StyleParsingContext, xml_attrib: str):
453453

454454
if xml_attrib == "auto":
455455

456-
r = styles.PositionType(
456+
r = styles.CoordinateType(
457457
x=styles.LengthType(0, styles.LengthType.Units.pct),
458458
y=styles.LengthType(0, styles.LengthType.Units.pct)
459459
)
@@ -464,7 +464,7 @@ def extract(cls, context: StyleParsingContext, xml_attrib: str):
464464
if len(s) != 2:
465465
raise ValueError("tts:origin has not two components")
466466

467-
r = styles.PositionType(
467+
r = styles.CoordinateType(
468468
x=StyleProperties.ttml_length_to_model(context, s[0]),
469469
y=StyleProperties.ttml_length_to_model(context, s[1])
470470
)
@@ -475,7 +475,7 @@ def extract(cls, context: StyleParsingContext, xml_attrib: str):
475475
def from_model(cls, xml_element, model_value):
476476
xml_element.set(
477477
f"{{{cls.ns}}}{cls.local_name}",
478-
f"{model_value.x.value}{model_value.x.units.value} {model_value.y.value}{model_value.y.units.value}"
478+
f"{model_value.x.value:g}{model_value.x.units.value} {model_value.y.value:g}{model_value.y.units.value}"
479479
)
480480

481481

@@ -557,45 +557,30 @@ class Position(StyleProperty):
557557

558558
ns = xml_ns.TTS
559559
local_name = "position"
560-
model_prop = None
560+
model_prop = styles.StyleProperties.Position
561561

562562
@classmethod
563563
def extract(cls, context: StyleParsingContext, xml_attrib: str):
564564

565565
(h_edge, h_offset, v_edge, v_offset) = utils.parse_position(xml_attrib)
566566

567-
if h_edge == "right":
568-
if h_offset.units is styles.LengthType.Units.px:
569-
h_offset = styles.LengthType(context.doc.get_px_resolution().width - h_offset.value, h_offset.units)
570-
elif h_offset.units is styles.LengthType.Units.pct or h_offset.units is styles.LengthType.Units.rw:
571-
h_offset = styles.LengthType(100 - h_offset.value, h_offset.units)
572-
else:
573-
raise ValueError("Units other than px, pct, rh, rw used in tts:position")
574-
575-
if v_edge == "bottom":
576-
if v_offset.units is styles.LengthType.Units.px:
577-
v_offset = styles.LengthType(context.doc.get_px_resolution().height - v_offset.value, v_offset.units)
578-
elif v_offset.units is styles.LengthType.Units.pct or v_offset.units is styles.LengthType.Units.rh:
579-
v_offset = styles.LengthType(100 - v_offset.value, v_offset.units)
580-
else:
581-
raise ValueError("Units other than px, pct, rh, rw used in tts:position")
582-
583567
return styles.PositionType(
584-
x=h_offset,
585-
y=v_offset
568+
h_offset=h_offset,
569+
v_offset=v_offset,
570+
h_edge=styles.PositionType.HEdge(h_edge),
571+
v_edge=styles.PositionType.VEdge(v_edge)
586572
)
587573

588574
@classmethod
589-
def to_model(cls, context: StyleParsingContext, xml_element) -> typing.Tuple[typing.Type[model.StyleProperty], typing.Any]:
590-
return (
591-
styles.StyleProperties.Origin,
592-
cls.extract(context, xml_element.get(f"{{{cls.ns}}}{cls.local_name}"))
575+
def from_model(cls, xml_element, model_value: styles.PositionType):
576+
xml_element.set(
577+
f"{{{cls.ns}}}{cls.local_name}",
578+
f"{model_value.h_edge.value} " \
579+
f"{model_value.h_offset.value:g}{model_value.h_offset.units.value} " \
580+
f"{model_value.v_edge.value} " \
581+
f"{model_value.v_offset.value:g}{model_value.v_offset.units.value}"
593582
)
594583

595-
@classmethod
596-
def from_model(cls, xml_element, model_value):
597-
raise NotImplementedError
598-
599584

600585
class RubyAlign(StyleProperty):
601586
'''Corresponds to tts:rubyAlign.'''

src/main/python/ttconv/isd.py

+97-9
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ def generate_isd_sequence(
307307
styles.StyleProperties.FontSize,
308308
styles.StyleProperties.Extent,
309309
styles.StyleProperties.Origin,
310+
styles.StyleProperties.Position,
310311
styles.StyleProperties.LineHeight,
311312
styles.StyleProperties.LinePadding,
312313
styles.StyleProperties.RubyReserve,
@@ -467,10 +468,22 @@ def _process_element(
467468
if isd_element.has_style(initial_style):
468469
continue
469470

470-
initial_value = doc.get_initial_value(initial_style) if doc.has_initial_value(initial_style) \
471-
else initial_style.make_initial_value()
471+
if doc.has_initial_value(initial_style):
472+
473+
initial_value = doc.get_initial_value(initial_style)
474+
475+
elif initial_style is not styles.StyleProperties.Position:
476+
477+
# the initial value of the Position style property is set to Origin as part of style computation
478+
479+
initial_value = initial_style.make_initial_value()
480+
481+
else:
482+
483+
initial_value = None
472484

473485
styles_to_be_computed.add(initial_style)
486+
474487
isd_element.set_style(initial_style, initial_value)
475488

476489
# compute style properties
@@ -535,9 +548,9 @@ def _process_element(
535548

536549
# remove styles that are not applicable
537550

538-
for computed_style_prop in list(isd_element.iter_styles()):
539-
if not isd_element.is_style_applicable(computed_style_prop):
540-
isd_element.set_style(computed_style_prop, None)
551+
for style_prop in list(isd_element.iter_styles()):
552+
if not isd_element.is_style_applicable(style_prop):
553+
isd_element.set_style(style_prop, None)
541554

542555
# prune or keep the element
543556

@@ -903,14 +916,14 @@ class Origin(StyleProcessor):
903916
@classmethod
904917
def compute(cls, parent: model.ContentElement, element: model.ContentElement):
905918

906-
style_value: styles.PositionType = element.get_style(cls.style_prop)
919+
style_value: styles.CoordinateType = element.get_style(cls.style_prop)
907920

908921
# height
909922

910923
y = _compute_length(
911924
style_value.y,
912925
_make_rh_length(100),
913-
element.get_style(styles.StyleProperties.FontSize),
926+
None,
914927
_make_rh_length(100 / element.get_doc().get_cell_resolution().rows),
915928
_make_rh_length(100 / element.get_doc().get_px_resolution().height)
916929
)
@@ -920,14 +933,14 @@ def compute(cls, parent: model.ContentElement, element: model.ContentElement):
920933
x = _compute_length(
921934
style_value.x,
922935
_make_rw_length(100),
923-
element.get_style(styles.StyleProperties.FontSize),
936+
None,
924937
_make_rw_length(100 / element.get_doc().get_cell_resolution().columns),
925938
_make_rw_length(100 / element.get_doc().get_px_resolution().width)
926939
)
927940

928941
element.set_style(
929942
cls.style_prop,
930-
styles.PositionType(
943+
styles.CoordinateType(
931944
x=x,
932945
y=y
933946
)
@@ -992,6 +1005,81 @@ def compute(cls, parent: model.ContentElement, element: model.ContentElement):
9921005
styles.PaddingType(c_before, c_end, c_after, c_start)
9931006
)
9941007

1008+
class Position(StyleProcessor):
1009+
style_prop = styles.StyleProperties.Position
1010+
1011+
@classmethod
1012+
def compute(cls, parent: model.ContentElement, element: model.ContentElement):
1013+
1014+
position : styles.PositionType = element.get_style(styles.StyleProperties.Position)
1015+
1016+
if position is None:
1017+
1018+
# if no Position style property was specified, use the value of the Origin style property
1019+
1020+
origin : styles.CoordinateType = element.get_style(styles.StyleProperties.Origin)
1021+
1022+
element.set_style(
1023+
styles.StyleProperties.Position,
1024+
styles.PositionType(
1025+
h_offset=origin.x,
1026+
v_offset=origin.y
1027+
)
1028+
)
1029+
1030+
return
1031+
1032+
extent : styles.ExtentType = element.get_style(styles.StyleProperties.Extent)
1033+
1034+
assert extent.height.units is styles.LengthType.Units.rh
1035+
assert extent.width.units is styles.LengthType.Units.rw
1036+
1037+
v_offset = _compute_length(
1038+
position.v_offset,
1039+
_make_rh_length(100 - extent.height.value),
1040+
None,
1041+
_make_rh_length(100 / element.get_doc().get_cell_resolution().rows),
1042+
_make_rh_length(100 / element.get_doc().get_px_resolution().height)
1043+
)
1044+
1045+
if position.v_edge is styles.PositionType.VEdge.bottom:
1046+
v_offset = styles.LengthType(
1047+
value=100 - v_offset.value,
1048+
units=v_offset.units
1049+
)
1050+
1051+
h_offset = _compute_length(
1052+
position.h_offset,
1053+
_make_rw_length(100 - extent.width.value),
1054+
None,
1055+
_make_rw_length(100 / element.get_doc().get_cell_resolution().columns),
1056+
_make_rw_length(100 / element.get_doc().get_px_resolution().width)
1057+
)
1058+
1059+
if position.h_edge is styles.PositionType.HEdge.right:
1060+
h_offset = styles.LengthType(
1061+
value=100 - h_offset.value,
1062+
units=h_offset.units
1063+
)
1064+
1065+
# if specified, the Position style property overrides the Origin style property
1066+
1067+
element.set_style(
1068+
styles.StyleProperties.Origin,
1069+
styles.CoordinateType(
1070+
x=h_offset,
1071+
y=v_offset
1072+
)
1073+
)
1074+
1075+
element.set_style(
1076+
styles.StyleProperties.Position,
1077+
styles.PositionType(
1078+
h_offset=h_offset,
1079+
v_offset=v_offset
1080+
)
1081+
)
1082+
9951083
class RubyAlign(StyleProcessor):
9961084
style_prop = styles.StyleProperties.RubyAlign
9971085

src/main/python/ttconv/model.py

+1
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,7 @@ class Region(ContentElement):
769769
StyleProperties.Origin,
770770
StyleProperties.Overflow,
771771
StyleProperties.Padding,
772+
StyleProperties.Position,
772773
StyleProperties.ShowBackground,
773774
StyleProperties.Visibility,
774775
StyleProperties.WritingMode

src/main/python/ttconv/scc/content.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333
from ttconv.time_code import SmpteTimeCode
3434
from ttconv.scc.utils import get_position_from_offsets
35-
from ttconv.style_properties import PositionType
35+
from ttconv.style_properties import CoordinateType
3636

3737
ROLL_UP_BASE_ROW = 15
3838

@@ -99,8 +99,8 @@ def get_y_offset(self) -> int:
9999
"""Returns the y offset"""
100100
return self._y_offset
101101

102-
def get_position(self) -> PositionType:
103-
"""Returns current row and column offsets as a cell-based PositionType"""
102+
def get_position(self) -> CoordinateType:
103+
"""Returns current row and column offsets as a cell-based CoordinateType"""
104104
return get_position_from_offsets(self._x_offset, self._y_offset)
105105

106106
def get_style_properties(self) -> dict:

src/main/python/ttconv/scc/paragraph.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from ttconv.scc.style import SccCaptionStyle
3636
from ttconv.time_code import SmpteTimeCode
3737
from ttconv.scc.utils import get_position_from_offsets, get_extent_from_dimensions, convert_cells_to_percentages
38-
from ttconv.style_properties import PositionType, ExtentType, StyleProperties, LengthType, DisplayAlignType
38+
from ttconv.style_properties import CoordinateType, ExtentType, StyleProperties, LengthType, DisplayAlignType
3939

4040
LOGGER = logging.getLogger(__name__)
4141

@@ -168,7 +168,7 @@ def apply_roll_up_row_offsets(self):
168168

169169
self.set_row_offset(ROLL_UP_BASE_ROW)
170170

171-
def get_origin(self) -> PositionType:
171+
def get_origin(self) -> CoordinateType:
172172
"""Computes and returns the current paragraph origin, based on its content"""
173173
if len(self._caption_contents) > 0:
174174
x_offsets = [text.get_x_offset() for text in self._caption_contents if isinstance(text, SccCaptionText)]
@@ -312,7 +312,7 @@ def get_region(self) -> Region:
312312

313313
def _has_same_origin_as_region(self, region: Region) -> bool:
314314
"""Checks whether the region origin is the same as the paragraph origin"""
315-
region_origin: Optional[PositionType] = region.get_style(StyleProperties.Origin)
315+
region_origin: Optional[CoordinateType] = region.get_style(StyleProperties.Origin)
316316

317317
# Convert paragraph origin units into percentages
318318
paragraph_origin = convert_cells_to_percentages(self._paragraph.get_origin(), self._doc.get_cell_resolution())
@@ -352,7 +352,7 @@ def _extend_region_to_paragraph(self, region: Region):
352352
if paragraph_extent_pct.width.value > region_extent.width.value:
353353
# Resets the region width on the paragraph line width (up to the right of the safe area)
354354
# The region height always remains the same (depending on the region origin)
355-
region_origin: PositionType = region.get_style(StyleProperties.Origin)
355+
region_origin: CoordinateType = region.get_style(StyleProperties.Origin)
356356

357357
# Convert right cells coordinate to percentages
358358
right_pct = self._right * 100 / self._doc.get_cell_resolution().columns

src/main/python/ttconv/scc/utils.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,15 @@
2828
from typing import Union
2929

3030
from ttconv.model import CellResolutionType
31-
from ttconv.style_properties import LengthType, PositionType, ExtentType
31+
from ttconv.style_properties import LengthType, CoordinateType, ExtentType
3232

3333

34-
def get_position_from_offsets(x_offset: int, y_offset: int, units=LengthType.Units.c) -> PositionType:
34+
def get_position_from_offsets(x_offset: int, y_offset: int, units=LengthType.Units.c) -> CoordinateType:
3535
"""Converts offsets into position"""
3636
x_position = LengthType(value=x_offset, units=units)
3737
y_position = LengthType(value=y_offset, units=units)
3838

39-
return PositionType(x_position, y_position)
39+
return CoordinateType(x_position, y_position)
4040

4141

4242
def get_extent_from_dimensions(width: Union[int, Number], height: Union[int, Number], units=LengthType.Units.c) -> ExtentType:
@@ -47,10 +47,10 @@ def get_extent_from_dimensions(width: Union[int, Number], height: Union[int, Num
4747
return ExtentType(height, width)
4848

4949

50-
def convert_cells_to_percentages(dimensions: Union[PositionType, ExtentType], cell_resolution: CellResolutionType) -> Union[
51-
PositionType, ExtentType]:
50+
def convert_cells_to_percentages(dimensions: Union[CoordinateType, ExtentType], cell_resolution: CellResolutionType) -> Union[
51+
CoordinateType, ExtentType]:
5252
"""Converts dimensions from cell units to percentage units"""
53-
if isinstance(dimensions, PositionType):
53+
if isinstance(dimensions, CoordinateType):
5454
if dimensions.x.units is not LengthType.Units.c or dimensions.y.units is not LengthType.Units.c:
5555
raise ValueError("Dimensions to convert must be in cell units")
5656

0 commit comments

Comments
 (0)