-
Notifications
You must be signed in to change notification settings - Fork 45
Use a database object to manage fonts #714
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| # (C) Copyright 2005-2021 Enthought, Inc., Austin, TX | ||
| # All rights reserved. | ||
| # | ||
| # This software is provided without warranty under the terms of the BSD | ||
| # license included in LICENSE.txt and may be redistributed only under | ||
| # the conditions described in the aforementioned license. The license | ||
| # is also available online at http://www.enthought.com/licenses/BSD.txt | ||
| # | ||
| # Thanks for using Enthought open source! | ||
| import itertools | ||
| import os | ||
|
|
||
| from kiva.fonttools._constants import preferred_fonts | ||
|
|
||
|
|
||
| class FontEntry(object): | ||
| """ A class for storing properties of fonts which have been discovered on | ||
| the local machine. | ||
|
|
||
| `__hash__` is implemented so that set() can be used to prune duplicates. | ||
| """ | ||
| def __init__(self, fname="", family="", style="normal", variant="normal", | ||
| weight="normal", stretch="normal", size="medium", | ||
| face_index=0): | ||
|
|
||
| self.fname = fname | ||
| self.family = family | ||
| self.style = style | ||
| self.variant = variant | ||
| self.weight = weight | ||
| self.stretch = stretch | ||
| self.face_index = face_index | ||
|
|
||
| try: | ||
| self.size = str(float(size)) | ||
| except ValueError: | ||
| self.size = size | ||
|
|
||
| def __hash__(self): | ||
| c = tuple(getattr(self, k) for k in sorted(self.__dict__)) | ||
| return hash(c) | ||
|
|
||
| def __repr__(self): | ||
| fname = os.path.basename(self.fname) | ||
| return ( | ||
| f"<FontEntry '{self.family}' ({fname}[{self.face_index}]) " | ||
| f"{self.style} {self.variant} {self.weight} {self.stretch}>" | ||
| ) | ||
|
|
||
|
|
||
| class FontDatabase: | ||
| """ A container for :class`FontEntry` instances of a specific type | ||
| (TrueType/OpenType, AFM) which can be queried in different ways. | ||
| """ | ||
| def __init__(self, entries): | ||
| # Use a set to keep out the duplicates | ||
| 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) | ||
|
|
||
| def add_fonts(self, entries): | ||
| """ Add more :class`FontEntry` instances to the database. | ||
| """ | ||
| for entry in entries: | ||
| # Avoid non-FontEntry objects and duplicates | ||
| if not isinstance(entry, FontEntry) or entry in self._entries: | ||
| continue | ||
|
|
||
| self._entries.add(entry) | ||
| self._family_map.setdefault(entry.family, []).append(entry) | ||
| self._file_map.setdefault(entry.fname, []).append(entry) | ||
|
|
||
| def fonts_for_directory(self, directory): | ||
| """ Returns all fonts whose file is in a directory. | ||
| """ | ||
| result = [] | ||
| for fname, entries in self._file_map.items(): | ||
| if os.path.commonprefix([fname, directory]): | ||
| result.extend(entries) | ||
| return result | ||
|
|
||
| def fonts_for_family(self, families): | ||
| """ Returns all fonts which best match a particular family query or | ||
| all possible fonts if exact families are not matched. | ||
|
|
||
| `families` is a list of real and generic family names. An iterable | ||
| of `FontEntry` instances is returned. | ||
| """ | ||
| flat_list = (lambda it: list(itertools.chain.from_iterable(it))) | ||
|
|
||
| # Translate generic families into lists of families | ||
| fams = flat_list(preferred_fonts.get(fam, [fam]) for fam in families) | ||
| # Then collect all entries for those families | ||
| entries = flat_list(self._family_map.get(fam, []) for fam in fams) | ||
| if entries: | ||
| return entries | ||
|
|
||
| # Return all entries if no families found | ||
| # Yes, self._entries is a set. Consumers should only expect an iterable | ||
| return self._entries | ||
|
|
||
| def __len__(self): | ||
| return len(self._entries) | ||
|
|
||
| @staticmethod | ||
| def _build_family_map(entries): | ||
| ret = {} | ||
| for entry in entries: | ||
| ret.setdefault(entry.family, []).append(entry) | ||
|
|
||
| return ret | ||
|
|
||
| @staticmethod | ||
| def _build_file_map(entries): | ||
| ret = {} | ||
| for entry in entries: | ||
| ret.setdefault(entry.fname, []).append(entry) | ||
|
|
||
| return ret | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,7 +37,9 @@ | |
|
|
||
| from traits.etsconfig.api import ETSConfig | ||
|
|
||
| from kiva.fonttools._scan_parse import create_font_list | ||
| from kiva.fonttools._scan_parse import ( | ||
| create_font_database, update_font_database | ||
| ) | ||
| from kiva.fonttools._scan_sys import scan_system_fonts, scan_user_fonts | ||
| from kiva.fonttools._score import ( | ||
| score_family, score_size, score_stretch, score_style, score_variant, | ||
|
|
@@ -74,17 +76,18 @@ 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__ = 9 | ||
| __version__ = 10 | ||
|
|
||
| def __init__(self, size=None, weight="normal"): | ||
| self._version = self.__version__ | ||
|
|
||
| self.__default_weight = weight | ||
| self.default_size = size if size is not None else 12.0 | ||
|
|
||
| paths = [] | ||
| self.default_family = "sans-serif" | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is basically equivalent to the previous value. Bitstream Vera and Helvetica are both sans-serif families. But since not every system may have them, we can use the generic family name for the default and get good results thanks to the |
||
| self.default_font = {} | ||
|
|
||
| # Create list of font paths | ||
| paths = [] | ||
| for pathname in ["TTFPATH", "AFMPATH"]: | ||
| if pathname in os.environ: | ||
| ttfpath = os.environ[pathname] | ||
|
|
@@ -96,28 +99,26 @@ def __init__(self, size=None, weight="normal"): | |
| paths.append(ttfpath) | ||
|
|
||
| logger.debug("font search path %s", str(paths)) | ||
| # Load TrueType fonts and create font dictionary. | ||
|
|
||
| self.ttffiles = scan_system_fonts(paths) + scan_system_fonts() | ||
| self.defaultFamily = {"ttf": "Bitstream Vera Sans", "afm": "Helvetica"} | ||
| self.defaultFont = {} | ||
|
|
||
| for fname in self.ttffiles: | ||
| # Load TrueType fonts and create font database. | ||
| ttffiles = scan_system_fonts(paths) + scan_system_fonts() | ||
| for fname in ttffiles: | ||
| logger.debug("trying fontname %s", fname) | ||
| if fname.lower().find("vera.ttf") >= 0: | ||
| self.defaultFont["ttf"] = fname | ||
| self.default_font["ttf"] = fname | ||
| break | ||
| else: | ||
| # use anything | ||
| self.defaultFont["ttf"] = self.ttffiles[0] | ||
| self.default_font["ttf"] = ttffiles[0] | ||
|
|
||
| self.ttflist = create_font_list(self.ttffiles) | ||
| self.ttf_db = create_font_database(ttffiles, fontext="ttf") | ||
|
|
||
| self.afmfiles = scan_system_fonts( | ||
| # Load AFM fonts and create font database. | ||
| afmfiles = scan_system_fonts( | ||
| paths, fontext="afm" | ||
| ) + scan_system_fonts(fontext="afm") | ||
| self.afmlist = create_font_list(self.afmfiles, fontext="afm") | ||
| self.defaultFont["afm"] = None | ||
| self.afm_db = create_font_database(afmfiles, fontext="afm") | ||
| self.default_font["afm"] = None | ||
|
|
||
| self.ttf_lookup_cache = {} | ||
| self.afm_lookup_cache = {} | ||
|
|
@@ -152,8 +153,8 @@ def update_fonts(self, paths): | |
| afm_paths = scan_user_fonts(paths, fontext="afm") | ||
| ttf_paths = scan_user_fonts(paths, fontext="ttf") | ||
|
|
||
| self.afmlist.extend(create_font_list(afm_paths)) | ||
| self.ttflist.extend(create_font_list(ttf_paths)) | ||
| update_font_database(self.afm_db, afm_paths, fontext="afm") | ||
| update_font_database(self.ttf_db, ttf_paths, fontext="ttf") | ||
|
|
||
| def findfont(self, prop, fontext="ttf", directory=None, | ||
| fallback_to_default=True, rebuild_if_missing=True): | ||
|
|
@@ -210,24 +211,32 @@ def __repr__(self): | |
|
|
||
| if fontext == "afm": | ||
| font_cache = self.afm_lookup_cache | ||
| fontlist = self.afmlist | ||
| font_db = self.afm_db | ||
| else: | ||
| font_cache = self.ttf_lookup_cache | ||
| fontlist = self.ttflist | ||
| font_db = self.ttf_db | ||
|
|
||
| if directory is None: | ||
| cached = font_cache.get(hash(prop)) | ||
| if cached: | ||
| return cached | ||
|
|
||
| best_score = 1e64 | ||
| # Narrow the search | ||
| if directory is not None: | ||
| # Only search the fonts from `directory` | ||
| fontlist = font_db.fonts_for_directory(directory) | ||
| else: | ||
| # Only search the fonts included in the families list of the query. | ||
| # This is safe because `score_family` will return 1.0 (no match) if | ||
| # none of the listed families match an entry's family. Further, | ||
| # both `fonts_for_family` and `score_family` will expand generic | ||
| # families ("serif", "monospace") into lists of candidate families, | ||
| # which ensures that all possible matching fonts will be scored. | ||
| fontlist = font_db.fonts_for_family(prop.get_family()) | ||
|
|
||
| best_score = 20.0 | ||
|
jwiggins marked this conversation as resolved.
|
||
| best_font = None | ||
|
|
||
| for font in fontlist: | ||
| fname = font.fname | ||
| if (directory is not None | ||
| and os.path.commonprefix([fname, directory]) != directory): | ||
| continue | ||
|
jwiggins marked this conversation as resolved.
|
||
| # Matching family should have highest priority, so it is multiplied | ||
| # by 10.0 | ||
| score = ( | ||
|
|
@@ -250,10 +259,10 @@ def __repr__(self): | |
| if fallback_to_default: | ||
| warnings.warn( | ||
| "findfont: Font family %s not found. Falling back to %s" | ||
| % (prop.get_family(), self.defaultFamily[fontext]) | ||
| % (prop.get_family(), self.default_family) | ||
| ) | ||
| default_prop = prop.copy() | ||
| default_prop.set_family(self.defaultFamily[fontext]) | ||
| default_prop.set_family(self.default_family) | ||
| return self.findfont( | ||
| default_prop, fontext, directory, | ||
| fallback_to_default=False, | ||
|
|
@@ -263,11 +272,11 @@ def __repr__(self): | |
| # so just return the vera.ttf | ||
| warnings.warn( | ||
| "findfont: Could not match %s. Returning %s" | ||
| % (prop, self.defaultFont[fontext]), | ||
| % (prop, self.default_font[fontext]), | ||
| UserWarning, | ||
| ) | ||
| # Assume this is never a .ttc font, so 0 is ok for face index. | ||
| result = FontSpec(self.defaultFont[fontext]) | ||
| result = FontSpec(self.default_font[fontext]) | ||
| else: | ||
| logger.debug( | ||
| "findfont: Matching %s to %s (%s[%d]) with score of %f", | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.