diff --git a/README.md b/README.md index b11f2f6..671b0ee 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ It uses some APIs to find the font filename: - Windows: [DirectWrite API](https://learn.microsoft.com/en-us/windows/win32/directwrite/direct-write-portal) - macOS: [Core Text API](https://developer.apple.com/documentation/coretext) - Linux: [Fontconfig API](https://www.freedesktop.org/wiki/Software/fontconfig/) +- Android: [NDK Font API](https://developer.android.com/ndk/reference/group/font) ## Installation ``` @@ -13,15 +14,17 @@ pip install FindSystemFontsFilename ## How to use it ```python -from find_system_fonts_filename import get_system_fonts_filename, FontConfigNotFound, OSNotSupported +from find_system_fonts_filename import AndroidLibraryNotFound, get_system_fonts_filename, FontConfigNotFound, OSNotSupported try: fonts_filename = get_system_fonts_filename() -except (FontConfigNotFound, OSNotSupported): +except (AndroidLibraryNotFound, FontConfigNotFound, OSNotSupported): # Deal with the exception - # OSNotSupported can only happen in Windows and macOS + # OSNotSupported can only happen in Windows, macOS and Android # - Windows Vista SP2 and more are supported # - macOS 10.6 and more are supported + # - Android SDK/API 29 and more are supported # FontConfigNotFound can only happen on Linux when Fontconfig could't be found. + # AndroidLibraryNotFound can only happen on Android when the android library could't be found. pass ``` diff --git a/find_system_fonts_filename/__init__.py b/find_system_fonts_filename/__init__.py index ab7bc20..21e08eb 100644 --- a/find_system_fonts_filename/__init__.py +++ b/find_system_fonts_filename/__init__.py @@ -1,4 +1,4 @@ from .fonts_filename import get_system_fonts_filename -from .exceptions import FontConfigNotFound, OSNotSupported +from .exceptions import AndroidLibraryNotFound, FontConfigNotFound, OSNotSupported -__version__ = "0.0.4" +__version__ = "0.1.0" diff --git a/find_system_fonts_filename/android_fonts.py b/find_system_fonts_filename/android_fonts.py new file mode 100644 index 0000000..7b3c87d --- /dev/null +++ b/find_system_fonts_filename/android_fonts.py @@ -0,0 +1,93 @@ +from contextlib import contextmanager +from ctypes import c_char_p, c_void_p, cdll, util +from os import close, devnull, dup, dup2, O_WRONLY, open +from sys import stderr, stdout +from typing import Set +from .exceptions import AndroidLibraryNotFound, OSNotSupported +from .system_fonts import SystemFonts + + +class AndroidFonts(SystemFonts): + _android = None + + def get_system_fonts_filename() -> Set[str]: + if AndroidFonts._android is None: + AndroidFonts._load_android_library() + + fonts_filename = set() + + # Redirect the stderr_and_stdout to null since we don't care about android logs + # There is __android_log_set_minimum_priority to set the log level, but even if I set it to 8 (which correspond to ANDROID_LOG_SILENT), + # it was still logging: https://developer.android.com/ndk/reference/group/logging#__android_log_set_minimum_priority + with AndroidFonts._silence_stderr_and_stdout(): + font_iterator = AndroidFonts._android.ASystemFontIterator_open() + + while True: + font = AndroidFonts._android.ASystemFontIterator_next(font_iterator) + + if font is None: + break + + font_filename = AndroidFonts._android.AFont_getFontFilePath(font).decode("utf-8") + fonts_filename.add(font_filename) + + AndroidFonts._android.AFont_close(font) + AndroidFonts._android.ASystemFontIterator_close(font_iterator) + + return fonts_filename + + @staticmethod + def _load_android_library(): + android_library_name = util.find_library("android") + + if android_library_name is None: + raise AndroidLibraryNotFound("You need to have the libandroid library. It is only available since the SDK/API level 29.") + + AndroidFonts._android = cdll.LoadLibrary(android_library_name) + + # The android device need to be at least on the level 29. + # The function android_get_device_api_level is only available since the level 29, so we can't use it. See: https://developer.android.com/ndk/reference/group/apilevels#android_get_device_api_level + # So, we try and see if the function are available + try: + # https://developer.android.com/ndk/reference/group/font#asystemfontiterator_open + AndroidFonts._android.ASystemFontIterator_open.restype = c_void_p + AndroidFonts._android.ASystemFontIterator_open.argtypes = [] + except AttributeError: + raise OSNotSupported("FindSystemFontsFilename only works on Android API level 29 or more.") + + # https://developer.android.com/ndk/reference/group/font#asystemfontiterator_next + AndroidFonts._android.ASystemFontIterator_next.restype = c_void_p + AndroidFonts._android.ASystemFontIterator_next.argtypes = [c_void_p] + + # https://developer.android.com/ndk/reference/group/font#asystemfontiterator_close + AndroidFonts._android.ASystemFontIterator_close.restype = None + AndroidFonts._android.ASystemFontIterator_close.argtypes = [c_void_p] + + # https://developer.android.com/ndk/reference/group/font#afont_getfontfilepath + AndroidFonts._android.AFont_getFontFilePath.restype = c_char_p + AndroidFonts._android.AFont_getFontFilePath.argtypes = [c_void_p] + + # https://developer.android.com/ndk/reference/group/font#afont_close + AndroidFonts._android.AFont_close.restype = None + AndroidFonts._android.AFont_close.argtypes = [c_void_p] + + @contextmanager + def _silence_stderr_and_stdout(): + # From: https://stackoverflow.com/a/75037627/15835974 + stderr_fd = stderr.fileno() + orig_stderr_fd = dup(stderr_fd) + + stdout_fd = stdout.fileno() + orig_stdout_fd = dup(stdout_fd) + + null_fd = open(devnull, O_WRONLY) + dup2(null_fd, stderr_fd) + dup2(null_fd, stdout_fd) + try: + yield + finally: + dup2(orig_stderr_fd, stderr_fd) + dup2(orig_stdout_fd, stdout_fd) + close(orig_stderr_fd) + close(orig_stdout_fd) + close(null_fd) diff --git a/find_system_fonts_filename/exceptions.py b/find_system_fonts_filename/exceptions.py index 3c046a5..7e974b6 100644 --- a/find_system_fonts_filename/exceptions.py +++ b/find_system_fonts_filename/exceptions.py @@ -4,5 +4,10 @@ class OSNotSupported(Exception): class FontConfigNotFound(Exception): - "Raised when a Fontconfig API haven't been found" - pass \ No newline at end of file + "Raised when the Fontconfig API haven't been found" + pass + + +class AndroidLibraryNotFound(Exception): + "Raised when the android library haven't been found" + pass diff --git a/find_system_fonts_filename/fonts_filename.py b/find_system_fonts_filename/fonts_filename.py index ca4d1a0..40d880a 100644 --- a/find_system_fonts_filename/fonts_filename.py +++ b/find_system_fonts_filename/fonts_filename.py @@ -1,8 +1,10 @@ from platform import system +from os import environ +from typing import Set from .exceptions import OSNotSupported -def get_system_fonts_filename(): +def get_system_fonts_filename() -> Set[str]: system_name = system() if system_name == "Windows": @@ -10,8 +12,13 @@ def get_system_fonts_filename(): return WindowsFonts.get_system_fonts_filename() elif system_name == "Linux": - from .linux_fonts import LinuxFonts - return LinuxFonts.get_system_fonts_filename() + # ANDROID_ROOT or ANDROID_BOOTLOGO - https://stackoverflow.com/a/66174754/15835974 + if any(t in environ.values() for t in ("ANDROID_ROOT", "ANDROID_BOOTLOGO")): + from .android_fonts import AndroidFonts + return AndroidFonts.get_system_fonts_filename() + else: + from .linux_fonts import LinuxFonts + return LinuxFonts.get_system_fonts_filename() elif system_name == "Darwin": from .mac_fonts import MacFonts