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
17 changes: 17 additions & 0 deletions kiva/fonttools/_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def __init__(self, entries):
self._entries = {ent for ent in entries if isinstance(ent, FontEntry)}
self._family_map = self._build_family_map(self._entries)
self._file_map = self._build_file_map(self._entries)
self._language_map = self._build_language_map(self._entries)

def add_fonts(self, entries):
""" Add more :class`FontEntry` instances to the database.
Expand All @@ -71,6 +72,8 @@ def add_fonts(self, entries):
self._entries.add(entry)
self._family_map.setdefault(entry.family, []).append(entry)
self._file_map.setdefault(entry.fname, []).append(entry)
for lang in entry.languages:
self._language_map.setdefault(lang, []).append(entry)

def fonts_for_directory(self, directory):
""" Returns all fonts whose file is in a directory.
Expand Down Expand Up @@ -101,6 +104,11 @@ def fonts_for_family(self, families):
# Yes, self._entries is a set. Consumers should only expect an iterable
return self._entries

def fonts_for_language(self, language):
""" Returns all fonts which support a particular language.
"""
return self._language_map.get(language, [])

def __len__(self):
return len(self._entries)

Expand All @@ -119,3 +127,12 @@ def _build_file_map(entries):
ret.setdefault(entry.fname, []).append(entry)

return ret

@staticmethod
def _build_language_map(entries):
ret = {}
for entry in entries:
for lang in entry.languages:
ret.setdefault(lang, []).append(entry)

return ret
26 changes: 23 additions & 3 deletions kiva/fonttools/font.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,18 +112,38 @@ def __init__(self, face_name="", size=12, family=SWISS, weight=NORMAL,
self.underline = underline
self.encoding = encoding

def findfont(self):
def findfont(self, language=None):
""" Returns the file name and face index of the font that most closely
matches our font properties.

Parameters
----------
language : str [optional]
If provided, attempt to find a font which supports ``language``.
"""
query = self._make_font_query()
if language is not None:
spec = default_font_manager().find_fallback(query, language)
if spec is not None:
return spec

return default_font_manager().findfont(query)

def findfontname(self):
def findfontname(self, language=None):
""" Returns the name of the font that most closely matches our font
properties
properties.

Parameters
----------
language : str [optional]
If provided, attempt to find a font which supports ``language``.
"""
query = self._make_font_query()
if language is not None:
spec = default_font_manager().find_fallback(query, language)
if spec is not None:
return spec.family

return query.get_name()

def _make_font_query(self):
Expand Down
106 changes: 82 additions & 24 deletions kiva/fonttools/font_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class FontManager:
# Increment this version number whenever the font cache data
# format or behavior has changed and requires a existing font
# cache files to be rebuilt.
__version__ = 11
__version__ = 12

def __init__(self):
self._version = self.__version__
Expand Down Expand Up @@ -118,8 +118,10 @@ def __init__(self):
self.afm_db = create_font_database(afmfiles, fontext="afm")
self.default_font["afm"] = None

self.ttf_lookup_cache = {}
self.afm_lookup_cache = {}
self._ttf_lookup_cache = {}
self._afm_lookup_cache = {}
self._ttf_fallback_cache = {}
self._afm_fallback_cache = {}

def update_fonts(self, paths):
""" Update the font lists with new font files.
Expand All @@ -139,6 +141,55 @@ def update_fonts(self, paths):
update_font_database(self.afm_db, afm_paths, fontext="afm")
update_font_database(self.ttf_db, ttf_paths, fontext="ttf")

def find_fallback(self, query, language, fontext="ttf"):
""" Search the font list for a font which most closely matches
the :class:`FontQuery` *query*, and has support for ``language``.
"""
if fontext == "afm":
font_cache = self._afm_fallback_cache
font_db = self.afm_db
else:
font_cache = self._ttf_fallback_cache
font_db = self.ttf_db

key = hash(language + str(query))
cached = font_cache.get(key)
if cached:
return cached

# Narrow the search to a single language
fontlist = font_db.fonts_for_language(language)

best_score = 3.0
best_font = None
for font in fontlist:
score = (
score_family(query.get_family(), font.family)
+ score_style(query.get_style(), font.style)
+ score_weight(query.get_weight(), font.weight)
)
# Lowest score wins
if score < best_score:
best_score = score
best_font = font
# Exact matches stop the search
if score == 0:
break

# If no suitable font is found, return None
if best_font is None or best_score >= 3.0:
msg = "find_fallback: Font for {} in language {} not found"
warnings.warn(msg.format(query, language))
return None

result = _FontSpec(
best_font.fname,
best_font.family,
best_font.face_index,
)
font_cache[key] = result
return result

def findfont(self, query, fontext="ttf", directory=None,
fallback_to_default=True, rebuild_if_missing=True):
""" Search the font list for the font that most closely matches
Expand Down Expand Up @@ -166,22 +217,6 @@ def findfont(self, query, fontext="ttf", directory=None,
"""
from kiva.fonttools._query import FontQuery

class FontSpec(object):
""" An object to represent the return value of findfont().
"""
def __init__(self, filename, face_index=0):
self.filename = str(filename)
self.face_index = face_index

def __fspath__(self):
""" Implement the os.PathLike abstract interface.
"""
return self.filename

def __repr__(self):
args = f"{self.filename}, face_index={self.face_index}"
return f"FontSpec({args})"

if not isinstance(query, FontQuery):
query = FontQuery(query)

Expand All @@ -190,13 +225,13 @@ def __repr__(self):
logger.debug("findfont returning %s", fname)
# It's not at all clear where a `FontQuery` instance with
# `fname` already set would come from. Assume face_index == 0.
return FontSpec(fname)
return _FontSpec(fname, query.family[0])

if fontext == "afm":
font_cache = self.afm_lookup_cache
font_cache = self._afm_lookup_cache
font_db = self.afm_db
else:
font_cache = self.ttf_lookup_cache
font_cache = self._ttf_lookup_cache
font_db = self.ttf_db

if directory is None:
Expand Down Expand Up @@ -259,7 +294,7 @@ def __repr__(self):
UserWarning,
)
# Assume this is never a .ttc font, so 0 is ok for face index.
result = FontSpec(self.default_font[fontext])
result = _FontSpec(self.default_font[fontext], "Default")
else:
logger.debug(
"findfont: Matching %s to %s (%s[%d]) with score of %f",
Expand All @@ -269,7 +304,11 @@ def __repr__(self):
best_font.face_index,
best_score,
)
result = FontSpec(best_font.fname, best_font.face_index)
result = _FontSpec(
best_font.fname,
best_font.family,
best_font.face_index,
)

if not os.path.isfile(result.filename):
if rebuild_if_missing:
Expand All @@ -290,6 +329,25 @@ def __repr__(self):
return result


class _FontSpec(object):
""" An object to represent the return value of findfont() and
find_fallback().
"""
def __init__(self, filename, family, face_index=0):
self.filename = str(filename)
self.family = family
self.face_index = face_index

def __fspath__(self):
""" Implement the os.PathLike abstract interface.
"""
return self.filename

def __repr__(self):
args = f"{self.filename}, {self.family}, face_index={self.face_index}"
return f"_FontSpec({args})"


# ---------------------------------------------------------------------------
# Utilities

Expand Down
12 changes: 12 additions & 0 deletions kiva/fonttools/tests/test_font.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ def test_find_font_name(self):
# Name should be nonempty.
self.assertGreater(len(name), 0)

def test_find_font_for_language(self):
Comment thread
rahulporuri marked this conversation as resolved.
font = Font(face_name="")

# Nearly every font supports Latin script, so this shouldn't fail
spec = font.findfont(language="Latin")
self.assertTrue(os.path.exists(spec.filename))

# There will be warnings for an unknown language
with self.assertWarns(UserWarning):
spec = font.findfont(language="FancyTalk")
self.assertTrue(os.path.exists(spec.filename))

def test_str_to_font(self):
# Simple
from_str = str_to_font("modern 10")
Expand Down