Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 47 additions & 30 deletions kiva/fonttools/_scan_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,16 @@ class FontEntry(object):
""" A class for storing Font properties. It is used when populating
the font lookup dictionary.
"""
def __init__(self, fname="", name="", style="normal", variant="normal",
weight="normal", stretch="normal", size="medium"):
def __init__(self, fname="", family="", style="normal", variant="normal",
weight="normal", stretch="normal", size="medium",
face_index=0):
self.fname = fname
self.name = name
self.family = family
self.style = style
self.variant = variant
self.weight = weight
self.stretch = stretch
self.face_index = face_index
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is integrating the face_index pieces that we're introduced by #605 to the refactored font parsing code.

try:
self.size = str(float(size))
except ValueError:
Expand All @@ -74,8 +76,8 @@ def __init__(self, fname="", name="", style="normal", variant="normal",
def __repr__(self):
fname = os.path.basename(self.fname)
return (
f"<FontEntry '{self.name}' ({fname}) {self.style} {self.variant} "
f"{self.weight} {self.stretch}>"
f"<FontEntry '{self.family}' ({fname}[{self.face_index}]) "
f"{self.style} {self.variant} {self.weight} {self.stretch}>"
)


Expand Down Expand Up @@ -120,8 +122,8 @@ def _build_ttf_entries(fpath):
if ext.lower() == ".ttc":
collection = TTCollection(fp)
try:
for font in collection.fonts:
entries.append(_ttf_font_property(fpath, font))
for idx, font in enumerate(collection.fonts):
entries.append(_ttf_font_property(fpath, font, idx))
except Exception:
logger.error(_FONT_ENTRY_ERR_MSG, fpath, exc_info=True)
else:
Expand All @@ -144,22 +146,22 @@ def _afm_font_property(fontpath, font):

*font* is a class:`AFM` instance.
"""
name = font.get_familyname()
family = font.get_familyname()
fontname = font.get_fontname().lower()

# Styles are: italic, oblique, and normal (default)
if font.get_angle() != 0 or name.lower().find("italic") >= 0:
if font.get_angle() != 0 or family.lower().find("italic") >= 0:
style = "italic"
elif name.lower().find("oblique") >= 0:
elif family.lower().find("oblique") >= 0:
style = "oblique"
else:
style = "normal"

# Variants are: small-caps and normal (default)
# NOTE: Not sure how many fonts actually have these strings in their name
# NOTE: Not sure how many fonts actually have these strings in their family
variant = "normal"
for value in ("capitals", "small-caps"):
if value in name.lower():
if value in family.lower():
variant = "small-caps"
break

Expand Down Expand Up @@ -194,34 +196,40 @@ def _afm_font_property(fontpath, font):

# All AFM fonts are apparently scalable.
size = "scalable"
return FontEntry(fontpath, name, style, variant, weight, stretch, size)
return FontEntry(fontpath, family, style, variant, weight, stretch, size)


def _ttf_font_property(fpath, font):
def _ttf_font_property(fpath, font, face_index=0):
""" A function for populating the :class:`FontEntry` by extracting
information from the TrueType font file.

*font* is a :class:`TTFont` instance.
"""
props = get_ttf_prop_dict(font)
name = props.get("name")
if name is None:
raise KeyError("No name could be found for: {}".format(fpath))
family = props.get("family")
if family is None:
raise KeyError("No family could be found for: {}".format(fpath))

# Some properties
full_name = props.get("full_name", "").lower()
style_prop = props.get("style", "").lower()
if style_prop == "":
# For backwards compatibility with previous parsing behavior
style_prop = full_name
Comment on lines +217 to +218
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We used to use the full_name when checking the style and weight of a font.


# Styles are: italic, oblique, and normal (default)
sfnt4 = props.get("sfnt4", "").lower()
if sfnt4.find("oblique") >= 0:
if style_prop.find("oblique") >= 0:
style = "oblique"
elif sfnt4.find("italic") >= 0:
elif style_prop.find("italic") >= 0:
style = "italic"
else:
style = "normal"

# Variants are: small-caps and normal (default)
# NOTE: Not sure how many fonts actually have these strings in their name
# NOTE: Not sure how many fonts actually have these strings in their family
variant = "normal"
for value in ("capitals", "small-caps"):
if value in name.lower():
for value in ("capitals", "small-caps", "smallcaps"):
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made this addition because there's a font named "Bodoni 72 Smallcaps Book" on macOS.

if value in family.lower():
variant = "small-caps"
break

Expand All @@ -230,7 +238,7 @@ def _ttf_font_property(fpath, font):
# lighter and bolder are also allowed.
weight = None
for w in weight_dict.keys():
if sfnt4.find(w) >= 0:
if style_prop.find(w) >= 0:
weight = w
break
if not weight:
Expand All @@ -243,13 +251,13 @@ def _ttf_font_property(fpath, font):
# and ultra-expanded.
# Relative stretches are: wider, narrower
# Child value is: inherit
if sfnt4.find("demi cond") >= 0:
if full_name.find("demi cond") >= 0:
stretch = "semi-condensed"
elif (sfnt4.find("narrow") >= 0
or sfnt4.find("condensed") >= 0
or sfnt4.find("cond") >= 0):
elif (full_name.find("narrow") >= 0
or full_name.find("condensed") >= 0
or full_name.find("cond") >= 0):
stretch = "condensed"
elif sfnt4.find("wide") >= 0 or sfnt4.find("expanded") >= 0:
elif full_name.find("wide") >= 0 or full_name.find("expanded") >= 0:
stretch = "expanded"
else:
stretch = "normal"
Expand All @@ -263,4 +271,13 @@ def _ttf_font_property(fpath, font):

# !!!! Incomplete
size = "scalable"
return FontEntry(fpath, name, style, variant, weight, stretch, size)
return FontEntry(
fname=fpath,
family=family,
style=style,
variant=variant,
weight=weight,
stretch=stretch,
size=size,
face_index=face_index,
)
82 changes: 55 additions & 27 deletions kiva/fonttools/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,67 @@
# Thanks for using Enthought open source!
from kiva.fonttools._constants import weight_dict

# Unicode & Apple
_plat_ids = (0, 1)
_english_id = 0
# MS
# https://docs.microsoft.com/en-us/typography/opentype/spec/name#windows-language-ids # noqa: E501
_ms_plat_id = 3
_ms_english_ids = {
0x0C09: "Australia",
0x2809: "Belize",
0x1009: "Canada",
0x2409: "Caribbean",
0x4009: "India",
0x1809: "Ireland",
0x2009: "Jamaica",
0x4409: "Malaysia",
0x1409: "New Zealand",
0x3409: "Republic of the Philippines",
0x4809: "Singapore",
0x1C09: "South Africa",
0x2C09: "Trinidad and Tobago",
0x0809: "United Kingdom",
0x0409: "United States",
0x3009: "Zimbabwe",
}
# TrueType 'name' table IDs
# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html # noqa: E501
_name_ids = {
0: "copyright",
1: "family",
2: "style",
3: "unique_subfamily_id",
4: "full_name",
5: "version",
6: "postscript_name",
}


def get_ttf_prop_dict(font):
""" Return the property dictionary from a :class:`TTFont` instance.
""" Parse the 'name' table of a :class:`TTFont` instance.
"""
n = font["name"]
propdict = {}
for prop in n.names:
try:
if "name" in propdict and "sfnt4" in propdict:
break
elif prop.nameID == 1 and "name" not in propdict:
propdict["name"] = _decode_prop(prop.string)
elif prop.nameID == 4 and "sfnt4" not in propdict:
propdict["sfnt4"] = _decode_prop(prop.string)
except UnicodeDecodeError:
table = font["name"]
for rec in table.names:
# We only care about records in English
plat, lang = rec.platformID, rec.langID
if not ((plat in _plat_ids and lang == _english_id)
or (plat == _ms_plat_id and lang in _ms_english_ids)):
continue
Comment on lines +55 to +59
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need this check to pick up older TTF/TTC files and newer OTF files (which use the Microsoft plaformID)

# And of those, just the ones we have names for
if rec.nameID not in _name_ids:
continue

# Convert the nameID to a nice string
key = _name_ids[rec.nameID]
# Skip duplicate records
if key in propdict:
continue

# Use the NameRecord's toStr() method instead of ad-hoc decoding
propdict[key] = rec.toStr()

return propdict


Expand All @@ -45,19 +89,3 @@ def weight_as_number(weight):
else:
raise ValueError("weight not a valid integer")
return weight


def _decode_prop(prop):
""" Decode a prop string.

Parameters
----------
prop : bytestring

Returns
-------
string
"""
# Adapted from: https://gist.github.com/pklaus/dce37521579513c574d0
encoding = "utf-16-be" if b"\x00" in prop else "utf-8"
return prop.decode(encoding)
34 changes: 19 additions & 15 deletions kiva/fonttools/tests/test_scan_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ def test_fontlist_from_ttc(self):

# Then
self.assertEqual(len(fontlist), 2)
for fontprop in fontlist:
for idx, fontprop in enumerate(fontlist):
self.assertIsInstance(fontprop, FontEntry)
self.assertEqual(fontprop.face_index, idx)
Comment thread
rahulporuri marked this conversation as resolved.

@mock.patch("kiva.fonttools._scan_parse._ttf_font_property",
side_effect=ValueError)
Expand Down Expand Up @@ -103,7 +104,7 @@ def get_weight(self):

# Given
fake_font = FakeAFM("TestyFont", "Testy", 0, "Bold")
exp_name = "Testy"
exp_family = "Testy"
exp_style = "normal"
exp_variant = "normal"
exp_weight = 700
Expand All @@ -112,7 +113,7 @@ def get_weight(self):
# When
entry = _afm_font_property(fake_path, fake_font)
# Then
self.assertEqual(entry.name, exp_name)
self.assertEqual(entry.family, exp_family)
self.assertEqual(entry.style, exp_style)
self.assertEqual(entry.variant, exp_variant)
self.assertEqual(entry.weight, exp_weight)
Expand Down Expand Up @@ -158,7 +159,7 @@ class TestTTFFontEntry(unittest.TestCase):
def test_font(self):
# Given
test_font = os.path.join(data_dir, "TestTTF.ttf")
exp_name = "Test TTF"
exp_family = "Test TTF"
exp_style = "normal"
exp_variant = "normal"
exp_weight = 400
Expand All @@ -169,7 +170,7 @@ def test_font(self):
entry = _ttf_font_property(test_font, TTFont(test_font))

# Then
self.assertEqual(entry.name, exp_name)
self.assertEqual(entry.family, exp_family)
self.assertEqual(entry.style, exp_style)
self.assertEqual(entry.variant, exp_variant)
self.assertEqual(entry.weight, exp_weight)
Expand All @@ -182,7 +183,7 @@ def test_font_with_italic_style(self):
"""
# Given
test_font = os.path.join(data_dir, "TestTTF Italic.ttf")
exp_name = "Test TTF"
exp_family = "Test TTF"
exp_style = "italic"
exp_variant = "normal"
exp_weight = 400
Expand All @@ -193,7 +194,7 @@ def test_font_with_italic_style(self):
entry = _ttf_font_property(test_font, TTFont(test_font))

# Then
self.assertEqual(entry.name, exp_name)
self.assertEqual(entry.family, exp_family)
self.assertEqual(entry.style, exp_style)
self.assertEqual(entry.variant, exp_variant)
self.assertEqual(entry.weight, exp_weight)
Expand All @@ -212,14 +213,16 @@ def test_nameless_font(self):
_ttf_font_property(test_font, None)

def test_property_branches(self):
# These tests mock `get_ttf_prop_dict` in order to test the various
# branches of `_ttf_font_property`.
target = "kiva.fonttools._scan_parse.get_ttf_prop_dict"
test_font = os.path.join(data_dir, "TestTTF.ttf")

# Given
exp_name = "TestyFont Bold Capitals"
prop_dict = {
"name": exp_name,
"sfnt4": exp_name,
"family": "TestyFont Capitals",
"style": "Bold",
"full_name": "TestyFont Capitals Bold",
}
# When
with mock.patch(target, return_value=prop_dict):
Expand All @@ -229,10 +232,10 @@ def test_property_branches(self):
self.assertEqual(entry.variant, "small-caps")

# Given
exp_name = "TestyFont Bold Oblique"
prop_dict = {
"name": exp_name,
"sfnt4": exp_name,
"family": "TestyFont",
"style": "Bold Oblique",
"full_name": "TestyFont Bold Oblique",
}
# When
with mock.patch(target, return_value=prop_dict):
Expand All @@ -251,8 +254,9 @@ def test_property_branches(self):
for name, stretch in stretch_options.items():
# Given
prop_dict = {
"name": name,
"sfnt4": name,
"family": "TestyFont",
"style": "Regular",
"full_name": name,
}
# When
with mock.patch(target, return_value=prop_dict):
Expand Down