Skip to content

Commit 7683e09

Browse files
authored
Add support for tts:position in IMSC reader (#36)
1 parent 19ab99b commit 7683e09

File tree

7 files changed

+280
-16
lines changed

7 files changed

+280
-16
lines changed

.gitmodules

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
[submodule "src/test/resources/ttml/imsc-tests"]
22
path = src/test/resources/ttml/imsc-tests
33
url = https://github.com/w3c/imsc-tests
4+
[submodule "imsc-tests/"]
5+
url = https://github.com/sandflow/imsc-tests.git

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

+5-7
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,13 @@ class TTMLElement:
4343
'''Static information about a TTML element
4444
'''
4545

46-
class ParsingContext:
46+
class ParsingContext(imsc_styles.StyleParsingContext):
4747
'''State information when parsing a TTML element'''
4848

4949
def __init__(self, ttml_class: TTMLElement, parent_ctx: typing.Optional[TTMLElement.ParsingContext] = None):
5050

5151
self.doc = parent_ctx.doc if parent_ctx is not None else model.Document()
5252

53-
self.style_context = parent_ctx.style_context if parent_ctx else imsc_styles.StyleParsingContext()
54-
5553
self.style_elements: typing.Dict[str, StyleElement] = parent_ctx.style_elements if parent_ctx else {}
5654

5755
self.temporal_context = parent_ctx.temporal_context if parent_ctx else imsc_attr.TemporalAttributeParsingContext()
@@ -463,7 +461,7 @@ def from_xml(parent_ctx: TTMLElement.ParsingContext, xml_elem) -> typing.Optiona
463461

464462
try:
465463

466-
style_ctx.styles[prop.model_prop] = prop.extract(style_ctx.style_context, xml_elem.attrib.get(attr))
464+
style_ctx.styles[prop.model_prop] = prop.extract(style_ctx, xml_elem.attrib.get(attr))
467465

468466
except ValueError:
469467

@@ -526,7 +524,7 @@ def from_xml(parent_ctx: TTMLElement.ParsingContext, xml_elem) -> typing.Optiona
526524

527525
initial_ctx.doc.put_initial_value(
528526
prop.model_prop,
529-
prop.extract(initial_ctx.style_context, xml_elem.attrib.get(attr))
527+
prop.extract(initial_ctx, xml_elem.attrib.get(attr))
530528
)
531529

532530
except (ValueError, TypeError):
@@ -608,7 +606,7 @@ def process_specified_styling(self, xml_elem):
608606
try:
609607
self.model_element.set_style(
610608
prop.model_prop,
611-
prop.extract(self.style_context, xml_elem.attrib.get(attr))
609+
prop.extract(self, xml_elem.attrib.get(attr))
612610
)
613611
except ValueError:
614612
LOGGER.error("Error reading style property: %s", prop.__name__)
@@ -633,7 +631,7 @@ def process_set_style_properties(self, parent_ctx: TTMLElement, xml_elem):
633631
prop.model_prop,
634632
self.desired_begin,
635633
self.desired_end,
636-
prop.extract(self.style_context, xml_elem.attrib.get(attr))
634+
prop.extract(self, xml_elem.attrib.get(attr))
637635
)
638636
)
639637
break

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

+22-3
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@
3131
import ttconv.imsc.namespaces as xml_ns
3232
import ttconv.style_properties as styles
3333
import ttconv.imsc.utils as utils
34+
import ttconv.model as model
3435

3536
class StyleParsingContext:
36-
pass
37+
doc: model.Document
3738

3839
class StyleProperty:
3940
'''Base class for style properties'''
@@ -486,9 +487,27 @@ class Position(StyleProperty):
486487
@classmethod
487488
def extract(cls, context: StyleParsingContext, xml_attrib: str):
488489

490+
(h_edge, h_offset, v_edge, v_offset) = utils.parse_position(xml_attrib)
491+
492+
if h_edge == "right":
493+
if h_offset.units is styles.LengthType.Units.px:
494+
h_offset = styles.LengthType(context.doc.get_px_resolution().width - h_offset.value, h_offset.units)
495+
elif h_offset.units is styles.LengthType.Units.pct or h_offset.units is styles.LengthType.Units.rw:
496+
h_offset = styles.LengthType(100 - h_offset.value, h_offset.units)
497+
else:
498+
raise ValueError("Units other than px, pct, rh, rw used in tts:position")
499+
500+
if v_edge == "bottom":
501+
if v_offset.units is styles.LengthType.Units.px:
502+
v_offset = styles.LengthType(context.doc.get_px_resolution().height - v_offset.value, v_offset.units)
503+
elif v_offset.units is styles.LengthType.Units.pct or v_offset.units is styles.LengthType.Units.rh:
504+
v_offset = styles.LengthType(100 - v_offset.value, v_offset.units)
505+
else:
506+
raise ValueError("Units other than px, pct, rh, rw used in tts:position")
507+
489508
return styles.PositionType(
490-
x=styles.LengthType(),
491-
y=styles.LengthType()
509+
x=h_offset,
510+
y=v_offset
492511
)
493512

494513
@classmethod

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

+125
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,128 @@ def parse_time_expression(tick_rate: int, frame_rate: Fraction, time_expr: str)
186186
frames / frame_rate
187187

188188
raise ValueError("Syntax error")
189+
190+
def parse_position(attr_value: str) -> typing.Tuple[str, styles.LengthType, str, styles.LengthType]:
191+
'''Parse a TTML \\<position\\> value into offsets from a horizontal and vertical edge
192+
'''
193+
194+
length_50pct = styles.LengthType(value=50, units=styles.LengthType.Units.pct)
195+
length_0pct = styles.LengthType(value=0, units=styles.LengthType.Units.pct)
196+
197+
h_edges = {"left", "right"}
198+
v_edges = {"top", "bottom"}
199+
200+
h_edge: str = None
201+
h_offset: styles.LengthType = None
202+
v_edge: str = None
203+
v_offset: styles.LengthType = None
204+
205+
items = attr_value.split()
206+
207+
if len(items) in (1, 2):
208+
209+
# begin processing 1 and 2 components
210+
211+
while len(items) > 0:
212+
213+
cur_item = items.pop(0)
214+
215+
if cur_item in h_edges:
216+
217+
h_edge = cur_item
218+
h_offset = length_0pct
219+
220+
elif cur_item in v_edges:
221+
222+
v_edge = cur_item
223+
v_offset = length_0pct
224+
225+
elif cur_item == "center":
226+
227+
if h_edge is None:
228+
229+
h_edge = "left"
230+
h_offset = length_50pct
231+
232+
elif v_edge is None:
233+
234+
v_edge = "top"
235+
v_offset = length_50pct
236+
237+
else:
238+
239+
(value, units) = parse_length(cur_item)
240+
241+
if h_edge is None:
242+
243+
h_edge = "left"
244+
h_offset = styles.LengthType(value, styles.LengthType.Units(units))
245+
246+
elif v_edge is None:
247+
248+
v_edge = "top"
249+
v_offset = styles.LengthType(value, styles.LengthType.Units(units))
250+
251+
# end processing 1 and 2 components
252+
253+
else:
254+
255+
# begin processing 3 and 4 components
256+
257+
while len(items) > 0:
258+
259+
cur_item = items.pop(0)
260+
261+
if cur_item in h_edges:
262+
263+
h_edge = cur_item
264+
265+
if v_edge is not None and v_offset is None:
266+
v_offset = length_0pct
267+
268+
elif cur_item in v_edges:
269+
270+
v_edge = cur_item
271+
272+
if h_edge is not None and h_offset is None:
273+
h_offset = length_0pct
274+
275+
elif cur_item == "center":
276+
277+
pass
278+
279+
else:
280+
281+
(value, units) = parse_length(cur_item)
282+
283+
if h_edge is not None and h_offset is None:
284+
285+
h_offset = styles.LengthType(value, styles.LengthType.Units(units))
286+
287+
if v_edge is not None and v_offset is None:
288+
289+
v_offset = styles.LengthType(value, styles.LengthType.Units(units))
290+
291+
# end processing 3 and 4 components
292+
293+
# fill-in missing components, if any
294+
295+
if h_offset is None:
296+
297+
if h_edge is None:
298+
h_edge = "left"
299+
h_offset = length_50pct
300+
else:
301+
h_offset = length_0pct
302+
303+
if v_offset is None:
304+
305+
if v_edge is None:
306+
v_edge = "top"
307+
v_offset = length_50pct
308+
else:
309+
v_offset = length_0pct
310+
311+
312+
return (h_edge, h_offset, v_edge, v_offset)
313+

src/main/python/ttconv/style_properties.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -317,11 +317,11 @@ class WritingModeType(Enum):
317317

318318
@dataclass(frozen=True)
319319
class PositionType:
320-
'''Coordinates in the root container region
320+
'''Coordinates (`x`, `y`) in the root container region, as measure from the left and top edges, respectively.
321321
'''
322+
x: LengthType
323+
y: LengthType
322324

323-
x: LengthType = None
324-
y: LengthType = None
325325

326326
#
327327
# Style properties
@@ -618,8 +618,8 @@ def make_initial_value():
618618
@staticmethod
619619
def validate(value: PositionType):
620620
return isinstance(value, PositionType) \
621-
and value.x.units in (LengthType.Units.pct, LengthType.Units.px, LengthType.Units.c) \
622-
and value.y.units in (LengthType.Units.pct, LengthType.Units.px, LengthType.Units.c)
621+
and value.x.units in (LengthType.Units.pct, LengthType.Units.px, LengthType.Units.c, LengthType.Units.rw) \
622+
and value.y.units in (LengthType.Units.pct, LengthType.Units.px, LengthType.Units.c, LengthType.Units.rh)
623623

624624

625625
class Overflow(StyleProperty):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#!/usr/bin/env python
2+
# -*- coding: UTF-8 -*-
3+
4+
# Copyright (c) 2020, Sandflow Consulting LLC
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
#
9+
# 1. Redistributions of source code must retain the above copyright notice, this
10+
# list of conditions and the following disclaimer.
11+
# 2. Redistributions in binary form must reproduce the above copyright notice,
12+
# this list of conditions and the following disclaimer in the documentation
13+
# and/or other materials provided with the distribution.
14+
#
15+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
19+
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20+
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22+
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25+
26+
'''Unit tests for the IMSC \\<position\\> parser'''
27+
28+
# pylint: disable=R0201,C0115,C0116
29+
30+
import unittest
31+
from ttconv.imsc.utils import parse_position
32+
import ttconv.style_properties as styles
33+
34+
def position(h_edge, h_offval, h_offunit, v_edge, v_offval, v_offunit):
35+
return (
36+
h_edge,
37+
styles.LengthType(value=h_offval, units=styles.LengthType.Units(h_offunit)),
38+
v_edge,
39+
styles.LengthType(value=v_offval, units=styles.LengthType.Units(v_offunit))
40+
)
41+
42+
class IMSCPositionTest(unittest.TestCase):
43+
44+
tests = [
45+
# one component
46+
["center", position("left", 50, "%", "top", 50, "%")],
47+
["left", position("left", 0, "%", "top", 50, "%")],
48+
["right", position("right", 0, "%", "top", 50, "%")],
49+
["top", position("left", 50, "%", "top", 0, "%")],
50+
["bottom", position("left", 50, "%", "bottom", 0, "%")],
51+
["25%", position("left", 25, "%", "top", 50, "%")],
52+
# two components
53+
["bottom center", position("left", 50, "%", "bottom", 0, "%")],
54+
["bottom left", position("left", 0, "%", "bottom", 0, "%")],
55+
["bottom right", position("right", 0, "%", "bottom", 0, "%")],
56+
["center center", position("left", 50, "%", "top", 50, "%")],
57+
["center top", position("left", 50, "%", "top", 0, "%")],
58+
["center bottom", position("left", 50, "%", "bottom", 0, "%")],
59+
["center left", position("left", 0, "%", "top", 50, "%")],
60+
["center right", position("right", 0, "%", "top", 50, "%")],
61+
["center 33%", position("left", 50, "%", "top", 33, "%")],
62+
["left center", position("left", 0, "%", "top", 50, "%")],
63+
["left top", position("left", 0, "%", "top", 0, "%")],
64+
["left bottom", position("left", 0, "%", "bottom", 0, "%")],
65+
["left 45%", position("left", 0, "%", "top", 45, "%")],
66+
["right center", position("right", 0, "%", "top", 50, "%")],
67+
["right top", position("right", 0, "%", "top", 0, "%")],
68+
["right bottom", position("right", 0, "%", "bottom", 0, "%")],
69+
["right 20%", position("right", 0, "%", "top", 20, "%")],
70+
["top center", position("left", 50, "%", "top", 0, "%")],
71+
["top left", position("left", 0, "%", "top", 0, "%")],
72+
["top right", position("right", 0, "%", "top", 0, "%")],
73+
["75% center", position("left", 75, "%", "top", 50, "%")],
74+
["75% top", position("left", 75, "%", "top", 0, "%")],
75+
["75% bottom", position("left", 75, "%", "bottom", 0, "%")],
76+
["75% 75%", position("left", 75, "%", "top", 75, "%")],
77+
# three components
78+
["bottom left 1%", position("left", 1, "%", "bottom", 0, "%")],
79+
["bottom right 2%", position("right", 2, "%", "bottom", 0, "%")],
80+
["bottom 3% center", position("left", 50, "%", "bottom", 3, "%")],
81+
["bottom 4% left", position("left", 0, "%", "bottom", 4, "%")],
82+
["bottom 5% right", position("right", 0, "%", "bottom", 5, "%")],
83+
["center bottom 6%", position("left", 50, "%", "bottom", 6, "%")],
84+
["center left 7%", position("left", 7, "%", "top", 50, "%")],
85+
["center right 8%", position("right", 8, "%", "top", 50, "%")],
86+
["center top 9%", position("left", 50, "%", "top", 9, "%")],
87+
["left bottom 10%", position("left", 0, "%", "bottom", 10, "%")],
88+
["left top 11%", position("left", 0, "%", "top", 11, "%")],
89+
["left 12% bottom", position("left", 12, "%", "bottom", 0, "%")],
90+
["left 13% center", position("left", 13, "%", "top", 50, "%")],
91+
["left 14% top", position("left", 14, "%", "top", 0, "%")],
92+
["right bottom 15%", position("right", 0, "%", "bottom", 15, "%")],
93+
["right top 16%", position("right", 0, "%", "top", 16, "%")],
94+
["right 17% bottom", position("right", 17, "%", "bottom", 0, "%")],
95+
["right 18% center", position("right", 18, "%", "top", 50, "%")],
96+
["right 19% top", position("right", 19, "%", "top", 0, "%")],
97+
["top left 20%", position("left", 20, "%", "top", 0, "%")],
98+
["top right 21%", position("right", 21, "%", "top", 0, "%")],
99+
["top 22% center", position("left", 50, "%", "top", 22, "%")],
100+
["top 23% left", position("left", 0, "%", "top", 23, "%")],
101+
["top 24% right", position("right", 0, "%", "top", 24, "%")],
102+
# four components
103+
["bottom 25% left 75%", position("left", 75, "%", "bottom", 25, "%")],
104+
["bottom 25% right 75%", position("right", 75, "%", "bottom", 25, "%")],
105+
["left 25% bottom 75%", position("left", 25, "%", "bottom", 75, "%")],
106+
["right 25% bottom 75%", position("right", 25, "%", "bottom", 75, "%")],
107+
["top 25% left 75%", position("left", 75, "%", "top", 25, "%")],
108+
["top 25% right 75%", position("right", 75, "%", "top", 25, "%")],
109+
["left 25% top 75%", position("left", 25, "%", "top", 75, "%")],
110+
["right 25% top 75%", position("right", 25, "%", "top", 75, "%")]
111+
]
112+
113+
def test_positions(self):
114+
for test in self.tests:
115+
with self.subTest(test[0]):
116+
c = parse_position(test[0])
117+
self.assertEqual(c, test[1])
118+
119+
if __name__ == '__main__':
120+
unittest.main()

0 commit comments

Comments
 (0)