From 05c39fefd2f1e59b6f04736254f6e99d423400d4 Mon Sep 17 00:00:00 2001 From: shenjack <3695888@qq.com> Date: Sat, 17 Jun 2023 01:09:43 +0800 Subject: [PATCH] sync pyglet --- libs/pyglet/font/__init__.py | 96 ++++++++++++++++++++++++++++++++---- libs/pyglet/font/user.py | 86 ++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+), 9 deletions(-) create mode 100644 libs/pyglet/font/user.py diff --git a/libs/pyglet/font/__init__.py b/libs/pyglet/font/__init__.py index 78d7f00..828e28a 100644 --- a/libs/pyglet/font/__init__.py +++ b/libs/pyglet/font/__init__.py @@ -19,12 +19,17 @@ by this package. import os import sys import weakref +from typing import Dict, Union, BinaryIO, Optional, List, Iterable import pyglet +from pyglet.font.user import UserDefinedFont from pyglet import gl -if not getattr(sys, 'is_pyglet_doc_run', False): +def _get_system_font_class(): + """Get the appropriate class for the system being used. + Pyglet relies on OS dependent font systems for loading fonts and glyph creation. + """ if pyglet.compat_platform == 'darwin': from pyglet.font.quartz import QuartzFont _font_class = QuartzFont @@ -37,18 +42,86 @@ if not getattr(sys, 'is_pyglet_doc_run', False): else: from pyglet.font.win32 import GDIPlusFont _font_class = GDIPlusFont - else: from pyglet.font.freetype import FreeTypeFont _font_class = FreeTypeFont + return _font_class -def have_font(name): + +def create_font(name: str, mappings: Dict[str, pyglet.image.ImageData], default_char: str, + ascent: Optional[float] = None, + descent: Optional[float] = None, size: Optional[float] = None, bold: bool = False, + italic: bool = False, stretch: bool = False, dpi: Optional[float] = None, + font_class=UserDefinedFont): + """ + Create a custom font with the mappings provided. + + If a character in a string is not mapped in the font, it will use the default_char as fallback. + + Default size is 12. + Ascent and descent will use the image dimensions as default, but can be provided. + + The rest of the font parameters are used for font lookups. + """ + # Arbitrary default size + if size is None: + size = 12 + + if dpi is None: + dpi = 96 + + # Locate or create font cache + shared_object_space = gl.current_context.object_space + if not hasattr(shared_object_space, 'pyglet_font_font_cache'): + shared_object_space.pyglet_font_font_cache = weakref.WeakValueDictionary() + shared_object_space.pyglet_font_font_hold = [] + shared_object_space.pyglet_font_font_name_match = {} # Match a tuple to specific name to reduce lookups. + + font_cache = shared_object_space.pyglet_font_font_cache + font_hold = shared_object_space.pyglet_font_font_hold + + # Look for font name in font cache + descriptor = (name, size, bold, italic, stretch, dpi) + if descriptor in font_cache: + raise Exception("A font with these parameters has already been created.", descriptor) + + if _system_font_class.have_font(name): + raise Exception(f"Font name: '{name}' already exists within the system fonts.") + + # Not in cache, create from scratch + font = font_class(mappings, default_char, name, ascent, descent, size, bold, italic, stretch, dpi) + + # Save parameters for new-style layout classes to recover + # TODO: add properties to the Font classes, so these can be queried: + font.size = size + font.bold = bold + font.italic = italic + font.stretch = stretch + font.dpi = dpi + + if name not in _user_fonts: + _user_fonts.append(name) + + # Cache font in weak-ref dictionary to avoid reloading while still in use + font_cache[descriptor] = font + + # Hold onto refs of last three loaded fonts to prevent them being + # collected if momentarily dropped. + del font_hold[3:] + font_hold.insert(0, font) + + return font + + +def have_font(name: str) -> bool: """Check if specified system font name is available.""" - return _font_class.have_font(name) + return name in _user_fonts or _system_font_class.have_font(name) -def load(name=None, size=None, bold=False, italic=False, stretch=False, dpi=None): +def load(name: Optional[Union[str, Iterable[str]]] = None, size: Optional[float] = None, bold: bool = False, + italic: bool = False, + stretch: bool = False, dpi: Optional[float] = None): """Load a font for rendering. :Parameters: @@ -101,7 +174,7 @@ def load(name=None, size=None, bold=False, italic=False, stretch=False, dpi=None # Find first matching name, cache it. found_name = None for n in name: - if _font_class.have_font(n): + if n in _user_fonts or _system_font_class.have_font(n): found_name = n break @@ -114,7 +187,7 @@ def load(name=None, size=None, bold=False, italic=False, stretch=False, dpi=None return font_cache[descriptor] # Not in cache, create from scratch - font = _font_class(name, size, bold=bold, italic=italic, stretch=stretch, dpi=dpi) + font = _system_font_class(name, size, bold=bold, italic=italic, stretch=stretch, dpi=dpi) # Save parameters for new-style layout classes to recover # TODO: add properties to the Font classes, so these can be queried: @@ -135,7 +208,12 @@ def load(name=None, size=None, bold=False, italic=False, stretch=False, dpi=None return font -def add_file(font): +if not getattr(sys, 'is_pyglet_doc_run', False): + _system_font_class = _get_system_font_class() + _user_fonts = [] + + +def add_file(font: Union[str, BinaryIO]): """Add a font to pyglet's search path. In order to load a font that is not installed on the system, you must @@ -156,7 +234,7 @@ def add_file(font): font = open(font, 'rb') if hasattr(font, 'read'): font = font.read() - _font_class.add_font_data(font) + _system_font_class.add_font_data(font) def add_directory(directory): diff --git a/libs/pyglet/font/user.py b/libs/pyglet/font/user.py new file mode 100644 index 0000000..8bc4201 --- /dev/null +++ b/libs/pyglet/font/user.py @@ -0,0 +1,86 @@ +from typing import Dict, Optional + +import pyglet +from pyglet.font import base + + +class UserGlyphRenderer(base.GlyphRenderer): + def __init__(self, font: 'UserDefinedFont'): + self._font = font + self._font.glyphs[self._font.default_char] = self.render(self._font.default_char) + super().__init__(font) + + def render(self, text: str): + image_data = self._font.mappings[text] + glyph = self._font.create_glyph(image_data) + glyph.set_bearings(-self._font.descent, 0, image_data.width) + return glyph + + +class UserDefinedFont(base.Font): + """A basic UserDefinedFont, it takes a mappings of ImageData """ + glyph_renderer_class = UserGlyphRenderer + + def __init__(self, mappings: Dict[str, pyglet.image.ImageData], default_char: str, name: str, ascent: float, + descent: float, size: float, bold: bool = False, italic: bool = False, stretch: bool = False, + dpi: int = None, + locale: Optional[str] = None): + super().__init__() + self._name = name + self.mappings: Dict[str, pyglet.image.ImageData] = mappings + + if default_char not in self.mappings: + raise Exception(f"Default character: '{default_char}' must exist within your mappings.") + + if ascent is None or descent is None: + image = list(mappings.values())[0] + + if ascent is None: + ascent = image.height + + if descent is None: + descent = 0 + + self.ascent = ascent + self.descent = descent + self.default_char = default_char + + self.bold = bold + self.italic = italic + self.stretch = stretch + self.dpi = dpi + self.size = size + self.locale = locale + + @property + def name(self): + return self._name + + def get_glyphs(self, text): + """Create and return a list of Glyphs for `text`. + + If any characters do not have a known glyph representation in this + font, a substitution will be made. + + :Parameters: + `text` : str or unicode + Text to render. + + :rtype: list of `Glyph` + """ + glyph_renderer = None + glyphs = [] # glyphs that are committed. + for c in base.get_grapheme_clusters(str(text)): + # Get the glyph for 'c'. Hide tabs (Windows and Linux render + # boxes) + if c == '\t': + c = ' ' + if c not in self.glyphs: + if not glyph_renderer: + glyph_renderer = self.glyph_renderer_class(self) + if c not in self.mappings: + c = self.default_char + else: + self.glyphs[c] = glyph_renderer.render(c) + glyphs.append(self.glyphs[c]) + return glyphs