Skip to content

Commit f77af29

Browse files
authored
Add support for IMSC 1.3
* Bump IMSC tests * Add support for tts:fontVariant (superscript/subscript)
1 parent 174bcb9 commit f77af29

17 files changed

+211
-15
lines changed

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -610,9 +610,9 @@ def from_xml(
610610
model_prop, model_value = prop.to_model(style_ctx, xml_elem)
611611
style_ctx.styles[model_prop] = model_value
612612

613-
except ValueError:
613+
except ValueError as e:
614614

615-
LOGGER.error("Error reading style property: %s", prop.__name__)
615+
LOGGER.error("Error reading style property %s with %s", prop.__name__, str(e))
616616

617617
# merge nested style attributes if the parent is a region element
618618

@@ -683,9 +683,9 @@ def from_xml(
683683

684684
initial_ctx.doc.put_initial_value(model_prop, model_value)
685685

686-
except (ValueError, TypeError):
686+
except (ValueError, TypeError) as e:
687687

688-
LOGGER.error("Error reading style property: %s", prop.__name__)
688+
LOGGER.error("Error reading style property %s with %s", prop.__name__, str(e))
689689

690690
return initial_ctx
691691

@@ -767,9 +767,9 @@ def process_specified_styling(self, xml_elem):
767767

768768
self.model_element.set_style(model_prop, model_value)
769769

770-
except ValueError:
770+
except ValueError as e:
771771

772-
LOGGER.error("Error reading style property: %s", prop.__name__)
772+
LOGGER.error("Error reading style property %s with %s", prop.__name__, str(e))
773773

774774
def process_set_style_properties(self, parent_ctx: ContentElement.ParsingContext, xml_elem):
775775
'''Processes style properties on `<set>` element
@@ -796,8 +796,8 @@ def process_set_style_properties(self, parent_ctx: ContentElement.ParsingContext
796796
)
797797
)
798798
break
799-
except ValueError:
800-
LOGGER.error("Error reading style property: %s", prop.__name__)
799+
except ValueError as e:
800+
LOGGER.error("Error reading style property %s with %s", prop.__name__, str(e))
801801

802802
def process_lang_attribute(self, parent_ctx: TTMLElement.ParsingContext, xml_elem):
803803
super().process_lang_attribute(parent_ctx, xml_elem)

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,22 @@ def from_model(cls, xml_element, model_value: styles.FontStyleType):
317317
xml_element.set(f"{{{cls.ns}}}{cls.local_name}", model_value.value)
318318

319319

320+
class FontVariant(StyleProperty):
321+
'''Corresponds to tts:fontVariant.'''
322+
323+
ns = xml_ns.TTS
324+
local_name = "fontVariant"
325+
model_prop = styles.StyleProperties.FontVariant
326+
327+
@classmethod
328+
def extract(cls, context: StyleParsingContext, xml_attrib: str):
329+
return styles.FontVariantType(xml_attrib)
330+
331+
@classmethod
332+
def from_model(cls, xml_element, model_value: styles.FontVariantType):
333+
xml_element.set(f"{{{cls.ns}}}{cls.local_name}", model_value.value)
334+
335+
320336
class FontWeight(StyleProperty):
321337
'''Corresponds to tts:fontWeight.'''
322338

src/main/python/ttconv/isd.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,11 @@ def get_region(self, region_id) -> typing.Optional[ISD.Region]:
161161

162162
def iter_regions(self) -> typing.Iterator[ISD.Region]:
163163
'''Returns an iterator over regions.'''
164-
return self._regions.values()
164+
return iter(self._regions.values())
165+
166+
def __iter__(self) -> typing.Iterator[ISD.Region]:
167+
'''Returns an iterator over the children of the element.'''
168+
return iter(self.iter_regions())
165169

166170
def __len__(self) -> int:
167171
'''Returns the number of regions of the ISD.'''
@@ -958,6 +962,9 @@ def compute(cls, parent: model.ContentElement, element: model.ContentElement):
958962
class FontStyle(StyleProcessor):
959963
style_prop = styles.StyleProperties.FontStyle
960964

965+
class FontVariant(StyleProcessor):
966+
style_prop = styles.StyleProperties.FontVariant
967+
961968
class FontWeight(StyleProcessor):
962969
style_prop = styles.StyleProperties.FontWeight
963970

src/main/python/ttconv/model.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@ class P(ContentElement):
495495
StyleProperties.FontFamily,
496496
StyleProperties.FontSize,
497497
StyleProperties.FontStyle,
498+
StyleProperties.FontVariant,
498499
StyleProperties.FontWeight,
499500
StyleProperties.LineHeight,
500501
StyleProperties.LinePadding,
@@ -525,6 +526,7 @@ class Span(ContentElement):
525526
StyleProperties.FontFamily,
526527
StyleProperties.FontSize,
527528
StyleProperties.FontStyle,
529+
StyleProperties.FontVariant,
528530
StyleProperties.FontWeight,
529531
StyleProperties.Opacity,
530532
StyleProperties.TextCombine,

src/main/python/ttconv/style_properties.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,14 @@ class FontStyleType(Enum):
165165
oblique = "oblique"
166166

167167

168+
class FontVariantType(Enum):
169+
'''tts:fontVariant value
170+
'''
171+
normal = "normal"
172+
superscript = "super"
173+
subscript = "sub"
174+
175+
168176
class FontWeightType(Enum):
169177
'''tts:fontWeight value
170178
'''
@@ -610,6 +618,21 @@ def validate(value):
610618
return isinstance(value, FontStyleType)
611619

612620

621+
class FontVariant(StyleProperty):
622+
'''Corresponds to tts:fontVariant.'''
623+
624+
is_inherited = True
625+
is_animatable = True
626+
627+
@staticmethod
628+
def make_initial_value():
629+
return FontVariantType.normal
630+
631+
@staticmethod
632+
def validate(value):
633+
return isinstance(value, FontVariantType)
634+
635+
613636
class FontWeight(StyleProperty):
614637
'''Corresponds to tts:fontWeight.'''
615638

@@ -717,8 +740,8 @@ def make_initial_value():
717740
@staticmethod
718741
def validate(value: CoordinateType):
719742
return isinstance(value, CoordinateType) \
720-
and value.x.units in (LengthType.Units.pct, LengthType.Units.px, LengthType.Units.rw) \
721-
and value.y.units in (LengthType.Units.pct, LengthType.Units.px, LengthType.Units.rh) \
743+
and value.x.units in (LengthType.Units.pct, LengthType.Units.px, LengthType.Units.rh, LengthType.Units.rw) \
744+
and value.y.units in (LengthType.Units.pct, LengthType.Units.px, LengthType.Units.rh, LengthType.Units.rw) \
722745
and value.x.is_non_negative() and value.y.is_non_negative()
723746

724747

@@ -771,8 +794,8 @@ def make_initial_value():
771794
@staticmethod
772795
def validate(value: PositionType):
773796
return isinstance(value, PositionType) \
774-
and value.h_offset.units in (LengthType.Units.pct, LengthType.Units.px, LengthType.Units.rw) \
775-
and value.v_offset.units in (LengthType.Units.pct, LengthType.Units.px, LengthType.Units.rh) \
797+
and value.h_offset.units in (LengthType.Units.pct, LengthType.Units.px, LengthType.Units.rh, LengthType.Units.rw) \
798+
and value.v_offset.units in (LengthType.Units.pct, LengthType.Units.px, LengthType.Units.rh, LengthType.Units.rw) \
776799
and value.h_offset.is_non_negative() and value.v_offset.is_non_negative()
777800

778801
class RubyAlign(StyleProperty):

src/test/python/test_imsc11text_filter.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,18 @@ def test_imsc_1_1_test_suite(self):
630630
filt = IMSC11TextFilter()
631631
filt.process(model)
632632

633+
def test_imsc_1_3_test_suite(self):
634+
for root, _subdirs, files in os.walk("src/test/resources/ttml/imsc-tests/imsc1_3/ttml"):
635+
for filename in files:
636+
(name, ext) = os.path.splitext(filename)
637+
if ext == ".ttml":
638+
with self.subTest(name):
639+
tree = et.parse(os.path.join(root, filename))
640+
model = imsc_reader.to_model(tree)
641+
self.assertIsNotNone(model)
642+
filt = IMSC11TextFilter()
643+
with self.assertRaises(ValueError):
644+
filt.process(model)
633645

634646
if __name__ == "__main__":
635647
unittest.main()

src/test/python/test_imsc_reader.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,15 @@ def test_imsc_1_1_test_suite(self):
189189
tree = et.parse(os.path.join(root, filename))
190190
self.assertIsNotNone(imsc_reader.to_model(tree))
191191

192+
def test_imsc_1_3_test_suite(self):
193+
for root, _subdirs, files in os.walk("src/test/resources/ttml/imsc-tests/imsc1_3/ttml"):
194+
for filename in files:
195+
(name, ext) = os.path.splitext(filename)
196+
if ext == ".ttml":
197+
with self.subTest(name):
198+
tree = et.parse(os.path.join(root, filename))
199+
self.assertIsNotNone(imsc_reader.to_model(tree))
200+
192201
def test_referential_styling(self):
193202
tree = et.parse('src/test/resources/ttml/referential_styling.ttml')
194203
doc = imsc_reader.to_model(tree)

src/test/python/test_imsc_writer.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,37 @@ def test_imsc_1_1_test_suite(self):
157157
with open(os.path.join(base_path, "tests.json"), "w", encoding="utf8") as fp:
158158
json.dump(manifest, fp)
159159

160+
def test_imsc_1_3_test_suite(self):
161+
manifest = []
162+
base_path = "build/imsc1_3"
163+
164+
for root, _subdirs, files in os.walk("src/test/resources/ttml/imsc-tests/imsc1_3/ttml"):
165+
for filename in files:
166+
(name, ext) = os.path.splitext(filename)
167+
if ext == ".ttml":
168+
with self.subTest(name), self.assertLogs() as logs:
169+
logging.getLogger().info("*****dummy*****") # dummy log
170+
tree = et.parse(os.path.join(root, filename))
171+
test_model = imsc_reader.to_model(tree)
172+
tree_from_model = imsc_writer.from_model(test_model)
173+
174+
test_dir_relative_path = os.path.basename(root)
175+
176+
test_relative_path = os.path.join(test_dir_relative_path, filename)
177+
178+
os.makedirs(os.path.join(base_path, "ttml", test_dir_relative_path), exist_ok=True)
179+
180+
with open(os.path.join(base_path, "ttml", test_relative_path), "wb") as f:
181+
f.write(et.tostring(tree_from_model.getroot(), 'utf-8'))
182+
183+
manifest.append({"path" : str(test_relative_path).replace('\\', '/')})
184+
185+
if len(logs.output) > 1:
186+
self.fail(logs.output)
187+
188+
with open(os.path.join(base_path, "tests.json"), "w", encoding="utf8") as fp:
189+
json.dump(manifest, fp)
190+
160191

161192
class FromModelBodyWriterTest(unittest.TestCase):
162193

src/test/python/test_isd.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ def setUp(self):
140140
t1 = model.Text(self.doc, "hello")
141141
span1.push_child(t1)
142142

143+
def test_iter(self):
144+
isd = ISD.from_model(self.doc, 2)
145+
r_ids = sorted(r.get_id() for r in isd)
146+
self.assertSequenceEqual(r_ids, sorted(("r1", "r2")))
147+
143148
def test_significant_times(self):
144149
self.assertSequenceEqual(ISD.significant_times(self.doc), sorted((0, 2, 3, 9, 1, 10, 4)))
145150

@@ -273,6 +278,23 @@ def test_imsc_1_1_test_suite(self):
273278
self.assertIsNotNone(isd)
274279
if len(logs.output) > 1:
275280
self.fail(logs.output)
281+
282+
def test_imsc_1_3_test_suite(self):
283+
for root, _subdirs, files in os.walk("src/test/resources/ttml/imsc-tests/imsc1_3/ttml"):
284+
for filename in files:
285+
(name, ext) = os.path.splitext(filename)
286+
if ext == ".ttml":
287+
with self.subTest(name), self.assertLogs() as logs:
288+
logging.getLogger().info("*****dummy*****") # dummy log
289+
tree = et.parse(os.path.join(root, filename))
290+
m = imsc_reader.to_model(tree)
291+
self.assertIsNotNone(m)
292+
sig_times = ISD.significant_times(m)
293+
for t in sig_times:
294+
isd = ISD.from_model(m, t)
295+
self.assertIsNotNone(isd)
296+
if len(logs.output) > 1:
297+
self.fail(logs.output)
276298

277299
class ComputeStyleTest(unittest.TestCase):
278300

src/test/python/test_model_style_properties.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,53 @@
2727

2828
# pylint: disable=R0201,C0115,C0116
2929

30+
from fractions import Fraction
3031
import unittest
3132
from ttconv import model
32-
from ttconv.style_properties import ExtentType, LengthType, StyleProperties, TextShadowType
33+
from ttconv.isd import ISD
34+
from ttconv.style_properties import ExtentType, FontVariantType, LengthType, NamedColors, StyleProperties, TextShadowType
3335

3436
class TestModelStyleProperties(unittest.TestCase):
3537

38+
def setUp(self):
39+
self.doc = model.ContentDocument()
40+
41+
self.r1 = model.Region("r1", self.doc)
42+
self.doc.put_region(self.r1)
43+
44+
self.b = model.Body(self.doc)
45+
self.doc.set_body(self.b)
46+
47+
self.div1 = model.Div(self.doc)
48+
self.div1.set_region(self.r1)
49+
self.b.push_child(self.div1)
50+
51+
self.p1 = model.P(self.doc)
52+
self.div1.push_child(self.p1)
53+
54+
self.span1 = model.Span(self.doc)
55+
self.p1.push_child(self.span1)
56+
57+
self.text1 = model.Text(self.doc, "span1")
58+
self.span1.push_child(self.text1)
59+
60+
def test_fontVariant(self):
61+
self.b.set_style(StyleProperties.FontVariant, FontVariantType.subscript)
62+
self.assertFalse(self.b.is_style_applicable(StyleProperties.FontVariant))
63+
64+
a = model.DiscreteAnimationStep(StyleProperties.FontVariant, 1, None, FontVariantType.superscript)
65+
self.p1.add_animation_step(a)
66+
67+
isd = ISD.from_model(self.doc, 0)
68+
r = list(isd)[0]
69+
self.assertIsNone(r[0][0].get_style(StyleProperties.FontVariant))
70+
self.assertEqual(r[0][0][0][0].get_style(StyleProperties.FontVariant), FontVariantType.subscript)
71+
72+
isd = ISD.from_model(self.doc, 1)
73+
r = list(isd)[0]
74+
self.assertIsNone(r[0][0].get_style(StyleProperties.FontVariant))
75+
self.assertEqual(r[0][0][0][0].get_style(StyleProperties.FontVariant), FontVariantType.superscript)
76+
3677
def test_make_initial(self):
3778
for style in StyleProperties.ALL:
3879
with self.subTest(style.__name__):

0 commit comments

Comments
 (0)