Skip to content

[MNT]: findSystemFonts - Use system API instead to look into folder #24001

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

Open
moi15moi opened this issue Sep 24, 2022 · 11 comments
Open

[MNT]: findSystemFonts - Use system API instead to look into folder #24001

moi15moi opened this issue Sep 24, 2022 · 11 comments

Comments

@moi15moi
Copy link

moi15moi commented Sep 24, 2022

Summary

Currently, findSystemFonts look at all the file in the specified folders for macos and windows.

This is not a very good method, because a font in a "Font folder" does not necessarily need to be picked up.
A very good example of that is in this issue: #22859

This is why I propose to use CoreText (for mac) and DirectWrite (for Windows).
These are API provided by Apple and Microsoft.

Proposed fix

CoreText - For MAC

In this example, I use this library: https://pypi.org/project/pyobjc-framework-CoreText/

import CoreText

fontURLs = CoreText.CTFontManagerCopyAvailableFontURLs()
fontPath = set()

for i in range(CoreText.CFArrayGetCount(fontURLs)):
    url = CoreText.CFArrayGetValueAtIndex(fontURLs, i)

    fontPath.add(str(url.path()))

DirectWrite - For Windows

I don't know much C/C++, so I can't give an good example.
On recent Windows build, it is pretty simple to get the font path with DirectWrite, but since matplotlib seems to support Windows 7, we would maybe need to use the "complex" solution.

But, here is how I understand it (I have no proof that it works):

Logic for to support only Windows 10:

Here is an "pseudo-code" of how I see it.

font_collection = GetSystemFontCollection()
font_set = font_collection.GetFontSet()

for i in range(font_set.GetFontCount()):
	font_face_reference = font_set.GetFontFaceReference(i)
	font_file = font_face_reference.GetFontFile()
	reference_key, reference_key_size = font_file.GetReferenceKey()
	path = GetFilePathFromKey(reference_key, reference_key_size)

Logic for Windows Vista to this day:

font_collection = GetSystemFontCollection()
 
for i in range(font_collection.GetFontFamilyCount()):
	font_family = font_collection.GetFontFamily(i)
	matching_fonts = GetMatchingFonts(ANY_WEIGHT, ANY_STRETCH, ANY_STYLE)
	
	for j in range(matching_fonts.GetFontCount()):
		font = matching_fonts.GetFont(j)
		font_face = font.CreateFontFace()
		
		for file in font_face.GetFiles():
			reference_key, reference_key_size = font_file.GetReferenceKey()
			path = GetFilePathFromKey(reference_key, reference_key_size)

I have try to see if the GDI api could help us to get the path of the fonts, but from what I can see, it is not an available feature: https://learn.microsoft.com/en-us/windows/win32/gdi/font-and-text-functions

FontConfig (Bonus) - For Linux

Currently, matplotlib parse the result from subprocess.

It would be an good idea to directly use FontConfig. See this answer from stackoverflow to know how to do it: https://stackoverflow.com/a/14634033

I did not found any FontConfig bindings library that are still maintained, so it would need to be implemented with Cython.

@ashb
Copy link

ashb commented Oct 26, 2022

So I've been working on the Windows part of this (it turns out it was slightly more complex than the functions you highlighted, but that gave me the pointers I needed):

In [1]: __import__('windows_fonts').FontCollection()
Out[1]: <windows_fonts.FontCollection at 0x29a415bdb90>

In [2]: for f in _1:
   ...:     print(repr(f))
   ...:
<FontFamily name="Iosevka Term">
<FontFamily name="Cascadia Code">
<FontFamily name="Cascadia Mono">
<FontFamily name="Arial">
<FontFamily name="Bahnschrift">
<FontFamily name="Calibri">
<FontFamily name="Cambria">
<FontFamily name="Cambria Math">
<FontFamily name="Candara">
<FontFamily name="Comic Sans MS">
<FontFamily name="Consolas">
<FontFamily name="Constantia">
<FontFamily name="Corbel">
<FontFamily name="Courier New">
<FontFamily name="Ebrima">
<FontFamily name="Franklin Gothic">
<FontFamily name="Gabriola">
...
<FontFamily name="Marlett">

In [3]: list(f)
Out[3]:
[<FontVariant family=<FontFamily name="Marlett">, style=Style.NORMAL weight=Weight.MEDIUM>,
 <FontVariant family=<FontFamily name="Marlett">, style=Style.OBLIQUE weight=Weight.MEDIUM>,
 <FontVariant family=<FontFamily name="Marlett">, style=Style.NORMAL weight=Weight.BOLD>,
 <FontVariant family=<FontFamily name="Marlett">, style=Style.OBLIQUE weight=Weight.BOLD>]

In [4]: list(map(lambda f: f.filename, f))
Out[4]:
['C:\\WINDOWS\\FONTS\\MARLETT.TTF',
 'C:\\WINDOWS\\FONTS\\MARLETT.TTF',
 'C:\\WINDOWS\\FONTS\\MARLETT.TTF',
 'C:\\WINDOWS\\FONTS\\MARLETT.TTF']

(I'm planning to release this as a stand-alone python module as I actually need it for a separate project)

@moi15moi
Copy link
Author

it turns out it was slightly more complex than the functions you highlighted, but that gave me the pointers I needed

I am sorry. I thought it was the right logic.
I hope i was in the right direction, so you did not waste too much time on that part.

By curiosity, which method have you used?

  • Logic for Windows 10 to this day
    OR
  • Logic for Windows Vista to this day

@ashb
Copy link

ashb commented Oct 27, 2022

@moi15moi You had the right functions, but the issue was that GetFilePathFromKey needs to be called on an instance of IDWriteLocalFontFileLoader, and unlike most other COM objects where you can just do CoCreateInstance to get one, the only way I've found (from searching) is to get an instance of this is to load a local font file, then ask for it's loader.

And I think I've used the Vista+ approach.

@moi15moi
Copy link
Author

moi15moi commented Oct 27, 2022

Ok, then, i think you could have called GetLoader from the object you have with GetFiles

Here is the updated version

Logic for Windows Vista to this day:

Here is the pseudo-code

font_collection = GetSystemFontCollection()
 
for i in range(font_collection.GetFontFamilyCount()):
	font_family = font_collection.GetFontFamily(i)
	matching_fonts = GetMatchingFonts(ANY_WEIGHT, ANY_STRETCH, ANY_STYLE)
	
	for j in range(matching_fonts.GetFontCount()):
		font = matching_fonts.GetFont(j)
		font_face = font.CreateFontFace()
		
		files = font_face.GetFiles()
                loader = files.GetLoader()
                reference_key, reference_key_size = files.GetReferenceKey()
		path = loader.GetFilePathFromKey(reference_key, reference_key_size)

Edit: GetLoader return an instance of IDWriteFontFileLoader. It doesn't return of instance of IDWriteLocalFontFileLoader.

Sorry, i missread that.

But, i just found this: https://social.msdn.microsoft.com/Forums/vstudio/en-US/8f57b186-9cf6-417a-8923-092b406d94ea/obtain-filename-from-directwrite-font?forum=windowsuidevelopment

@ashb
Copy link

ashb commented Oct 27, 2022

@ashb
Copy link

ashb commented Oct 31, 2022

First alpha release available at https://pypi.org/project/windows-fonts/0.1.0a1/ pip install --pre windows-fonts

@kostyafarber
Copy link
Contributor

Hey is anybody working on the mac portion of this? If not I'd like to have a crack.

@greglucas
Copy link
Contributor

@kostyafarber, it doesn't look like anyone is working on the mac side of this and in general we have very few people looking into the objective c portion of the code, so I would say go for it! You can also find other mac issues with this label: GUI: MacOSX

@kostyafarber
Copy link
Contributor

kostyafarber commented Nov 10, 2022

For the windows version would we need to write up the code in c++ and write a Python binding and use it in font_manager.py?

For the Mac version are we happy to explore using https://pypi.org/project/pyobjc-framework-CoreText/ or would we prefer to use a binding as well?

@tacaswell
Copy link
Member

I would much rather have bindings in Matplotlib than pick up a dependency for this. If it is so complex that we need a dependency rather than just implementing it, than I think that continuing to search the directories is fine.

@moi15moi
Copy link
Author

moi15moi commented Apr 28, 2023

I decided to do a repos to show you how I implemented it: https://github.com/moi15moi/FindSystemFontsFilename

I haven't open an PR here since font_manager can search for .afm file which cannot be done with DirectWrite and CoreText (it may be possible with FontConfig, but I haven't check)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants