Skip to content

Commit

Permalink
Fix FontFamily model and parsing/serialization (#167, #168)
Browse files Browse the repository at this point in the history
  • Loading branch information
palemieux authored Dec 22, 2020
1 parent a19473d commit befa19e
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 20 deletions.
15 changes: 10 additions & 5 deletions src/main/python/ttconv/imsc/style_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,14 +261,19 @@ class FontFamily(StyleProperty):
@classmethod
def extract(cls, context: StyleParsingContext, xml_attrib: str):

return tuple(map(
lambda f: "monospaceSerif" if f == "default" else f,
utils.parse_font_families(xml_attrib)
))
return tuple(
map(
lambda f: styles.GenericFontFamilyType.monospaceSerif if f is styles.GenericFontFamilyType.default else f,
utils.parse_font_families(xml_attrib)
)
)

@classmethod
def from_model(cls, xml_element, model_value):
xml_element.set(f"{{{cls.ns}}}{cls.local_name}", model_value[0])
xml_element.set(
f"{{{cls.ns}}}{cls.local_name}",
utils.serialize_font_family(model_value)
)


class FontSize(StyleProperty):
Expand Down
42 changes: 36 additions & 6 deletions src/main/python/ttconv/imsc/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,6 @@

_LENGTH_RE = re.compile(r"^((?:\+|\-)?\d*(?:\.\d+)?)(px|em|c|%|rh|rw)$")

_FAMILIES_SEPARATOR = re.compile(r"(?<=[^\\]),")
_FAMILIES_ESCAPED_CHAR = re.compile(r"\\(.)")

_CLOCK_TIME_FRACTION_RE = re.compile(r"^(\d{2,}):(\d\d):(\d\d(?:\.\d+)?)$")
_CLOCK_TIME_FRAMES_RE = re.compile(r"^(\d{2,}):(\d\d):(\d\d):(\d{2,})$")
_OFFSET_FRAME_RE = re.compile(r"^(\d+(?:\.\d+)?)f")
Expand Down Expand Up @@ -114,22 +111,55 @@ def parse_length(attr_value: str) -> typing.Tuple[float, str]:
raise ValueError("Bad length syntax")


_FAMILIES_ESCAPED_CHAR = re.compile(r"\\(.)")
_SINGLE_QUOTE_PATTERN = "(?:'(?P<single_quote>(.+?)(?<!\\\\))')"
_DOUBLE_QUOTE_PATTERN = "(?:\"(?P<double_quote>(.+?)(?<!\\\\))\")"
_NO_QUOTE_PATTERN = "(?P<no_quote>(?:\\\\.|[^'\", ])(?:\\\\.|[^'\",])+)"

_FONT_FAMILY_PATTERN = re.compile(
"|".join(
(
_SINGLE_QUOTE_PATTERN,
_DOUBLE_QUOTE_PATTERN,
_NO_QUOTE_PATTERN
)
)
)

def parse_font_families(attr_value: str) -> typing.List[str]:
'''Parses th TTML \\<font-family\\> value in `attr_value` into a list of font families'''

rslt = []

for family in map(str.strip, _FAMILIES_SEPARATOR.split(attr_value)):
for m in _FONT_FAMILY_PATTERN.finditer(attr_value):

unquoted_family = family[1:-1] if family[0] == "'" or family[0] == '"' else family
is_quoted = m.lastgroup in ("single_quote", "double_quote")

rslt.append(_FAMILIES_ESCAPED_CHAR.sub(r"\1", unquoted_family))
escaped_family = _FAMILIES_ESCAPED_CHAR.sub(r"\1", m.group(m.lastgroup))

if not is_quoted and escaped_family in styles.GenericFontFamilyType.__members__:
rslt.append(styles.GenericFontFamilyType(escaped_family))
else:
rslt.append(escaped_family)


if len(rslt) == 0:
raise ValueError("Bad syntax")

return rslt

def serialize_font_family(font_family: typing.Tuple[typing.Union[str, styles.GenericFontFamilyType], ...]):
'''Serialize model FontFamily to tts:fontFamily
'''

def _serialize_one_family(family):
if isinstance(family, styles.GenericFontFamilyType):
return family.value

return '"' + family.replace('"', r'\"') + '"'

return ", ".join(map(_serialize_one_family, font_family))


def parse_time_expression(tick_rate: typing.Optional[int], frame_rate: typing.Optional[Fraction], time_expr: str) -> Fraction:
'''Parse a TTML time expression in a fractional number in seconds
Expand Down
17 changes: 14 additions & 3 deletions src/main/python/ttconv/style_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,17 @@ class ExtentType:
height: LengthType = LengthType()
width: LengthType = LengthType()

class GenericFontFamilyType(Enum):
'''\\<generic-family-name\\>
'''
default = "default"
monospace = "monospace"
sansSerif = "sansSerif"
serif = "serif"
monospaceSansSerif = "monospaceSansSerif"
monospaceSerif = "monospaceSerif"
proportionalSansSerif = "proportionalSansSerif"
proportionalSerif = "proportionalSerif"

class FontStyleType(Enum):
'''tts:fontStyle value
Expand Down Expand Up @@ -507,11 +518,11 @@ class FontFamily(StyleProperty):

@staticmethod
def make_initial_value():
return ("default",)
return (GenericFontFamilyType.default,)

@staticmethod
def validate(value: typing.List[str]):
return isinstance(value, tuple) and all(lambda i: isinstance(i, str) for i in value)
def validate(value: typing.Tuple[typing.Union[str, GenericFontFamilyType]]):
return isinstance(value, tuple) and all(lambda i: isinstance(i, (str, GenericFontFamilyType)) for i in value)


class FontSize(StyleProperty):
Expand Down
30 changes: 24 additions & 6 deletions src/test/python/test_imsc_font_families_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,41 @@
# pylint: disable=R0201,C0115,C0116

import unittest
from ttconv.imsc.utils import parse_font_families
from ttconv.imsc.utils import parse_font_families, serialize_font_family
import ttconv.style_properties as styles

class IMSCReaderTest(unittest.TestCase):

tests = [
["default", ["default"]],
_parse_tests = [
["default", [styles.GenericFontFamilyType.default]],
["'default'", ["default"]],
["foo, 'bar good'", ["foo", "bar good"]],
['foo, "bar good"', ["foo", "bar good"]],
[r'foo, "bar \good"', ["foo", "bar good"]],
[r'foo, "bar \,good"', ["foo", "bar ,good"]]
[r'foo, "bar \,good"', ["foo", "bar ,good"]],
[r'foo, "bar,good"', ["foo", "bar,good"]]
]

def test_font_families(self):
for test in self.tests:
def test_parse_font_families(self):
for test in self._parse_tests:
with self.subTest(test[0]):
c = parse_font_families(test[0])
self.assertEqual(c, test[1])


_serialize_tests = [
[(styles.GenericFontFamilyType.default,), "default"],
[("default",), '"default"'],
[(styles.GenericFontFamilyType.proportionalSansSerif, "bar good"), 'proportionalSansSerif, "bar good"'],
[("foo", "bar, good"), r'"foo", "bar, good"'],
[("bar\"good",), r'"bar\"good"']
]

def test_serialize_font_families(self):
for test in self._serialize_tests:
with self.subTest(test[0]):
c = serialize_font_family(test[0])
self.assertEqual(c, test[1])

if __name__ == '__main__':
unittest.main()

0 comments on commit befa19e

Please sign in to comment.