Skip to content

Commit

Permalink
Add a way for applications to supply custom fonts (#711)
Browse files Browse the repository at this point in the history
  • Loading branch information
jwiggins authored Mar 15, 2021
1 parent 94522a3 commit ba6f246
Show file tree
Hide file tree
Showing 10 changed files with 231 additions and 13 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/bleeding-edge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,17 @@ jobs:
- name: Sanity check package version
run: python -m pip list
- name: Run enable test suite
env:
PYTHONFAULTHANDLER: 1
uses: GabrielBB/xvfb-action@v1
with:
# kiva agg requires at least 15-bit color depth.
# The --server-args assumes xvfb-run is called, hence Linux only.
run: --server-args="-screen 0 1024x768x24" python -m unittest discover -v enable
working-directory: ${{ runner.temp }}
- name: Run kiva test suite
env:
PYTHONFAULTHANDLER: 1
uses: GabrielBB/xvfb-action@v1
with:
run: python -m unittest discover -v kiva
Expand Down
9 changes: 5 additions & 4 deletions ci/edmtool.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def install(runtime, toolkit, environment, source):
("edm --config {edm_config} environments create {environment} "
"--force --version={runtime}"),
("edm --config {edm_config} install -y -e {environment} {packages} "
"--add-repository enthought/lgpl"),
"--add-repository enthought/lgpl"),
("edm run -e {environment} -- pip install -r ci/requirements.txt"
" --no-dependencies"),
]
Expand Down Expand Up @@ -352,12 +352,13 @@ def test(runtime, toolkit, environment):
"""
parameters = get_parameters(runtime, toolkit, environment)
environ = environment_vars.get(toolkit, {}).copy()
environ['PYTHONUNBUFFERED'] = "1"
environ["PYTHONUNBUFFERED"] = "1"
environ["PYTHONFAULTHANDLER"] = "1"
commands = [
("edm run -e {environment} -- python -W default -m"
"coverage run -m unittest discover enable -v"),
"coverage run -m unittest discover enable -v"),
("edm run -e {environment} -- python -W default -m"
"coverage run -a -m unittest discover kiva -v"),
"coverage run -a -m unittest discover kiva -v"),
]

# We run in a tempdir to avoid accidentally picking up wrong traitsui
Expand Down
3 changes: 2 additions & 1 deletion kiva/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
=====
- :class:`~.Font`
- :func:`~.add_application_fonts`
Font Constants
--------------
Expand Down Expand Up @@ -146,4 +147,4 @@
INVERTED_TRIANGLE_MARKER, PLUS_MARKER, DOT_MARKER, PIXEL_MARKER
)
from ._cython_speedups import points_in_polygon
from .fonttools import Font
from .fonttools import add_application_fonts, Font
3 changes: 3 additions & 0 deletions kiva/fonttools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
from .app_font import add_application_fonts
from .font import Font, str_to_font

__all__ = ["add_application_fonts", "Font", "str_to_font"]
37 changes: 37 additions & 0 deletions kiva/fonttools/_scan_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,43 @@ def scan_system_fonts(fontpaths=None, fontext="ttf"):
return [fname for fname in fontfiles if os.path.exists(fname)]


def scan_user_fonts(fontpaths=None, fontext="ttf"):
""" Search for fonts in the specified font paths.
Returns
-------
filepaths : list of str
A list of unique font file paths.
"""
if fontpaths is None:
return []

if isinstance(fontpaths, str):
fontpaths = [fontpaths]

fontfiles = set()
fontexts = _get_fontext_synonyms(fontext)
for path in fontpaths:
path = os.path.abspath(path)
if os.path.isdir(path):
# For directories, find all the fonts within
files = []
for ext in fontexts:
files.extend(glob.glob(os.path.join(path, "*." + ext)))
files.extend(glob.glob(os.path.join(path, "*." + ext.upper())))

for fname in files:
if os.path.exists(fname) and not os.path.isdir(fname):
fontfiles.add(fname)
elif os.path.exists(path):
# For files, make sure they have the correct extension
ext = os.path.splitext(path)[-1][1:].lower()
if ext in fontexts:
fontfiles.add(path)

return sorted(fontfiles)


# ----------------------------------------------------------------------------
# utility funcs

Expand Down
54 changes: 54 additions & 0 deletions kiva/fonttools/app_font.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# (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 warnings

from traits.etsconfig.api import ETSConfig

from kiva.fonttools.font_manager import default_font_manager


def add_application_fonts(filenames):
""" Add a TrueType font to the system in a way that makes it available to
both the GUI toolkit and Kiva.
Parameters
----------
filenames : list of str
Filesystem paths of TrueType or OpenType font files.
"""
if isinstance(filenames, str):
filenames = [filenames]

# Handle Kiva
fm = default_font_manager()
fm.update_fonts(filenames)

# Handle the GUI toolkit
if ETSConfig.toolkit.startswith("qt"):
_qt_impl(filenames)
elif ETSConfig.toolkit == "wx":
_wx_impl(filenames)


def _qt_impl(filenames):
from pyface.qt import QtGui

for fname in filenames:
QtGui.QFontDatabase.addApplicationFont(fname)


def _wx_impl(filenames):
import wx

if hasattr(wx.Font, "CanUsePrivateFont") and wx.Font.CanUsePrivateFont():
for fname in filenames:
wx.Font.AddPrivateFont(fname)
else:
warnings.warn("Wx does not support private fonts! Failed to add.")
21 changes: 16 additions & 5 deletions kiva/fonttools/font_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from traits.etsconfig.api import ETSConfig

from kiva.fonttools._scan_parse import create_font_list
from kiva.fonttools._scan_sys import scan_system_fonts
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,
score_weight
Expand Down Expand Up @@ -137,12 +137,23 @@ def set_default_weight(self, weight):
"""
self.__default_weight = weight

def update_fonts(self, filenames):
def update_fonts(self, paths):
""" Update the font lists with new font files.
Currently not implemented.
The specified ``paths`` will be searched for valid font files and those
files will have their fonts added to internal collections searched by
:meth:`findfont`.
Parameters
----------
filenames : list of str
A list of font file paths or directory paths.
"""
# !!!! Needs implementing
raise NotImplementedError
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))

def findfont(self, prop, fontext="ttf", directory=None,
fallback_to_default=True, rebuild_if_missing=True):
Expand Down
100 changes: 100 additions & 0 deletions kiva/fonttools/tests/test_app_font.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# (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 os
import unittest

import pkg_resources

from traits.etsconfig.api import ETSConfig

from kiva.api import add_application_fonts, Font

is_null = (ETSConfig.toolkit in ("", "null"))
is_qt = ETSConfig.toolkit.startswith("qt")
is_wx = (ETSConfig.toolkit == "wx")
data_dir = pkg_resources.resource_filename("kiva.fonttools.tests", "data")


@unittest.skipIf(not is_null, "Test only for null toolkit")
class TestNullApplicationFonts(unittest.TestCase):
def test_add_application_font(self):
path = os.path.join(data_dir, "TestTTF.ttf")
family = "Test TTF"
kivafont = Font(family)

# Before adding the font
with self.assertWarns(UserWarning):
self.assertNotEqual(kivafont.findfont().filename, path)

add_application_fonts([path])

# After adding the font
self.assertEqual(kivafont.findfont().filename, path)


@unittest.skipIf(not is_qt, "Test only for qt")
class TestQtApplicationFonts(unittest.TestCase):
def setUp(self):
from pyface.qt import QtGui

application = QtGui.QApplication.instance()
if application is None:
self.application = QtGui.QApplication([])
else:
self.application = application
unittest.TestCase.setUp(self)

def test_add_application_font(self):
from pyface.qt import QtGui

path = os.path.join(data_dir, "TestTTF.ttf")
family = "Test TTF"
font_db = QtGui.QFontDatabase()

# Before adding the font
self.assertNotIn(family, font_db.families())

add_application_fonts([path])

# After adding the font
self.assertIn(family, font_db.families())


@unittest.skipIf(not is_wx, "Test only for wx")
class TestWxApplicationFonts(unittest.TestCase):
def setUp(self):
import wx

application = wx.App.Get()
if application is None:
self.application = wx.App()
else:
self.application = application
unittest.TestCase.setUp(self)

# XXX: How do we check to see if Wx loaded our font?
@unittest.expectedFailure
def test_add_application_font(self):
import wx

path = os.path.join(data_dir, "TestTTF.ttf")
family = "Test TTF"

fontinfo = wx.FontInfo()
fontinfo.FaceName(family)
wxfont = wx.Font(fontinfo)

# Before adding the font
self.assertFalse(wxfont.IsOk())

add_application_fonts([path])

# After adding the font
self.assertTrue(wxfont.IsOk())
9 changes: 8 additions & 1 deletion kiva/fonttools/tests/test_scan_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from pkg_resources import resource_filename

from .._scan_sys import scan_system_fonts
from .._scan_sys import scan_system_fonts, scan_user_fonts

data_dir = resource_filename("kiva.fonttools.tests", "data")
is_macos = (sys.platform == "darwin")
Expand Down Expand Up @@ -49,6 +49,13 @@ def test_directories_scanning(self):
fonts = scan_system_fonts([data_dir], fontext="ttf")
self.assertListEqual(sorted(expected), sorted(fonts))

def test_user_font_scanning(self):
ttf_fonts = scan_user_fonts(data_dir, fontext="ttf")
self.assertEqual(len(ttf_fonts), 3)

afm_fonts = scan_user_fonts(data_dir, fontext="afm")
self.assertEqual(len(afm_fonts), 1)

@unittest.skipIf(not is_generic, "This test is only for generic platforms")
def test_generic_scanning(self):
fonts = scan_system_fonts(fontext="ttf")
Expand Down
4 changes: 2 additions & 2 deletions kiva/tests/test_gl_drawing.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@

from kiva.tests.drawing_tester import DrawingImageTester

is_windows = sys.platform in ("win32", "cygwin")
is_linux = (sys.platform == "linux")


@unittest.skipIf(is_windows, "Pyglet/GL backend issues on Windows")
@unittest.skipIf(not is_linux, "Pyglet/GL backend issues on most platforms")
@unittest.skipIf(PYGLET_NOT_AVAILABLE, "Cannot import pyglet")
class TestGLDrawing(DrawingImageTester, unittest.TestCase):
def tearDown(self):
Expand Down

0 comments on commit ba6f246

Please sign in to comment.