Repository: python-pillow/Pillow

Issue:
System font support for Linux/OSX
ImageFont.truetype will look in the windows font directory for font files. We should extend this to look in well known locations on OSX and Linux. 



Discussion:
This is decidedly "meh" unless it's easy, in which case feel free to include in 2.3.0.

Punt. I think it's easy, but it's not happening this pass.

Now targeting 2.4.0

Now targeting Future

Linux is being discussed in https://github.com/python-pillow/Pillow/pull/682.


Resolving patch:
diff --git a/PIL/ImageFont.py b/PIL/ImageFont.py
--- a/PIL/ImageFont.py
+++ b/PIL/ImageFont.py
@@ -239,6 +239,10 @@ def truetype(font=None, size=10, index=0, encoding="", filename=None):
     try:
         return FreeTypeFont(font, size, index, encoding)
     except IOError:
+        if font.endswith(".ttf"):
+            ttf_filename = font
+        else:
+            ttf_filename = "%s.ttf" % font
         if sys.platform == "win32":
             # check the windows font repository
             # NOTE: must use uppercase WINDIR, to work around bugs in
@@ -247,6 +251,25 @@ def truetype(font=None, size=10, index=0, encoding="", filename=None):
             if windir:
                 filename = os.path.join(windir, "fonts", font)
                 return FreeTypeFont(filename, size, index, encoding)
+        elif sys.platform in ('linux', 'linux2'):
+            lindirs = os.environ.get("XDG_DATA_DIRS", "")
+            if not lindirs:
+                #According to the freedesktop spec, XDG_DATA_DIRS should
+                #default to /usr/share
+                lindirs = '/usr/share'
+            lindirs = lindirs.split(":")
+            for lindir in lindirs:
+                parentpath = os.path.join(lindir, "fonts")
+                for walkroot, walkdir, walkfilenames in os.walk(parentpath):
+                    if ttf_filename in walkfilenames:
+                        filepath = os.path.join(walkroot, ttf_filename)
+                        return FreeTypeFont(filepath, size, index, encoding)
+        elif sys.platform == 'darwin':
+            macdirs = ['/Library/Fonts/', '/System/Library/Fonts/', os.path.expanduser('~/Library/Fonts/')]
+            for macdir in macdirs:
+                filepath = os.path.join(macdir, ttf_filename)
+                if os.path.exists(filepath):
+                    return FreeTypeFont(filepath, size, index, encoding)
         raise
 
 


Test patch:
diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -4,6 +4,8 @@
 from PIL import ImageDraw
 from io import BytesIO
 import os
+import sys
+import copy
 
 FONT_PATH = "Tests/fonts/FreeMono.ttf"
 FONT_SIZE = 20
@@ -13,6 +15,29 @@
     from PIL import ImageFont
     ImageFont.core.getfont  # check if freetype is available
 
+    class SimplePatcher():
+        def __init__(self, parent_obj, attr_name, value):
+            self._parent_obj = parent_obj
+            self._attr_name = attr_name
+            self._saved = None
+            self._is_saved = False
+            self._value = value
+        def __enter__(self):
+            # Patch the attr on the object
+            if hasattr(self._parent_obj, self._attr_name):
+                self._saved = getattr(self._parent_obj, self._attr_name)
+                setattr(self._parent_obj, self._attr_name, self._value)
+                self._is_saved = True
+            else:
+                setattr(self._parent_obj, self._attr_name, self._value)
+                self._is_saved = False
+        def __exit__(self, type, value, traceback):
+            # Restore the original value
+            if self._is_saved:
+                setattr(self._parent_obj, self._attr_name, self._saved)
+            else:
+                delattr(self._parent_obj, self._attr_name)
+
     class TestImageFont(PillowTestCase):
 
         def test_sanity(self):
@@ -192,6 +217,42 @@ def test_default_font(self):
             # Assert
             self.assert_image_equal(im, target_img)
 
+        def _test_fake_loading_font(self, path_to_fake):
+            #Make a copy of FreeTypeFont so we can patch the original
+            free_type_font = copy.deepcopy(ImageFont.FreeTypeFont)
+            with SimplePatcher(ImageFont, '_FreeTypeFont', free_type_font):
+                def loadable_font(filepath, size, index, encoding):
+                    if filepath == path_to_fake:
+                        return ImageFont._FreeTypeFont(FONT_PATH, size, index, encoding)
+                    return ImageFont._FreeTypeFont(filepath, size, index, encoding)
+                with SimplePatcher(ImageFont, 'FreeTypeFont', loadable_font):
+                    font = ImageFont.truetype('Arial')
+                    #Make sure it's loaded
+                    name = font.getname()
+                    self.assertEqual(('FreeMono', 'Regular'), name)
+
+        def test_find_linux_font(self):
+            #A lot of mocking here - this is more for hitting code and catching
+            #syntax like errors
+            with SimplePatcher(sys, 'platform', 'linux'):
+                patched_env = copy.deepcopy(os.environ)
+                patched_env['XDG_DATA_DIRS'] = '/usr/share/:/usr/local/share/'
+                with SimplePatcher(os, 'environ', patched_env):
+                    def fake_walker(path):
+                        if path == '/usr/local/share/fonts':
+                            return [(path, [], ['Arial.ttf'], )]
+                        return [(path, [], ['some_random_font.ttf'], )]
+                    with SimplePatcher(os, 'walk', fake_walker):
+                        self._test_fake_loading_font('/usr/local/share/fonts/Arial.ttf')
+
+        def test_find_osx_font(self):
+            #Like the linux test, more cover hitting code rather than testing
+            #correctness.
+            with SimplePatcher(sys, 'platform', 'darwin'):
+                fake_font_path = '/System/Library/Fonts/Arial.ttf'
+                with SimplePatcher(os.path, 'exists', lambda x: x == fake_font_path):
+                    self._test_fake_loading_font(fake_font_path)
+
 
 except ImportError:
     class TestImageFont(PillowTestCase):