From 22dba5d5d12c4a0eca5880b40782908f950ad540 Mon Sep 17 00:00:00 2001 From: caffeinepills Date: Tue, 16 Apr 2024 20:00:25 -0500 Subject: [PATCH 1/5] Add annotations and doc style to font package. (#1093) * Update __init__ font. * Update font base. * Update font base and DirectWrite. * Freetype annotations. * DirectWrite Annotations * Add Quartz Annoations * Add typings for TTF. Probably all wrong as nothing can be inspected to do table creation inside a function and data being decoded from data. * Add ClassVar * Add User defined font to font rst. Add some documentation for adding and creating a UserDefinedFont. * Update typing for win32 font module. * Keep quote style to preservation level. Add per file ignores for some of the platform and non-public specific modules. * Fix ctypes imports coming from _ctypes. --- doc/modules/font.rst | 6 + pyglet/font/__init__.py | 95 +-- pyglet/font/base.py | 280 ++++---- pyglet/font/directwrite.py | 1251 ++++++++++++++++++----------------- pyglet/font/fontconfig.py | 200 +++--- pyglet/font/freetype.py | 173 +++-- pyglet/font/freetype_lib.py | 652 +++++++++--------- pyglet/font/quartz.py | 78 ++- pyglet/font/ttf.py | 477 +++++++------ pyglet/font/user.py | 223 +++++-- pyglet/font/win32.py | 68 +- pyproject.toml | 12 +- 12 files changed, 1907 insertions(+), 1608 deletions(-) diff --git a/doc/modules/font.rst b/doc/modules/font.rst index f03aad19c..7081216fe 100644 --- a/doc/modules/font.rst +++ b/doc/modules/font.rst @@ -4,3 +4,9 @@ pyglet.font .. automodule:: pyglet.font :members: :undoc-members: + +pyglet.font.user +================ +.. automodule:: pyglet.font.user + :members: + :undoc-members: diff --git a/pyglet/font/__init__.py b/pyglet/font/__init__.py index 740abe054..a4de4b807 100644 --- a/pyglet/font/__init__.py +++ b/pyglet/font/__init__.py @@ -15,29 +15,33 @@ See the :mod:`pyglet.font.base` module for documentation on the base classes used by this package. """ +from __future__ import annotations import os import sys import weakref -from typing import Union, BinaryIO, Optional, Iterable +from typing import TYPE_CHECKING, BinaryIO, Iterable import pyglet -from pyglet.font.user import UserDefinedFontBase from pyglet import gl +from pyglet.font.user import UserDefinedFontBase +if TYPE_CHECKING: + from pyglet.font.base import Font -def _get_system_font_class(): + +def _get_system_font_class() -> type[Font]: """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': + if pyglet.compat_platform == "darwin": from pyglet.font.quartz import QuartzFont _font_class = QuartzFont - elif pyglet.compat_platform in ('win32', 'cygwin'): + elif pyglet.compat_platform in ("win32", "cygwin"): from pyglet.libs.win32.constants import WINDOWS_7_OR_GREATER - if WINDOWS_7_OR_GREATER and not pyglet.options['win32_gdi_font']: + if WINDOWS_7_OR_GREATER and not pyglet.options["win32_gdi_font"]: from pyglet.font.directwrite import Win32DirectWriteFont _font_class = Win32DirectWriteFont else: @@ -50,22 +54,26 @@ def _get_system_font_class(): return _font_class -def add_user_font(font: UserDefinedFontBase): +def add_user_font(font: UserDefinedFontBase) -> None: """Add a custom font created by the user. A strong reference needs to be applied to the font object, otherwise pyglet may not find the font later. - :Parameters: - `font` : `~pyglet.font.user.UserDefinedFont` + Args: + font: A font class instance defined by user. + + Raises: + Exception: If font provided is not derived from :py:class:`~pyglet.font.user.UserDefinedFontBase`. """ if not isinstance(font, UserDefinedFontBase): - raise Exception("Font is not must be created fromm the UserDefinedFontBase.") + msg = "Font is not must be created fromm the UserDefinedFontBase." + raise Exception(msg) # Locate or create font cache shared_object_space = gl.current_context.object_space - if not hasattr(shared_object_space, 'pyglet_font_font_cache'): + 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 = [] # Match a tuple to specific name to reduce lookups. @@ -76,9 +84,11 @@ def add_user_font(font: UserDefinedFontBase): # Look for font name in font cache descriptor = (font.name, font.size, font.bold, font.italic, font.stretch, font.dpi) if descriptor in font_cache: - raise Exception(f"A font with parameters {descriptor} has already been created.") + msg = f"A font with parameters {descriptor} has already been created." + raise Exception(msg) if _system_font_class.have_font(font.name): - raise Exception(f"Font name '{font.name}' already exists within the system fonts.") + msg = f"Font name '{font.name}' already exists within the system fonts." + raise Exception(msg) if font.name not in _user_fonts: _user_fonts.append(font.name) @@ -91,33 +101,36 @@ def add_user_font(font: UserDefinedFontBase): def have_font(name: str) -> bool: - """Check if specified system font name is available.""" + """Check if specified font name is available in the system database or user font database.""" return name in _user_fonts or _system_font_class.have_font(name) -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): +def load(name: str | Iterable[str] | None = None, size: float | None = None, bold: bool | str = False, + italic: bool | str = False, stretch: bool | str = False, dpi: float | None = None) -> Font: """Load a font for rendering. - :Parameters: - `name` : str, or list of str + Args: + name: Font family, for example, "Times New Roman". If a list of names is provided, the first one matching a known font is used. If no - font can be matched to the name(s), a default font is used. In - pyglet 1.1, the name may be omitted. - `size` : float + font can be matched to the name(s), a default font is used. The default font + will be platform dependent. + size: Size of the font, in points. The returned font may be an exact match or the closest available. - `bold` : bool + bold: If True, a bold variant is returned, if one exists for the given - family and size. - `italic` : bool - If True, an italic variant is returned, if one exists for the given - family and size. - `dpi` : float + family and size. For some Font renderers, bold is the weight of the font, and a string + can be provided specifying the weight. For example, "semibold" or "light". + italic: + If True, an italic variant is returned, if one exists for the given family and size. For some Font + renderers, italics may have an "oblique" variation which can be specified as a string. + stretch: + If True, a stretch variant is returned, if one exists for the given family and size. Currently only + supported by Windows through the ``DirectWrite`` font renderer. For example, "condensed" or "expanded". + dpi: float The assumed resolution of the display device, for the purposes of determining the pixel size of the font. Defaults to 96. - :rtype: `Font` """ # Arbitrary default size if size is None: @@ -127,7 +140,7 @@ def load(name: Optional[Union[str, Iterable[str]]] = None, size: Optional[float] # Locate or create font cache shared_object_space = gl.current_context.object_space - if not hasattr(shared_object_space, 'pyglet_font_font_cache'): + 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 = [] # Match a tuple to specific name to reduce lookups. @@ -162,7 +175,7 @@ def load(name: Optional[Union[str, Iterable[str]]] = None, size: Optional[float] # Not in cache, create from scratch 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: + # TODO: add properties to the base Font so completion is proper: font.size = size font.bold = bold font.italic = italic @@ -178,12 +191,12 @@ def load(name: Optional[Union[str, Iterable[str]]] = None, size: Optional[float] return font -if not getattr(sys, 'is_pyglet_doc_run', False): +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]): +def add_file(font: str | BinaryIO) -> None: """Add a font to pyglet's search path. In order to load a font that is not installed on the system, you must @@ -195,32 +208,32 @@ def add_file(font: Union[str, BinaryIO]): you should pass the face name (not the file name) to :meth::py:func:`pyglet.font.load` or any other place where you normally specify a font. - :Parameters: - `font` : str or file-like object + Args: + font: Filename or file-like object to load fonts from. """ if isinstance(font, str): - font = open(font, 'rb') - if hasattr(font, 'read'): + font = open(font, "rb") # noqa: SIM115 + if hasattr(font, "read"): font = font.read() _system_font_class.add_font_data(font) -def add_directory(directory): +def add_directory(directory: str) -> None: """Add a directory of fonts to pyglet's search path. This function simply calls :meth:`pyglet.font.add_file` for each file with a ``.ttf`` extension in the given directory. Subdirectories are not searched. - :Parameters: - `dir` : str + Args: + directory: Directory that contains font files. """ for file in os.listdir(directory): - if file[-4:].lower() == '.ttf': + if file[-4:].lower() == ".ttf": add_file(os.path.join(directory, file)) -__all__ = ('add_file', 'add_directory', 'add_user_font', 'load', 'have_font') +__all__ = ("add_file", "add_directory", "add_user_font", "load", "have_font") diff --git a/pyglet/font/base.py b/pyglet/font/base.py index 7cde5bb27..63ccb0c77 100644 --- a/pyglet/font/base.py +++ b/pyglet/font/base.py @@ -4,30 +4,32 @@ in `pyglet.font` to obtain platform-specific instances. You can use these classes as a documented interface to the concrete classes. """ +from __future__ import annotations +import abc import unicodedata +from typing import BinaryIO, ClassVar -from pyglet.gl import * from pyglet import image +from pyglet.gl import GL_LINEAR, GL_RGBA, GL_TEXTURE_2D _other_grapheme_extend = list(map(chr, [0x09be, 0x09d7, 0x0be3, 0x0b57, 0x0bbe, 0x0bd7, 0x0cc2, 0x0cd5, 0x0cd6, 0x0d3e, 0x0d57, 0x0dcf, 0x0ddf, 0x200c, 0x200d, 0xff9e, 0xff9f])) # skip codepoints above U+10000 _logical_order_exception = list(map(chr, list(range(0xe40, 0xe45)) + list(range(0xec0, 0xec4)))) -_grapheme_extend = lambda c, cc: cc in ('Me', 'Mn') or c in _other_grapheme_extend +_grapheme_extend = lambda c, cc: cc in ("Me", "Mn") or c in _other_grapheme_extend -_CR = u'\u000d' -_LF = u'\u000a' -_control = lambda c, cc: cc in ('ZI', 'Zp', 'Cc', 'Cf') and not \ - c in list(map(chr, [0x000d, 0x000a, 0x200c, 0x200d])) +_CR = "\u000d" +_LF = "\u000a" +_control = lambda c, cc: cc in ("ZI", "Zp", "Cc", "Cf") and c not in list(map(chr, [0x000d, 0x000a, 0x200c, 0x200d])) _extend = lambda c, cc: _grapheme_extend(c, cc) or \ c in list(map(chr, [0xe30, 0xe32, 0xe33, 0xe45, 0xeb0, 0xeb2, 0xeb3])) -_prepend = lambda c, cc: c in _logical_order_exception -_spacing_mark = lambda c, cc: cc == 'Mc' and c not in _other_grapheme_extend +_prepend = lambda c, cc: c in _logical_order_exception # noqa: ARG005 +_spacing_mark = lambda c, cc: cc == "Mc" and c not in _other_grapheme_extend -def grapheme_break(left, right): +def grapheme_break(left: str, right: str) -> bool: # noqa: D103 # GB1 if left is None: return True @@ -68,30 +70,28 @@ def grapheme_break(left, right): return True -def get_grapheme_clusters(text): +def get_grapheme_clusters(text: str) -> list[str]: """Implements Table 2 of UAX #29: Grapheme Cluster Boundaries. Does not currently implement Hangul syllable rules. - - :Parameters: - `text` : unicode - String to cluster. - .. versionadded:: 1.1.2 + Args: + text: unicode + String to cluster. - :rtype: List of `unicode` - :return: List of Unicode grapheme clusters + Returns: + List of Unicode grapheme clusters. """ clusters = [] - cluster = '' + cluster = "" left = None for right in text: if cluster and grapheme_break(left, right): clusters.append(cluster) - cluster = '' + cluster = "" elif cluster: # Add a zero-width space to keep len(clusters) == len(text) - clusters.append(u'\u200b') + clusters.append("\u200b") cluster += right left = right @@ -104,40 +104,32 @@ def get_grapheme_clusters(text): class Glyph(image.TextureRegion): """A single glyph located within a larger texture. - Glyphs are drawn most efficiently using the higher level APIs, for example - `GlyphString`. + Glyphs are drawn most efficiently using the higher level APIs. + """ + baseline: int = 0 + lsb: int = 0 + advance: int = 0 - :Ivariables: - `advance` : int - The horizontal advance of this glyph, in pixels. - `vertices` : (int, int, int, int) - The vertices of this glyph, with (0,0) originating at the - left-side bearing at the baseline. - `colored` : bool - If a glyph is colored by the font renderer, such as an emoji, it may - be treated differently by pyglet. For example, being omitted from text color shaders. + #: :The vertices of this glyph, with (0,0) originating at the left-side bearing at the baseline. + vertices: tuple[int, int, int, int] = (0, 0, 0, 0) - """ - baseline = 0 - lsb = 0 - advance = 0 - vertices = (0, 0, 0, 0) + #: :If a glyph is colored by the font renderer, such as an emoji, it may be treated differently by pyglet. colored = False - def set_bearings(self, baseline, left_side_bearing, advance, x_offset=0, y_offset=0): + def set_bearings(self, baseline: int, left_side_bearing: int, advance: int, x_offset: int = 0, + y_offset: int = 0) -> None: """Set metrics for this glyph. - :Parameters: - `baseline` : int - Distance from the bottom of the glyph to its baseline; - typically negative. - `left_side_bearing` : int + Args: + baseline: + Distance from the bottom of the glyph to its baseline. Typically negative. + left_side_bearing: Distance to add to the left edge of the glyph. - `advance` : int - Distance to move the horizontal advance to the next glyph. - `offset_x` : int + advance: + Distance to move the horizontal advance to the next glyph, in pixels. + x_offset: Distance to move the glyph horizontally from its default position. - `offset_y` : int + y_offset: Distance to move the glyph vertically from its default position. """ self.baseline = baseline @@ -150,33 +142,35 @@ def set_bearings(self, baseline, left_side_bearing, advance, x_offset=0, y_offse left_side_bearing + self.width + x_offset, -baseline + self.height + y_offset) - def get_kerning_pair(self, right_glyph): - """Not implemented. - """ - return 0 - class GlyphTexture(image.Texture): + """A texture containing a glyph.""" region_class = Glyph class GlyphTextureAtlas(image.atlas.TextureAtlas): - """A texture atlas containing glyphs.""" + """A texture atlas containing many glyphs.""" texture_class = GlyphTexture - def __init__(self, width=2048, height=2048, fmt=GL_RGBA, min_filter=GL_LINEAR, mag_filter=GL_LINEAR): + def __init__(self, width: int = 2048, height: int = 2048, fmt: int = GL_RGBA, min_filter: int = GL_LINEAR, # noqa: D107 + mag_filter: int = GL_LINEAR) -> None: + super().__init__(width, height) self.texture = self.texture_class.create(width, height, GL_TEXTURE_2D, fmt, min_filter, mag_filter, fmt=fmt) self.allocator = image.atlas.Allocator(width, height) + def add(self, img: image.AbstractImage, border: int = 0) -> Glyph: + return super().add(img, border) + class GlyphTextureBin(image.atlas.TextureBin): """Same as a TextureBin but allows you to specify filter of Glyphs.""" - def add(self, img, fmt=GL_RGBA, min_filter=GL_LINEAR, mag_filter=GL_LINEAR, border=0): + def add(self, img: image.AbstractImage, fmt: int = GL_RGBA, min_filter: int = GL_LINEAR, + mag_filter: int = GL_LINEAR, border: int = 0) -> Glyph: for atlas in list(self.atlases): try: return atlas.add(img, border) - except image.atlas.AllocatorException: + except image.atlas.AllocatorException: # noqa: PERF203 # Remove atlases that are no longer useful (so that their textures # can later be freed if the images inside them get collected). if img.width < 64 and img.height < 64: @@ -187,21 +181,31 @@ def add(self, img, fmt=GL_RGBA, min_filter=GL_LINEAR, mag_filter=GL_LINEAR, bord return atlas.add(img, border) -class GlyphRenderer: - """Abstract class for creating glyph images. - """ +class GlyphRenderer(abc.ABC): + """Abstract class for creating glyph images.""" - def __init__(self, font): - pass + @abc.abstractmethod + def __init__(self, font: Font) -> None: + """Initialize the glyph renderer. - def render(self, text): - raise NotImplementedError('Subclass must override') + Args: + font: The :py:class:`~pyglet.font.base.Font` object to be rendered. + """ + @abc.abstractmethod + def render(self, text: str) -> Glyph: + """Render the string of text into an image. -class FontException(Exception): - """Generic exception related to errors from the font module. Typically - these relate to invalid font data.""" - pass + Args: + text: The initial string to be rendered, typically one character. + + Returns: + A Glyph with the proper metrics for that specific character. + """ + + +class FontException(Exception): # noqa: N818 + """Generic exception related to errors from the font module. Typically, from invalid font data.""" class Font: @@ -213,40 +217,65 @@ class Font: Internally, this class is used by the platform classes to manage the set of textures into which glyphs are written. - :Ivariables: - `ascent` : int - Maximum ascent above the baseline, in pixels. - `descent` : int - Maximum descent below the baseline, in pixels. Usually negative. + Attributes: + texture_width: + Default Texture width to use if ``optimize_glyph`` is False. + texture_height: + Default Texture height to use if ``optimize_glyph`` is False. + optimize_fit: + Determines max texture size by the ``glyph_fit`` attribute. If False, ``texture_width`` and + ``texture_height`` are used. + glyph_fit: + Standard keyboard characters amount to around ~100 alphanumeric characters. This value is used to + pre-calculate how many glyphs can be saved into a single texture atlas. Increase this if you plan to + support more than this standard scenario. Performance is increased the less textures are used. However, + it does consume more video memory. + texture_internalformat: + Determines how textures are stored in internal format. By default, ``GL_RGBA``. + texture_min_filter: + The default minification filter for glyph textures. By default, ``GL_LINEAR``. Can be changed to + ``GL_NEAREST`` to prevent aliasing with pixelated fonts. + texture_mag_filter: + The default magnification filter for glyph textures. By default, ``GL_LINEAR``. Can be changed to + ``GL_NEAREST`` to prevent aliasing with pixelated fonts. """ - texture_width = 512 - texture_height = 512 + #: :meta private: + glyphs: dict[str, Glyph] + + texture_width: int = 512 + texture_height: int = 512 - optimize_fit = True - glyph_fit = 100 + optimize_fit: int = True + glyph_fit: int = 100 - texture_internalformat = GL_RGBA - texture_min_filter = GL_LINEAR - texture_mag_filter = GL_LINEAR + texture_internalformat: int = GL_RGBA + texture_min_filter: int = GL_LINEAR + texture_mag_filter: int = GL_LINEAR # These should also be set by subclass when known - ascent = 0 - descent = 0 + ascent: int = 0 + descent: int = 0 - glyph_renderer_class = GlyphRenderer - texture_class = GlyphTextureBin + #: :meta private: + # The default glyph renderer class. Should not be overridden by users, only other renderer variations. + glyph_renderer_class: ClassVar[type[GlyphRenderer]] = GlyphRenderer - def __init__(self): + #: :meta private: + # The default type of texture bins. Should not be overridden by users. + texture_class: ClassVar[type[GlyphTextureBin]] = GlyphTextureBin + + def __init__(self) -> None: + """Initialize a font that can be used with Pyglet.""" self.texture_bin = None self.glyphs = {} @property - def name(self): + @abc.abstractmethod + def name(self) -> str: """Return the Family Name of the font as a string.""" - raise NotImplementedError @classmethod - def add_font_data(cls, data): + def add_font_data(cls: type[Font], data: BinaryIO) -> None: """Add font data to the font loader. This is a class method and affects all fonts loaded. Data must be @@ -257,62 +286,53 @@ def add_font_data(cls, data): There is no way to instantiate a font given the data directly, you must use :py:func:`pyglet.font.load` specifying the font name. """ - pass @classmethod - def have_font(cls, name): + def have_font(cls: type[Font], name: str) -> bool: """Determine if a font with the given name is installed. - :Parameters: - `name` : str - Name of a font to search for - - :rtype: bool + Args: + name: + Name of a font to search for. """ return True - def create_glyph(self, image, fmt=None): + def create_glyph(self, img: image.AbstractImage, fmt: int | None = None) -> Glyph: """Create a glyph using the given image. This is used internally by `Font` subclasses to add glyph data - to the font. Glyphs are packed within large textures maintained by - `Font`. This method inserts the image into a font texture and returns - a glyph reference; it is up to the subclass to add metadata to the - glyph. + to the font. Glyphs are packed within large textures maintained by the + `Font` instance. This method inserts the image into a texture atlas managed by the font. Applications should not use this method directly. - :Parameters: - `image` : `pyglet.image.AbstractImage` + Args: + img: The image to write to the font texture. - `fmt` : `int` - Override for the format and internalformat of the atlas texture - - :rtype: `Glyph` + fmt: + Override for the format and internalformat of the atlas texture. None will use default. """ if self.texture_bin is None: if self.optimize_fit: - self.texture_width, self.texture_height = self._get_optimal_atlas_size(image) + self.texture_width, self.texture_height = self._get_optimal_atlas_size(img) self.texture_bin = GlyphTextureBin(self.texture_width, self.texture_height) - glyph = self.texture_bin.add( - image, fmt or self.texture_internalformat, self.texture_min_filter, self.texture_mag_filter, border=1) - - return glyph + return self.texture_bin.add( + img, fmt or self.texture_internalformat, self.texture_min_filter, self.texture_mag_filter, border=1) - def _get_optimal_atlas_size(self, image_data): - """Return the smallest size of atlas that can fit around 100 glyphs based on the image_data provided.""" + def _get_optimal_atlas_size(self, image_data: image.AbstractImage) -> tuple[int, int]: + """Retrieves the optimal atlas size to fit ``image_data`` with ``glyph_fit`` number of glyphs.""" # A texture glyph sheet should be able to handle all standard keyboard characters in one sheet. # 26 Alpha upper, 26 lower, 10 numbers, 33 symbols, space = around 96 characters. (Glyph Fit) aw, ah = self.texture_width, self.texture_height - atlas_size = None + atlas_size: tuple[int, int] | None = None # Just a fast check to get the smallest atlas size possible to fit. i = 0 while not atlas_size: fit = ((aw - (image_data.width + 2)) // (image_data.width + 2) + 1) * ( - (ah - (image_data.height + 2)) // (image_data.height + 2) + 1) + (ah - (image_data.height + 2)) // (image_data.height + 2) + 1) if fit >= self.glyph_fit: atlas_size = (aw, ah) @@ -326,25 +346,23 @@ def _get_optimal_atlas_size(self, image_data): return atlas_size - def get_glyphs(self, text): + def get_glyphs(self, text: str) -> list[Glyph]: """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 + Args: + text: Text to render. - - :rtype: list of `Glyph` """ glyph_renderer = None glyphs = [] # glyphs that are committed. for c in get_grapheme_clusters(str(text)): # Get the glyph for 'c'. Hide tabs (Windows and Linux render # boxes) - if c == '\t': - c = ' ' + if c == "\t": + c = " " # noqa: PLW2901 if c not in self.glyphs: if not glyph_renderer: glyph_renderer = self.glyph_renderer_class(self) @@ -352,9 +370,9 @@ def get_glyphs(self, text): glyphs.append(self.glyphs[c]) return glyphs - def get_glyphs_for_width(self, text, width): - """Return a list of glyphs for `text` that fit within the given width. - + def get_glyphs_for_width(self, text: str, width: int) -> list[Glyph]: + """Return a list of glyphs for ``text`` that fit within the given width. + If the entire text is larger than 'width', as much as possible will be used while breaking after a space or zero-width space character. If a newline is encountered in text, only text up to that newline will be @@ -367,21 +385,17 @@ def get_glyphs_for_width(self, text, width): exactly one glyph; so the amount of text "used up" can be determined by examining the length of the returned glyph list. - :Parameters: - `text` : str or unicode + Args: + text: Text to render. - `width` : int + width: Maximum width of returned glyphs. - - :rtype: list of `Glyph` - - :see: `GlyphString` """ glyph_renderer = None glyph_buffer = [] # next glyphs to be added, as soon as a BP is found glyphs = [] # glyphs that are committed. for c in text: - if c == '\n': + if c == "\n": glyphs += glyph_buffer break @@ -401,7 +415,7 @@ def get_glyphs_for_width(self, text, width): break # If a valid breakpoint, commit holding buffer - if c in u'\u0020\u200b': + if c in "\u0020\u200b": glyphs += glyph_buffer glyph_buffer = [] @@ -411,5 +425,5 @@ def get_glyphs_for_width(self, text, width): return glyphs - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}('{self.name}')" diff --git a/pyglet/font/directwrite.py b/pyglet/font/directwrite.py index 232c1fa2c..f9070227b 100644 --- a/pyglet/font/directwrite.py +++ b/pyglet/font/directwrite.py @@ -1,40 +1,74 @@ +from __future__ import annotations + import copy +import math import os import pathlib import platform -from ctypes import * -from typing import List, Optional, Tuple +from ctypes import ( + HRESULT, + POINTER, + Array, + Structure, + byref, + c_int, + c_int16, + c_int32, + c_ubyte, + c_uint8, + c_uint16, + c_uint32, + c_uint64, + c_void_p, + c_wchar, + c_wchar_p, + cast, + create_unicode_buffer, + pointer, + sizeof, + windll, +) +from ctypes.wintypes import BOOL, FLOAT, HDC, UINT, WCHAR +from typing import TYPE_CHECKING, BinaryIO, NoReturn -import math import pyglet from pyglet.font import base -from pyglet.image.codecs.wic import IWICBitmap, WICDecoder, GUID_WICPixelFormat32bppPBGRA +from pyglet.image import ImageData +from pyglet.image.codecs.wic import GUID_WICPixelFormat32bppPBGRA, IWICBitmap, WICDecoder +from pyglet.libs.win32 import LOGFONTW, c_void, com from pyglet.libs.win32 import _kernel32 as kernel32 -from pyglet.libs.win32.constants import * -from pyglet.libs.win32.types import * +from pyglet.libs.win32.constants import ( + LOCALE_NAME_MAX_LENGTH, + WINDOWS_8_1_OR_GREATER, + WINDOWS_10_CREATORS_UPDATE_OR_GREATER, +) from pyglet.util import debug_print +if TYPE_CHECKING: + from pyglet.font.base import Glyph + try: - dwrite = 'dwrite' + dwrite = "dwrite" # System32 and SysWOW64 folders are opposite perception in Windows x64. # System32 = x64 dll's | SysWOW64 = x86 dlls # By default ctypes only seems to look in system32 regardless of Python architecture, which has x64 dlls. - if platform.architecture()[0] == '32bit': - if platform.machine().endswith('64'): # Machine is 64 bit, Python is 32 bit. - dwrite = os.path.join(os.environ['WINDIR'], 'SysWOW64', 'dwrite.dll') + if platform.architecture()[0] == "32bit": + if platform.machine().endswith("64"): # Machine is 64 bit, Python is 32 bit. + dwrite = os.path.join(os.environ["WINDIR"], "SysWOW64", "dwrite.dll") - dwrite_lib = ctypes.windll.LoadLibrary(dwrite) -except OSError as err: + dwrite_lib = windll.LoadLibrary(dwrite) +except OSError: # Doesn't exist? Should stop import of library. - pass + msg = "DirectWrite Not Found" + raise ImportError(msg) # noqa: B904 -_debug_font = pyglet.options['debug_font'] +_debug_font = pyglet.options["debug_font"] -_debug_print = debug_print('debug_font') +_debug_print = debug_print("debug_font") -def DWRITE_MAKE_OPENTYPE_TAG(a, b, c, d): +def DWRITE_MAKE_OPENTYPE_TAG(a: str, b: str, c: str, d: str) -> int: return ord(d) << 24 | ord(c) << 16 | ord(b) << 8 | ord(a) @@ -60,23 +94,24 @@ def DWRITE_MAKE_OPENTYPE_TAG(a, b, c, d): DWRITE_FONT_WEIGHT_HEAVY = 900 DWRITE_FONT_WEIGHT_EXTRA_BLACK = 950 -name_to_weight = {"thin": DWRITE_FONT_WEIGHT_THIN, - "extralight": DWRITE_FONT_WEIGHT_EXTRA_LIGHT, - "ultralight": DWRITE_FONT_WEIGHT_ULTRA_LIGHT, - "light": DWRITE_FONT_WEIGHT_LIGHT, - "semilight": DWRITE_FONT_WEIGHT_SEMI_LIGHT, - "normal": DWRITE_FONT_WEIGHT_NORMAL, - "regular": DWRITE_FONT_WEIGHT_REGULAR, - "medium": DWRITE_FONT_WEIGHT_MEDIUM, - "demibold": DWRITE_FONT_WEIGHT_DEMI_BOLD, - "semibold": DWRITE_FONT_WEIGHT_SEMI_BOLD, - "bold": DWRITE_FONT_WEIGHT_BOLD, - "extrabold": DWRITE_FONT_WEIGHT_EXTRA_BOLD, - "ultrabold": DWRITE_FONT_WEIGHT_ULTRA_BOLD, - "black": DWRITE_FONT_WEIGHT_BLACK, - "heavy": DWRITE_FONT_WEIGHT_HEAVY, - "extrablack": DWRITE_FONT_WEIGHT_EXTRA_BLACK, - } +name_to_weight = { + "thin": DWRITE_FONT_WEIGHT_THIN, + "extralight": DWRITE_FONT_WEIGHT_EXTRA_LIGHT, + "ultralight": DWRITE_FONT_WEIGHT_ULTRA_LIGHT, + "light": DWRITE_FONT_WEIGHT_LIGHT, + "semilight": DWRITE_FONT_WEIGHT_SEMI_LIGHT, + "normal": DWRITE_FONT_WEIGHT_NORMAL, + "regular": DWRITE_FONT_WEIGHT_REGULAR, + "medium": DWRITE_FONT_WEIGHT_MEDIUM, + "demibold": DWRITE_FONT_WEIGHT_DEMI_BOLD, + "semibold": DWRITE_FONT_WEIGHT_SEMI_BOLD, + "bold": DWRITE_FONT_WEIGHT_BOLD, + "extrabold": DWRITE_FONT_WEIGHT_EXTRA_BOLD, + "ultrabold": DWRITE_FONT_WEIGHT_ULTRA_BOLD, + "black": DWRITE_FONT_WEIGHT_BLACK, + "heavy": DWRITE_FONT_WEIGHT_HEAVY, + "extrablack": DWRITE_FONT_WEIGHT_EXTRA_BLACK, +} DWRITE_FONT_STRETCH = UINT DWRITE_FONT_STRETCH_UNDEFINED = 0 @@ -90,18 +125,19 @@ def DWRITE_MAKE_OPENTYPE_TAG(a, b, c, d): DWRITE_FONT_STRETCH_EXPANDED = 7 DWRITE_FONT_STRETCH_EXTRA_EXPANDED = 8 -name_to_stretch = {"undefined": DWRITE_FONT_STRETCH_UNDEFINED, - "ultracondensed": DWRITE_FONT_STRETCH_ULTRA_CONDENSED, - "extracondensed": DWRITE_FONT_STRETCH_EXTRA_CONDENSED, - "condensed": DWRITE_FONT_STRETCH_CONDENSED, - "semicondensed": DWRITE_FONT_STRETCH_SEMI_CONDENSED, - "normal": DWRITE_FONT_STRETCH_NORMAL, - "medium": DWRITE_FONT_STRETCH_MEDIUM, - "semiexpanded": DWRITE_FONT_STRETCH_SEMI_EXPANDED, - "expanded": DWRITE_FONT_STRETCH_EXPANDED, - "extraexpanded": DWRITE_FONT_STRETCH_EXTRA_EXPANDED, - "narrow": DWRITE_FONT_STRETCH_CONDENSED, - } +name_to_stretch = { + "undefined": DWRITE_FONT_STRETCH_UNDEFINED, + "ultracondensed": DWRITE_FONT_STRETCH_ULTRA_CONDENSED, + "extracondensed": DWRITE_FONT_STRETCH_EXTRA_CONDENSED, + "condensed": DWRITE_FONT_STRETCH_CONDENSED, + "semicondensed": DWRITE_FONT_STRETCH_SEMI_CONDENSED, + "normal": DWRITE_FONT_STRETCH_NORMAL, + "medium": DWRITE_FONT_STRETCH_MEDIUM, + "semiexpanded": DWRITE_FONT_STRETCH_SEMI_EXPANDED, + "expanded": DWRITE_FONT_STRETCH_EXPANDED, + "extraexpanded": DWRITE_FONT_STRETCH_EXTRA_EXPANDED, + "narrow": DWRITE_FONT_STRETCH_CONDENSED, +} DWRITE_GLYPH_IMAGE_FORMATS = c_int @@ -134,9 +170,11 @@ def DWRITE_MAKE_OPENTYPE_TAG(a, b, c, d): DWRITE_FONT_STYLE_OBLIQUE = 1 DWRITE_FONT_STYLE_ITALIC = 2 -name_to_style = {"normal": DWRITE_FONT_STYLE_NORMAL, - "oblique": DWRITE_FONT_STYLE_OBLIQUE, - "italic": DWRITE_FONT_STYLE_ITALIC} +name_to_style = { + "normal": DWRITE_FONT_STYLE_NORMAL, + "oblique": DWRITE_FONT_STYLE_OBLIQUE, + "italic": DWRITE_FONT_STYLE_ITALIC, +} UINT8 = c_uint8 UINT16 = c_uint16 @@ -176,127 +214,128 @@ def DWRITE_MAKE_OPENTYPE_TAG(a, b, c, d): class D2D_POINT_2F(Structure): _fields_ = ( - ('x', FLOAT), - ('y', FLOAT), + ("x", FLOAT), + ("y", FLOAT), ) class D2D1_RECT_F(Structure): _fields_ = ( - ('left', FLOAT), - ('top', FLOAT), - ('right', FLOAT), - ('bottom', FLOAT), + ("left", FLOAT), + ("top", FLOAT), + ("right", FLOAT), + ("bottom", FLOAT), ) class D2D1_COLOR_F(Structure): _fields_ = ( - ('r', FLOAT), - ('g', FLOAT), - ('b', FLOAT), - ('a', FLOAT), + ("r", FLOAT), + ("g", FLOAT), + ("b", FLOAT), + ("a", FLOAT), ) -class DWRITE_TEXT_METRICS(ctypes.Structure): +class DWRITE_TEXT_METRICS(Structure): _fields_ = ( - ('left', FLOAT), - ('top', FLOAT), - ('width', FLOAT), - ('widthIncludingTrailingWhitespace', FLOAT), - ('height', FLOAT), - ('layoutWidth', FLOAT), - ('layoutHeight', FLOAT), - ('maxBidiReorderingDepth', UINT32), - ('lineCount', UINT32), + ("left", FLOAT), + ("top", FLOAT), + ("width", FLOAT), + ("widthIncludingTrailingWhitespace", FLOAT), + ("height", FLOAT), + ("layoutWidth", FLOAT), + ("layoutHeight", FLOAT), + ("maxBidiReorderingDepth", UINT32), + ("lineCount", UINT32), ) -class DWRITE_FONT_METRICS(ctypes.Structure): +class DWRITE_FONT_METRICS(Structure): _fields_ = ( - ('designUnitsPerEm', UINT16), - ('ascent', UINT16), - ('descent', UINT16), - ('lineGap', INT16), - ('capHeight', UINT16), - ('xHeight', UINT16), - ('underlinePosition', INT16), - ('underlineThickness', UINT16), - ('strikethroughPosition', INT16), - ('strikethroughThickness', UINT16), + ("designUnitsPerEm", UINT16), + ("ascent", UINT16), + ("descent", UINT16), + ("lineGap", INT16), + ("capHeight", UINT16), + ("xHeight", UINT16), + ("underlinePosition", INT16), + ("underlineThickness", UINT16), + ("strikethroughPosition", INT16), + ("strikethroughThickness", UINT16), ) -class DWRITE_GLYPH_METRICS(ctypes.Structure): +class DWRITE_GLYPH_METRICS(Structure): _fields_ = ( - ('leftSideBearing', INT32), - ('advanceWidth', UINT32), - ('rightSideBearing', INT32), - ('topSideBearing', INT32), - ('advanceHeight', UINT32), - ('bottomSideBearing', INT32), - ('verticalOriginY', INT32), + ("leftSideBearing", INT32), + ("advanceWidth", UINT32), + ("rightSideBearing", INT32), + ("topSideBearing", INT32), + ("advanceHeight", UINT32), + ("bottomSideBearing", INT32), + ("verticalOriginY", INT32), ) -class DWRITE_GLYPH_OFFSET(ctypes.Structure): +class DWRITE_GLYPH_OFFSET(Structure): _fields_ = ( - ('advanceOffset', FLOAT), - ('ascenderOffset', FLOAT), + ("advanceOffset", FLOAT), + ("ascenderOffset", FLOAT), ) - def __repr__(self): + def __repr__(self) -> str: return f"DWRITE_GLYPH_OFFSET({self.advanceOffset}, {self.ascenderOffset})" -class DWRITE_CLUSTER_METRICS(ctypes.Structure): +class DWRITE_CLUSTER_METRICS(Structure): _fields_ = ( - ('width', FLOAT), - ('length', UINT16), - ('canWrapLineAfter', UINT16, 1), - ('isWhitespace', UINT16, 1), - ('isNewline', UINT16, 1), - ('isSoftHyphen', UINT16, 1), - ('isRightToLeft', UINT16, 1), - ('padding', UINT16, 11), + ("width", FLOAT), + ("length", UINT16), + ("canWrapLineAfter", UINT16, 1), + ("isWhitespace", UINT16, 1), + ("isNewline", UINT16, 1), + ("isSoftHyphen", UINT16, 1), + ("isRightToLeft", UINT16, 1), + ("padding", UINT16, 11), ) class IDWriteFontFileStream(com.IUnknown): _methods_ = [ - ('ReadFileFragment', + ("ReadFileFragment", com.STDMETHOD(POINTER(c_void_p), UINT64, UINT64, POINTER(c_void_p))), - ('ReleaseFileFragment', + ("ReleaseFileFragment", com.STDMETHOD(c_void_p)), - ('GetFileSize', + ("GetFileSize", com.STDMETHOD(POINTER(UINT64))), - ('GetLastWriteTime', + ("GetLastWriteTime", com.STDMETHOD(POINTER(UINT64))), ] class IDWriteFontFileLoader_LI(com.IUnknown): # Local implementation use only. _methods_ = [ - ('CreateStreamFromKey', - com.STDMETHOD(c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream)))) + ("CreateStreamFromKey", + com.STDMETHOD(c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream)))), ] class IDWriteFontFileLoader(com.pIUnknown): _methods_ = [ - ('CreateStreamFromKey', - com.STDMETHOD(c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream)))) + ("CreateStreamFromKey", + com.STDMETHOD(c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream)))), ] + class IDWriteLocalFontFileLoader(IDWriteFontFileLoader, com.pIUnknown): _methods_ = [ - ('GetFilePathLengthFromKey', + ("GetFilePathLengthFromKey", com.STDMETHOD(c_void_p, UINT32, POINTER(UINT32))), - ('GetFilePathFromKey', + ("GetFilePathFromKey", com.STDMETHOD(c_void_p, UINT32, c_wchar_p, UINT32)), - ('GetLastWriteTimeFromKey', - com.STDMETHOD()) + ("GetLastWriteTimeFromKey", + com.STDMETHOD()), ] @@ -305,46 +344,46 @@ class IDWriteLocalFontFileLoader(IDWriteFontFileLoader, com.pIUnknown): class IDWriteFontFile(com.pIUnknown): _methods_ = [ - ('GetReferenceKey', + ("GetReferenceKey", com.STDMETHOD(POINTER(c_void_p), POINTER(UINT32))), - ('GetLoader', + ("GetLoader", com.STDMETHOD(POINTER(IDWriteFontFileLoader))), - ('Analyze', + ("Analyze", com.STDMETHOD()), ] class IDWriteFontFace(com.pIUnknown): _methods_ = [ - ('GetType', + ("GetType", com.STDMETHOD()), - ('GetFiles', + ("GetFiles", com.STDMETHOD(POINTER(UINT32), POINTER(IDWriteFontFile))), - ('GetIndex', + ("GetIndex", com.STDMETHOD()), - ('GetSimulations', + ("GetSimulations", com.STDMETHOD()), - ('IsSymbolFont', + ("IsSymbolFont", com.STDMETHOD()), - ('GetMetrics', + ("GetMetrics", com.METHOD(c_void, POINTER(DWRITE_FONT_METRICS))), - ('GetGlyphCount', + ("GetGlyphCount", com.METHOD(UINT16)), - ('GetDesignGlyphMetrics', + ("GetDesignGlyphMetrics", com.STDMETHOD(POINTER(UINT16), UINT32, POINTER(DWRITE_GLYPH_METRICS), BOOL)), - ('GetGlyphIndices', + ("GetGlyphIndices", com.STDMETHOD(POINTER(UINT32), UINT32, POINTER(UINT16))), - ('TryGetFontTable', + ("TryGetFontTable", com.STDMETHOD(UINT32, c_void_p, POINTER(UINT32), c_void_p, POINTER(BOOL))), - ('ReleaseFontTable', + ("ReleaseFontTable", com.METHOD(c_void)), - ('GetGlyphRunOutline', + ("GetGlyphRunOutline", com.STDMETHOD()), - ('GetRecommendedRenderingMode', + ("GetRecommendedRenderingMode", com.STDMETHOD()), - ('GetGdiCompatibleMetrics', + ("GetGdiCompatibleMetrics", com.STDMETHOD()), - ('GetGdiCompatibleGlyphMetrics', + ("GetGdiCompatibleGlyphMetrics", com.STDMETHOD()), ] @@ -354,43 +393,43 @@ class IDWriteFontFace(com.pIUnknown): class IDWriteFontFace1(IDWriteFontFace, com.pIUnknown): _methods_ = [ - ('GetMetric1', + ("GetMetric1", com.STDMETHOD()), - ('GetGdiCompatibleMetrics1', + ("GetGdiCompatibleMetrics1", com.STDMETHOD()), - ('GetCaretMetrics', + ("GetCaretMetrics", com.STDMETHOD()), - ('GetUnicodeRanges', + ("GetUnicodeRanges", com.STDMETHOD()), - ('IsMonospacedFont', + ("IsMonospacedFont", com.STDMETHOD()), - ('GetDesignGlyphAdvances', + ("GetDesignGlyphAdvances", com.METHOD(c_void, POINTER(DWRITE_FONT_METRICS))), - ('GetGdiCompatibleGlyphAdvances', + ("GetGdiCompatibleGlyphAdvances", com.STDMETHOD()), - ('GetKerningPairAdjustments', + ("GetKerningPairAdjustments", com.STDMETHOD(UINT32, POINTER(UINT16), POINTER(INT32))), - ('HasKerningPairs', + ("HasKerningPairs", com.METHOD(BOOL)), - ('GetRecommendedRenderingMode1', + ("GetRecommendedRenderingMode1", + com.STDMETHOD()), + ("GetVerticalGlyphVariants", com.STDMETHOD()), - ('GetVerticalGlyphVariants', + ("HasVerticalGlyphVariants", com.STDMETHOD()), - ('HasVerticalGlyphVariants', - com.STDMETHOD()) ] -class DWRITE_GLYPH_RUN(ctypes.Structure): +class DWRITE_GLYPH_RUN(Structure): _fields_ = ( - ('fontFace', IDWriteFontFace), - ('fontEmSize', FLOAT), - ('glyphCount', UINT32), - ('glyphIndices', POINTER(UINT16)), - ('glyphAdvances', POINTER(FLOAT)), - ('glyphOffsets', POINTER(DWRITE_GLYPH_OFFSET)), - ('isSideways', BOOL), - ('bidiLevel', UINT32), + ("fontFace", IDWriteFontFace), + ("fontEmSize", FLOAT), + ("glyphCount", UINT32), + ("glyphIndices", POINTER(UINT16)), + ("glyphAdvances", POINTER(FLOAT)), + ("glyphOffsets", POINTER(DWRITE_GLYPH_OFFSET)), + ("isSideways", BOOL), + ("bidiLevel", UINT32), ) @@ -398,49 +437,49 @@ class DWRITE_GLYPH_RUN(ctypes.Structure): DWRITE_SCRIPT_SHAPES_DEFAULT = 0 -class DWRITE_SCRIPT_ANALYSIS(ctypes.Structure): +class DWRITE_SCRIPT_ANALYSIS(Structure): _fields_ = ( - ('script', UINT16), - ('shapes', DWRITE_SCRIPT_SHAPES), + ("script", UINT16), + ("shapes", DWRITE_SCRIPT_SHAPES), ) DWRITE_FONT_FEATURE_TAG = UINT -class DWRITE_FONT_FEATURE(ctypes.Structure): +class DWRITE_FONT_FEATURE(Structure): _fields_ = ( - ('nameTag', DWRITE_FONT_FEATURE_TAG), - ('parameter', UINT32), + ("nameTag", DWRITE_FONT_FEATURE_TAG), + ("parameter", UINT32), ) -class DWRITE_TYPOGRAPHIC_FEATURES(ctypes.Structure): +class DWRITE_TYPOGRAPHIC_FEATURES(Structure): _fields_ = ( - ('features', POINTER(DWRITE_FONT_FEATURE)), - ('featureCount', UINT32), + ("features", POINTER(DWRITE_FONT_FEATURE)), + ("featureCount", UINT32), ) -class DWRITE_SHAPING_TEXT_PROPERTIES(ctypes.Structure): +class DWRITE_SHAPING_TEXT_PROPERTIES(Structure): _fields_ = ( - ('isShapedAlone', UINT16, 1), - ('reserved1', UINT16, 1), - ('canBreakShapingAfter', UINT16, 1), - ('reserved', UINT16, 13), + ("isShapedAlone", UINT16, 1), + ("reserved1", UINT16, 1), + ("canBreakShapingAfter", UINT16, 1), + ("reserved", UINT16, 13), ) - def __repr__(self): + def __repr__(self) -> str: return f"DWRITE_SHAPING_TEXT_PROPERTIES({self.isShapedAlone}, {self.reserved1}, {self.canBreakShapingAfter})" -class DWRITE_SHAPING_GLYPH_PROPERTIES(ctypes.Structure): +class DWRITE_SHAPING_GLYPH_PROPERTIES(Structure): _fields_ = ( - ('justification', UINT16, 4), - ('isClusterStart', UINT16, 1), - ('isDiacritic', UINT16, 1), - ('isZeroWidthSpace', UINT16, 1), - ('reserved', UINT16, 9), + ("justification", UINT16, 4), + ("isClusterStart", UINT16, 1), + ("isDiacritic", UINT16, 1), + ("isZeroWidthSpace", UINT16, 1), + ("reserved", UINT16, 9), ) @@ -450,34 +489,34 @@ class DWRITE_SHAPING_GLYPH_PROPERTIES(ctypes.Structure): class IDWriteTextAnalysisSource(com.IUnknown): _methods_ = [ - ('GetTextAtPosition', + ("GetTextAtPosition", com.STDMETHOD(UINT32, POINTER(c_wchar_p), POINTER(UINT32))), - ('GetTextBeforePosition', + ("GetTextBeforePosition", com.STDMETHOD(UINT32, POINTER(c_wchar_p), POINTER(UINT32))), - ('GetParagraphReadingDirection', + ("GetParagraphReadingDirection", com.METHOD(DWRITE_READING_DIRECTION)), - ('GetLocaleName', + ("GetLocaleName", com.STDMETHOD(UINT32, POINTER(UINT32), POINTER(c_wchar_p))), - ('GetNumberSubstitution', + ("GetNumberSubstitution", com.STDMETHOD(UINT32, POINTER(UINT32), c_void_p)), ] class IDWriteTextAnalysisSink(com.IUnknown): _methods_ = [ - ('SetScriptAnalysis', + ("SetScriptAnalysis", com.STDMETHOD(UINT32, UINT32, POINTER(DWRITE_SCRIPT_ANALYSIS))), - ('SetLineBreakpoints', + ("SetLineBreakpoints", com.STDMETHOD(UINT32, UINT32, c_void_p)), - ('SetBidiLevel', + ("SetBidiLevel", com.STDMETHOD(UINT32, UINT32, UINT8, UINT8)), - ('SetNumberSubstitution', + ("SetNumberSubstitution", com.STDMETHOD(UINT32, UINT32, c_void_p)), ] class Run: - def __init__(self): + def __init__(self) -> None: self.text_start = 0 self.text_length = 0 self.glyph_start = 0 @@ -489,14 +528,14 @@ def __init__(self): self.next_run = None - def ContainsTextPosition(self, textPosition): + def ContainsTextPosition(self, textPosition: int) -> bool: return textPosition >= self.text_start and textPosition < self.text_start + self.text_length class TextAnalysis(com.COMObject): _interfaces_ = [IDWriteTextAnalysisSource, IDWriteTextAnalysisSink] - def __init__(self): + def __init__(self) -> None: super().__init__() self._textstart = 0 self._textlength = 0 @@ -506,9 +545,9 @@ def __init__(self): self._script = None self._bidi = 0 - # self._sideways = False + # self._sideways = False # noqa: ERA001 - def GenerateResults(self, analyzer, text, text_length): + def GenerateResults(self, analyzer: IDWriteTextAnalyzer, text: c_wchar_p, text_length: int): self._text = text self._textstart = 0 self._textlength = text_length @@ -523,7 +562,8 @@ def GenerateResults(self, analyzer, text, text_length): analyzer.AnalyzeScript(self, 0, text_length, self) - def SetScriptAnalysis(self, textPosition, textLength, scriptAnalysis): + def SetScriptAnalysis(self, textPosition: UINT32, textLength: UINT32, + scriptAnalysis: POINTER(DWRITE_SCRIPT_ANALYSIS)) -> int: # textPosition - The index of the first character in the string that the result applies to # textLength - How many characters of the string from the index that the result applies to # scriptAnalysis - The analysis information for all glyphs starting at position for length. @@ -541,10 +581,12 @@ def SetScriptAnalysis(self, textPosition, textLength, scriptAnalysis): return 0 # return 0x80004001 - def GetTextBeforePosition(self, textPosition, textString, textLength): - raise Exception("Currently not implemented.") + def GetTextBeforePosition(self, textPosition: UINT32, textString: POINTER(POINTER(WCHAR)), + textLength: POINTER(UINT32)) -> NoReturn: + msg = "Currently not implemented." + raise Exception(msg) - def GetTextAtPosition(self, textPosition, textString, textLength): + def GetTextAtPosition(self, textPosition: UINT32, textString: c_wchar_p, textLength: POINTER(UINT32)) -> int: # This method will retrieve a substring of the text in this layout # to be used in an analysis step. # Arguments: @@ -564,23 +606,24 @@ def GetTextAtPosition(self, textPosition, textString, textLength): return 0 - def GetParagraphReadingDirection(self): + def GetParagraphReadingDirection(self) -> int: return 0 - def GetLocaleName(self, textPosition, textLength, localeName): + def GetLocaleName(self, textPosition: UINT32, textLength: POINTER(UINT32), + localeName: POINTER(POINTER(WCHAR))) -> int: self.__local_name = c_wchar_p("") # TODO: Add more locales. localeName[0] = self.__local_name textLength[0] = self._textlength - textPosition return 0 - def GetNumberSubstitution(self): + def GetNumberSubstitution(self) -> int: return 0 - def SetCurrentRun(self, textPosition): + def SetCurrentRun(self, textPosition: UINT32) -> None: if self._current_run and self._current_run.ContainsTextPosition(textPosition): return - def SplitCurrentRun(self, textPosition): + def SplitCurrentRun(self, textPosition: UINT32) -> None: if not self._current_run: return @@ -600,7 +643,7 @@ def SplitCurrentRun(self, textPosition): self._current_run.text_length = splitPoint self._current_run = new_run - def FetchNextRun(self, textLength): + def FetchNextRun(self, textLength: UINT32) -> tuple[Run, int]: original_run = self._current_run if (textLength < self._current_run.text_length): @@ -615,137 +658,137 @@ def FetchNextRun(self, textLength): class IDWriteTextAnalyzer(com.pIUnknown): _methods_ = [ - ('AnalyzeScript', + ("AnalyzeScript", com.STDMETHOD(POINTER(IDWriteTextAnalysisSource), UINT32, UINT32, POINTER(IDWriteTextAnalysisSink))), - ('AnalyzeBidi', + ("AnalyzeBidi", com.STDMETHOD()), - ('AnalyzeNumberSubstitution', + ("AnalyzeNumberSubstitution", com.STDMETHOD()), - ('AnalyzeLineBreakpoints', + ("AnalyzeLineBreakpoints", com.STDMETHOD()), - ('GetGlyphs', + ("GetGlyphs", com.STDMETHOD(c_wchar_p, UINT32, IDWriteFontFace, BOOL, BOOL, POINTER(DWRITE_SCRIPT_ANALYSIS), c_wchar_p, c_void_p, POINTER(POINTER(DWRITE_TYPOGRAPHIC_FEATURES)), POINTER(UINT32), UINT32, UINT32, POINTER(UINT16), POINTER(DWRITE_SHAPING_TEXT_PROPERTIES), POINTER(UINT16), POINTER(DWRITE_SHAPING_GLYPH_PROPERTIES), POINTER(UINT32))), - ('GetGlyphPlacements', + ("GetGlyphPlacements", com.STDMETHOD(c_wchar_p, POINTER(UINT16), POINTER(DWRITE_SHAPING_TEXT_PROPERTIES), UINT32, POINTER(UINT16), POINTER(DWRITE_SHAPING_GLYPH_PROPERTIES), UINT32, IDWriteFontFace, FLOAT, BOOL, BOOL, POINTER(DWRITE_SCRIPT_ANALYSIS), c_wchar_p, POINTER(DWRITE_TYPOGRAPHIC_FEATURES), POINTER(UINT32), UINT32, POINTER(FLOAT), POINTER(DWRITE_GLYPH_OFFSET))), - ('GetGdiCompatibleGlyphPlacements', + ("GetGdiCompatibleGlyphPlacements", com.STDMETHOD()), ] class IDWriteLocalizedStrings(com.pIUnknown): _methods_ = [ - ('GetCount', + ("GetCount", com.METHOD(UINT32)), - ('FindLocaleName', + ("FindLocaleName", com.STDMETHOD(c_wchar_p, POINTER(UINT32), POINTER(BOOL))), - ('GetLocaleNameLength', + ("GetLocaleNameLength", com.STDMETHOD(UINT32, POINTER(UINT32))), - ('GetLocaleName', + ("GetLocaleName", com.STDMETHOD(UINT32, c_wchar_p, UINT32)), - ('GetStringLength', + ("GetStringLength", com.STDMETHOD(UINT32, POINTER(UINT32))), - ('GetString', + ("GetString", com.STDMETHOD(UINT32, c_wchar_p, UINT32)), ] class IDWriteFontList(com.pIUnknown): _methods_ = [ - ('GetFontCollection', + ("GetFontCollection", com.STDMETHOD()), - ('GetFontCount', + ("GetFontCount", com.METHOD(UINT32)), - ('GetFont', + ("GetFont", com.STDMETHOD(UINT32, c_void_p)), # IDWriteFont, use void because of forward ref. ] class IDWriteFontFamily(IDWriteFontList, com.pIUnknown): _methods_ = [ - ('GetFamilyNames', + ("GetFamilyNames", com.STDMETHOD(POINTER(IDWriteLocalizedStrings))), - ('GetFirstMatchingFont', + ("GetFirstMatchingFont", com.STDMETHOD(DWRITE_FONT_WEIGHT, DWRITE_FONT_STRETCH, DWRITE_FONT_STYLE, c_void_p)), - ('GetMatchingFonts', + ("GetMatchingFonts", com.STDMETHOD()), ] class IDWriteFontFamily1(IDWriteFontFamily, IDWriteFontList, com.pIUnknown): _methods_ = [ - ('GetFontLocality', + ("GetFontLocality", com.STDMETHOD()), - ('GetFont1', + ("GetFont1", com.STDMETHOD()), - ('GetFontFaceReference', + ("GetFontFaceReference", com.STDMETHOD()), ] class IDWriteFont(com.pIUnknown): _methods_ = [ - ('GetFontFamily', + ("GetFontFamily", com.STDMETHOD(POINTER(IDWriteFontFamily))), - ('GetWeight', + ("GetWeight", com.METHOD(DWRITE_FONT_WEIGHT)), - ('GetStretch', + ("GetStretch", com.METHOD(DWRITE_FONT_STRETCH)), - ('GetStyle', + ("GetStyle", com.METHOD(DWRITE_FONT_STYLE)), - ('IsSymbolFont', + ("IsSymbolFont", com.METHOD(BOOL)), - ('GetFaceNames', + ("GetFaceNames", com.STDMETHOD(POINTER(IDWriteLocalizedStrings))), - ('GetInformationalStrings', + ("GetInformationalStrings", com.STDMETHOD(DWRITE_INFORMATIONAL_STRING_ID, POINTER(IDWriteLocalizedStrings), POINTER(BOOL))), - ('GetSimulations', + ("GetSimulations", com.STDMETHOD()), - ('GetMetrics', + ("GetMetrics", com.STDMETHOD()), - ('HasCharacter', + ("HasCharacter", com.STDMETHOD(UINT32, POINTER(BOOL))), - ('CreateFontFace', + ("CreateFontFace", com.STDMETHOD(POINTER(IDWriteFontFace))), ] class IDWriteFont1(IDWriteFont, com.pIUnknown): _methods_ = [ - ('GetMetrics1', + ("GetMetrics1", com.STDMETHOD()), - ('GetPanose', + ("GetPanose", com.STDMETHOD()), - ('GetUnicodeRanges', + ("GetUnicodeRanges", + com.STDMETHOD()), + ("IsMonospacedFont", com.STDMETHOD()), - ('IsMonospacedFont', - com.STDMETHOD()) ] class IDWriteFontCollection(com.pIUnknown): _methods_ = [ - ('GetFontFamilyCount', + ("GetFontFamilyCount", com.METHOD(UINT32)), - ('GetFontFamily', + ("GetFontFamily", com.STDMETHOD(UINT32, POINTER(IDWriteFontFamily))), - ('FindFamilyName', + ("FindFamilyName", com.STDMETHOD(c_wchar_p, POINTER(UINT), POINTER(BOOL))), - ('GetFontFromFontFace', + ("GetFontFromFontFace", com.STDMETHOD()), ] class IDWriteFontCollection1(IDWriteFontCollection, com.pIUnknown): _methods_ = [ - ('GetFontSet', + ("GetFontSet", com.STDMETHOD()), - ('GetFontFamily1', + ("GetFontFamily1", com.STDMETHOD(POINTER(IDWriteFontFamily1))), ] @@ -759,209 +802,209 @@ class IDWriteFontCollection1(IDWriteFontCollection, com.pIUnknown): class IDWriteGdiInterop(com.pIUnknown): _methods_ = [ - ('CreateFontFromLOGFONT', + ("CreateFontFromLOGFONT", com.STDMETHOD(POINTER(LOGFONTW), POINTER(IDWriteFont))), - ('ConvertFontToLOGFONT', + ("ConvertFontToLOGFONT", com.STDMETHOD()), - ('ConvertFontFaceToLOGFONT', + ("ConvertFontFaceToLOGFONT", com.STDMETHOD()), - ('CreateFontFaceFromHdc', + ("CreateFontFaceFromHdc", com.STDMETHOD(HDC, POINTER(IDWriteFontFace))), - ('CreateBitmapRenderTarget', - com.STDMETHOD()) + ("CreateBitmapRenderTarget", + com.STDMETHOD()), ] class IDWriteTextFormat(com.pIUnknown): _methods_ = [ - ('SetTextAlignment', + ("SetTextAlignment", com.STDMETHOD(DWRITE_TEXT_ALIGNMENT)), - ('SetParagraphAlignment', + ("SetParagraphAlignment", com.STDMETHOD()), - ('SetWordWrapping', + ("SetWordWrapping", com.STDMETHOD()), - ('SetReadingDirection', + ("SetReadingDirection", com.STDMETHOD()), - ('SetFlowDirection', + ("SetFlowDirection", com.STDMETHOD()), - ('SetIncrementalTabStop', + ("SetIncrementalTabStop", com.STDMETHOD()), - ('SetTrimming', + ("SetTrimming", com.STDMETHOD()), - ('SetLineSpacing', + ("SetLineSpacing", com.STDMETHOD()), - ('GetTextAlignment', + ("GetTextAlignment", com.STDMETHOD()), - ('GetParagraphAlignment', + ("GetParagraphAlignment", com.STDMETHOD()), - ('GetWordWrapping', + ("GetWordWrapping", com.STDMETHOD()), - ('GetReadingDirection', + ("GetReadingDirection", com.STDMETHOD()), - ('GetFlowDirection', + ("GetFlowDirection", com.STDMETHOD()), - ('GetIncrementalTabStop', + ("GetIncrementalTabStop", com.STDMETHOD()), - ('GetTrimming', + ("GetTrimming", com.STDMETHOD()), - ('GetLineSpacing', + ("GetLineSpacing", com.STDMETHOD()), - ('GetFontCollection', + ("GetFontCollection", com.STDMETHOD()), - ('GetFontFamilyNameLength', + ("GetFontFamilyNameLength", com.STDMETHOD(UINT32, POINTER(UINT32))), - ('GetFontFamilyName', + ("GetFontFamilyName", com.STDMETHOD(UINT32, c_wchar_p, UINT32)), - ('GetFontWeight', + ("GetFontWeight", com.STDMETHOD()), - ('GetFontStyle', + ("GetFontStyle", com.STDMETHOD()), - ('GetFontStretch', + ("GetFontStretch", com.STDMETHOD()), - ('GetFontSize', + ("GetFontSize", com.STDMETHOD()), - ('GetLocaleNameLength', + ("GetLocaleNameLength", com.STDMETHOD()), - ('GetLocaleName', + ("GetLocaleName", com.STDMETHOD()), ] class IDWriteTypography(com.pIUnknown): _methods_ = [ - ('AddFontFeature', + ("AddFontFeature", com.STDMETHOD(DWRITE_FONT_FEATURE)), - ('GetFontFeatureCount', + ("GetFontFeatureCount", com.METHOD(UINT32)), - ('GetFontFeature', - com.STDMETHOD()) + ("GetFontFeature", + com.STDMETHOD()), ] -class DWRITE_TEXT_RANGE(ctypes.Structure): +class DWRITE_TEXT_RANGE(Structure): _fields_ = ( - ('startPosition', UINT32), - ('length', UINT32), + ("startPosition", UINT32), + ("length", UINT32), ) -class DWRITE_OVERHANG_METRICS(ctypes.Structure): +class DWRITE_OVERHANG_METRICS(Structure): _fields_ = ( - ('left', FLOAT), - ('top', FLOAT), - ('right', FLOAT), - ('bottom', FLOAT), + ("left", FLOAT), + ("top", FLOAT), + ("right", FLOAT), + ("bottom", FLOAT), ) class IDWriteTextLayout(IDWriteTextFormat, com.pIUnknown): _methods_ = [ - ('SetMaxWidth', + ("SetMaxWidth", com.STDMETHOD()), - ('SetMaxHeight', + ("SetMaxHeight", com.STDMETHOD()), - ('SetFontCollection', + ("SetFontCollection", com.STDMETHOD()), - ('SetFontFamilyName', + ("SetFontFamilyName", com.STDMETHOD()), - ('SetFontWeight', # 30 + ("SetFontWeight", # 30 com.STDMETHOD()), - ('SetFontStyle', + ("SetFontStyle", com.STDMETHOD()), - ('SetFontStretch', + ("SetFontStretch", com.STDMETHOD()), - ('SetFontSize', + ("SetFontSize", com.STDMETHOD()), - ('SetUnderline', + ("SetUnderline", com.STDMETHOD()), - ('SetStrikethrough', + ("SetStrikethrough", com.STDMETHOD()), - ('SetDrawingEffect', + ("SetDrawingEffect", com.STDMETHOD()), - ('SetInlineObject', + ("SetInlineObject", com.STDMETHOD()), - ('SetTypography', + ("SetTypography", com.STDMETHOD(IDWriteTypography, DWRITE_TEXT_RANGE)), - ('SetLocaleName', + ("SetLocaleName", com.STDMETHOD()), - ('GetMaxWidth', # 40 + ("GetMaxWidth", # 40 com.METHOD(FLOAT)), - ('GetMaxHeight', + ("GetMaxHeight", com.METHOD(FLOAT)), - ('GetFontCollection2', + ("GetFontCollection2", com.STDMETHOD()), - ('GetFontFamilyNameLength2', + ("GetFontFamilyNameLength2", com.STDMETHOD(UINT32, POINTER(UINT32), c_void_p)), - ('GetFontFamilyName2', + ("GetFontFamilyName2", com.STDMETHOD(UINT32, c_wchar_p, UINT32, c_void_p)), - ('GetFontWeight2', + ("GetFontWeight2", com.STDMETHOD(UINT32, POINTER(DWRITE_FONT_WEIGHT), POINTER(DWRITE_TEXT_RANGE))), - ('GetFontStyle2', + ("GetFontStyle2", com.STDMETHOD()), - ('GetFontStretch2', + ("GetFontStretch2", com.STDMETHOD()), - ('GetFontSize2', + ("GetFontSize2", com.STDMETHOD()), - ('GetUnderline', + ("GetUnderline", com.STDMETHOD()), - ('GetStrikethrough', + ("GetStrikethrough", com.STDMETHOD(UINT32, POINTER(BOOL), POINTER(DWRITE_TEXT_RANGE))), - ('GetDrawingEffect', + ("GetDrawingEffect", com.STDMETHOD()), - ('GetInlineObject', + ("GetInlineObject", com.STDMETHOD()), - ('GetTypography', # Always returns NULL without SetTypography being called. + ("GetTypography", # Always returns NULL without SetTypography being called. com.STDMETHOD(UINT32, POINTER(IDWriteTypography), POINTER(DWRITE_TEXT_RANGE))), - ('GetLocaleNameLength1', + ("GetLocaleNameLength1", com.STDMETHOD()), - ('GetLocaleName1', + ("GetLocaleName1", com.STDMETHOD()), - ('Draw', + ("Draw", com.STDMETHOD()), - ('GetLineMetrics', + ("GetLineMetrics", com.STDMETHOD()), - ('GetMetrics', + ("GetMetrics", com.STDMETHOD(POINTER(DWRITE_TEXT_METRICS))), - ('GetOverhangMetrics', + ("GetOverhangMetrics", com.STDMETHOD(POINTER(DWRITE_OVERHANG_METRICS))), - ('GetClusterMetrics', + ("GetClusterMetrics", com.STDMETHOD(POINTER(DWRITE_CLUSTER_METRICS), UINT32, POINTER(UINT32))), - ('DetermineMinWidth', + ("DetermineMinWidth", com.STDMETHOD(POINTER(FLOAT))), - ('HitTestPoint', + ("HitTestPoint", com.STDMETHOD()), - ('HitTestTextPosition', + ("HitTestTextPosition", com.STDMETHOD()), - ('HitTestTextRange', + ("HitTestTextRange", com.STDMETHOD()), ] class IDWriteTextLayout1(IDWriteTextLayout, IDWriteTextFormat, com.pIUnknown): _methods_ = [ - ('SetPairKerning', + ("SetPairKerning", com.STDMETHOD()), - ('GetPairKerning', + ("GetPairKerning", com.STDMETHOD()), - ('SetCharacterSpacing', + ("SetCharacterSpacing", com.STDMETHOD()), - ('GetCharacterSpacing', + ("GetCharacterSpacing", com.STDMETHOD(UINT32, POINTER(FLOAT), POINTER(FLOAT), POINTER(FLOAT), POINTER(DWRITE_TEXT_RANGE))), ] class IDWriteFontFileEnumerator(com.IUnknown): _methods_ = [ - ('MoveNext', + ("MoveNext", com.STDMETHOD(POINTER(BOOL))), - ('GetCurrentFontFile', + ("GetCurrentFontFile", com.STDMETHOD(c_void_p)), ] class IDWriteFontCollectionLoader(com.IUnknown): _methods_ = [ - ('CreateEnumeratorFromKey', + ("CreateEnumeratorFromKey", com.STDMETHOD(c_void_p, c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileEnumerator)))), ] @@ -969,18 +1012,19 @@ class IDWriteFontCollectionLoader(com.IUnknown): class MyFontFileStream(com.COMObject): _interfaces_ = [IDWriteFontFileStream] - def __init__(self, data): + def __init__(self, data: bytes) -> None: super().__init__() self._data = data self._size = len(data) self._ptrs = [] - def ReadFileFragment(self, fragmentStart, fileOffset, fragmentSize, fragmentContext): + def ReadFileFragment(self, fragmentStart: POINTER(c_void_p), fileOffset: UINT64, fragmentSize: UINT64, + fragmentContext: POINTER(c_void_p)) -> int: if fileOffset + fragmentSize > self._size: return 0x80004005 # E_FAIL fragment = self._data[fileOffset:] - buffer = (ctypes.c_ubyte * len(fragment)).from_buffer(bytearray(fragment)) + buffer = (c_ubyte * len(fragment)).from_buffer(bytearray(fragment)) ptr = cast(buffer, c_void_p) self._ptrs.append(ptr) @@ -988,40 +1032,41 @@ def ReadFileFragment(self, fragmentStart, fileOffset, fragmentSize, fragmentCont fragmentContext[0] = None return 0 - def ReleaseFileFragment(self, fragmentContext): + def ReleaseFileFragment(self, fragmentContext: c_void_p) -> int: return 0 - def GetFileSize(self, fileSize): + def GetFileSize(self, fileSize: POINTER(UINT64)) -> int: fileSize[0] = self._size return 0 - def GetLastWriteTime(self, lastWriteTime): + def GetLastWriteTime(self, lastWriteTime: POINTER(UINT64)) -> int: return 0x80004001 # E_NOTIMPL class LegacyFontFileLoader(com.COMObject): _interfaces_ = [IDWriteFontFileLoader_LI] - def __init__(self): + def __init__(self) -> None: super().__init__() self._streams = {} - def CreateStreamFromKey(self, fontfileReferenceKey, fontFileReferenceKeySize, fontFileStream): + def CreateStreamFromKey(self, fontfileReferenceKey: c_void_p, fontFileReferenceKeySize: UINT32, + fontFileStream: POINTER(IDWriteFontFileStream)) -> int: convert_index = cast(fontfileReferenceKey, POINTER(c_uint32)) - self._ptr = ctypes.cast(self._streams[convert_index.contents.value].as_interface(IDWriteFontFileStream), - POINTER(IDWriteFontFileStream)) + self._ptr = cast(self._streams[convert_index.contents.value].as_interface(IDWriteFontFileStream), + POINTER(IDWriteFontFileStream)) fontFileStream[0] = self._ptr return 0 - def SetCurrentFont(self, index, data): + def SetCurrentFont(self, index: int, data: bytes) -> int: self._streams[index] = MyFontFileStream(data) class MyEnumerator(com.COMObject): _interfaces_ = [IDWriteFontFileEnumerator] - def __init__(self, factory, loader): + def __init__(self, factory: c_void_p, loader: LegacyFontFileLoader) -> None: super().__init__() self.factory = cast(factory, IDWriteFactory) self.key = "pyglet_dwrite" @@ -1038,10 +1083,10 @@ def __init__(self, factory, loader): self._file_loader = loader - def AddFontData(self, fonts): + def AddFontData(self, fonts: list[str]) -> None: self._font_data = fonts - def MoveNext(self, hasCurrentFile): + def MoveNext(self, hasCurrentFile: BOOL) -> None: self.current_index += 1 if self.current_index != len(self._font_data): @@ -1051,7 +1096,7 @@ def MoveNext(self, hasCurrentFile): key = self.current_index - if not self.current_index in self._keys: + if self.current_index not in self._keys: buffer = pointer(c_uint32(key)) ptr = cast(buffer, c_void_p) @@ -1069,9 +1114,7 @@ def MoveNext(self, hasCurrentFile): else: hasCurrentFile[0] = 0 - pass - - def GetCurrentFontFile(self, fontFile): + def GetCurrentFontFile(self, fontFile: IDWriteFontFile) -> int: fontFile = cast(fontFile, POINTER(IDWriteFontFile)) fontFile[0] = self._font_files[self.current_index] return 0 @@ -1080,16 +1123,17 @@ def GetCurrentFontFile(self, fontFile): class LegacyCollectionLoader(com.COMObject): _interfaces_ = [IDWriteFontCollectionLoader] - def __init__(self, factory, loader): + def __init__(self, factory: c_void_p, loader: LegacyFontFileLoader) -> None: super().__init__() self._enumerator = MyEnumerator(factory, loader) - def AddFontData(self, fonts): + def AddFontData(self, fonts) -> None: self._enumerator.AddFontData(fonts) - def CreateEnumeratorFromKey(self, factory, key, key_size, enumerator): - self._ptr = ctypes.cast(self._enumerator.as_interface(IDWriteFontFileEnumerator), - POINTER(IDWriteFontFileEnumerator)) + def CreateEnumeratorFromKey(self, factory: IDWriteFactory, key: c_void_p, key_size: UINT32, + enumerator: MyEnumerator) -> int: + self._ptr = cast(self._enumerator.as_interface(IDWriteFontFileEnumerator), + POINTER(IDWriteFontFileEnumerator)) enumerator[0] = self._ptr return 0 @@ -1100,63 +1144,63 @@ def CreateEnumeratorFromKey(self, factory, key, key_size, enumerator): class IDWriteRenderingParams(com.pIUnknown): _methods_ = [ - ('GetGamma', + ("GetGamma", com.METHOD(FLOAT)), - ('GetEnhancedContrast', + ("GetEnhancedContrast", com.METHOD(FLOAT)), - ('GetClearTypeLevel', + ("GetClearTypeLevel", com.METHOD(FLOAT)), - ('GetPixelGeometry', + ("GetPixelGeometry", com.METHOD(UINT)), - ('GetRenderingMode', + ("GetRenderingMode", com.METHOD(UINT)), ] class IDWriteFactory(com.pIUnknown): _methods_ = [ - ('GetSystemFontCollection', + ("GetSystemFontCollection", com.STDMETHOD(POINTER(IDWriteFontCollection), BOOL)), - ('CreateCustomFontCollection', + ("CreateCustomFontCollection", com.STDMETHOD(POINTER(IDWriteFontCollectionLoader), c_void_p, UINT32, POINTER(IDWriteFontCollection))), - ('RegisterFontCollectionLoader', + ("RegisterFontCollectionLoader", com.STDMETHOD(POINTER(IDWriteFontCollectionLoader))), - ('UnregisterFontCollectionLoader', + ("UnregisterFontCollectionLoader", com.STDMETHOD(POINTER(IDWriteFontCollectionLoader))), - ('CreateFontFileReference', + ("CreateFontFileReference", com.STDMETHOD(c_wchar_p, c_void_p, POINTER(IDWriteFontFile))), - ('CreateCustomFontFileReference', + ("CreateCustomFontFileReference", com.STDMETHOD(c_void_p, UINT32, POINTER(IDWriteFontFileLoader_LI), POINTER(IDWriteFontFile))), - ('CreateFontFace', + ("CreateFontFace", com.STDMETHOD()), - ('CreateRenderingParams', + ("CreateRenderingParams", com.STDMETHOD(POINTER(IDWriteRenderingParams))), - ('CreateMonitorRenderingParams', + ("CreateMonitorRenderingParams", com.STDMETHOD()), - ('CreateCustomRenderingParams', + ("CreateCustomRenderingParams", com.STDMETHOD(FLOAT, FLOAT, FLOAT, UINT, UINT, POINTER(IDWriteRenderingParams))), - ('RegisterFontFileLoader', + ("RegisterFontFileLoader", com.STDMETHOD(c_void_p)), # Ambigious as newer is a pIUnknown and legacy is IUnknown. - ('UnregisterFontFileLoader', + ("UnregisterFontFileLoader", com.STDMETHOD(POINTER(IDWriteFontFileLoader_LI))), - ('CreateTextFormat', + ("CreateTextFormat", com.STDMETHOD(c_wchar_p, IDWriteFontCollection, DWRITE_FONT_WEIGHT, DWRITE_FONT_STYLE, DWRITE_FONT_STRETCH, FLOAT, c_wchar_p, POINTER(IDWriteTextFormat))), - ('CreateTypography', + ("CreateTypography", com.STDMETHOD(POINTER(IDWriteTypography))), - ('GetGdiInterop', + ("GetGdiInterop", com.STDMETHOD(POINTER(IDWriteGdiInterop))), - ('CreateTextLayout', + ("CreateTextLayout", com.STDMETHOD(c_wchar_p, UINT32, IDWriteTextFormat, FLOAT, FLOAT, POINTER(IDWriteTextLayout))), - ('CreateGdiCompatibleTextLayout', + ("CreateGdiCompatibleTextLayout", com.STDMETHOD()), - ('CreateEllipsisTrimmingSign', + ("CreateEllipsisTrimmingSign", com.STDMETHOD()), - ('CreateTextAnalyzer', + ("CreateTextAnalyzer", com.STDMETHOD(POINTER(IDWriteTextAnalyzer))), - ('CreateNumberSubstitution', + ("CreateNumberSubstitution", com.STDMETHOD()), - ('CreateGlyphRunAnalysis', + ("CreateGlyphRunAnalysis", com.STDMETHOD()), ] @@ -1166,16 +1210,16 @@ class IDWriteFactory(com.pIUnknown): class IDWriteFactory1(IDWriteFactory, com.pIUnknown): _methods_ = [ - ('GetEudcFontCollection', + ("GetEudcFontCollection", com.STDMETHOD()), - ('CreateCustomRenderingParams1', + ("CreateCustomRenderingParams1", com.STDMETHOD()), ] class IDWriteFontFallback(com.pIUnknown): _methods_ = [ - ('MapCharacters', + ("MapCharacters", com.STDMETHOD(POINTER(IDWriteTextAnalysisSource), UINT32, UINT32, IDWriteFontCollection, c_wchar_p, DWRITE_FONT_WEIGHT, DWRITE_FONT_STYLE, DWRITE_FONT_STRETCH, POINTER(UINT32), POINTER(IDWriteFont), @@ -1185,25 +1229,25 @@ class IDWriteFontFallback(com.pIUnknown): class IDWriteColorGlyphRunEnumerator(com.pIUnknown): _methods_ = [ - ('MoveNext', + ("MoveNext", com.STDMETHOD()), - ('GetCurrentRun', + ("GetCurrentRun", com.STDMETHOD()), ] class IDWriteFactory2(IDWriteFactory1, IDWriteFactory, com.pIUnknown): _methods_ = [ - ('GetSystemFontFallback', + ("GetSystemFontFallback", com.STDMETHOD(POINTER(IDWriteFontFallback))), - ('CreateFontFallbackBuilder', + ("CreateFontFallbackBuilder", com.STDMETHOD()), - ('TranslateColorGlyphRun', + ("TranslateColorGlyphRun", com.STDMETHOD(FLOAT, FLOAT, POINTER(DWRITE_GLYPH_RUN), c_void_p, DWRITE_MEASURING_MODE, c_void_p, UINT32, POINTER(IDWriteColorGlyphRunEnumerator))), - ('CreateCustomRenderingParams2', + ("CreateCustomRenderingParams2", com.STDMETHOD()), - ('CreateGlyphRunAnalysis', + ("CreateGlyphRunAnalysis", com.STDMETHOD()), ] @@ -1213,64 +1257,64 @@ class IDWriteFactory2(IDWriteFactory1, IDWriteFactory, com.pIUnknown): class IDWriteFontSet(com.pIUnknown): _methods_ = [ - ('GetFontCount', + ("GetFontCount", com.STDMETHOD()), - ('GetFontFaceReference', + ("GetFontFaceReference", com.STDMETHOD()), - ('FindFontFaceReference', + ("FindFontFaceReference", com.STDMETHOD()), - ('FindFontFace', + ("FindFontFace", com.STDMETHOD()), - ('GetPropertyValues', + ("GetPropertyValues", com.STDMETHOD()), - ('GetPropertyOccurrenceCount', + ("GetPropertyOccurrenceCount", com.STDMETHOD()), - ('GetMatchingFonts', + ("GetMatchingFonts", com.STDMETHOD()), - ('GetMatchingFonts', + ("GetMatchingFonts", com.STDMETHOD()), ] class IDWriteFontSetBuilder(com.pIUnknown): _methods_ = [ - ('AddFontFaceReference', + ("AddFontFaceReference", com.STDMETHOD()), - ('AddFontFaceReference', + ("AddFontFaceReference", com.STDMETHOD()), - ('AddFontSet', + ("AddFontSet", com.STDMETHOD()), - ('CreateFontSet', + ("CreateFontSet", com.STDMETHOD(POINTER(IDWriteFontSet))), ] class IDWriteFontSetBuilder1(IDWriteFontSetBuilder, com.pIUnknown): _methods_ = [ - ('AddFontFile', + ("AddFontFile", com.STDMETHOD(IDWriteFontFile)), ] class IDWriteFactory3(IDWriteFactory2, com.pIUnknown): _methods_ = [ - ('CreateGlyphRunAnalysis', + ("CreateGlyphRunAnalysis", com.STDMETHOD()), - ('CreateCustomRenderingParams3', + ("CreateCustomRenderingParams3", com.STDMETHOD()), - ('CreateFontFaceReference', + ("CreateFontFaceReference", com.STDMETHOD()), - ('CreateFontFaceReference', + ("CreateFontFaceReference", com.STDMETHOD()), - ('GetSystemFontSet', + ("GetSystemFontSet", com.STDMETHOD()), - ('CreateFontSetBuilder', + ("CreateFontSetBuilder", com.STDMETHOD(POINTER(IDWriteFontSetBuilder))), - ('CreateFontCollectionFromFontSet', + ("CreateFontCollectionFromFontSet", com.STDMETHOD(IDWriteFontSet, POINTER(IDWriteFontCollection1))), - ('GetSystemFontCollection3', + ("GetSystemFontCollection3", com.STDMETHOD()), - ('GetFontDownloadQueue', + ("GetFontDownloadQueue", com.STDMETHOD()), # ('GetSystemFontSet', # com.STDMETHOD()), @@ -1279,29 +1323,30 @@ class IDWriteFactory3(IDWriteFactory2, com.pIUnknown): class IDWriteColorGlyphRunEnumerator1(IDWriteColorGlyphRunEnumerator, com.pIUnknown): _methods_ = [ - ('GetCurrentRun1', + ("GetCurrentRun1", com.STDMETHOD()), ] + class IDWriteFactory4(IDWriteFactory3, com.pIUnknown): _methods_ = [ - ('TranslateColorGlyphRun4', # Renamed to prevent clash from previous factories. + ("TranslateColorGlyphRun4", # Renamed to prevent clash from previous factories. com.STDMETHOD(D2D_POINT_2F, POINTER(DWRITE_GLYPH_RUN), c_void_p, DWRITE_GLYPH_IMAGE_FORMATS, DWRITE_MEASURING_MODE, c_void_p, UINT32, POINTER(IDWriteColorGlyphRunEnumerator1))), - ('ComputeGlyphOrigins_', + ("ComputeGlyphOrigins_", com.STDMETHOD()), - ('ComputeGlyphOrigins', + ("ComputeGlyphOrigins", com.STDMETHOD()), ] class IDWriteInMemoryFontFileLoader(com.pIUnknown): _methods_ = [ - ('CreateStreamFromKey', + ("CreateStreamFromKey", com.STDMETHOD()), - ('CreateInMemoryFontFileReference', + ("CreateInMemoryFontFileReference", com.STDMETHOD(IDWriteFactory, c_void_p, UINT, c_void_p, POINTER(IDWriteFontFile))), - ('GetFileCount', + ("GetFileCount", com.STDMETHOD()), ] @@ -1312,14 +1357,14 @@ class IDWriteInMemoryFontFileLoader(com.pIUnknown): class IDWriteFactory5(IDWriteFactory4, IDWriteFactory3, IDWriteFactory2, IDWriteFactory1, IDWriteFactory, com.pIUnknown): _methods_ = [ - ('CreateFontSetBuilder1', + ("CreateFontSetBuilder1", com.STDMETHOD(POINTER(IDWriteFontSetBuilder1))), - ('CreateInMemoryFontFileLoader', + ("CreateInMemoryFontFileLoader", com.STDMETHOD(POINTER(IDWriteInMemoryFontFileLoader))), - ('CreateHttpFontFileLoader', + ("CreateHttpFontFileLoader", + com.STDMETHOD()), + ("AnalyzeContainerType", com.STDMETHOD()), - ('AnalyzeContainerType', - com.STDMETHOD()) ] @@ -1330,29 +1375,29 @@ class IDWriteFactory5(IDWriteFactory4, IDWriteFactory3, IDWriteFactory2, IDWrite class ID2D1Resource(com.pIUnknown): _methods_ = [ - ('GetFactory', + ("GetFactory", com.STDMETHOD()), ] class ID2D1Brush(ID2D1Resource, com.pIUnknown): _methods_ = [ - ('SetOpacity', + ("SetOpacity", com.STDMETHOD()), - ('SetTransform', + ("SetTransform", com.STDMETHOD()), - ('GetOpacity', + ("GetOpacity", com.STDMETHOD()), - ('GetTransform', + ("GetTransform", com.STDMETHOD()), ] class ID2D1SolidColorBrush(ID2D1Brush, ID2D1Resource, com.pIUnknown): _methods_ = [ - ('SetColor', + ("SetColor", com.STDMETHOD()), - ('GetColor', + ("GetColor", com.STDMETHOD()), ] @@ -1396,19 +1441,19 @@ class ID2D1SolidColorBrush(ID2D1Brush, ID2D1Resource, com.pIUnknown): class D2D1_PIXEL_FORMAT(Structure): _fields_ = ( - ('format', DXGI_FORMAT), - ('alphaMode', D2D1_ALPHA_MODE), + ("format", DXGI_FORMAT), + ("alphaMode", D2D1_ALPHA_MODE), ) class D2D1_RENDER_TARGET_PROPERTIES(Structure): _fields_ = ( - ('type', D2D1_RENDER_TARGET_TYPE), - ('pixelFormat', D2D1_PIXEL_FORMAT), - ('dpiX', FLOAT), - ('dpiY', FLOAT), - ('usage', D2D1_RENDER_TARGET_USAGE), - ('minLevel', D2D1_FEATURE_LEVEL), + ("type", D2D1_RENDER_TARGET_TYPE), + ("pixelFormat", D2D1_PIXEL_FORMAT), + ("dpiX", FLOAT), + ("dpiY", FLOAT), + ("usage", D2D1_RENDER_TARGET_USAGE), + ("minLevel", D2D1_FEATURE_LEVEL), ) @@ -1429,112 +1474,112 @@ class D2D1_RENDER_TARGET_PROPERTIES(Structure): class ID2D1RenderTarget(ID2D1Resource, com.pIUnknown): _methods_ = [ - ('CreateBitmap', + ("CreateBitmap", com.STDMETHOD()), - ('CreateBitmapFromWicBitmap', + ("CreateBitmapFromWicBitmap", com.STDMETHOD()), - ('CreateSharedBitmap', + ("CreateSharedBitmap", com.STDMETHOD()), - ('CreateBitmapBrush', + ("CreateBitmapBrush", com.STDMETHOD()), - ('CreateSolidColorBrush', + ("CreateSolidColorBrush", com.STDMETHOD(POINTER(D2D1_COLOR_F), c_void_p, POINTER(ID2D1SolidColorBrush))), - ('CreateGradientStopCollection', + ("CreateGradientStopCollection", com.STDMETHOD()), - ('CreateLinearGradientBrush', + ("CreateLinearGradientBrush", com.STDMETHOD()), - ('CreateRadialGradientBrush', + ("CreateRadialGradientBrush", com.STDMETHOD()), - ('CreateCompatibleRenderTarget', + ("CreateCompatibleRenderTarget", com.STDMETHOD()), - ('CreateLayer', + ("CreateLayer", com.STDMETHOD()), - ('CreateMesh', + ("CreateMesh", com.STDMETHOD()), - ('DrawLine', + ("DrawLine", com.STDMETHOD()), - ('DrawRectangle', + ("DrawRectangle", com.STDMETHOD()), - ('FillRectangle', + ("FillRectangle", com.STDMETHOD()), - ('DrawRoundedRectangle', + ("DrawRoundedRectangle", com.STDMETHOD()), - ('FillRoundedRectangle', + ("FillRoundedRectangle", com.STDMETHOD()), - ('DrawEllipse', + ("DrawEllipse", com.STDMETHOD()), - ('FillEllipse', + ("FillEllipse", com.STDMETHOD()), - ('DrawGeometry', + ("DrawGeometry", com.STDMETHOD()), - ('FillGeometry', + ("FillGeometry", com.STDMETHOD()), - ('FillMesh', + ("FillMesh", com.STDMETHOD()), - ('FillOpacityMask', + ("FillOpacityMask", com.STDMETHOD()), - ('DrawBitmap', + ("DrawBitmap", com.STDMETHOD()), - ('DrawText', + ("DrawText", com.STDMETHOD(c_wchar_p, UINT, IDWriteTextFormat, POINTER(D2D1_RECT_F), ID2D1Brush, D2D1_DRAW_TEXT_OPTIONS, DWRITE_MEASURING_MODE)), - ('DrawTextLayout', + ("DrawTextLayout", com.METHOD(c_void, D2D_POINT_2F, IDWriteTextLayout, ID2D1Brush, UINT32)), - ('DrawGlyphRun', + ("DrawGlyphRun", com.METHOD(c_void, D2D_POINT_2F, POINTER(DWRITE_GLYPH_RUN), ID2D1Brush, UINT32)), - ('SetTransform', + ("SetTransform", com.METHOD(c_void)), - ('GetTransform', + ("GetTransform", com.STDMETHOD()), - ('SetAntialiasMode', + ("SetAntialiasMode", com.METHOD(c_void, D2D1_TEXT_ANTIALIAS_MODE)), - ('GetAntialiasMode', + ("GetAntialiasMode", com.STDMETHOD()), - ('SetTextAntialiasMode', + ("SetTextAntialiasMode", com.METHOD(c_void, D2D1_TEXT_ANTIALIAS_MODE)), - ('GetTextAntialiasMode', + ("GetTextAntialiasMode", com.STDMETHOD()), - ('SetTextRenderingParams', + ("SetTextRenderingParams", com.STDMETHOD(IDWriteRenderingParams)), - ('GetTextRenderingParams', + ("GetTextRenderingParams", com.STDMETHOD()), - ('SetTags', + ("SetTags", com.STDMETHOD()), - ('GetTags', + ("GetTags", com.STDMETHOD()), - ('PushLayer', + ("PushLayer", com.STDMETHOD()), - ('PopLayer', + ("PopLayer", com.STDMETHOD()), - ('Flush', + ("Flush", com.STDMETHOD(c_void_p, c_void_p)), - ('SaveDrawingState', + ("SaveDrawingState", com.STDMETHOD()), - ('RestoreDrawingState', + ("RestoreDrawingState", com.STDMETHOD()), - ('PushAxisAlignedClip', + ("PushAxisAlignedClip", com.STDMETHOD()), - ('PopAxisAlignedClip', + ("PopAxisAlignedClip", com.STDMETHOD()), - ('Clear', + ("Clear", com.METHOD(c_void, POINTER(D2D1_COLOR_F))), - ('BeginDraw', + ("BeginDraw", com.METHOD(c_void)), - ('EndDraw', + ("EndDraw", com.STDMETHOD(c_void_p, c_void_p)), - ('GetPixelFormat', + ("GetPixelFormat", com.STDMETHOD()), - ('SetDpi', + ("SetDpi", com.STDMETHOD()), - ('GetDpi', + ("GetDpi", com.STDMETHOD()), - ('GetSize', + ("GetSize", com.STDMETHOD()), - ('GetPixelSize', + ("GetPixelSize", com.STDMETHOD()), - ('GetMaximumBitmapSize', + ("GetMaximumBitmapSize", com.STDMETHOD()), - ('IsSupported', + ("IsSupported", com.STDMETHOD()), ] @@ -1544,38 +1589,38 @@ class ID2D1RenderTarget(ID2D1Resource, com.pIUnknown): class ID2D1Factory(com.pIUnknown): _methods_ = [ - ('ReloadSystemMetrics', + ("ReloadSystemMetrics", com.STDMETHOD()), - ('GetDesktopDpi', + ("GetDesktopDpi", com.STDMETHOD()), - ('CreateRectangleGeometry', + ("CreateRectangleGeometry", com.STDMETHOD()), - ('CreateRoundedRectangleGeometry', + ("CreateRoundedRectangleGeometry", com.STDMETHOD()), - ('CreateEllipseGeometry', + ("CreateEllipseGeometry", com.STDMETHOD()), - ('CreateGeometryGroup', + ("CreateGeometryGroup", com.STDMETHOD()), - ('CreateTransformedGeometry', + ("CreateTransformedGeometry", com.STDMETHOD()), - ('CreatePathGeometry', + ("CreatePathGeometry", com.STDMETHOD()), - ('CreateStrokeStyle', + ("CreateStrokeStyle", com.STDMETHOD()), - ('CreateDrawingStateBlock', + ("CreateDrawingStateBlock", com.STDMETHOD()), - ('CreateWicBitmapRenderTarget', + ("CreateWicBitmapRenderTarget", com.STDMETHOD(IWICBitmap, POINTER(D2D1_RENDER_TARGET_PROPERTIES), POINTER(ID2D1RenderTarget))), - ('CreateHwndRenderTarget', + ("CreateHwndRenderTarget", com.STDMETHOD()), - ('CreateDxgiSurfaceRenderTarget', + ("CreateDxgiSurfaceRenderTarget", com.STDMETHOD()), - ('CreateDCRenderTarget', + ("CreateDCRenderTarget", com.STDMETHOD()), ] -d2d_lib = ctypes.windll.d2d1 +d2d_lib = windll.d2d1 D2D1_FACTORY_TYPE = UINT D2D1_FACTORY_TYPE_SINGLE_THREADED = 0 @@ -1603,16 +1648,17 @@ def get_system_locale() -> str: class DirectWriteGlyphRenderer(base.GlyphRenderer): + font: Win32DirectWriteFont antialias_mode = D2D1_TEXT_ANTIALIAS_MODE_DEFAULT draw_options = D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT if WINDOWS_8_1_OR_GREATER else D2D1_DRAW_TEXT_OPTIONS_NONE measuring_mode = DWRITE_MEASURING_MODE_NATURAL - def __init__(self, font): + def __init__(self, font: Win32DirectWriteFont) -> None: self._render_target = None self._bitmap = None self._brush = None self._bitmap_dimensions = (0, 0) - super(DirectWriteGlyphRenderer, self).__init__(font) + super().__init__(font) self.font = font self._analyzer = IDWriteTextAnalyzer() @@ -1620,9 +1666,13 @@ def __init__(self, font): self._text_analysis = TextAnalysis() - def render_to_image(self, text, width, height): + def render(self, text: str) -> Glyph: + pass + + def render_to_image(self, text: str, width: int, height: int) -> ImageData: """This process takes Pyglet out of the equation and uses only DirectWrite to shape and render text. - This may allows more accurate fonts (bidi, rtl, etc) in very special circumstances.""" + This may allows more accurate fonts (bidi, rtl, etc) in very special circumstances. + """ text_buffer = create_unicode_buffer(text) text_layout = IDWriteTextLayout() @@ -1632,7 +1682,7 @@ def render_to_image(self, text, width, height): self.font._text_format, width, # Doesn't affect bitmap size. height, - byref(text_layout) + byref(text_layout), ) layout_metrics = DWRITE_TEXT_METRICS() @@ -1646,7 +1696,7 @@ def render_to_image(self, text, width, height): height, GUID_WICPixelFormat32bppPBGRA, WICBitmapCacheOnDemand, - byref(bitmap) + byref(bitmap), ) rt = ID2D1RenderTarget() @@ -1673,13 +1723,12 @@ def render_to_image(self, text, width, height): rt.Release() - image_data = wic_decoder.get_image(bitmap) - - return image_data + return wic_decoder.get_image(bitmap) - def get_string_info(self, text, font_face): + def get_string_info(self, text: str, font_face: IDWriteFontFace) -> tuple[ + c_wchar, int, Array[UINT16], Array[FLOAT], Array[DWRITE_GLYPH_OFFSET], Array[UINT16]]: """Converts a string of text into a list of indices and advances used for shaping.""" - text_length = len(text.encode('utf-16-le')) // 2 + text_length = len(text.encode("utf-16-le")) // 2 # Unicode buffer splits each two byte chars into separate indices. text_buffer = create_unicode_buffer(text, text_length) @@ -1715,7 +1764,7 @@ def get_string_info(self, text, font_face): text_props, # text props indices, # glyph indices glyph_props, # glyph pops - byref(actual_count) # glyph count + byref(actual_count), # glyph count ) advances = (FLOAT * length)() @@ -1737,14 +1786,15 @@ def get_string_info(self, text, font_face): None, 0, advances, - offsets + offsets, ) return text_buffer, actual_count.value, indices, advances, offsets, clusters - def get_glyph_metrics(self, font_face, indices, count): + def get_glyph_metrics(self, font_face: IDWriteFontFace, indices: Array[UINT16], count: int) -> list[ + tuple[float, float, float, float, float]]: """Returns a list of tuples with the following metrics per indice: - (glyph width, glyph height, lsb, advanceWidth) + . (glyph width, glyph height, lsb, advanceWidth) """ glyph_metrics = (DWRITE_GLYPH_METRICS * count)() font_face.GetDesignGlyphMetrics(indices, count, glyph_metrics, False) @@ -1769,7 +1819,9 @@ def get_glyph_metrics(self, font_face, indices, count): return metrics_out - def _get_single_glyph_run(self, font_face, size, indices, advances, offsets, sideways, bidi): + def _get_single_glyph_run(self, font_face: IDWriteFontFace, size: float, indices: Array[UINT16], + advances: Array[FLOAT], offsets: Array[DWRITE_GLYPH_OFFSET], sideways: bool, + bidi: int) -> DWRITE_GLYPH_RUN: run = DWRITE_GLYPH_RUN( font_face, size, @@ -1778,11 +1830,11 @@ def _get_single_glyph_run(self, font_face, size, indices, advances, offsets, sid advances, offsets, sideways, - bidi + bidi, ) return run - def is_color_run(self, run): + def is_color_run(self, run: DWRITE_GLYPH_RUN) -> bool: """Will return True if the run contains a colored glyph.""" try: if WINDOWS_10_CREATORS_UPDATE_OR_GREATER: @@ -1795,7 +1847,7 @@ def is_color_run(self, run): self.measuring_mode, None, 0, - byref(enumerator) + byref(enumerator), ) elif WINDOWS_8_1_OR_GREATER: enumerator = IDWriteColorGlyphRunEnumerator() @@ -1806,7 +1858,7 @@ def is_color_run(self, run): self.measuring_mode, None, 0, - byref(enumerator) + byref(enumerator), ) else: return False @@ -1819,9 +1871,11 @@ def is_color_run(self, run): return False - def render_single_glyph(self, font_face, indice, advance, offset, metrics): + def render_single_glyph(self, font_face: IDWriteFontFace, indice: int, advance: float, offset: DWRITE_GLYPH_OFFSET, + metrics: tuple[float, float, float, float, float]): """Renders a single glyph using D2D DrawGlyphRun""" - glyph_width, glyph_height, glyph_lsb, glyph_advance, glyph_bsb = metrics # We use a shaped advance instead of the fonts. + glyph_width, glyph_height, glyph_lsb, glyph_advance, glyph_bsb = metrics # We use a shaped advance instead + # of the fonts. # Slicing an array turns it into a python object. Maybe a better way to keep it a ctypes value? new_indice = (UINT16 * 1)(indice) @@ -1834,7 +1888,7 @@ def render_single_glyph(self, font_face, indice, advance, offset, metrics): new_advance, # advance, pointer(offset), # offset, False, - 0 + 0, ) # If it's colored, return to render it using layout. @@ -1889,10 +1943,12 @@ def render_single_glyph(self, font_face, indice, advance, offset, metrics): return glyph - def render_using_layout(self, text): - """This will render text given the built in DirectWrite layout. This process allows us to take - advantage of color glyphs and fallback handling that is built into DirectWrite. - This can also handle shaping and many other features if you want to render directly to a texture.""" + def render_using_layout(self, text: str) -> Glyph | None: + """This will render text given the built in DirectWrite layout. + + This process allows us to take advantage of color glyphs and fallback handling that is built into DirectWrite. + This can also handle shaping and many other features if you want to render directly to a texture. + """ text_layout = self.font.create_text_layout(text) layout_metrics = DWRITE_TEXT_METRICS() @@ -1926,9 +1982,11 @@ def render_using_layout(self, text): glyph.set_bearings(-self.font.descent, 0, int(math.ceil(layout_metrics.width))) return glyph - def create_zero_glyph(self): - """Zero glyph is a 1x1 image that has a -1 advance. This is to fill in for ligature substitutions since - font system requires 1 glyph per character in a string.""" + def create_zero_glyph(self) -> Glyph: + """Zero glyph is a 1x1 image that has a -1 advance. + + This is to fill in for ligature substitutions since font system requires 1 glyph per character in a string. + """ self._create_bitmap(1, 1) image = wic_decoder.get_image(self._bitmap) @@ -1936,7 +1994,7 @@ def create_zero_glyph(self): glyph.set_bearings(-self.font.descent, 0, -1) return glyph - def _create_bitmap(self, width, height): + def _create_bitmap(self, width: int, height: int) -> None: """Creates a bitmap using Direct2D and WIC.""" # Create a new bitmap, try to re-use the bitmap as much as we can to minimize creations. if self._bitmap_dimensions[0] != width or self._bitmap_dimensions[1] != height: @@ -1978,7 +2036,7 @@ class Win32DirectWriteFont(base.Font): _font_cache = [] _font_loader_key = None - _default_name = 'Segoe UI' # Default font for Win7/10. + _default_name = "Segoe UI" # Default font for Win7/10. _glyph_renderer = None _empty_glyph = None @@ -1987,11 +2045,12 @@ class Win32DirectWriteFont(base.Font): glyph_renderer_class = DirectWriteGlyphRenderer texture_internalformat = pyglet.gl.GL_RGBA - def __init__(self, name, size, bold=False, italic=False, stretch=False, dpi=None, locale=None): - self._filename: Optional[str] = None + def __init__(self, name: str, size: float, bold: bool | str = False, italic: bool | str = False, + stretch: bool | str = False, dpi: float | None = None, locale: str | None = None) -> None: + self._filename: str | None = None self._advance_cache = {} # Stores glyph's by the indice and advance. - super(Win32DirectWriteFont, self).__init__() + super().__init__() if not name: name = self._default_name @@ -2016,7 +2075,7 @@ def __init__(self, name, size, bold=False, italic=False, stretch=False, dpi=None self._real_size = (self.size * self.dpi) // 72 if self.bold: - if type(self.bold) is str: + if isinstance(self.bold, str): self._weight = name_to_weight[self.bold] else: self._weight = DWRITE_FONT_WEIGHT_BOLD @@ -2024,7 +2083,7 @@ def __init__(self, name, size, bold=False, italic=False, stretch=False, dpi=None self._weight = DWRITE_FONT_WEIGHT_NORMAL if self.italic: - if type(self.italic) is str: + if isinstance(self.italic, str): self._style = name_to_style[self.italic] else: self._style = DWRITE_FONT_STYLE_ITALIC @@ -2032,7 +2091,7 @@ def __init__(self, name, size, bold=False, italic=False, stretch=False, dpi=None self._style = DWRITE_FONT_STYLE_NORMAL if self.stretch: - if type(self.stretch) is str: + if isinstance(self.stretch, str): self._stretch = name_to_stretch[self.stretch] else: self._stretch = DWRITE_FONT_STRETCH_EXPANDED @@ -2057,7 +2116,7 @@ def __init__(self, name, size, bold=False, italic=False, stretch=False, dpi=None self._weight, self._stretch, self._style, - byref(write_font) + byref(write_font), ) # Create the text format this font will use permanently. @@ -2071,7 +2130,7 @@ def __init__(self, name, size, bold=False, italic=False, stretch=False, dpi=None self._stretch, self._real_size, create_unicode_buffer(self.locale), - byref(self._text_format) + byref(self._text_format), ) font_face = IDWriteFontFace() @@ -2099,9 +2158,11 @@ def __init__(self, name, size, bold=False, italic=False, stretch=False, dpi=None assert _debug_print("Windows 8.1+ is required for font fallback. Colored glyphs cannot be omitted.") @property - def filename(self): + def filename(self) -> str: """Returns a filename associated with the font face. - Note: Capable of returning more than 1 file in the future, but will do just one for now.""" + + Note: Capable of returning more than 1 file in the future, but will do just one for now. + """ if self._filename is not None: return self._filename @@ -2146,40 +2207,34 @@ def filename(self): return self._filename @property - def name(self): + def name(self) -> str: return self._name - def render_to_image(self, text, width=10000, height=80): + def render_to_image(self, text: str, width: int=10000, height: int=80) -> ImageData: """This process takes Pyglet out of the equation and uses only DirectWrite to shape and render text. This may allow more accurate fonts (bidi, rtl, etc) in very special circumstances at the cost of additional texture space. - - :Parameters: - `text` : str - String of text to render. - - :rtype: `ImageData` - :return: An image of the text. """ if not self._glyph_renderer: self._glyph_renderer = self.glyph_renderer_class(self) return self._glyph_renderer.render_to_image(text, width, height) - def copy_glyph(self, glyph, advance, offset): + def copy_glyph(self, glyph: base.Glyph, advance: int, offset: DWRITE_GLYPH_OFFSET) -> base.Glyph: """This takes the existing glyph texture and puts it into a new Glyph with a new advance. - Texture memory is shared between both glyphs.""" + Texture memory is shared between both glyphs. + """ new_glyph = base.Glyph(glyph.x, glyph.y, glyph.z, glyph.width, glyph.height, glyph.owner) new_glyph.set_bearings( glyph.baseline, glyph.lsb, advance, offset.advanceOffset, - offset.ascenderOffset + offset.ascenderOffset, ) return new_glyph - def _render_layout_glyph(self, text_buffer, i, clusters, check_color=True): + def _render_layout_glyph(self, text_buffer: str, i: int, clusters: list[DWRITE_CLUSTER_METRICS], check_color: bool=True): # Some glyphs can be more than 1 char. We use the clusters to determine how many of an index exist. text_length = clusters.count(i) @@ -2205,7 +2260,7 @@ def _render_layout_glyph(self, text_buffer, i, clusters, check_color=True): return self.glyphs[actual_text] - def is_fallback_str_colored(self, font_face, text): + def is_fallback_str_colored(self, font_face: IDWriteFontFace, text: str) -> bool: indice = UINT16() code_points = (UINT32 * len(text))(*[ord(c) for c in text]) @@ -2222,12 +2277,12 @@ def is_fallback_str_colored(self, font_face, text): new_advance, # advance, offset, # offset, False, - False + False, ) return self._glyph_renderer.is_color_run(run) - def _get_fallback_font_face(self, text_index, text_length): + def _get_fallback_font_face(self, text_index: int, text_length: int) -> IDWriteFontFace | None: if WINDOWS_8_1_OR_GREATER: out_length = UINT32() fb_font = IDWriteFont() @@ -2244,7 +2299,7 @@ def _get_fallback_font_face(self, text_index, text_length): self._stretch, byref(out_length), byref(fb_font), - byref(scale) + byref(scale), ) if fb_font: @@ -2255,18 +2310,19 @@ def _get_fallback_font_face(self, text_index, text_length): return None - def get_glyphs_no_shape(self, text): + def get_glyphs_no_shape(self, text: str) -> list[Glyph]: """This differs in that it does not attempt to shape the text at all. May be useful in cases where your font has no special shaping requirements, spacing is the same, or some other reason where faster performance is - wanted and you can get away with this.""" + wanted and you can get away with this. + """ if not self._glyph_renderer: self._glyph_renderer = self.glyph_renderer_class(self) self._empty_glyph = self._glyph_renderer.render_using_layout(" ") glyphs = [] for c in text: - if c == '\t': - c = ' ' + if c == "\t": + c = " " if c not in self.glyphs: self.glyphs[c] = self._glyph_renderer.render_using_layout(c) @@ -2277,7 +2333,7 @@ def get_glyphs_no_shape(self, text): return glyphs - def get_glyphs(self, text): + def get_glyphs(self, text: str) -> list[Glyph]: if not self._glyph_renderer: self._glyph_renderer = self.glyph_renderer_class(self) self._empty_glyph = self._glyph_renderer.render_using_layout(" ") @@ -2346,7 +2402,7 @@ def get_glyphs(self, text): return glyphs - def create_text_layout(self, text): + def create_text_layout(self, text: str) -> IDWriteTextLayout: text_buffer = create_unicode_buffer(text) text_layout = IDWriteTextLayout() @@ -2355,17 +2411,17 @@ def create_text_layout(self, text): self._text_format, 10000, # Doesn't affect bitmap size. 80, - byref(text_layout) + byref(text_layout), ) return text_layout @classmethod - def _initialize_direct_write(cls): - """ All direct write fonts needs factory access as well as the loaders.""" + def _initialize_direct_write(cls: type[Win32DirectWriteFont]) -> None: + """All direct write fonts needs factory access as well as the loaders.""" if WINDOWS_10_CREATORS_UPDATE_OR_GREATER: - cls._write_factory = IDWriteFactory5() - guid = IID_IDWriteFactory5 + cls._write_factory = IDWriteFactory5() + guid = IID_IDWriteFactory5 elif WINDOWS_8_1_OR_GREATER: cls._write_factory = IDWriteFactory2() guid = IID_IDWriteFactory2 @@ -2376,7 +2432,7 @@ def _initialize_direct_write(cls): DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, guid, byref(cls._write_factory)) @classmethod - def _initialize_custom_loaders(cls): + def _initialize_custom_loaders(cls: type[Win32DirectWriteFont]) -> None: """Initialize the loaders needed to load custom fonts.""" if WINDOWS_10_CREATORS_UPDATE_OR_GREATER: # Windows 10 finally has a built in loader that can take data and make a font out of it w/ COMs. @@ -2400,7 +2456,7 @@ def _initialize_custom_loaders(cls): cls._font_loader_key = cast(create_unicode_buffer("legacy_font_loader"), c_void_p) @classmethod - def add_font_data(cls, data): + def add_font_data(cls: type[Win32DirectWriteFont], data: BinaryIO) -> None: if not cls._write_factory: cls._initialize_direct_write() @@ -2458,9 +2514,10 @@ def add_font_data(cls, data): byref(cls._custom_collection)) @classmethod - def get_collection(cls, font_name) -> Tuple[Optional[int], Optional[IDWriteFontCollection1]]: + def get_collection(cls: type[Win32DirectWriteFont], font_name: str) -> tuple[int | None, IDWriteFontCollection1 | None]: """Returns which collection this font belongs to (system or custom collection), as well as its index in the - collection.""" + collection. + """ if not cls._write_factory: cls._initialize_direct_write() @@ -2492,8 +2549,8 @@ def get_collection(cls, font_name) -> Tuple[Optional[int], Optional[IDWriteFontC return None, None @classmethod - def find_font_face(cls, font_name, bold, italic, stretch) -> Tuple[ - Optional[IDWriteFont], Optional[IDWriteFontCollection]]: + def find_font_face(cls, font_name: str, bold: bool | str, italic: bool | str, stretch: bool | str) -> tuple[ + IDWriteFont | None, IDWriteFontCollection | None]: """This will search font collections for legacy RBIZ names. However, matching to bold, italic, stretch is problematic in that there are many values. We parse the font name looking for matches to the name database, and attempt to pick the closest match. @@ -2517,18 +2574,17 @@ def find_font_face(cls, font_name, bold, italic, stretch) -> Tuple[ return None, None @classmethod - def have_font(cls, name: str): + def have_font(cls: type[Win32DirectWriteFont], name: str) -> bool: if cls.get_collection(name)[0] is not None: return True return False @staticmethod - def parse_name(font_name: str, weight: int, style: int, stretch: int): + def parse_name(font_name: str, weight: int, style: int, stretch: int) -> tuple[int, int, int]: """Attempt at parsing any special names in a font for legacy checks. Takes the first found.""" - font_name = font_name.lower() - split_name = font_name.split(' ') + split_name = font_name.split(" ") found_weight = weight found_style = style @@ -2554,8 +2610,7 @@ def parse_name(font_name: str, weight: int, style: int, stretch: int): return found_weight, found_style, found_stretch @staticmethod - def find_legacy_font(collection: IDWriteFontCollection, font_name: str, bold, italic, stretch, full_debug=False) -> \ - Optional[IDWriteFont]: + def find_legacy_font(collection: IDWriteFontCollection, font_name: str, bold: bool | str, italic: bool | str, stretch: bool | str, full_debug: bool=False) -> IDWriteFont | None: coll_count = collection.GetFontFamilyCount() assert _debug_print(f"directwrite: Found {coll_count} fonts in collection.") @@ -2610,7 +2665,8 @@ def find_legacy_font(collection: IDWriteFontCollection, font_name: str, bold, it for compat_name in Win32DirectWriteFont.unpack_localized_string(compat_names, locale): if compat_name == font_name: assert _debug_print( - f"Found legacy name '{font_name}' as '{family_name}' in font face '{j}' (collection id #{i}).") + f"Found legacy name '{font_name}' as '{family_name}' in font face '{j}' (collection " + f"id #{i}).") match_found = True matches.append((temp_ft.GetWeight(), temp_ft.GetStyle(), temp_ft.GetStretch(), temp_ft)) @@ -2636,11 +2692,12 @@ def find_legacy_font(collection: IDWriteFontCollection, font_name: str, bold, it return None @staticmethod - def match_closest_font(font_list: List[Tuple[int, int, int, IDWriteFont]], bold: int, italic: int, stretch: int) -> \ - Optional[IDWriteFont]: - """Match the closest font to the parameters specified. If a full match is not found, a secondary match will be - found based on similar features. This can probably be improved, but it is possible you could get a different - font style than expected.""" + def match_closest_font(font_list: list[tuple[int, int, int, IDWriteFont]], bold: int, italic: int, stretch: int) -> IDWriteFont | None: + """Match the closest font to the parameters specified. + + If a full match is not found, a secondary match will be found based on similar features. This can probably + be improved, but it is possible you could get a different font style than expected. + """ closest = [] for match in font_list: (f_weight, f_style, f_stretch, writefont) = match @@ -2685,7 +2742,7 @@ def match_closest_font(font_list: List[Tuple[int, int, int, IDWriteFont]], bold: return None @staticmethod - def unpack_localized_string(local_string: IDWriteLocalizedStrings, locale: str) -> List[str]: + def unpack_localized_string(local_string: IDWriteLocalizedStrings, locale: str) -> list[str]: """Takes IDWriteLocalizedStrings and unpacks the strings inside of it into a list.""" str_array_len = local_string.GetCount() @@ -2710,7 +2767,7 @@ def unpack_localized_string(local_string: IDWriteLocalizedStrings, locale: str) return strings @staticmethod - def get_localized_index(strings: IDWriteLocalizedStrings, locale: str): + def get_localized_index(strings: IDWriteLocalizedStrings, locale: str) -> int: idx = UINT32() exists = BOOL() @@ -2719,7 +2776,7 @@ def get_localized_index(strings: IDWriteLocalizedStrings, locale: str): if not exists.value: # fallback to english. - strings.FindLocaleName('en-us', byref(idx), byref(exists)) + strings.FindLocaleName("en-us", byref(idx), byref(exists)) if not exists: return 0 diff --git a/pyglet/font/fontconfig.py b/pyglet/font/fontconfig.py index a15f617d2..43ed5b598 100644 --- a/pyglet/font/fontconfig.py +++ b/pyglet/font/fontconfig.py @@ -1,14 +1,16 @@ -""" -Wrapper around the Linux FontConfig library. Used to find available fonts. -""" +"""Wrapper around the Linux FontConfig library. Used to find available fonts.""" +from __future__ import annotations from collections import OrderedDict -from ctypes import * +from ctypes import CDLL, Structure, Union, byref, c_char_p, c_double, c_int, c_uint, c_void_p +from typing import TYPE_CHECKING -import pyglet.lib -from pyglet.util import asbytes, asstr from pyglet.font.base import FontException +from pyglet.lib import load_library +from pyglet.util import asbytes, asstr +if TYPE_CHECKING: + from pyglet.font.freetype_lib import FT_Face # fontconfig library definitions @@ -17,14 +19,15 @@ FcResultTypeMismatch, FcResultNoId, FcResultOutOfMemory) = range(5) + FcResult = c_int -FC_FAMILY = asbytes('family') -FC_SIZE = asbytes('size') -FC_SLANT = asbytes('slant') -FC_WEIGHT = asbytes('weight') -FC_FT_FACE = asbytes('ftface') -FC_FILE = asbytes('file') +FC_FAMILY = asbytes("family") +FC_SIZE = asbytes("size") +FC_SLANT = asbytes("slant") +FC_WEIGHT = asbytes("weight") +FC_FT_FACE = asbytes("ftface") +FC_FILE = asbytes("file") FC_WEIGHT_REGULAR = 80 FC_WEIGHT_BOLD = 200 @@ -50,44 +53,51 @@ class _FcValueUnion(Union): _fields_ = [ - ('s', c_char_p), - ('i', c_int), - ('b', c_int), - ('d', c_double), - ('m', c_void_p), - ('c', c_void_p), - ('f', c_void_p), - ('p', c_void_p), - ('l', c_void_p), + ("s", c_char_p), + ("i", c_int), + ("b", c_int), + ("d", c_double), + ("m", c_void_p), + ("c", c_void_p), + ("f", c_void_p), + ("p", c_void_p), + ("l", c_void_p), ] class FcValue(Structure): _fields_ = [ - ('type', FcType), - ('u', _FcValueUnion) + ("type", FcType), + ("u", _FcValueUnion), ] + # End of library definitions class FontConfig: - def __init__(self): + _search_cache: OrderedDict[tuple[str, float, bool, bool], FontConfigSearchResult] + _fontconfig: CDLL | None + + def __init__(self) -> None: self._fontconfig = self._load_fontconfig_library() + assert self._fontconfig is not None self._search_cache = OrderedDict() self._cache_size = 20 - def dispose(self): + def dispose(self) -> None: while len(self._search_cache) > 0: - self._search_cache.popitem().dispose() + k, v = self._search_cache.popitem() + v.dispose() self._fontconfig.FcFini() self._fontconfig = None - def create_search_pattern(self): + def create_search_pattern(self) -> FontConfigSearchPattern: return FontConfigSearchPattern(self._fontconfig) - def find_font(self, name, size=12, bold=False, italic=False): + def find_font(self, name: str, size: float = 12, bold: bool = False, + italic: bool = False) -> FontConfigSearchResult: result = self._get_from_search_cache(name, size, bold, italic) if result: return result @@ -103,20 +113,21 @@ def find_font(self, name, size=12, bold=False, italic=False): search_pattern.dispose() return result - def have_font(self, name): + def have_font(self, name: str) -> bool: result = self.find_font(name) if result: # Check the name matches, fontconfig can return a default if name and result.name and result.name.lower() != name.lower(): return False return True - else: - return False - def char_index(self, ft_face, character): + return False + + def char_index(self, ft_face: FT_Face, character: str) -> int: return self._fontconfig.FcFreeTypeCharIndex(ft_face, ord(character)) - def _add_to_search_cache(self, search_pattern, result_pattern): + def _add_to_search_cache(self, search_pattern: FontConfigSearchPattern, + result_pattern: FontConfigSearchResult) -> None: self._search_cache[(search_pattern.name, search_pattern.size, search_pattern.bold, @@ -124,17 +135,17 @@ def _add_to_search_cache(self, search_pattern, result_pattern): if len(self._search_cache) > self._cache_size: self._search_cache.popitem(last=False)[1].dispose() - def _get_from_search_cache(self, name, size, bold, italic): + def _get_from_search_cache(self, name: str, size: float, bold: bool, italic: bool) -> FontConfigSearchResult | None: result = self._search_cache.get((name, size, bold, italic), None) if result and result.is_valid: return result - else: - return None + + return None @staticmethod - def _load_fontconfig_library(): - fontconfig = pyglet.lib.load_library('fontconfig') + def _load_fontconfig_library() -> CDLL: + fontconfig = load_library("fontconfig") fontconfig.FcInit() fontconfig.FcPatternBuild.restype = c_void_p @@ -157,34 +168,34 @@ def _load_fontconfig_library(): class FontConfigPattern: - def __init__(self, fontconfig, pattern=None): + def __init__(self, fontconfig: CDLL, pattern: c_void_p | None = None) -> None: self._fontconfig = fontconfig self._pattern = pattern @property - def is_valid(self): - return self._fontconfig and self._pattern + def is_valid(self) -> bool: + return bool(self._fontconfig and self._pattern) - def _create(self): + def _create(self) -> None: assert not self._pattern assert self._fontconfig self._pattern = self._fontconfig.FcPatternCreate() - def _destroy(self): + def _destroy(self) -> None: assert self._pattern assert self._fontconfig self._fontconfig.FcPatternDestroy(self._pattern) self._pattern = None @staticmethod - def _bold_to_weight(bold): + def _bold_to_weight(bold: bool) -> int: return FC_WEIGHT_BOLD if bold else FC_WEIGHT_REGULAR @staticmethod - def _italic_to_slant(italic): + def _italic_to_slant(italic: bool) -> int: return FC_SLANT_ITALIC if italic else FC_SLANT_ROMAN - def _set_string(self, name, value): + def _set_string(self, name: bytes, value: str) -> None: assert self._pattern assert name assert self._fontconfig @@ -192,11 +203,11 @@ def _set_string(self, name, value): if not value: return - value = value.encode('utf8') + value = value.encode("utf8") self._fontconfig.FcPatternAddString(self._pattern, name, asbytes(value)) - def _set_double(self, name, value): + def _set_double(self, name: bytes, value: int) -> None: assert self._pattern assert name assert self._fontconfig @@ -206,7 +217,7 @@ def _set_double(self, name, value): self._fontconfig.FcPatternAddDouble(self._pattern, name, c_double(value)) - def _set_integer(self, name, value): + def _set_integer(self, name: bytes, value: int) -> None: assert self._pattern assert name assert self._fontconfig @@ -216,70 +227,75 @@ def _set_integer(self, name, value): self._fontconfig.FcPatternAddInteger(self._pattern, name, c_int(value)) - def _get_value(self, name): + def _get_value(self, name: bytes) -> FcValue | None: assert self._pattern assert name assert self._fontconfig value = FcValue() - result = self._fontconfig.FcPatternGet(self._pattern, name, 0, byref(value)) + result: FcResult = self._fontconfig.FcPatternGet(self._pattern, name, 0, byref(value)) if _handle_fcresult(result): return value - else: - return None - def _get_string(self, name): + return None + + def _get_string(self, name: bytes) -> str | None: value = self._get_value(name) if value and value.type == FcTypeString: return asstr(value.u.s) - else: - return None - def _get_face(self, name): + return None + + def _get_face(self, name: bytes) -> FT_Face | None: value = self._get_value(name) if value and value.type == FcTypeFTFace: return value.u.f - else: - return None - def _get_integer(self, name): + return None + + def _get_integer(self, name: bytes) -> int | None: value = self._get_value(name) if value and value.type == FcTypeInteger: return value.u.i - else: - return None - def _get_double(self, name): + return None + + def _get_double(self, name: bytes) -> int | None: value = self._get_value(name) if value and value.type == FcTypeDouble: return value.u.d - else: - return None + + return None class FontConfigSearchPattern(FontConfigPattern): - def __init__(self, fontconfig): - super(FontConfigSearchPattern, self).__init__(fontconfig) + size: int | None + italic: bool + bold: bool + name: str | None + + def __init__(self, fontconfig: CDLL) -> None: + super().__init__(fontconfig) self.name = None self.bold = False self.italic = False self.size = None - def match(self): + def match(self) -> FontConfigSearchResult | None: self._prepare_search_pattern() result_pattern = self._get_match() if result_pattern: return FontConfigSearchResult(self._fontconfig, result_pattern) - else: - return None - def _prepare_search_pattern(self): + return None + + def _prepare_search_pattern(self) -> None: self._create() self._set_string(FC_FAMILY, self.name) self._set_double(FC_SIZE, self.size) @@ -288,14 +304,14 @@ def _prepare_search_pattern(self): self._substitute_defaults() - def _substitute_defaults(self): + def _substitute_defaults(self) -> None: assert self._pattern assert self._fontconfig self._fontconfig.FcConfigSubstitute(None, self._pattern, FcMatchPattern) self._fontconfig.FcDefaultSubstitute(self._pattern) - def _get_match(self): + def _get_match(self) -> c_void_p | None: assert self._pattern assert self._fontconfig @@ -304,59 +320,61 @@ def _get_match(self): if _handle_fcresult(match_result.value): return match_pattern - else: - return None - def dispose(self): + return None + + def dispose(self) -> None: self._destroy() class FontConfigSearchResult(FontConfigPattern): - def __init__(self, fontconfig, result_pattern): - super(FontConfigSearchResult, self).__init__(fontconfig, result_pattern) + def __init__(self, fontconfig: CDLL, result_pattern: c_void_p | None) -> None: + super().__init__(fontconfig, result_pattern) @property - def name(self): + def name(self) -> str: return self._get_string(FC_FAMILY) @property - def size(self): + def size(self) -> int: return self._get_double(FC_SIZE) @property - def bold(self): + def bold(self) -> bool: return self._get_integer(FC_WEIGHT) == FC_WEIGHT_BOLD @property - def italic(self): + def italic(self) -> bool: return self._get_integer(FC_SLANT) == FC_SLANT_ITALIC @property - def face(self): + def face(self) -> FT_Face: return self._get_face(FC_FT_FACE) @property - def file(self): + def file(self) -> str: return self._get_string(FC_FILE) - def dispose(self): + def dispose(self) -> None: self._destroy() -def _handle_fcresult(result): +def _handle_fcresult(result: int) -> bool | None: if result == FcResultMatch: return True - elif result in (FcResultNoMatch, FcResultTypeMismatch, FcResultNoId): + if result in (FcResultNoMatch, FcResultTypeMismatch, FcResultNoId): return False - elif result == FcResultOutOfMemory: - raise FontException('FontConfig ran out of memory.') + if result == FcResultOutOfMemory: + msg = "FontConfig ran out of memory." + raise FontException(msg) + return None _fontconfig_instance = None -def get_fontconfig(): - global _fontconfig_instance +def get_fontconfig() -> FontConfig: + global _fontconfig_instance # noqa: PLW0603 if not _fontconfig_instance: _fontconfig_instance = FontConfig() return _fontconfig_instance diff --git a/pyglet/font/freetype.py b/pyglet/font/freetype.py index e6d3d7cf8..8a97ca50e 100644 --- a/pyglet/font/freetype.py +++ b/pyglet/font/freetype.py @@ -1,16 +1,38 @@ -import ctypes +from __future__ import annotations + import warnings -from collections import namedtuple +from ctypes import POINTER, byref, c_ubyte, cast, memmove +from typing import NamedTuple -from pyglet.util import asbytes, asstr -from pyglet.font import base +import pyglet from pyglet import image +from pyglet.font import base from pyglet.font.fontconfig import get_fontconfig -from pyglet.font.freetype_lib import * +from pyglet.font.freetype_lib import ( + FT_LOAD_RENDER, + FT_PIXEL_MODE_GRAY, + FT_PIXEL_MODE_MONO, + FT_STYLE_FLAG_BOLD, + FT_STYLE_FLAG_ITALIC, + FreeTypeError, + FT_Byte, + FT_Done_Face, + FT_Face, + FT_GlyphSlot, + FT_Load_Glyph, + FT_New_Face, + FT_New_Memory_Face, + FT_Reference_Face, + FT_Set_Char_Size, + f26p6_to_float, + float_to_f26p6, + ft_get_library, +) +from pyglet.util import asbytes, asstr class FreeTypeGlyphRenderer(base.GlyphRenderer): - def __init__(self, font): + def __init__(self, font: FreeTypeFont) -> None: super().__init__(font) self.font = font @@ -28,14 +50,14 @@ def __init__(self, font): self._data = None - def _get_glyph(self, character): + def _get_glyph(self, character: str) -> None: assert self.font assert len(character) == 1 self._glyph_slot = self.font.get_glyph_slot(character) self._bitmap = self._glyph_slot.bitmap - def _get_glyph_metrics(self): + def _get_glyph_metrics(self) -> None: self._width = self._glyph_slot.bitmap.width self._height = self._glyph_slot.bitmap.rows self._mode = self._glyph_slot.bitmap.pixel_mode @@ -45,7 +67,7 @@ def _get_glyph_metrics(self): self._lsb = self._glyph_slot.bitmap_left self._advance_x = int(f26p6_to_float(self._glyph_slot.advance.x)) - def _get_bitmap_data(self): + def _get_bitmap_data(self) -> None: if self._mode == FT_PIXEL_MODE_MONO: # BCF fonts always render to 1 bit mono, regardless of render # flags. (freetype 2.3.5) @@ -55,9 +77,10 @@ def _get_bitmap_data(self): assert self._glyph_slot.bitmap.num_grays == 256 self._data = self._glyph_slot.bitmap.buffer else: - raise base.FontException('Unsupported render mode for this glyph') + msg = "Unsupported render mode for this glyph" + raise base.FontException(msg) - def _convert_mono_to_gray_bitmap(self): + def _convert_mono_to_gray_bitmap(self) -> None: bitmap_data = cast(self._bitmap.buffer, POINTER(c_ubyte * (self._pitch * self._height))).contents data = (c_ubyte * (self._pitch * 8 * self._height))() @@ -76,13 +99,13 @@ def _convert_mono_to_gray_bitmap(self): self._data = data self._pitch <<= 3 - def _create_glyph(self): + def _create_glyph(self) -> base.Glyph: # In FT positive pitch means `down` flow, in Pyglet ImageData # negative values indicate a top-to-bottom arrangement. So pitch must be inverted. # Using negative pitch causes conversions, so much faster to just swap tex_coords img = image.ImageData(self._width, self._height, - 'A', + "A", self._data, abs(self._pitch)) @@ -104,29 +127,33 @@ def _create_glyph(self): return glyph - def render(self, text): + def render(self, text: str) -> base.Glyph: self._get_glyph(text[0]) self._get_glyph_metrics() self._get_bitmap_data() return self._create_glyph() -FreeTypeFontMetrics = namedtuple('FreeTypeFontMetrics', ['ascent', 'descent']) +class FreeTypeFontMetrics(NamedTuple): + ascent: int + descent: int class MemoryFaceStore: - def __init__(self): + _dict: dict[tuple[str, bool, bool], FreeTypeMemoryFace] + + def __init__(self) -> None: self._dict = {} - def add(self, face): + def add(self, face: FreeTypeMemoryFace) -> None: self._dict[face.name.lower(), face.bold, face.italic] = face - def contains(self, name): - lname = name and name.lower() or '' - return len([name for name, _, _ in self._dict.keys() if name == lname]) > 0 + def contains(self, name: str) -> bool: + lname = name and name.lower() or "" + return len([name for name, _, _ in self._dict if name == lname]) > 0 - def get(self, name, bold, italic): - lname = name and name.lower() or '' + def get(self, name: str, bold: bool, italic: bool) -> FreeTypeMemoryFace | None: + lname = name and name.lower() or "" return self._dict.get((lname, bold, italic), None) @@ -135,13 +162,13 @@ class FreeTypeFont(base.Font): # Map font (name, bold, italic) to FreeTypeMemoryFace _memory_faces = MemoryFaceStore() + face: FreeTypeFace - def __init__(self, name, size, bold=False, italic=False, stretch=False, dpi=None): - # assert type(bold) is bool, "Only a boolean value is supported for bold in the current font renderer." - # assert type(italic) is bool, "Only a boolean value is supported for bold in the current font renderer." + def __init__(self, name: str, size: float, bold: bool = False, italic: bool = False, stretch: bool = False, + dpi: float | None = None) -> None: if stretch: - warnings.warn("The current font render does not support stretching.") + warnings.warn("The current font render does not support stretching.") # noqa: B028 super().__init__() self._name = name @@ -154,43 +181,44 @@ def __init__(self, name, size, bold=False, italic=False, stretch=False, dpi=None self.metrics = self.face.get_font_metrics(self.size, self.dpi) @property - def name(self): + def name(self) -> str: return self.face.family_name @property - def ascent(self): + def ascent(self) -> int: return self.metrics.ascent @property - def descent(self): + def descent(self) -> int: return self.metrics.descent - def get_glyph_slot(self, character): + def get_glyph_slot(self, character: str) -> FT_GlyphSlot: glyph_index = self.face.get_character_index(character) self.face.set_char_size(self.size, self.dpi) return self.face.get_glyph_slot(glyph_index) - def _load_font_face(self): + def _load_font_face(self) -> None: self.face = self._memory_faces.get(self._name, self.bold, self.italic) if self.face is None: self._load_font_face_from_system() - def _load_font_face_from_system(self): + def _load_font_face_from_system(self) -> None: match = get_fontconfig().find_font(self._name, self.size, self.bold, self.italic) if not match: - raise base.FontException(f"Could not match font '{self._name}'") + msg = f"Could not match font '{self._name}'" + raise base.FontException(msg) self.filename = match.file self.face = FreeTypeFace.from_fontconfig(match) @classmethod - def have_font(cls, name): + def have_font(cls: type[FreeTypeFont], name: str) -> bool: if cls._memory_faces.contains(name): return True - else: - return get_fontconfig().have_font(name) + + return get_fontconfig().have_font(name) @classmethod - def add_font_data(cls, data): + def add_font_data(cls: type[FreeTypeFont], data: bytes) -> None: face = FreeTypeMemoryFace(data) cls._memory_faces.add(face) @@ -202,13 +230,16 @@ class FreeTypeFace: want to keep a face without a reference to this object, they should increase the reference counter themselves and decrease it again when done. """ - def __init__(self, ft_face): + _name: str + ft_face: FT_Face + + def __init__(self, ft_face: FT_Face) -> None: assert ft_face is not None self.ft_face = ft_face self._get_best_name() @classmethod - def from_file(cls, file_name): + def from_file(cls: type[FreeTypeFace], file_name: str) -> FreeTypeFace: ft_library = ft_get_library() ft_face = FT_Face() FT_New_Face(ft_library, @@ -218,45 +249,46 @@ def from_file(cls, file_name): return cls(ft_face) @classmethod - def from_fontconfig(cls, match): + def from_fontconfig(cls: type[FreeTypeFace], match): if match.face is not None: FT_Reference_Face(match.face) return cls(match.face) else: if not match.file: - raise base.FontException(f'No filename for "{match.name}"') + msg = f'No filename for "{match.name}"' + raise base.FontException(msg) return cls.from_file(match.file) @property - def name(self): + def name(self) -> str: return self._name @property - def family_name(self): + def family_name(self) -> str: return asstr(self.ft_face.contents.family_name) @property - def style_flags(self): + def style_flags(self) -> int: return self.ft_face.contents.style_flags @property - def bold(self): + def bold(self) -> bool: return self.style_flags & FT_STYLE_FLAG_BOLD != 0 @property - def italic(self): + def italic(self) -> bool: return self.style_flags & FT_STYLE_FLAG_ITALIC != 0 @property - def face_flags(self): + def face_flags(self) -> int: return self.ft_face.contents.face_flags - def __del__(self): + def __del__(self) -> None: if self.ft_face is not None: FT_Done_Face(self.ft_face) self.ft_face = None - def set_char_size(self, size, dpi): + def set_char_size(self, size: float, dpi: float) -> bool: face_size = float_to_f26p6(size) try: FT_Set_Char_Size(self.ft_face, @@ -270,45 +302,45 @@ def set_char_size(self, size, dpi): # TODO Warn the user? if e.errcode == 0x17: return False - else: - raise - def get_character_index(self, character): + raise + + def get_character_index(self, character: str) -> int: return get_fontconfig().char_index(self.ft_face, character) - def get_glyph_slot(self, glyph_index): + def get_glyph_slot(self, glyph_index: int) -> FT_GlyphSlot: FT_Load_Glyph(self.ft_face, glyph_index, FT_LOAD_RENDER) return self.ft_face.contents.glyph.contents - def get_font_metrics(self, size, dpi): + def get_font_metrics(self, size: float, dpi: float) -> FreeTypeFontMetrics: if self.set_char_size(size, dpi): metrics = self.ft_face.contents.size.contents.metrics if metrics.ascender == 0 and metrics.descender == 0: return self._get_font_metrics_workaround() - else: - return FreeTypeFontMetrics(ascent=int(f26p6_to_float(metrics.ascender)), - descent=int(f26p6_to_float(metrics.descender))) - else: - return self._get_font_metrics_workaround() - def _get_font_metrics_workaround(self): + return FreeTypeFontMetrics(ascent=int(f26p6_to_float(metrics.ascender)), + descent=int(f26p6_to_float(metrics.descender))) + + return self._get_font_metrics_workaround() + + def _get_font_metrics_workaround(self) -> FreeTypeFontMetrics: # Workaround broken fonts with no metrics. Has been observed with # courR12-ISO8859-1.pcf.gz: "Courier" "Regular" # # None of the metrics fields are filled in, so render a glyph and # grab its height as the ascent, and make up an arbitrary # descent. - i = self.get_character_index('X') + i = self.get_character_index("X") self.get_glyph_slot(i) - ascent=self.ft_face.contents.available_sizes.contents.height + ascent = self.ft_face.contents.available_sizes.contents.height return FreeTypeFontMetrics(ascent=ascent, descent=-ascent // 4) # arbitrary. - def _get_best_name(self): + def _get_best_name(self) -> None: self._name = asstr(self.ft_face.contents.family_name) - self._get_font_family_from_ttf + self._get_font_family_from_ttf() - def _get_font_family_from_ttf(self): + def _get_font_family_from_ttf(self) -> None: # Replace Freetype's generic family name with TTF/OpenType specific # name if we can find one; there are some instances where Freetype # gets it wrong. @@ -324,21 +356,21 @@ def _get_font_family_from_ttf(self): name.encoding_id == TT_MS_ID_UNICODE_CS): continue # name.string is not 0 terminated! use name.string_len - self._name = name.string.decode('utf-16be', 'ignore') + self._name = name.string.decode("utf-16be", "ignore") except: continue class FreeTypeMemoryFace(FreeTypeFace): - def __init__(self, data): + def __init__(self, data: bytes) -> None: self._copy_font_data(data) super().__init__(self._create_font_face()) - def _copy_font_data(self, data): + def _copy_font_data(self, data: bytes) -> None: self.font_data = (FT_Byte * len(data))() - ctypes.memmove(self.font_data, data, len(data)) + memmove(self.font_data, data, len(data)) - def _create_font_face(self): + def _create_font_face(self) -> FT_Face: ft_library = ft_get_library() ft_face = FT_Face() FT_New_Memory_Face(ft_library, @@ -347,4 +379,3 @@ def _create_font_face(self): 0, byref(ft_face)) return ft_face - diff --git a/pyglet/font/freetype_lib.py b/pyglet/font/freetype_lib.py index 7672eb8cd..7ade5201e 100644 --- a/pyglet/font/freetype_lib.py +++ b/pyglet/font/freetype_lib.py @@ -1,20 +1,48 @@ -from ctypes import * -from .base import FontException +from __future__ import annotations + +from ctypes import ( + CFUNCTYPE, + POINTER, + Structure, + byref, + c_byte, + c_char, + c_char_p, + c_int, + c_int16, + c_int32, + c_int64, + c_long, + c_short, + c_size_t, + c_ubyte, + c_uint, + c_uint16, + c_uint32, + c_uint64, + c_ulong, + c_ushort, + c_void_p, +) +from typing import Any, Callable, Iterable, NoReturn + import pyglet.lib -_libfreetype = pyglet.lib.load_library('freetype') +from .base import FontException + +_libfreetype = pyglet.lib.load_library("freetype") _font_data = {} -def _get_function(name, argtypes, rtype): +def _get_function(name: str, argtypes: Iterable[Any], rtype: Any) -> Callable: try: func = getattr(_libfreetype, name) func.argtypes = argtypes func.restype = rtype return func except AttributeError as e: - raise ImportError(e) + raise ImportError(e) # noqa: B904 FT_Byte = c_char @@ -46,28 +74,29 @@ def _get_function(name, argtypes, rtype): class FT_Vector(Structure): _fields_ = [ - ('x', FT_Pos), - ('y', FT_Pos) + ("x", FT_Pos), + ("y", FT_Pos), ] class FT_BBox(Structure): _fields_ = [ - ('xMin', FT_Pos), - ('yMin', FT_Pos), - ('xMax', FT_Pos), - ('yMax', FT_Pos) + ("xMin", FT_Pos), + ("yMin", FT_Pos), + ("xMax", FT_Pos), + ("yMax", FT_Pos), ] class FT_Matrix(Structure): _fields_ = [ - ('xx', FT_Fixed), - ('xy', FT_Fixed), - ('yx', FT_Fixed), - ('yy', FT_Fixed) + ("xx", FT_Fixed), + ("xy", FT_Fixed), + ("yx", FT_Fixed), + ("yy", FT_Fixed), ] + FT_FWord = c_short FT_UFWord = c_ushort FT_F2Dot14 = c_short @@ -75,41 +104,44 @@ class FT_Matrix(Structure): class FT_UnitVector(Structure): _fields_ = [ - ('x', FT_F2Dot14), - ('y', FT_F2Dot14), + ("x", FT_F2Dot14), + ("y", FT_F2Dot14), ] + FT_F26Dot6 = c_long class FT_Data(Structure): _fields_ = [ - ('pointer', POINTER(FT_Byte)), - ('length', FT_Int), + ("pointer", POINTER(FT_Byte)), + ("length", FT_Int), ] + FT_Generic_Finalizer = CFUNCTYPE(None, (c_void_p)) class FT_Generic(Structure): _fields_ = [ - ('data', c_void_p), - ('finalizer', FT_Generic_Finalizer) + ("data", c_void_p), + ("finalizer", FT_Generic_Finalizer), ] class FT_Bitmap(Structure): _fields_ = [ - ('rows', c_uint), - ('width', c_uint), - ('pitch', c_int), - ('buffer', POINTER(c_ubyte)), - ('num_grays', c_short), - ('pixel_mode', c_ubyte), - ('palette_mode', c_ubyte), - ('palette', c_void_p), + ("rows", c_uint), + ("width", c_uint), + ("pitch", c_int), + ("buffer", POINTER(c_ubyte)), + ("num_grays", c_short), + ("pixel_mode", c_ubyte), + ("palette_mode", c_ubyte), + ("palette", c_void_p), ] + FT_PIXEL_MODE_NONE = 0 FT_PIXEL_MODE_MONO = 1 FT_PIXEL_MODE_GRAY = 2 @@ -122,69 +154,73 @@ class FT_Bitmap(Structure): class FT_LibraryRec(Structure): _fields_ = [ - ('dummy', c_int), + ("dummy", c_int), ] - def __del__(self): - global _library + def __del__(self) -> None: + global _library # noqa: PLW0603 try: - print('FT_LibraryRec.__del__') + print("FT_LibraryRec.__del__") # noqa: T201 FT_Done_FreeType(byref(self)) _library = None - except: + except: # noqa: S110, E722 pass + + FT_Library = POINTER(FT_LibraryRec) class FT_Bitmap_Size(Structure): _fields_ = [ - ('height', c_ushort), - ('width', c_ushort), - ('size', c_long), - ('x_ppem', c_long), - ('y_ppem', c_long), + ("height", c_ushort), + ("width", c_ushort), + ("size", c_long), + ("x_ppem", c_long), + ("y_ppem", c_long), ] class FT_Glyph_Metrics(Structure): _fields_ = [ - ('width', FT_Pos), - ('height', FT_Pos), + ("width", FT_Pos), + ("height", FT_Pos), - ('horiBearingX', FT_Pos), - ('horiBearingY', FT_Pos), - ('horiAdvance', FT_Pos), + ("horiBearingX", FT_Pos), + ("horiBearingY", FT_Pos), + ("horiAdvance", FT_Pos), - ('vertBearingX', FT_Pos), - ('vertBearingY', FT_Pos), - ('vertAdvance', FT_Pos), + ("vertBearingX", FT_Pos), + ("vertBearingY", FT_Pos), + ("vertAdvance", FT_Pos), ] - def dump(self): - for (name, type) in self._fields_: - print('FT_Glyph_Metrics', name, repr(getattr(self, name))) + def dump(self) -> None: + for (name, _) in self._fields_: + print("FT_Glyph_Metrics", name, repr(getattr(self, name))) # noqa: T201 + FT_Glyph_Format = c_ulong -def FT_IMAGE_TAG(tag): +def FT_IMAGE_TAG(tag: str) -> int: # noqa: N802 return (ord(tag[0]) << 24) | (ord(tag[1]) << 16) | (ord(tag[2]) << 8) | ord(tag[3]) + FT_GLYPH_FORMAT_NONE = 0 -FT_GLYPH_FORMAT_COMPOSITE = FT_IMAGE_TAG('comp') -FT_GLYPH_FORMAT_BITMAP = FT_IMAGE_TAG('bits') -FT_GLYPH_FORMAT_OUTLINE = FT_IMAGE_TAG('outl') -FT_GLYPH_FORMAT_PLOTTER = FT_IMAGE_TAG('plot') +FT_GLYPH_FORMAT_COMPOSITE = FT_IMAGE_TAG("comp") +FT_GLYPH_FORMAT_BITMAP = FT_IMAGE_TAG("bits") +FT_GLYPH_FORMAT_OUTLINE = FT_IMAGE_TAG("outl") +FT_GLYPH_FORMAT_PLOTTER = FT_IMAGE_TAG("plot") class FT_Outline(Structure): _fields_ = [ - ('n_contours', c_short), # number of contours in glyph - ('n_points', c_short), # number of points in the glyph - ('points', POINTER(FT_Vector)), # the outline's points - ('tags', c_char_p), # the points flags - ('contours', POINTER(c_short)), # the contour end points - ('flags', c_int), # outline masks + ("n_contours", c_short), # number of contours in glyph + ("n_points", c_short), # number of points in the glyph + ("points", POINTER(FT_Vector)), # the outline's points + ("tags", c_char_p), # the points flags + ("contours", POINTER(c_short)), # the contour end points + ("flags", c_int), # outline masks ] @@ -193,141 +229,145 @@ class FT_Outline(Structure): class FT_GlyphSlotRec(Structure): _fields_ = [ - ('library', FT_Library), - ('face', c_void_p), - ('next', c_void_p), - ('reserved', FT_UInt), - ('generic', FT_Generic), + ("library", FT_Library), + ("face", c_void_p), + ("next", c_void_p), + ("reserved", FT_UInt), + ("generic", FT_Generic), - ('metrics', FT_Glyph_Metrics), - ('linearHoriAdvance', FT_Fixed), - ('linearVertAdvance', FT_Fixed), - ('advance', FT_Vector), + ("metrics", FT_Glyph_Metrics), + ("linearHoriAdvance", FT_Fixed), + ("linearVertAdvance", FT_Fixed), + ("advance", FT_Vector), - ('format', FT_Glyph_Format), + ("format", FT_Glyph_Format), - ('bitmap', FT_Bitmap), - ('bitmap_left', FT_Int), - ('bitmap_top', FT_Int), + ("bitmap", FT_Bitmap), + ("bitmap_left", FT_Int), + ("bitmap_top", FT_Int), - ('outline', FT_Outline), - ('num_subglyphs', FT_UInt), - ('subglyphs', FT_SubGlyph), + ("outline", FT_Outline), + ("num_subglyphs", FT_UInt), + ("subglyphs", FT_SubGlyph), - ('control_data', c_void_p), - ('control_len', c_long), + ("control_data", c_void_p), + ("control_len", c_long), - ('lsb_delta', FT_Pos), - ('rsb_delta', FT_Pos), + ("lsb_delta", FT_Pos), + ("rsb_delta", FT_Pos), - ('other', c_void_p), + ("other", c_void_p), - ('internal', c_void_p), + ("internal", c_void_p), ] + + FT_GlyphSlot = POINTER(FT_GlyphSlotRec) class FT_Size_Metrics(Structure): _fields_ = [ - ('x_ppem', FT_UShort), # horizontal pixels per EM - ('y_ppem', FT_UShort), # vertical pixels per EM + ("x_ppem", FT_UShort), # horizontal pixels per EM + ("y_ppem", FT_UShort), # vertical pixels per EM - ('x_scale', FT_Fixed), # two scales used to convert font units - ('y_scale', FT_Fixed), # to 26.6 frac. pixel coordinates + ("x_scale", FT_Fixed), # two scales used to convert font units + ("y_scale", FT_Fixed), # to 26.6 frac. pixel coordinates - ('ascender', FT_Pos), # ascender in 26.6 frac. pixels - ('descender', FT_Pos), # descender in 26.6 frac. pixels - ('height', FT_Pos), # text height in 26.6 frac. pixels - ('max_advance', FT_Pos), # max horizontal advance, in 26.6 pixels + ("ascender", FT_Pos), # ascender in 26.6 frac. pixels + ("descender", FT_Pos), # descender in 26.6 frac. pixels + ("height", FT_Pos), # text height in 26.6 frac. pixels + ("max_advance", FT_Pos), # max horizontal advance, in 26.6 pixels ] class FT_SizeRec(Structure): _fields_ = [ - ('face', c_void_p), - ('generic', FT_Generic), - ('metrics', FT_Size_Metrics), - ('internal', c_void_p), + ("face", c_void_p), + ("generic", FT_Generic), + ("metrics", FT_Size_Metrics), + ("internal", c_void_p), ] + + FT_Size = POINTER(FT_SizeRec) class FT_FaceRec(Structure): _fields_ = [ - ('num_faces', FT_Long), - ('face_index', FT_Long), + ("num_faces", FT_Long), + ("face_index", FT_Long), - ('face_flags', FT_Long), - ('style_flags', FT_Long), + ("face_flags", FT_Long), + ("style_flags", FT_Long), - ('num_glyphs', FT_Long), + ("num_glyphs", FT_Long), - ('family_name', FT_String_Ptr), - ('style_name', FT_String_Ptr), + ("family_name", FT_String_Ptr), + ("style_name", FT_String_Ptr), - ('num_fixed_sizes', FT_Int), - ('available_sizes', POINTER(FT_Bitmap_Size)), + ("num_fixed_sizes", FT_Int), + ("available_sizes", POINTER(FT_Bitmap_Size)), - ('num_charmaps', FT_Int), - ('charmaps', c_void_p), + ("num_charmaps", FT_Int), + ("charmaps", c_void_p), - ('generic', FT_Generic), + ("generic", FT_Generic), - ('bbox', FT_BBox), + ("bbox", FT_BBox), - ('units_per_EM', FT_UShort), - ('ascender', FT_Short), - ('descender', FT_Short), - ('height', FT_Short), + ("units_per_EM", FT_UShort), + ("ascender", FT_Short), + ("descender", FT_Short), + ("height", FT_Short), - ('max_advance_width', FT_Short), - ('max_advance_height', FT_Short), + ("max_advance_width", FT_Short), + ("max_advance_height", FT_Short), - ('underline_position', FT_Short), - ('underline_thickness', FT_Short), + ("underline_position", FT_Short), + ("underline_thickness", FT_Short), - ('glyph', FT_GlyphSlot), - ('size', FT_Size), - ('charmap', c_void_p), + ("glyph", FT_GlyphSlot), + ("size", FT_Size), + ("charmap", c_void_p), - ('driver', c_void_p), - ('memory', c_void_p), - ('stream', c_void_p), + ("driver", c_void_p), + ("memory", c_void_p), + ("stream", c_void_p), - ('sizes_list', c_void_p), + ("sizes_list", c_void_p), - ('autohint', FT_Generic), - ('extensions', c_void_p), - ('internal', c_void_p), + ("autohint", FT_Generic), + ("extensions", c_void_p), + ("internal", c_void_p), ] - def dump(self): - for (name, type) in self._fields_: - print('FT_FaceRec', name, repr(getattr(self, name))) + def dump(self) -> None: + for (name, _) in self._fields_: + print("FT_FaceRec", name, repr(getattr(self, name))) # noqa: T201 - def has_kerning(self): + def has_kerning(self) -> bool: return self.face_flags & FT_FACE_FLAG_KERNING -FT_Face = POINTER(FT_FaceRec) +FT_Face = POINTER(FT_FaceRec) + # face_flags values -FT_FACE_FLAG_SCALABLE = 1 << 0 -FT_FACE_FLAG_FIXED_SIZES = 1 << 1 -FT_FACE_FLAG_FIXED_WIDTH = 1 << 2 -FT_FACE_FLAG_SFNT = 1 << 3 -FT_FACE_FLAG_HORIZONTAL = 1 << 4 -FT_FACE_FLAG_VERTICAL = 1 << 5 -FT_FACE_FLAG_KERNING = 1 << 6 -FT_FACE_FLAG_FAST_GLYPHS = 1 << 7 -FT_FACE_FLAG_MULTIPLE_MASTERS = 1 << 8 -FT_FACE_FLAG_GLYPH_NAMES = 1 << 9 -FT_FACE_FLAG_EXTERNAL_STREAM = 1 << 10 -FT_FACE_FLAG_HINTER = 1 << 11 +FT_FACE_FLAG_SCALABLE = 1 << 0 +FT_FACE_FLAG_FIXED_SIZES = 1 << 1 +FT_FACE_FLAG_FIXED_WIDTH = 1 << 2 +FT_FACE_FLAG_SFNT = 1 << 3 +FT_FACE_FLAG_HORIZONTAL = 1 << 4 +FT_FACE_FLAG_VERTICAL = 1 << 5 +FT_FACE_FLAG_KERNING = 1 << 6 +FT_FACE_FLAG_FAST_GLYPHS = 1 << 7 +FT_FACE_FLAG_MULTIPLE_MASTERS = 1 << 8 +FT_FACE_FLAG_GLYPH_NAMES = 1 << 9 +FT_FACE_FLAG_EXTERNAL_STREAM = 1 << 10 +FT_FACE_FLAG_HINTER = 1 << 11 FT_STYLE_FLAG_ITALIC = 1 FT_STYLE_FLAG_BOLD = 2 - (FT_RENDER_MODE_NORMAL, FT_RENDER_MODE_LIGHT, FT_RENDER_MODE_MONO, @@ -335,224 +375,224 @@ def has_kerning(self): FT_RENDER_MODE_LCD_V) = range(5) -def FT_LOAD_TARGET_(x): +def FT_LOAD_TARGET_(x: int) -> int: # noqa: N802 return (x & 15) << 16 + FT_LOAD_TARGET_NORMAL = FT_LOAD_TARGET_(FT_RENDER_MODE_NORMAL) FT_LOAD_TARGET_LIGHT = FT_LOAD_TARGET_(FT_RENDER_MODE_LIGHT) FT_LOAD_TARGET_MONO = FT_LOAD_TARGET_(FT_RENDER_MODE_MONO) FT_LOAD_TARGET_LCD = FT_LOAD_TARGET_(FT_RENDER_MODE_LCD) FT_LOAD_TARGET_LCD_V = FT_LOAD_TARGET_(FT_RENDER_MODE_LCD_V) -(FT_PIXEL_MODE_NONE, - FT_PIXEL_MODE_MONO, - FT_PIXEL_MODE_GRAY, - FT_PIXEL_MODE_GRAY2, - FT_PIXEL_MODE_GRAY4, - FT_PIXEL_MODE_LCD, - FT_PIXEL_MODE_LCD_V) = range(7) - -def f16p16_to_float(value): +def f16p16_to_float(value: float) -> float: return float(value) / (1 << 16) -def float_to_f16p16(value): +def float_to_f16p16(value: float) -> float: return int(value * (1 << 16)) -def f26p6_to_float(value): +def f26p6_to_float(value: float) -> float: return float(value) / (1 << 6) -def float_to_f26p6(value): +def float_to_f26p6(value: float) -> float: return int(value * (1 << 6)) class FreeTypeError(FontException): - def __init__(self, message, errcode): + def __init__(self, message: str | None, errcode: int) -> None: self.message = message self.errcode = errcode - def __str__(self): - return '%s: %s (%s)'%(self.__class__.__name__, self.message, - self._ft_errors.get(self.errcode, 'unknown error')) + def __str__(self) -> str: + return "{}: {} ({})".format(self.__class__.__name__, self.message, + self._ft_errors.get(self.errcode, "unknown error")) @classmethod - def check_and_raise_on_error(cls, errcode): + def check_and_raise_on_error(cls: type[FreeTypeError], errcode: int) -> NoReturn: if errcode != 0: raise cls(None, errcode) _ft_errors = { - 0x00: "no error" , - 0x01: "cannot open resource" , - 0x02: "unknown file format" , - 0x03: "broken file" , - 0x04: "invalid FreeType version" , - 0x05: "module version is too low" , - 0x06: "invalid argument" , - 0x07: "unimplemented feature" , - 0x08: "broken table" , - 0x09: "broken offset within table" , - 0x10: "invalid glyph index" , - 0x11: "invalid character code" , - 0x12: "unsupported glyph image format" , - 0x13: "cannot render this glyph format" , - 0x14: "invalid outline" , - 0x15: "invalid composite glyph" , - 0x16: "too many hints" , - 0x17: "invalid pixel size" , - 0x20: "invalid object handle" , - 0x21: "invalid library handle" , - 0x22: "invalid module handle" , - 0x23: "invalid face handle" , - 0x24: "invalid size handle" , - 0x25: "invalid glyph slot handle" , - 0x26: "invalid charmap handle" , - 0x27: "invalid cache manager handle" , - 0x28: "invalid stream handle" , - 0x30: "too many modules" , - 0x31: "too many extensions" , - 0x40: "out of memory" , - 0x41: "unlisted object" , - 0x51: "cannot open stream" , - 0x52: "invalid stream seek" , - 0x53: "invalid stream skip" , - 0x54: "invalid stream read" , - 0x55: "invalid stream operation" , - 0x56: "invalid frame operation" , - 0x57: "nested frame access" , - 0x58: "invalid frame read" , - 0x60: "raster uninitialized" , - 0x61: "raster corrupted" , - 0x62: "raster overflow" , - 0x63: "negative height while rastering" , - 0x70: "too many registered caches" , - 0x80: "invalid opcode" , - 0x81: "too few arguments" , - 0x82: "stack overflow" , - 0x83: "code overflow" , - 0x84: "bad argument" , - 0x85: "division by zero" , - 0x86: "invalid reference" , - 0x87: "found debug opcode" , - 0x88: "found ENDF opcode in execution stream" , - 0x89: "nested DEFS" , - 0x8A: "invalid code range" , - 0x8B: "execution context too long" , - 0x8C: "too many function definitions" , - 0x8D: "too many instruction definitions" , - 0x8E: "SFNT font table missing" , - 0x8F: "horizontal header (hhea, table missing" , - 0x90: "locations (loca, table missing" , - 0x91: "name table missing" , - 0x92: "character map (cmap, table missing" , - 0x93: "horizontal metrics (hmtx, table missing" , - 0x94: "PostScript (post, table missing" , - 0x95: "invalid horizontal metrics" , - 0x96: "invalid character map (cmap, format" , - 0x97: "invalid ppem value" , - 0x98: "invalid vertical metrics" , - 0x99: "could not find context" , - 0x9A: "invalid PostScript (post, table format" , - 0x9B: "invalid PostScript (post, table" , - 0xA0: "opcode syntax error" , - 0xA1: "argument stack underflow" , - 0xA2: "ignore" , - 0xB0: "`STARTFONT' field missing" , - 0xB1: "`FONT' field missing" , - 0xB2: "`SIZE' field missing" , - 0xB3: "`CHARS' field missing" , - 0xB4: "`STARTCHAR' field missing" , - 0xB5: "`ENCODING' field missing" , - 0xB6: "`BBX' field missing" , - 0xB7: "`BBX' too big" , + 0x00: "no error", + 0x01: "cannot open resource", + 0x02: "unknown file format", + 0x03: "broken file", + 0x04: "invalid FreeType version", + 0x05: "module version is too low", + 0x06: "invalid argument", + 0x07: "unimplemented feature", + 0x08: "broken table", + 0x09: "broken offset within table", + 0x10: "invalid glyph index", + 0x11: "invalid character code", + 0x12: "unsupported glyph image format", + 0x13: "cannot render this glyph format", + 0x14: "invalid outline", + 0x15: "invalid composite glyph", + 0x16: "too many hints", + 0x17: "invalid pixel size", + 0x20: "invalid object handle", + 0x21: "invalid library handle", + 0x22: "invalid module handle", + 0x23: "invalid face handle", + 0x24: "invalid size handle", + 0x25: "invalid glyph slot handle", + 0x26: "invalid charmap handle", + 0x27: "invalid cache manager handle", + 0x28: "invalid stream handle", + 0x30: "too many modules", + 0x31: "too many extensions", + 0x40: "out of memory", + 0x41: "unlisted object", + 0x51: "cannot open stream", + 0x52: "invalid stream seek", + 0x53: "invalid stream skip", + 0x54: "invalid stream read", + 0x55: "invalid stream operation", + 0x56: "invalid frame operation", + 0x57: "nested frame access", + 0x58: "invalid frame read", + 0x60: "raster uninitialized", + 0x61: "raster corrupted", + 0x62: "raster overflow", + 0x63: "negative height while rastering", + 0x70: "too many registered caches", + 0x80: "invalid opcode", + 0x81: "too few arguments", + 0x82: "stack overflow", + 0x83: "code overflow", + 0x84: "bad argument", + 0x85: "division by zero", + 0x86: "invalid reference", + 0x87: "found debug opcode", + 0x88: "found ENDF opcode in execution stream", + 0x89: "nested DEFS", + 0x8A: "invalid code range", + 0x8B: "execution context too long", + 0x8C: "too many function definitions", + 0x8D: "too many instruction definitions", + 0x8E: "SFNT font table missing", + 0x8F: "horizontal header (hhea, table missing", + 0x90: "locations (loca, table missing", + 0x91: "name table missing", + 0x92: "character map (cmap, table missing", + 0x93: "horizontal metrics (hmtx, table missing", + 0x94: "PostScript (post, table missing", + 0x95: "invalid horizontal metrics", + 0x96: "invalid character map (cmap, format", + 0x97: "invalid ppem value", + 0x98: "invalid vertical metrics", + 0x99: "could not find context", + 0x9A: "invalid PostScript (post, table format", + 0x9B: "invalid PostScript (post, table", + 0xA0: "opcode syntax error", + 0xA1: "argument stack underflow", + 0xA2: "ignore", + 0xB0: "`STARTFONT' field missing", + 0xB1: "`FONT' field missing", + 0xB2: "`SIZE' field missing", + 0xB3: "`CHARS' field missing", + 0xB4: "`STARTCHAR' field missing", + 0xB5: "`ENCODING' field missing", + 0xB6: "`BBX' field missing", + 0xB7: "`BBX' too big", } -def _get_function_with_error_handling(name, argtypes, rtype): +def _get_function_with_error_handling(name: str, argtypes: Iterable[Any], rtype: Any) -> Callable: func = _get_function(name, argtypes, rtype) - def _error_handling(*args, **kwargs): + + def _error_handling(*args, **kwargs) -> None: # noqa: ANN002, ANN003 err = func(*args, **kwargs) FreeTypeError.check_and_raise_on_error(err) + return _error_handling FT_LOAD_RENDER = 0x4 -FT_Init_FreeType = _get_function_with_error_handling('FT_Init_FreeType', - [POINTER(FT_Library)], FT_Error) -FT_Done_FreeType = _get_function_with_error_handling('FT_Done_FreeType', - [FT_Library], FT_Error) - -FT_New_Face = _get_function_with_error_handling('FT_New_Face', - [FT_Library, c_char_p, FT_Long, POINTER(FT_Face)], FT_Error) -FT_Done_Face = _get_function_with_error_handling('FT_Done_Face', - [FT_Face], FT_Error) -FT_Reference_Face = _get_function_with_error_handling('FT_Reference_Face', - [FT_Face], FT_Error) -FT_New_Memory_Face = _get_function_with_error_handling('FT_New_Memory_Face', - [FT_Library, POINTER(FT_Byte), FT_Long, FT_Long, POINTER(FT_Face)], FT_Error) - -FT_Set_Char_Size = _get_function_with_error_handling('FT_Set_Char_Size', - [FT_Face, FT_F26Dot6, FT_F26Dot6, FT_UInt, FT_UInt], FT_Error) -FT_Set_Pixel_Sizes = _get_function_with_error_handling('FT_Set_Pixel_Sizes', - [FT_Face, FT_UInt, FT_UInt], FT_Error) -FT_Load_Glyph = _get_function_with_error_handling('FT_Load_Glyph', - [FT_Face, FT_UInt, FT_Int32], FT_Error) -FT_Get_Char_Index = _get_function_with_error_handling('FT_Get_Char_Index', - [FT_Face, FT_ULong], FT_Error) -FT_Load_Char = _get_function_with_error_handling('FT_Load_Char', - [FT_Face, FT_ULong, FT_Int32], FT_Error) -FT_Get_Kerning = _get_function_with_error_handling('FT_Get_Kerning', - [FT_Face, FT_UInt, FT_UInt, FT_UInt, POINTER(FT_Vector)], FT_Error) +FT_Init_FreeType = _get_function_with_error_handling("FT_Init_FreeType", + [POINTER(FT_Library)], FT_Error) +FT_Done_FreeType = _get_function_with_error_handling("FT_Done_FreeType", + [FT_Library], FT_Error) + +FT_New_Face = _get_function_with_error_handling("FT_New_Face", + [FT_Library, c_char_p, FT_Long, POINTER(FT_Face)], FT_Error) +FT_Done_Face = _get_function_with_error_handling("FT_Done_Face", + [FT_Face], FT_Error) +FT_Reference_Face = _get_function_with_error_handling("FT_Reference_Face", + [FT_Face], FT_Error) +FT_New_Memory_Face = _get_function_with_error_handling("FT_New_Memory_Face", + [FT_Library, POINTER(FT_Byte), FT_Long, FT_Long, + POINTER(FT_Face)], FT_Error) + +FT_Set_Char_Size = _get_function_with_error_handling("FT_Set_Char_Size", + [FT_Face, FT_F26Dot6, FT_F26Dot6, FT_UInt, FT_UInt], FT_Error) +FT_Set_Pixel_Sizes = _get_function_with_error_handling("FT_Set_Pixel_Sizes", + [FT_Face, FT_UInt, FT_UInt], FT_Error) +FT_Load_Glyph = _get_function_with_error_handling("FT_Load_Glyph", + [FT_Face, FT_UInt, FT_Int32], FT_Error) +FT_Get_Char_Index = _get_function_with_error_handling("FT_Get_Char_Index", + [FT_Face, FT_ULong], FT_Error) +FT_Load_Char = _get_function_with_error_handling("FT_Load_Char", + [FT_Face, FT_ULong, FT_Int32], FT_Error) +FT_Get_Kerning = _get_function_with_error_handling("FT_Get_Kerning", + [FT_Face, FT_UInt, FT_UInt, FT_UInt, POINTER(FT_Vector)], FT_Error) + # SFNT interface class FT_SfntName(Structure): _fields_ = [ - ('platform_id', FT_UShort), - ('encoding_id', FT_UShort), - ('language_id', FT_UShort), - ('name_id', FT_UShort), - ('string', POINTER(FT_Byte)), - ('string_len', FT_UInt) + ("platform_id", FT_UShort), + ("encoding_id", FT_UShort), + ("language_id", FT_UShort), + ("name_id", FT_UShort), + ("string", POINTER(FT_Byte)), + ("string_len", FT_UInt), ] -FT_Get_Sfnt_Name_Count = _get_function('FT_Get_Sfnt_Name_Count', - [FT_Face], FT_UInt) -FT_Get_Sfnt_Name = _get_function_with_error_handling('FT_Get_Sfnt_Name', - [FT_Face, FT_UInt, POINTER(FT_SfntName)], FT_Error) + +FT_Get_Sfnt_Name_Count = _get_function("FT_Get_Sfnt_Name_Count", + [FT_Face], FT_UInt) +FT_Get_Sfnt_Name = _get_function_with_error_handling("FT_Get_Sfnt_Name", + [FT_Face, FT_UInt, POINTER(FT_SfntName)], FT_Error) TT_PLATFORM_MICROSOFT = 3 TT_MS_ID_UNICODE_CS = 1 -TT_NAME_ID_COPYRIGHT = 0 -TT_NAME_ID_FONT_FAMILY = 1 -TT_NAME_ID_FONT_SUBFAMILY = 2 -TT_NAME_ID_UNIQUE_ID = 3 -TT_NAME_ID_FULL_NAME = 4 -TT_NAME_ID_VERSION_STRING = 5 -TT_NAME_ID_PS_NAME = 6 -TT_NAME_ID_TRADEMARK = 7 -TT_NAME_ID_MANUFACTURER = 8 -TT_NAME_ID_DESIGNER = 9 -TT_NAME_ID_DESCRIPTION = 10 -TT_NAME_ID_VENDOR_URL = 11 -TT_NAME_ID_DESIGNER_URL = 12 -TT_NAME_ID_LICENSE = 13 -TT_NAME_ID_LICENSE_URL = 14 -TT_NAME_ID_PREFERRED_FAMILY = 16 -TT_NAME_ID_PREFERRED_SUBFAMILY= 17 -TT_NAME_ID_MAC_FULL_NAME = 18 -TT_NAME_ID_CID_FINDFONT_NAME = 20 +TT_NAME_ID_COPYRIGHT = 0 +TT_NAME_ID_FONT_FAMILY = 1 +TT_NAME_ID_FONT_SUBFAMILY = 2 +TT_NAME_ID_UNIQUE_ID = 3 +TT_NAME_ID_FULL_NAME = 4 +TT_NAME_ID_VERSION_STRING = 5 +TT_NAME_ID_PS_NAME = 6 +TT_NAME_ID_TRADEMARK = 7 +TT_NAME_ID_MANUFACTURER = 8 +TT_NAME_ID_DESIGNER = 9 +TT_NAME_ID_DESCRIPTION = 10 +TT_NAME_ID_VENDOR_URL = 11 +TT_NAME_ID_DESIGNER_URL = 12 +TT_NAME_ID_LICENSE = 13 +TT_NAME_ID_LICENSE_URL = 14 +TT_NAME_ID_PREFERRED_FAMILY = 16 +TT_NAME_ID_PREFERRED_SUBFAMILY = 17 +TT_NAME_ID_MAC_FULL_NAME = 18 +TT_NAME_ID_CID_FINDFONT_NAME = 20 _library = None -def ft_get_library(): - global _library + + +def ft_get_library() -> FT_Library: + global _library # noqa: PLW0603 if not _library: _library = FT_Library() error = FT_Init_FreeType(byref(_library)) if error: - raise FontException( - 'an error occurred during library initialization', error) + msg = "an error occurred during library initialization" + raise FontException(msg, error) return _library diff --git a/pyglet/font/quartz.py b/pyglet/font/quartz.py index fdc5dcc83..ade7f01bb 100644 --- a/pyglet/font/quartz.py +++ b/pyglet/font/quartz.py @@ -1,13 +1,14 @@ -# TODO Tiger and later: need to set kWindowApplicationScaledAttribute for DPI independence? +# Tiger and later: need to set kWindowApplicationScaledAttribute for DPI independence? +from __future__ import annotations import math import warnings -from ctypes import c_void_p, c_int32, byref, c_byte +from ctypes import byref, c_byte, c_int32, c_void_p +from typing import BinaryIO -from pyglet.font import base import pyglet.image - -from pyglet.libs.darwin import cocoapy, kCTFontURLAttribute, CGFloat +from pyglet.font import base +from pyglet.libs.darwin import CGFloat, cocoapy, kCTFontURLAttribute cf = cocoapy.cf ct = cocoapy.ct @@ -15,11 +16,13 @@ class QuartzGlyphRenderer(base.GlyphRenderer): - def __init__(self, font): + font: QuartzFont + + def __init__(self, font: QuartzFont) -> None: super().__init__(font) self.font = font - def render(self, text): + def render(self, text: str) -> base.Glyph: # Using CTLineDraw seems to be the only way to make sure that the text # is drawn with the specified font when that font is a graphics font loaded from # memory. For whatever reason, [NSAttributedString drawAtPoint:] ignores @@ -29,7 +32,8 @@ def render(self, text): ctFont = self.font.ctFont # Create an attributed string using text and font. - attributes = c_void_p(cf.CFDictionaryCreateMutable(None, 1, cf.kCFTypeDictionaryKeyCallBacks, cf.kCFTypeDictionaryValueCallBacks)) + attributes = c_void_p( + cf.CFDictionaryCreateMutable(None, 1, cf.kCFTypeDictionaryKeyCallBacks, cf.kCFTypeDictionaryValueCallBacks)) cf.CFDictionaryAddValue(attributes, cocoapy.kCTFontAttributeName, ctFont) string = c_void_p(cf.CFAttributedStringCreate(None, cocoapy.CFSTR(text), attributes)) @@ -40,7 +44,7 @@ def render(self, text): # Determine the glyphs involved for the text (if any) count = len(text) - chars = (cocoapy.UniChar * count)(*list(map(ord,str(text)))) + chars = (cocoapy.UniChar * count)(*list(map(ord, str(text)))) glyphs = (cocoapy.CGGlyph * count)() ct.CTFontGetGlyphsForCharacters(ctFont, chars, glyphs, count) @@ -74,16 +78,16 @@ def render(self, text): # Create bitmap context. bitsPerComponent = 8 - bytesPerRow = 4*width + bytesPerRow = 4 * width colorSpace = c_void_p(quartz.CGColorSpaceCreateDeviceRGB()) bitmap = c_void_p(quartz.CGBitmapContextCreate( - None, - width, - height, - bitsPerComponent, - bytesPerRow, - colorSpace, - cocoapy.kCGImageAlphaPremultipliedLast)) + None, + width, + height, + bitsPerComponent, + bytesPerRow, + colorSpace, + cocoapy.kCGImageAlphaPremultipliedLast)) # Draw text to bitmap context. quartz.CGContextSetShouldAntialias(bitmap, True) @@ -107,7 +111,7 @@ def render(self, text): cf.CFRelease(bitmap) cf.CFRelease(colorSpace) - glyph_image = pyglet.image.ImageData(width, height, 'RGBA', buffer, bytesPerRow) + glyph_image = pyglet.image.ImageData(width, height, "RGBA", buffer, bytesPerRow) glyph = self.font.create_glyph(glyph_image) glyph.set_bearings(baseline, lsb, advance) @@ -118,10 +122,10 @@ def render(self, text): class QuartzFont(base.Font): - glyph_renderer_class = QuartzGlyphRenderer - _loaded_CGFont_table = {} + glyph_renderer_class: type[base.GlyphRenderer] = QuartzGlyphRenderer + _loaded_CGFont_table: dict[str, dict[int, c_void_p]] = {} - def _lookup_font_with_family_and_traits(self, family, traits): + def _lookup_font_with_family_and_traits(self, family: str, traits: int) -> c_void_p | None: # This method searches the _loaded_CGFont_table to find a loaded # font of the given family with the desired traits. If it can't find # anything with the exact traits, it tries to fall back to whatever @@ -148,9 +152,10 @@ def _lookup_font_with_family_and_traits(self, family, traits): # Otherwise return whatever we have. return list(fonts.values())[0] - def _create_font_descriptor(self, family_name, traits): + def _create_font_descriptor(self, family_name: str, traits: int) -> c_void_p: # Create an attribute dictionary. - attributes = c_void_p(cf.CFDictionaryCreateMutable(None, 0, cf.kCFTypeDictionaryKeyCallBacks, cf.kCFTypeDictionaryValueCallBacks)) + attributes = c_void_p( + cf.CFDictionaryCreateMutable(None, 0, cf.kCFTypeDictionaryKeyCallBacks, cf.kCFTypeDictionaryValueCallBacks)) # Add family name to attributes. cfname = cocoapy.CFSTR(family_name) cf.CFDictionaryAddValue(attributes, cocoapy.kCTFontFamilyNameAttribute, cfname) @@ -160,7 +165,8 @@ def _create_font_descriptor(self, family_name, traits): symTraits = c_void_p(cf.CFNumberCreate(None, cocoapy.kCFNumberSInt32Type, byref(itraits))) if symTraits: # Construct a dictionary to hold the traits values. - traitsDict = c_void_p(cf.CFDictionaryCreateMutable(None, 0, cf.kCFTypeDictionaryKeyCallBacks, cf.kCFTypeDictionaryValueCallBacks)) + traitsDict = c_void_p(cf.CFDictionaryCreateMutable(None, 0, cf.kCFTypeDictionaryKeyCallBacks, + cf.kCFTypeDictionaryValueCallBacks)) if traitsDict: # Add CFNumber traits to traits dictionary. cf.CFDictionaryAddValue(traitsDict, cocoapy.kCTFontSymbolicTrait, symTraits) @@ -173,16 +179,15 @@ def _create_font_descriptor(self, family_name, traits): cf.CFRelease(attributes) return descriptor - def __init__(self, name, size, bold=False, italic=False, stretch=False, dpi=None): - # assert type(bold) is bool, "Only a boolean value is supported for bold in the current font renderer." - # assert type(italic) is bool, "Only a boolean value is supported for bold in the current font renderer." + def __init__(self, name: str, size: float, bold: bool = False, italic: bool = False, stretch: bool = False, + dpi: float | None = None) -> None: if stretch: - warnings.warn("The current font render does not support stretching.") + warnings.warn("The current font render does not support stretching.") # noqa: B028 super().__init__() - name = name or 'Helvetica' + name = name or "Helvetica" # I don't know what is the right thing to do here. dpi = dpi or 96 @@ -217,7 +222,7 @@ def __init__(self, name, size, bold=False, italic=False, stretch=False, dpi=None self.descent = -int(math.ceil(ct.CTFontGetDescent(self.ctFont))) @property - def filename(self): + def filename(self) -> str: descriptor = self._create_font_descriptor(self.name, self.traits) ref = c_void_p(ct.CTFontDescriptorCopyAttribute(descriptor, kCTFontURLAttribute)) if ref: @@ -227,19 +232,20 @@ def filename(self): return filepath cf.CFRelease(descriptor) - return 'Unknown' + return "Unknown" @property - def name(self): + def name(self) -> str: return self._family_name - def __del__(self): + def __del__(self) -> None: cf.CFRelease(self.ctFont) @classmethod - def have_font(cls, name): + def have_font(cls: type[QuartzFont], name: str) -> bool: name = str(name) - if name in cls._loaded_CGFont_table: return True + if name in cls._loaded_CGFont_table: + return True # Try to create the font to see if it exists. # TODO: Find a better way to check. cfstring = cocoapy.CFSTR(name) @@ -251,7 +257,7 @@ def have_font(cls, name): return False @classmethod - def add_font_data(cls, data): + def add_font_data(cls: type[QuartzFont], data: BinaryIO) -> None: # Create a cgFont with the data. There doesn't seem to be a way to # register a font loaded from memory such that the operating system will # find it later. So instead we just store the cgFont in a table where diff --git a/pyglet/font/ttf.py b/pyglet/font/ttf.py index c50373521..eb87638cf 100644 --- a/pyglet/font/ttf.py +++ b/pyglet/font/ttf.py @@ -8,11 +8,13 @@ * http://developer.apple.com/fonts/TTRefMan/RM06 * http://www.microsoft.com/typography/otspec """ +from __future__ import annotations -import os +import codecs import mmap +import os import struct -import codecs +from typing import Any class TruetypeInfo: @@ -28,63 +30,64 @@ class TruetypeInfo: """ _name_id_lookup = { - 'copyright': 0, - 'family': 1, - 'subfamily': 2, - 'identifier': 3, - 'name': 4, - 'version': 5, - 'postscript': 6, - 'trademark': 7, - 'manufacturer': 8, - 'designer': 9, - 'description': 10, - 'vendor-url': 11, - 'designer-url': 12, - 'license': 13, - 'license-url': 14, - 'preferred-family': 16, - 'preferred-subfamily': 17, - 'compatible-name': 18, - 'sample': 19, + "copyright": 0, + "family": 1, + "subfamily": 2, + "identifier": 3, + "name": 4, + "version": 5, + "postscript": 6, + "trademark": 7, + "manufacturer": 8, + "designer": 9, + "description": 10, + "vendor-url": 11, + "designer-url": 12, + "license": 13, + "license-url": 14, + "preferred-family": 16, + "preferred-subfamily": 17, + "compatible-name": 18, + "sample": 19, } _platform_id_lookup = { - 'unicode': 0, - 'macintosh': 1, - 'iso': 2, - 'microsoft': 3, - 'custom': 4 + "unicode": 0, + "macintosh": 1, + "iso": 2, + "microsoft": 3, + "custom": 4, } _microsoft_encoding_lookup = { - 1: 'utf_16_be', - 2: 'shift_jis', - 4: 'big5', - 6: 'johab', - 10: 'utf_16_be' + 1: "utf_16_be", + 2: "shift_jis", + 4: "big5", + 6: "johab", + 10: "utf_16_be", } _macintosh_encoding_lookup = { - 0: 'mac_roman' + 0: "mac_roman", } - def __init__(self, filename): + _character_advances: dict[str, float] | None + + def __init__(self, filename: str) -> None: """Read the given TrueType file. - :Parameters: - `filename` + Args: + filename: The name of any Windows, OS2 or Macintosh Truetype file. The object must be closed (see `close`) after use. - An exception will be raised if the file does not exist or cannot - be read. + An exception will be raised if the file does not exist or cannot be read. """ assert filename, "must provide a font file name" length = os.stat(filename).st_size self._fileno = os.open(filename, os.O_RDONLY) - if hasattr(mmap, 'MAP_SHARED'): + if hasattr(mmap, "MAP_SHARED"): self._data = mmap.mmap(self._fileno, length, mmap.MAP_SHARED, mmap.PROT_READ) else: self._data = mmap.mmap(self._fileno, length, None, mmap.ACCESS_READ) @@ -105,40 +108,40 @@ def __init__(self, filename): self._glyph_map = None self._font_selection_flags = None - self.header = _read_head_table(self._data, self._tables['head'].offset) - self.horizontal_header = _read_horizontal_header(self._data, self._tables['hhea'].offset) + self.header = _read_head_table(self._data, self._tables["head"].offset) + self.horizontal_header = _read_horizontal_header(self._data, self._tables["hhea"].offset) - def get_font_selection_flags(self): + def get_font_selection_flags(self) -> int: """Return the font selection flags, as defined in OS/2 table""" if not self._font_selection_flags: - OS2_table = _read_OS2_table(self._data, self._tables['OS/2'].offset) + OS2_table = _read_OS2_table(self._data, self._tables["OS/2"].offset) self._font_selection_flags = OS2_table.fs_selection return self._font_selection_flags - def is_bold(self): + def is_bold(self) -> bool: """Returns True iff the font describes itself as bold.""" return bool(self.get_font_selection_flags() & 0x20) - def is_italic(self): + def is_italic(self) -> bool: """Returns True iff the font describes itself as italic.""" return bool(self.get_font_selection_flags() & 0x1) - def get_names(self): + def get_names(self) -> dict[tuple[int, int], tuple[int, int, str]]: """Returns a dictionary of names defined in the file. The key of each item is a tuple of ``platform_id``, ``name_id``, where each ID is the number as described in the Truetype format. - The value of each item is a tuple of + The value of each item is a tuple of ``encoding_id``, ``language_id``, ``value``, where ``value`` is an encoded string. """ if self._names: return self._names - naming_table = _read_naming_table(self._data, self._tables['name'].offset) + naming_table = _read_naming_table(self._data, self._tables["name"].offset) name_records = _read_name_record.array( - self._data, self._tables['name'].offset + naming_table.size, naming_table.count) - storage = naming_table.string_offset + self._tables['name'].offset + self._data, self._tables["name"].offset + naming_table.size, naming_table.count) + storage = naming_table.string_offset + self._tables["name"].offset self._names = {} for record in name_records: value = self._data[record.offset + storage: record.offset + storage + record.length] @@ -149,13 +152,14 @@ def get_names(self): self._names[key].append(value) return self._names - def get_name(self, name, platform=None, languages=None): + def get_name(self, name: int | str, platform: int | str | None = None, + languages: int | str | None = None) -> str | None: """Returns the value of the given name in this font. - :Parameters: + Args: `name` Either an integer, representing the name_id desired (see - font format); or a string describing it, see below for + font format); or a string describing it, see below for valid names. `platform` Platform for the requested name. Can be the integer ID, @@ -202,16 +206,16 @@ def get_name(self, name, platform=None, languages=None): """ names = self.get_names() - if type(name) == str: + if isinstance(name, str): name = self._name_id_lookup[name] if not platform: - for platform in ('microsoft', 'macintosh'): + for platform in ("microsoft", "macintosh"): value = self.get_name(name, platform, languages) if value: return value - if type(platform) == str: + if isinstance(platform, str): platform = self._platform_id_lookup[platform] - if not (platform, name) in names: + if (platform, name) not in names: return None if platform == 3: # setup for microsoft @@ -231,16 +235,16 @@ def get_name(self, name, platform=None, languages=None): return decoder(record[2])[0] return None - def get_horizontal_metrics(self): + def get_horizontal_metrics(self) -> list: """Return all horizontal metric entries in table format.""" if not self._horizontal_metrics: ar = _read_long_hor_metric.array(self._data, - self._tables['hmtx'].offset, + self._tables["hmtx"].offset, self.horizontal_header.number_of_h_metrics) self._horizontal_metrics = ar return self._horizontal_metrics - def get_character_advances(self): + def get_character_advances(self) -> dict[str, float] | None: """Return a dictionary of character->advance. They key of the dictionary is a unit-length unicode string, @@ -253,20 +257,16 @@ def get_character_advances(self): gmap = self.get_glyph_map() self._character_advances = {} for i in range(len(ga)): - if i in gmap and not gmap[i] in self._character_advances: + if i in gmap and gmap[i] not in self._character_advances: self._character_advances[gmap[i]] = ga[i] return self._character_advances - def get_glyph_advances(self): - """Return a dictionary of glyph->advance. - - They key of the dictionary is the glyph index and the value is a float - giving the horizontal advance in em. - """ + def get_glyph_advances(self) -> list[float]: + """Return a list of advances.""" hm = self.get_horizontal_metrics() return [float(m.advance_width) / self.header.units_per_em for m in hm] - def get_character_kernings(self): + def get_character_kernings(self) -> dict[tuple[str, str], int]: """Return a dictionary of (left,right)->kerning The key of the dictionary is a tuple of ``(left, right)`` @@ -286,7 +286,7 @@ def get_character_kernings(self): self._character_kernings[(lchar, rchar)] = value return self._character_kernings - def get_glyph_kernings(self): + def get_glyph_kernings(self) -> dict[tuple[str, str], int]: """Return a dictionary of (left,right)->kerning The key of the dictionary is a tuple of ``(left, right)`` @@ -296,8 +296,8 @@ def get_glyph_kernings(self): if self._glyph_kernings: return self._glyph_kernings header = \ - _read_kern_header_table(self._data, self._tables['kern'].offset) - offset = self._tables['kern'].offset + header.size + _read_kern_header_table(self._data, self._tables["kern"].offset) + offset = self._tables["kern"].offset + header.size kernings = {} for i in range(header.n_tables): header = _read_kern_subtable_header(self._data, offset) @@ -322,7 +322,7 @@ def _add_kernings_format0(self, kernings, offset): kernings[(pair.left, pair.right)] = pair.value \ / float(self.header.units_per_em) - def get_glyph_map(self): + def get_glyph_map(self) -> dict[int, str]: """Calculate and return a reverse character map. Returns a dictionary where the key is a glyph index and the @@ -333,27 +333,26 @@ def get_glyph_map(self): cmap = self.get_character_map() self._glyph_map = {} for ch, glyph in cmap.items(): - if not glyph in self._glyph_map: + if glyph not in self._glyph_map: self._glyph_map[glyph] = ch return self._glyph_map - def get_character_map(self): + def get_character_map(self) -> dict[str, int]: """Return the character map. - Returns a dictionary where the key is a unit-length unicode - string and the value is a glyph index. Currently only - format 4 character maps are read. + Returns a dictionary where the key is a unit-length unicode string and the value is a glyph index. Currently + only format 4 character maps are read. """ if self._character_map: return self._character_map - cmap = _read_cmap_header(self._data, self._tables['cmap'].offset) + cmap = _read_cmap_header(self._data, self._tables["cmap"].offset) records = _read_cmap_encoding_record.array(self._data, - self._tables['cmap'].offset + cmap.size, cmap.num_tables) + self._tables["cmap"].offset + cmap.size, cmap.num_tables) self._character_map = {} for record in records: if record.platform_id == 3 and record.encoding_id == 1: # Look at Windows Unicode charmaps only - offset = self._tables['cmap'].offset + record.offset + offset = self._tables["cmap"].offset + record.offset format_header = _read_cmap_format_header(self._data, offset) if format_header.format == 4: self._character_map = \ @@ -361,32 +360,32 @@ def get_character_map(self): break return self._character_map - def _get_character_map_format4(self, offset): + def _get_character_map_format4(self, offset: int) -> dict[str, int]: # This is absolutely, without question, the *worst* file # format ever. Whoever the fuckwit is that thought this up is - # a fuckwit. + # a fuckwit. header = _read_cmap_format4Header(self._data, offset) seg_count = header.seg_count_x2 // 2 - array_size = struct.calcsize(f'>{seg_count}H') - end_count = self._read_array(f'>{seg_count}H', + array_size = struct.calcsize(f">{seg_count}H") + end_count = self._read_array(f">{seg_count}H", offset + header.size) - start_count = self._read_array(f'>{seg_count}H', + start_count = self._read_array(f">{seg_count}H", offset + header.size + array_size + 2) - id_delta = self._read_array(f'>{seg_count}H', + id_delta = self._read_array(f">{seg_count}H", offset + header.size + array_size + 2 + array_size) id_range_offset_address = \ offset + header.size + array_size + 2 + array_size + array_size - id_range_offset = self._read_array(f'>{seg_count}H', + id_range_offset = self._read_array(f">{seg_count}H", id_range_offset_address) character_map = {} - for i in range(0, seg_count): + for i in range(seg_count): if id_range_offset[i] != 0: if id_range_offset[i] == 65535: continue # Hack around a dodgy font (babelfish.ttf) for c in range(start_count[i], end_count[i] + 1): addr = id_range_offset[i] + 2 * (c - start_count[i]) + \ id_range_offset_address + 2 * i - g = struct.unpack('>H', self._data[addr:addr + 2])[0] + g = struct.unpack(">H", self._data[addr:addr + 2])[0] if g != 0: character_map[chr(c)] = (g + id_delta[i]) % 65536 else: @@ -396,11 +395,11 @@ def _get_character_map_format4(self, offset): character_map[chr(c)] = g return character_map - def _read_array(self, format, offset): - size = struct.calcsize(format) - return struct.unpack(format, self._data[offset:offset + size]) + def _read_array(self, fmt: str, offset: int) -> tuple[Any, ...]: + size = struct.calcsize(fmt) + return struct.unpack(fmt, self._data[offset:offset + size]) - def close(self): + def close(self) -> None: """Close the font file. This is a good idea, since the entire file is memory mapped in @@ -412,38 +411,38 @@ def close(self): os.close(self._fileno) self._closed = True - def __del__(self): + def __del__(self) -> None: if not self._closed: self.close() -def _read_table(*entries): +def _read_table(*entries: str): """ Generic table constructor used for table formats listed at end of file.""" - fmt = '>' + fmt = ">" names = [] for entry in entries: - name, entry_type = entry.split(':') + name, entry_type = entry.split(":") names.append(name) fmt += entry_type class TableClass: size = struct.calcsize(fmt) - def __init__(self, data, offset): + def __init__(self, data: mmap.mmap, offset: int) -> None: items = struct.unpack(fmt, data[offset:offset + self.size]) self.pairs = list(zip(names, items)) for pname, pvalue in self.pairs: if isinstance(pvalue, bytes): - pvalue = pvalue.decode('utf-8') + pvalue = pvalue.decode("utf-8") setattr(self, pname, pvalue) - def __repr__(self): - return '{'+', '.join([f'{pname} = {pvalue}' for pname, pvalue in self.pairs])+'}' + def __repr__(self) -> str: + return "{" + ", ".join([f"{pname} = {pvalue}" for pname, pvalue in self.pairs]) + "}" @staticmethod - def array(data, offset, count): + def array(data: mmap.mmap, offset: int, count: int) -> list[TableClass]: tables = [] for i in range(count): tables.append(TableClass(data, offset)) @@ -455,88 +454,88 @@ def array(data, offset, count): # Table formats (see references) -_read_offset_table = _read_table('scalertype:I', - 'num_tables:H', - 'search_range:H', - 'entry_selector:H', - 'range_shift:H') - -_read_table_directory_entry = _read_table('tag:4s', - 'check_sum:I', - 'offset:I', - 'length:I') - -_read_head_table = _read_table('version:i', - 'font_revision:i', - 'check_sum_adjustment:L', - 'magic_number:L', - 'flags:H', - 'units_per_em:H', - 'created:Q', - 'modified:Q', - 'x_min:h', - 'y_min:h', - 'x_max:h', - 'y_max:h', - 'mac_style:H', - 'lowest_rec_p_pEM:H', - 'font_direction_hint:h', - 'index_to_loc_format:h', - 'glyph_data_format:h') - -_read_OS2_table = _read_table('version:H', - 'x_avg_char_width:h', - 'us_weight_class:H', - 'us_width_class:H', - 'fs_type:H', - 'y_subscript_x_size:h', - 'y_subscript_y_size:h', - 'y_subscript_x_offset:h', - 'y_subscript_y_offset:h', - 'y_superscript_x_size:h', - 'y_superscript_y_size:h', - 'y_superscript_x_offset:h', - 'y_superscript_y_offset:h', - 'y_strikeout_size:h', - 'y_strikeout_position:h', - 's_family_class:h', - 'panose1:B', - 'panose2:B', - 'panose3:B', - 'panose4:B', - 'panose5:B', - 'panose6:B', - 'panose7:B', - 'panose8:B', - 'panose9:B', - 'panose10:B', - 'ul_unicode_range1:L', - 'ul_unicode_range2:L', - 'ul_unicode_range3:L', - 'ul_unicode_range4:L', - 'ach_vend_id:I', - 'fs_selection:H', - 'us_first_char_index:H', - 'us_last_char_index:H', - 's_typo_ascender:h', - 's_typo_descender:h', - 's_typo_line_gap:h', - 'us_win_ascent:H', - 'us_win_descent:H', - 'ul_code_page_range1:L', - 'ul_code_page_range2:L', - 'sx_height:h', - 's_cap_height:h', - 'us_default_char:H', - 'us_break_char:H', - 'us_max_context:H') - -_read_kern_header_table = _read_table('version_num:H', - 'n_tables:H') - -_read_kern_subtable_header = _read_table('version:H', - 'length:H', - 'coverage:H') +_read_offset_table = _read_table("scalertype:I", + "num_tables:H", + "search_range:H", + "entry_selector:H", + "range_shift:H") + +_read_table_directory_entry = _read_table("tag:4s", + "check_sum:I", + "offset:I", + "length:I") + +_read_head_table = _read_table("version:i", + "font_revision:i", + "check_sum_adjustment:L", + "magic_number:L", + "flags:H", + "units_per_em:H", + "created:Q", + "modified:Q", + "x_min:h", + "y_min:h", + "x_max:h", + "y_max:h", + "mac_style:H", + "lowest_rec_p_pEM:H", + "font_direction_hint:h", + "index_to_loc_format:h", + "glyph_data_format:h") + +_read_OS2_table = _read_table("version:H", + "x_avg_char_width:h", + "us_weight_class:H", + "us_width_class:H", + "fs_type:H", + "y_subscript_x_size:h", + "y_subscript_y_size:h", + "y_subscript_x_offset:h", + "y_subscript_y_offset:h", + "y_superscript_x_size:h", + "y_superscript_y_size:h", + "y_superscript_x_offset:h", + "y_superscript_y_offset:h", + "y_strikeout_size:h", + "y_strikeout_position:h", + "s_family_class:h", + "panose1:B", + "panose2:B", + "panose3:B", + "panose4:B", + "panose5:B", + "panose6:B", + "panose7:B", + "panose8:B", + "panose9:B", + "panose10:B", + "ul_unicode_range1:L", + "ul_unicode_range2:L", + "ul_unicode_range3:L", + "ul_unicode_range4:L", + "ach_vend_id:I", + "fs_selection:H", + "us_first_char_index:H", + "us_last_char_index:H", + "s_typo_ascender:h", + "s_typo_descender:h", + "s_typo_line_gap:h", + "us_win_ascent:H", + "us_win_descent:H", + "ul_code_page_range1:L", + "ul_code_page_range2:L", + "sx_height:h", + "s_cap_height:h", + "us_default_char:H", + "us_break_char:H", + "us_max_context:H") + +_read_kern_header_table = _read_table("version_num:H", + "n_tables:H") + +_read_kern_subtable_header = _read_table("version:H", + "length:H", + "coverage:H") _read_kern_subtable_header.horizontal_mask = 0x1 _read_kern_subtable_header.minimum_mask = 0x2 @@ -544,59 +543,59 @@ def array(data, offset, count): _read_kern_subtable_header.override_mask = 0x5 _read_kern_subtable_header.format_mask = 0xf0 -_read_kern_subtable_format0 = _read_table('n_pairs:H', - 'search_range:H', - 'entry_selector:H', - 'range_shift:H') -_read_kern_subtable_format0Pair = _read_table('left:H', - 'right:H', - 'value:h') - -_read_cmap_header = _read_table('version:H', - 'num_tables:H') - -_read_cmap_encoding_record = _read_table('platform_id:H', - 'encoding_id:H', - 'offset:L') - -_read_cmap_format_header = _read_table('format:H', - 'length:H') -_read_cmap_format4Header = _read_table('format:H', - 'length:H', - 'language:H', - 'seg_count_x2:H', - 'search_range:H', - 'entry_selector:H', - 'range_shift:H') - -_read_horizontal_header = _read_table('version:i', - 'Advance:h', - 'Descender:h', - 'LineGap:h', - 'advance_width_max:H', - 'min_left_side_bearing:h', - 'min_right_side_bearing:h', - 'x_max_extent:h', - 'caret_slope_rise:h', - 'caret_slope_run:h', - 'caret_offset:h', - 'reserved1:h', - 'reserved2:h', - 'reserved3:h', - 'reserved4:h', - 'metric_data_format:h', - 'number_of_h_metrics:H') - -_read_long_hor_metric = _read_table('advance_width:H', - 'lsb:h') - -_read_naming_table = _read_table('format:H', - 'count:H', - 'string_offset:H') - -_read_name_record = _read_table('platform_id:H', - 'encoding_id:H', - 'language_id:H', - 'name_id:H', - 'length:H', - 'offset:H') +_read_kern_subtable_format0 = _read_table("n_pairs:H", + "search_range:H", + "entry_selector:H", + "range_shift:H") +_read_kern_subtable_format0Pair = _read_table("left:H", + "right:H", + "value:h") + +_read_cmap_header = _read_table("version:H", + "num_tables:H") + +_read_cmap_encoding_record = _read_table("platform_id:H", + "encoding_id:H", + "offset:L") + +_read_cmap_format_header = _read_table("format:H", + "length:H") +_read_cmap_format4Header = _read_table("format:H", + "length:H", + "language:H", + "seg_count_x2:H", + "search_range:H", + "entry_selector:H", + "range_shift:H") + +_read_horizontal_header = _read_table("version:i", + "Advance:h", + "Descender:h", + "LineGap:h", + "advance_width_max:H", + "min_left_side_bearing:h", + "min_right_side_bearing:h", + "x_max_extent:h", + "caret_slope_rise:h", + "caret_slope_run:h", + "caret_offset:h", + "reserved1:h", + "reserved2:h", + "reserved3:h", + "reserved4:h", + "metric_data_format:h", + "number_of_h_metrics:H") + +_read_long_hor_metric = _read_table("advance_width:H", + "lsb:h") + +_read_naming_table = _read_table("format:H", + "count:H", + "string_offset:H") + +_read_name_record = _read_table("platform_id:H", + "encoding_id:H", + "language_id:H", + "name_id:H", + "length:H", + "offset:H") diff --git a/pyglet/font/user.py b/pyglet/font/user.py index 28a1d73b3..e33879bf2 100644 --- a/pyglet/font/user.py +++ b/pyglet/font/user.py @@ -1,6 +1,63 @@ +"""This module defines the usage and creation of user defined fonts in Pyglet. + +Previously, pyglet only supported font renderers that are built into the operating system, such as +``FreeType``, ``DirectWrite``, or ``Quartz``. However, there are situations in which a user may not want or need all the +features a font can provide. They just need to put characters in a particular order without the hassle of exporting +into a separate file. + +The :py:class:`~pyglet.font.user.UserDefinedMappingFont` is provided for most use cases, which will allow you to +make an internal font that can be used where a ``font_name`` is required to identify a font. + +A user defined font is also identified by its name. The name you choose should be unique to ensure it will not conflict +with a system font. For example, do not use `Arial`, as that will collide with Windows systems. + +With :py:class:`~pyglet.font.user.UserDefinedMappingFont` you can pass a mapping of characters that point to your +:py:class:`~pyglet.image.ImageData`. + +.. code-block:: python + + mappings={'c': my_image_data, 'b': my_image_data, 'a': my_image_data} + +For more custom behavior, a dict-like object can be used, such as a class. + +.. code-block:: python + + class MyCustomMapping: + def get(self, char: str) -> ImageData | None: + # return ImageData if a character is found. + # return None if no character is found + + mappings = MyCustomMapping() + +Once your font is created, you also must register it within pyglet to use it. This can be done through the + :py:func:`~pyglet.font.add_user_font` function. + +When you register a user defined font, only those parameters will used to identify the font. If you have a font, but +want to have a ``bold`` enabled version. You must make a new instance of your font, but with the ``bold`` +parameter set as ``True``. Same applies to the ``size`` parameter. + +Scaling +======= +By default, user font's will not be scaled. In most use cases, you have a single font at a specific size that you +want to use. + +There are cases where a user may want to scale their font to be used at any size. We provide the following function: +:py:func:`~pyglet.font.user.get_scaled_user_font`. By providing the user defined font instance, and a new size, you will +get back a new font instance that is scaled to the new size. This new instance must also be registered the same way as +the base font. + +When specifying the ``size`` parameter, that value is used to determine the ratio of scaling between the new size. So +if your base font is a size of 12, creating a scaled version at size 24 will be double the size of the base. + +.. warning:: + + The ``PIL`` library is a required dependency to use the scaling functionality. + +.. versionadded:: 2.0.15 +""" from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Protocol, List +from typing import TYPE_CHECKING, ClassVar, Protocol import pyglet from pyglet.font import base @@ -15,24 +72,24 @@ pass if TYPE_CHECKING: - from pyglet.image import ImageData from pyglet.font.base import Glyph + from pyglet.image import ImageData class UserDefinedGlyphRenderer(base.GlyphRenderer): - def __init__(self, font: UserDefinedFontBase): + def __init__(self, font: UserDefinedFontBase) -> None: super().__init__(font) self._font = font - def render(self, image_data: ImageData): - if self._font._scaling: - image_original = Image.frombytes('RGBA', (image_data.width, image_data.height), - image_data.get_image_data().get_data('RGBA')) + def render(self, image_data: ImageData) -> Glyph: + if self._font._scaling: # noqa: SLF001 + image_original = Image.frombytes("RGBA", (image_data.width, image_data.height), + image_data.get_image_data().get_data("RGBA")) scale_ratio = self._font.size / self._font._base_size image_resized = image_original.resize((int(image_data.width * scale_ratio), int(image_data.height * scale_ratio)), Resampling.NEAREST) new_image = pyglet.image.ImageData(image_resized.width, image_resized.height, - 'RGBA', image_resized.tobytes()) + "RGBA", image_resized.tobytes()) glyph = self._font.create_glyph(new_image) glyph.set_bearings(-self._font.descent, 0, image_resized.width) else: @@ -42,13 +99,42 @@ def render(self, image_data: ImageData): class UserDefinedFontBase(base.Font): - glyph_renderer_class = UserDefinedGlyphRenderer + """Used as a base for all user defined fonts. + + .. versionadded:: 2.0.15 + """ + glyph_renderer_class: ClassVar[type[base.GlyphRenderer]] = UserDefinedGlyphRenderer def __init__( - self, name: str, default_char: str, size: int, ascent: Optional[int] = None, descent: Optional[int] = None, - bold: bool = False, italic: bool = False, stretch: bool = False, dpi: int = 96, - locale: Optional[str] = None, - ): + self, name: str, default_char: str, size: int, ascent: int | None = None, descent: int | None = None, + bold: bool = False, italic: bool = False, stretch: bool = False, dpi: int = 96, locale: str | None = None, + ) -> None: + """Initialize a user defined font. + + Args: + name: + Name of the font. Used to identify the font. Must be unique to ensure it does not + collide with any system fonts. + default_char: + If a character in a string is not found in the font, it will use this as fallback. + size: + Font size, usually in pixels. + ascent: + Maximum ascent above the baseline, in pixels. If None, the image height is used. + descent: + Maximum descent below the baseline, in pixels. Usually negative. + bold: + If True, this font will be used when ``bold`` is enabled for the font name. + italic: + If True, this font will be used when ``italic`` is enabled for the font name. + stretch: + If True, this font will be used when ``stretch`` is enabled for the font name. + dpi: + The assumed resolution of the display device, for the purposes of determining the pixel size of the + font. Use a default of 96 for standard sizing. + locale: + Used to specify the locale of this font. + """ super().__init__() self._name = name self.default_char = default_char @@ -68,55 +154,67 @@ def __init__( def name(self) -> str: return self._name - def enable_scaling(self, base_size: int): + def enable_scaling(self, base_size: int) -> None: if not SCALING_ENABLED: - raise Exception("PIL is not installed. User Font Scaling requires PIL.") + msg = "PIL is not installed. User Font Scaling requires PIL." + raise Exception(msg) self._base_size = base_size self._scaling = True -class UserDefinedFontException(Exception): - pass +class UserDefinedFontException(Exception): # noqa: N818 + """An exception related to user font creation.""" class DictLikeObject(Protocol): - def get(self, char: str) -> Optional[ImageData]: + def get(self, char: str) -> ImageData | None: pass class UserDefinedMappingFont(UserDefinedFontBase): - """The default UserDefinedFont, it can take mappings of characters to ImageData to make a User defined font.""" - - def __init__( - self, name: str, default_char: str, size: int, mappings: DictLikeObject, - ascent: Optional[int] = None, descent: Optional[int] = None, bold: bool = False, italic: bool = False, - stretch: bool = False, dpi: int = 96, locale: Optional[str] = None, - - ): - """Create a custom font using the mapping dict. - - :Parameters: - `name` : str - Name of the font. - `default_char` : str - If a character in a string is not found in the font, - it will use this as fallback. - `size` : int - Font size. - `mappings` : DictLikeObject - A dict or dict-like object with a get function. - The get function must take a string character, and output ImageData if found. - It also must return None if no character is found. - `ascent` : int + """The class allows the creation of user defined fonts from a set of mappings. + + .. versionadded:: 2.0.15 + """ + + def __init__(self, name: str, default_char: str, size: int, mappings: DictLikeObject, + ascent: int | None = None, descent: int | None = None, bold: bool = False, italic: bool = False, + stretch: bool = False, dpi: int = 96, locale: str | None = None) -> None: + """Initialize the default parameters of your font. + + Args: + name: + Name of the font. Must be unique to ensure it does not collide with any system fonts. + default_char: + If a character in a string is not found in the font, it will use this as fallback. + size: + Font size. Should be in pixels. This value will affect scaling if enabled. + mappings: + A dict or dict-like object with a ``get`` function. + The ``get`` function must take a string character, and output :py:class:`~pyglet.iamge.ImageData` if + found. It also must return ``None`` if no character is found. + ascent: Maximum ascent above the baseline, in pixels. If None, the image height is used. - `descent` : int + descent: Maximum descent below the baseline, in pixels. Usually negative. + bold: + If ``True``, this font will be used when ``bold`` is enabled for the font name. + italic: + If ``True``, this font will be used when ``italic`` is enabled for the font name. + stretch: + If ``True``, this font will be used when ``stretch`` is enabled for the font name. + dpi: + The assumed resolution of the display device, for the purposes of determining the pixel size of the + font. Use a default of 96 for standard sizing. + locale: + Used to specify the locale of this font. """ self.mappings = mappings default_image = self.mappings.get(default_char) if not default_image: - raise UserDefinedFontException(f"Default character '{default_char}' must exist within your mappings.") + msg = f"Default character '{default_char}' must exist within your mappings." + raise UserDefinedFontException(msg) if ascent is None or descent is None: if ascent is None: @@ -127,30 +225,30 @@ def __init__( super().__init__(name, default_char, size, ascent, descent, bold, italic, stretch, dpi, locale) def enable_scaling(self, base_size: int) -> None: + """Enables scaling the font size. + + Args: + base_size: + The base size is used to calculate the ratio between new sizes and the original. + """ super().enable_scaling(base_size) glyphs = self.get_glyphs(self.default_char) self.ascent = glyphs[0].height self.descent = 0 - def get_glyphs(self, text: str) -> List[Glyph]: + def get_glyphs(self, text: str) -> list[Glyph]: """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 with the default_char. - - :Parameters: - `text` : str or unicode - Text to render. - - :rtype: list of `Glyph` + If any characters do not have a known glyph representation in this font, a substitution will be made with + the ``default_char``. """ glyph_renderer = None glyphs = [] # glyphs that are committed. for c in base.get_grapheme_clusters(text): # Get the glyph for 'c'. Hide tabs (Windows and Linux render # boxes) - if c == '\t': - c = ' ' + if c == "\t": + c = " " if c not in self.glyphs: if not glyph_renderer: glyph_renderer = self.glyph_renderer_class(self) @@ -164,8 +262,19 @@ def get_glyphs(self, text: str) -> List[Glyph]: return glyphs -def get_scaled_user_font(font_base: UserDefinedMappingFont, size: int): - """This function will return a new font that can scale it's size based off the original base font.""" +def get_scaled_user_font(font_base: UserDefinedMappingFont, size: int) -> UserDefinedMappingFont: + """This function will return a new font instance which can scale it's size based off the original base font. + + .. note:: The scaling functionality requires the PIL library to be installed. + + .. versionadded:: 2.0.15 + + Args: + font_base: + The base font object to create a new size from. + size: + The new font size. This will be scaled based on the ratio between the base size and the new size. + """ new_font = UserDefinedMappingFont(font_base.name, font_base.default_char, size, font_base.mappings, font_base.ascent, font_base.descent, font_base.bold, font_base.italic, font_base.stretch, font_base.dpi, font_base.locale) @@ -178,5 +287,5 @@ def get_scaled_user_font(font_base: UserDefinedMappingFont, size: int): "UserDefinedFontBase", "UserDefinedFontException", "UserDefinedMappingFont", - "get_scaled_user_font" + "get_scaled_user_font", ) diff --git a/pyglet/font/win32.py b/pyglet/font/win32.py index 29566d2c8..9aff158e2 100644 --- a/pyglet/font/win32.py +++ b/pyglet/font/win32.py @@ -1,20 +1,20 @@ -# TODO Windows Vista: need to call SetProcessDPIAware? May affect GDI+ calls as well as font. +# Windows Vista: need to call SetProcessDPIAware? May affect GDI+ calls as well as font. from __future__ import annotations -import ctypes +import ctypes import math import warnings -from typing import Optional, Sequence, TYPE_CHECKING +from typing import TYPE_CHECKING, ClassVar, Sequence import pyglet import pyglet.image from pyglet.font import base -from pyglet.image.codecs.gdiplus import ImageLockModeRead, BitmapData -from pyglet.image.codecs.gdiplus import PixelFormat32bppARGB, gdiplus, Rect -from pyglet.libs.win32 import _gdi32 as gdi32, _user32 as user32 -from pyglet.libs.win32.types import BYTE, ABC, TEXTMETRIC, LOGFONTW -from pyglet.libs.win32.constants import FW_BOLD, FW_NORMAL, ANTIALIASED_QUALITY +from pyglet.image.codecs.gdiplus import BitmapData, ImageLockModeRead, PixelFormat32bppARGB, Rect, gdiplus +from pyglet.libs.win32 import _gdi32 as gdi32 +from pyglet.libs.win32 import _user32 as user32 +from pyglet.libs.win32.constants import ANTIALIASED_QUALITY, FW_BOLD, FW_NORMAL from pyglet.libs.win32.context_managers import device_context +from pyglet.libs.win32.types import ABC, BYTE, LOGFONTW, TEXTMETRIC if TYPE_CHECKING: from pyglet.font.base import Glyph @@ -43,20 +43,20 @@ FontFamilyNotFound = 14 -_debug_font = pyglet.options['debug_font'] +_debug_font = pyglet.options["debug_font"] class Rectf(ctypes.Structure): _fields_ = [ - ('x', ctypes.c_float), - ('y', ctypes.c_float), - ('width', ctypes.c_float), - ('height', ctypes.c_float), + ("x", ctypes.c_float), + ("y", ctypes.c_float), + ("width", ctypes.c_float), + ("height", ctypes.c_float), ] class GDIPlusGlyphRenderer(base.GlyphRenderer): - def __init__(self, font: 'GDIPlusFont') -> None: + def __init__(self, font: GDIPlusFont) -> None: self._bitmap = None self._dc = None self._bitmap_rect = None @@ -84,7 +84,7 @@ def __del__(self) -> None: gdiplus.GdipDisposeImage(self._bitmap) if self._dc: user32.ReleaseDC(0, self._dc) - except Exception: + except Exception: # noqa: S110, BLE001 pass def _create_bitmap(self, width: int, height: int) -> None: @@ -177,8 +177,8 @@ def render(self, text: str) -> Glyph: # GDI functions only work for a single character so we transform # grapheme \r\n into \r - if text == '\r\n': - text = '\r' + if text == "\r\n": + text = "\r" # XXX END HACK HACK HACK @@ -247,7 +247,7 @@ def render(self, text: str) -> Glyph: image = pyglet.image.ImageData( width, height, - 'BGRA', buffer, -bitmap_data.Stride) + "BGRA", buffer, -bitmap_data.Stride) glyph = self.font.create_glyph(image) # Only pass negative LSB info @@ -263,7 +263,7 @@ def __init__( self, name: str, size: float, bold: bool = False, italic: bool = False, stretch: bool = False, - dpi: Optional[float] = None + dpi: float | None = None, ) -> None: super().__init__() @@ -280,7 +280,7 @@ def __init__( self.max_glyph_width = metrics.tmMaxCharWidth @staticmethod - def get_logfont(name: str, size: float, bold: bool, italic: bool, dpi: Optional[float] = None) -> LOGFONTW: + def get_logfont(name: str, size: float, bold: bool, italic: bool, dpi: float | None = None) -> LOGFONTW: """Get a raw Win32 :py:class:`.LOGFONTW` struct for the given arguments. Args: @@ -334,7 +334,7 @@ def _get_font_families(font_collection: ctypes.c_void_p) -> Sequence[ctypes.c_vo def _font_exists_in_collection(font_collection: ctypes.c_void_p, name: str) -> bool: font_name = ctypes.create_unicode_buffer(32) for gpfamily in _get_font_families(font_collection): - gdiplus.GdipGetFamilyName(gpfamily, font_name, '\0') + gdiplus.GdipGetFamilyName(gpfamily, font_name, "\0") if font_name.value == name: return True @@ -342,23 +342,20 @@ def _font_exists_in_collection(font_collection: ctypes.c_void_p, name: str) -> b class GDIPlusFont(Win32Font): - glyph_renderer_class = GDIPlusGlyphRenderer + glyph_renderer_class: ClassVar[type[base.GlyphRenderer]] = GDIPlusGlyphRenderer - _private_collection = None - _system_collection = None + _private_collection: ctypes.c_void_p | None = None + _system_collection: ctypes.c_void_p | None = None - _default_name = 'Arial' + _default_name = "Arial" def __init__(self, name: str, size: float, bold: bool=False, italic: bool=False, stretch: bool=False, - dpi: Optional[float]=None) -> None: + dpi: float | None=None) -> None: if not name: name = self._default_name - # assert type(bold) is bool, "Only a boolean value is supported for bold in the current font renderer." - # assert type(italic) is bool, "Only a boolean value is supported for bold in the current font renderer." - if stretch: - warnings.warn("The current font render does not support stretching.") + warnings.warn("The current font render does not support stretching.") # noqa: B028 super().__init__(name, size, bold, italic, stretch, dpi) @@ -380,7 +377,7 @@ def __init__(self, name: str, size: float, bold: bool=False, italic: bool=False, # Then in system collection: if not family: if _debug_font: - print(f"Warning: Font '{name}' was not found. Defaulting to: {self._default_name}") + print(f"Warning: Font '{name}' was not found. Defaulting to: {self._default_name}") # noqa: T201 gdiplus.GdipCreateFontFamilyFromName(name, None, ctypes.byref(family)) @@ -415,7 +412,7 @@ def __del__(self) -> None: gdiplus.GdipDeleteFont(self._gdipfont) @classmethod - def add_font_data(cls, data: bytes) -> None: + def add_font_data(cls: type[GDIPlusFont], data: bytes) -> None: numfonts = ctypes.c_uint32() _handle = gdi32.AddFontMemResourceEx(data, len(data), 0, ctypes.byref(numfonts)) @@ -430,11 +427,10 @@ def add_font_data(cls, data: bytes) -> None: gdiplus.GdipPrivateAddMemoryFont(cls._private_collection, data, len(data)) @classmethod - def have_font(cls, name: str) -> bool: + def have_font(cls: type[GDIPlusFont], name: str) -> bool: # Enumerate the private collection fonts first, as those are most likely to be used. - if cls._private_collection: - if _font_exists_in_collection(cls._private_collection, name): - return True + if cls._private_collection and _font_exists_in_collection(cls._private_collection, name): + return True # Instead of enumerating all fonts on the system, as there can potentially be thousands, attempt to create # the font family with the name. If it does not error (0), then it exists in the system. diff --git a/pyproject.toml b/pyproject.toml index 89fbf7d44..3dd8cc505 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,5 +53,15 @@ exclude = ["venv/*", ".venv/*", "build/*", "doc/*", ".github/*"] convention = "google" [tool.ruff.format] -quote-style = "single" +quote-style = "preserve" indent-style = "space" + +[tool.ruff.lint.per-file-ignores] +# Ignore doc requirements, naming, arguments in platform and lib files. +"pyglet/font/directwrite.py" = ["RUF012", "D", "N", "ARG"] +"pyglet/font/fontconfig.py" = ["RUF012", "D", "N", "ARG"] +"pyglet/font/freetype.py" = ["RUF012", "D", "N", "ARG"] +"pyglet/font/freetype_lib.py" = ["RUF012", "D", "N", "ARG"] +"pyglet/font/quartz.py" = ["RUF012", "D", "N", "ARG"] +"pyglet/font/ttf.py" = ["RUF012", "D", "N", "ARG"] +"pyglet/font/win32.py" = ["RUF012", "D", "N", "ARG"] \ No newline at end of file From 7a21361bb813887fa4b1642a85cfe9764ee30b54 Mon Sep 17 00:00:00 2001 From: Benjamin Moran Date: Wed, 17 Apr 2024 11:19:21 +0900 Subject: [PATCH 2/5] clock: type hinting, and docstring fixes --- pyglet/clock.py | 181 +++++++++++++++++++++--------------------------- 1 file changed, 79 insertions(+), 102 deletions(-) diff --git a/pyglet/clock.py b/pyglet/clock.py index 4a0d950fd..3fd7a2f3c 100644 --- a/pyglet/clock.py +++ b/pyglet/clock.py @@ -62,10 +62,12 @@ def move(dt, velocity, sprite): "wall-time", or to synchronise your clock to an audio or video stream instead of the system clock. """ +from __future__ import annotations import time as _time -from typing import Callable +from typing import Any, Callable + from heapq import heappop as _heappop from heapq import heappush as _heappush from heapq import heappushpop as _heappushpop @@ -76,7 +78,7 @@ def move(dt, velocity, sprite): class _ScheduledItem: __slots__ = ['func', 'args', 'kwargs'] - def __init__(self, func, args, kwargs): + def __init__(self, func: Callable, args: Any, kwargs: Any): self.func = func self.args = args self.kwargs = kwargs @@ -85,7 +87,7 @@ def __init__(self, func, args, kwargs): class _ScheduledIntervalItem: __slots__ = ['func', 'interval', 'last_ts', 'next_ts', 'args', 'kwargs'] - def __init__(self, func, interval, last_ts, next_ts, args, kwargs): + def __init__(self, func: Callable, interval: float, last_ts: float, next_ts: float, args: Any, kwargs: Any): self.func = func self.interval = interval self.last_ts = last_ts @@ -93,39 +95,36 @@ def __init__(self, func, interval, last_ts, next_ts, args, kwargs): self.args = args self.kwargs = kwargs - def __lt__(self, other): - try: - return self.next_ts < other.next_ts - except AttributeError: - return self.next_ts < other + def __lt__(self, other: _ScheduledIntervalItem) -> bool: + return self.next_ts < other.next_ts class Clock: # List of functions to call every tick. - _schedule_items = None + _schedule_items: list # List of schedule interval items kept in sort order. - _schedule_interval_items = None + _schedule_interval_items: list # If True, a sleep(0) is inserted on every tick. - _force_sleep = False + _force_sleep: bool = False - def __init__(self, time_function=_time.perf_counter): + def __init__(self, time_function: Callable = _time.perf_counter): """Initialise a Clock, with optional custom time function. You can provide a custom time function to return the elapsed - time of the application, in seconds. Defaults to time.perf_counter, + time of the application, in seconds. Defaults to ``time.perf_counter``, but can be replaced to allow for easy time dilation effects or game pausing. """ self.time = time_function - self.next_ts = self.time() self.last_ts = None + self.next_ts = self.time() # Used by self.get_frequency to show update frequency - self.times = _deque() - self.cumulative_time = 0 + self.times: _deque = _deque() + self.cumulative_time = 0.0 self.window_size = 60 self._schedule_items = [] @@ -133,7 +132,7 @@ def __init__(self, time_function=_time.perf_counter): self._current_interval_item = None @staticmethod - def sleep(microseconds: float): + def sleep(microseconds: float) -> None: _time.sleep(microseconds * 1e-6) def update_time(self) -> float: @@ -159,17 +158,19 @@ def update_time(self) -> float: def call_scheduled_functions(self, dt: float) -> bool: """Call scheduled functions that elapsed on the last `update_time`. - Returns True if any functions were called, otherwise False. + This method is called automatically when the clock is ticked + (see :py:meth:`~pyglet.clock.tick`), so you need not call it + yourself in most cases. - .. versionadded:: 1.2 - - :Parameters: - dt : float + Args: + dt: The elapsed time since the last update to pass to each - scheduled function. This is *not* used to calculate which + scheduled function. This is *not* used to calculate which functions have elapsed. + + Returns: ``True`` if any functions were called, else ``False``. """ - now = self.last_ts + now = self.last_ts or self.time() result = False # flag indicates if any function was called # handle items scheduled for every tick @@ -185,9 +186,8 @@ def call_scheduled_functions(self, dt: float) -> bool: try: if interval_items[0].next_ts > now: return result - - # raised when the interval_items list is empty except IndexError: + # The interval_items list is empty return result # NOTE: there is no special handling required to manage things @@ -248,7 +248,7 @@ def call_scheduled_functions(self, dt: float) -> bool: return True - def tick(self, poll=False) -> float: + def tick(self, poll: bool = False) -> float: """Signify that one frame has passed. This will call any scheduled functions that have elapsed, @@ -256,8 +256,8 @@ def tick(self, poll=False) -> float: method has been called. The first time this method is called, 0 is returned. - :Parameters: - `poll` : bool + Args: + poll: If True, the function will call any scheduled functions but will not sleep or busy-wait for any reason. Recommended for advanced applications managing their own sleep timers @@ -270,31 +270,25 @@ def tick(self, poll=False) -> float: self.call_scheduled_functions(delta_t) return delta_t - def get_sleep_time(self, sleep_idle: bool): - """Get the time until the next item is scheduled. + def get_sleep_time(self, sleep_idle: bool) -> float | None: + """Get the time until the next item is scheduled, if any. Applications can choose to continue receiving updates at the maximum framerate during idle time (when no functions are scheduled), or they can sleep through their idle time and allow the CPU to switch to other processes or run in low-power mode. - If `sleep_idle` is ``True`` the latter behaviour is selected, and + If ``sleep_idle`` is ``True`` the latter behaviour is selected, and ``None`` will be returned if there are no scheduled items. - Otherwise, if `sleep_idle` is ``False``, or if any scheduled items + Otherwise, if ``sleep_idle`` is ``False``, or if any scheduled items exist, a value of 0 is returned. - :Parameters: - `sleep_idle` : bool + Args: + sleep_idle: If True, the application intends to sleep through its idle time; otherwise it will continue ticking at the maximum frame rate allowed. - - :rtype: float - :return: Time until the next scheduled event in seconds, or ``None`` - if there is no event scheduled. - - .. versionadded:: 1.1 """ if self._schedule_items or not sleep_idle: return 0.0 @@ -309,7 +303,7 @@ def get_frequency(self) -> float: The result is the average of a sliding window of the last "n" updates, where "n" is some number designed to cover approximately 1 second. - This is **not** the Window redraw rate. + This is the clock frequence, **not** the Window redraw rate (fps). """ if not self.cumulative_time: return 0 @@ -328,7 +322,7 @@ def _get_nearest_ts(self) -> float: return ts return last_ts - def _get_soft_next_ts(self, last_ts, interval): + def _get_soft_next_ts(self, last_ts: float, interval: float) -> float: def taken(ts, e): """Check if `ts` has already got an item scheduled nearby.""" @@ -383,7 +377,7 @@ def taken(ts, e): if divs > 16: return next_ts - def schedule(self, func, *args, **kwargs): + def schedule(self, func: Callable, *args: Any, **kwargs: Any) -> None: """Schedule a function to be called every tick. The scheduled function should have a prototype that includes ``dt`` @@ -394,9 +388,6 @@ def schedule(self, func, *args, **kwargs): def callback(dt, *args, **kwargs): pass - :Parameters: - `func` : callable - The function to call each tick. .. note:: Functions scheduled using this method will be called every tick by the default pyglet event loop, which can @@ -407,70 +398,69 @@ def callback(dt, *args, **kwargs): item = _ScheduledItem(func, args, kwargs) self._schedule_items.append(item) - def schedule_once(self, func, delay, *args, **kwargs): - """Schedule a function to be called once after `delay` seconds. - - The callback function prototype is the same as for `schedule`. + def schedule_once(self, func: Callable, delay: float, *args: Any, **kwargs: Any) -> None: + """Schedule a function to be called once after ``delay`` seconds. - :Parameters: - `func` : callable - The function to call when the timer lapses. - `delay` : float - The number of seconds to wait before the timer lapses. + The callback function prototype is the same as for + :py:meth:`~pyglet.clock.Clock.schedule`. """ last_ts = self._get_nearest_ts() next_ts = last_ts + delay item = _ScheduledIntervalItem(func, 0, last_ts, next_ts, args, kwargs) _heappush(self._schedule_interval_items, item) - def schedule_interval(self, func, interval, *args, **kwargs): - """Schedule a function to be called every `interval` seconds. - - Specifying an interval of 0 prevents the function from being - called again (see `schedule` to call a function as often as possible). + def schedule_interval(self, func: Callable, interval: float, *args: Any, **kwargs: Any) -> None: + """Schedule a function to be called every ``interval`` seconds. - The callback function prototype is the same as for `schedule`. + To schedule a function to be called at 60Hz (60fps), you would use ``1/60`` + for the interval, and so on. If pyglet is unable to call the function on + time, the schedule will be skipped (not accumulated). This can occur if the + main thread is overloaded, or other hard blocking calls taking place. - :Parameters: - `func` : callable - The function to call when the timer lapses. - `interval` : float - The number of seconds to wait between each call. + The callback function prototype is the same as for + :py:meth:`~pyglet.clock.Clock.schedule`. + .. note:: Specifying an interval of ``0`` will prevent the function from + being called again. If you want to schedule a function to be called + as often as possible, see :py:meth:`~pyglet.clock.Clock.schedule`. """ last_ts = self._get_nearest_ts() next_ts = last_ts + interval item = _ScheduledIntervalItem(func, interval, last_ts, next_ts, args, kwargs) _heappush(self._schedule_interval_items, item) - def schedule_interval_for_duration(self, func, interval, duration, *args, **kwargs): - """Schedule a function to be called every `interval` seconds - (see `schedule_interval`) and unschedule it after `duration` seconds. + def schedule_interval_for_duration(self, func: Callable, interval: float, + duration: float, *args: Any, **kwargs: Any) -> None: + """Temporarily schedule a function to be called every ``interval`` seconds. - The callback function prototype is the same as for `schedule`. + This method will schedule a function to be called every ``interval`` + seconds (see :py:meth:`~pyglet.clock.Clock.schedule_interval`), but + will automatically unschedule it after ``duration`` seconds. - :Parameters: - `func` : callable + The callback function prototype is the same as for + :py:meth:`~pyglet.clock.Clock.schedule`. + + :Args: + func: The function to call when the timer lapses. - `interval` : float + interval: The number of seconds to wait between each call. - `duration` : float + duration: The number of seconds for which the function is scheduled. - """ # NOTE: unschedule wrapper that takes `dt` argument - def _unschedule(dt: float, _func: Callable) -> None: + def _unschedule(_dt: float, _func: Callable) -> None: self.unschedule(_func) self.schedule_interval(func, interval, *args, **kwargs) self.schedule_once(_unschedule, duration, func) - def schedule_interval_soft(self, func, interval, *args, **kwargs): - """Schedule a function to be called every ``interval`` seconds. + def schedule_interval_soft(self, func: Callable, interval: float, *args: Any, **kwargs: Any) -> None: + """Schedule a function to be called approximately every ``interval`` seconds. - This method is similar to `schedule_interval`, except that the - clock will move the interval out of phase with other scheduled - functions in order to distribute CPU load more evenly. + This method is similar to :py:meth:`~pyglet.clock.Clock.schedule_interval`, + except that the clock will move the interval out of phase with other + scheduled functions in order to distribute CPU load more evenly. This is useful for functions that need to be called regularly, but not relative to the initial start time. :py:mod:`pyglet.media` @@ -484,31 +474,17 @@ def schedule_interval_soft(self, func, interval, *args, **kwargs): Soft interval scheduling can also be used as an easy way to schedule graphics animations out of phase; for example, multiple flags waving in the wind. - - .. versionadded:: 1.1 - - :Parameters: - `func` : callable - The function to call when the timer lapses. - `interval` : float - The number of seconds to wait between each call. - """ next_ts = self._get_soft_next_ts(self._get_nearest_ts(), interval) last_ts = next_ts - interval item = _ScheduledIntervalItem(func, interval, last_ts, next_ts, args, kwargs) _heappush(self._schedule_interval_items, item) - def unschedule(self, func): + def unschedule(self, func: Callable) -> None: """Remove a function from the schedule. If the function appears in the schedule more than once, all occurrences are removed. If the function was not scheduled, no error is raised. - - :Parameters: - `func` : callable - The function to remove from the schedule. - """ # clever remove item without disturbing the heap: # 1. set function to an empty lambda -- original function is not called @@ -530,7 +506,7 @@ def unschedule(self, func): _default = Clock() -def set_default(default) -> None: +def set_default(default: Clock) -> None: """Set the default clock to use for all module-level functions. By default, an instance of :py:class:`~pyglet.clock.Clock` is used. @@ -539,7 +515,7 @@ def set_default(default) -> None: _default = default -def get_default(): +def get_default() -> Clock: """Get the pyglet default Clock. Return the :py:class:`~pyglet.clock.Clock` instance that is used by all @@ -553,7 +529,7 @@ def tick(poll: bool = False) -> float: return _default.tick(poll) -def get_sleep_time(sleep_idle: bool) -> float: +def get_sleep_time(sleep_idle: bool) -> float | None: """:see: :py:meth:`~pyglet.clock.Clock.get_sleep_time`""" return _default.get_sleep_time(sleep_idle) @@ -563,15 +539,16 @@ def get_frequency() -> float: return _default.get_frequency() -def schedule(func: Callable, *args, **kwargs) -> None: +def schedule(func: Callable, *args: Any, **kwargs: Any) -> None: """:see: :py:meth:`~pyglet.clock.Clock.schedule`""" _default.schedule(func, *args, **kwargs) -def schedule_interval(func: Callable, interval: float, *args, **kwargs) -> None: +def schedule_interval(func: Callable, interval: float, *args: Any, **kwargs: Any) -> None: """:see: :py:meth:`~pyglet.clock.Clock.schedule_interval`""" _default.schedule_interval(func, interval, *args, **kwargs) + def schedule_interval_for_duration(func: Callable, interval: float, duration: float, *args, **kwargs) -> None: """:see: :py:meth:`~pyglet.clock.Clock.schedule_interval_for_duration`""" _default.schedule_interval_for_duration(func, interval, duration, *args, **kwargs) From 10cd88f0df81a7032b0a97a47b1bfb0e5935061c Mon Sep 17 00:00:00 2001 From: Benjamin Moran Date: Wed, 17 Apr 2024 11:58:51 +0900 Subject: [PATCH 3/5] make.py: Add ruff cache dir to 'make.py clean' --- make.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/make.py b/make.py index 4367066b3..cb6c0475d 100755 --- a/make.py +++ b/make.py @@ -18,17 +18,25 @@ def clean(): op.join(THIS_DIR, '_build'), op.join(THIS_DIR, 'pyglet.egg-info'), op.join(THIS_DIR, '.pytest_cache'), - op.join(THIS_DIR, '.mypy_cache')] + op.join(THIS_DIR, '.mypy_cache'), + op.join(THIS_DIR, '.ruff_cache')] + files = [op.join(DOC_DIR, 'internal', 'build.rst')] + for d in dirs: - print(' Removing:', d) + if not op.exists(d): + continue shutil.rmtree(d, ignore_errors=True) + print(f" Removed: {d}") + for f in files: - print(' Removing:', f) + if not op.exists(f): + continue try: os.remove(f) + print(f" Removed: {f}") except OSError: - pass + print(f" Failed to remove: {f}") def docs(): From ffb913f978c15edd8fea644262c3741046092fc6 Mon Sep 17 00:00:00 2001 From: Benjamin Moran Date: Wed, 17 Apr 2024 11:59:53 +0900 Subject: [PATCH 4/5] clock: formatting changes --- pyglet/clock.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/pyglet/clock.py b/pyglet/clock.py index 3fd7a2f3c..6d39f2c03 100644 --- a/pyglet/clock.py +++ b/pyglet/clock.py @@ -78,7 +78,7 @@ def move(dt, velocity, sprite): class _ScheduledItem: __slots__ = ['func', 'args', 'kwargs'] - def __init__(self, func: Callable, args: Any, kwargs: Any): + def __init__(self, func: Callable, args: Any, kwargs: Any) -> None: self.func = func self.args = args self.kwargs = kwargs @@ -87,7 +87,7 @@ def __init__(self, func: Callable, args: Any, kwargs: Any): class _ScheduledIntervalItem: __slots__ = ['func', 'interval', 'last_ts', 'next_ts', 'args', 'kwargs'] - def __init__(self, func: Callable, interval: float, last_ts: float, next_ts: float, args: Any, kwargs: Any): + def __init__(self, func: Callable, interval: float, last_ts: float, next_ts: float, args: Any, kwargs: Any) -> None: self.func = func self.interval = interval self.last_ts = last_ts @@ -110,7 +110,7 @@ class Clock: # If True, a sleep(0) is inserted on every tick. _force_sleep: bool = False - def __init__(self, time_function: Callable = _time.perf_counter): + def __init__(self, time_function: Callable = _time.perf_counter) -> None: """Initialise a Clock, with optional custom time function. You can provide a custom time function to return the elapsed @@ -324,7 +324,7 @@ def _get_nearest_ts(self) -> float: def _get_soft_next_ts(self, last_ts: float, interval: float) -> float: - def taken(ts, e): + def taken(ts: float, e: float) -> bool: """Check if `ts` has already got an item scheduled nearby.""" # TODO this function is slow and called very often. # Optimise it, maybe? @@ -366,7 +366,7 @@ def taken(ts, e): divs = 1 while True: next_ts = last_ts - for i in range(divs - 1): + for _ in range(divs - 1): next_ts += dt if not taken(next_ts, dt / 4): return next_ts @@ -489,11 +489,10 @@ def unschedule(self, func: Callable) -> None: # clever remove item without disturbing the heap: # 1. set function to an empty lambda -- original function is not called # 2. set interval to 0 -- item will be removed from heap eventually - valid_items = set(item for item in self._schedule_interval_items if item.func == func) + valid_items = {item for item in self._schedule_interval_items if item.func == func} - if self._current_interval_item: - if self._current_interval_item.func == func: - valid_items.add(self._current_interval_item) + if self._current_interval_item and self._current_interval_item.func == func: + valid_items.add(self._current_interval_item) for item in valid_items: item.interval = 0 @@ -525,45 +524,45 @@ def get_default() -> Clock: def tick(poll: bool = False) -> float: - """:see: :py:meth:`~pyglet.clock.Clock.tick`""" + """:see: :py:meth:`~pyglet.clock.Clock.tick`.""" return _default.tick(poll) def get_sleep_time(sleep_idle: bool) -> float | None: - """:see: :py:meth:`~pyglet.clock.Clock.get_sleep_time`""" + """:see: :py:meth:`~pyglet.clock.Clock.get_sleep_time`.""" return _default.get_sleep_time(sleep_idle) def get_frequency() -> float: - """:see: :py:meth:`~pyglet.clock.Clock.get_frequency`""" + """:see: :py:meth:`~pyglet.clock.Clock.get_frequency`.""" return _default.get_frequency() def schedule(func: Callable, *args: Any, **kwargs: Any) -> None: - """:see: :py:meth:`~pyglet.clock.Clock.schedule`""" + """:see: :py:meth:`~pyglet.clock.Clock.schedule`.""" _default.schedule(func, *args, **kwargs) def schedule_interval(func: Callable, interval: float, *args: Any, **kwargs: Any) -> None: - """:see: :py:meth:`~pyglet.clock.Clock.schedule_interval`""" + """:see: :py:meth:`~pyglet.clock.Clock.schedule_interval`.""" _default.schedule_interval(func, interval, *args, **kwargs) def schedule_interval_for_duration(func: Callable, interval: float, duration: float, *args, **kwargs) -> None: - """:see: :py:meth:`~pyglet.clock.Clock.schedule_interval_for_duration`""" + """:see: :py:meth:`~pyglet.clock.Clock.schedule_interval_for_duration`.""" _default.schedule_interval_for_duration(func, interval, duration, *args, **kwargs) def schedule_interval_soft(func: Callable, interval: float, *args, **kwargs) -> None: - """:see: :py:meth:`~pyglet.clock.Clock.schedule_interval_soft`""" + """:see: :py:meth:`~pyglet.clock.Clock.schedule_interval_soft`.""" _default.schedule_interval_soft(func, interval, *args, **kwargs) def schedule_once(func: Callable, delay: float, *args, **kwargs) -> None: - """:see: :py:meth:`~pyglet.clock.Clock.schedule_once`""" + """:see: :py:meth:`~pyglet.clock.Clock.schedule_once`.""" _default.schedule_once(func, delay, *args, **kwargs) def unschedule(func: Callable) -> None: - """:see: :py:meth:`~pyglet.clock.Clock.unschedule`""" + """:see: :py:meth:`~pyglet.clock.Clock.unschedule`.""" _default.unschedule(func) From 5a130e143581c55fa0b20d2db03219e0ffaf8168 Mon Sep 17 00:00:00 2001 From: Benjamin Moran Date: Wed, 17 Apr 2024 12:45:08 +0900 Subject: [PATCH 5/5] gui: add type hints, and reformat docstrings --- pyglet/gui/frame.py | 95 +++++++++----- pyglet/gui/widgets.py | 292 +++++++++++++++++++++--------------------- pyglet/shapes.py | 4 +- 3 files changed, 209 insertions(+), 182 deletions(-) diff --git a/pyglet/gui/frame.py b/pyglet/gui/frame.py index 626eda6ee..e3c3de29f 100644 --- a/pyglet/gui/frame.py +++ b/pyglet/gui/frame.py @@ -1,3 +1,12 @@ +"""WIP.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pyglet.gui.widgets import WidgetBase + from pyglet.window import BaseWindow + class Frame: """The base Frame object, implementing a 2D spatial hash. @@ -10,17 +19,17 @@ class Frame: of Widgets are in use. """ - def __init__(self, window, cell_size=64, order=0): + def __init__(self, window: BaseWindow, cell_size: int = 64, order: int = 0) -> None: """Create an instance of a Frame. - :Parameters: - `window` : `~pyglet.window.Window` + Args: + window: The SpatialHash will recieve events from this Window. Appropriate events will be passed on to all added Widgets. - `cell_size` : int + cell_size: The cell ("bucket") size for each cell in the hash. Widgets may span multiple cells. - `order` : int + order: Widgets use internal OrderedGroups for draw sorting. This is the base value for these Groups. """ @@ -31,11 +40,11 @@ def __init__(self, window, cell_size=64, order=0): self._order = order self._mouse_pos = 0, 0 - def _hash(self, x, y): - """Normalize position to cell""" + def _hash(self, x: float, y: float) -> tuple[int, int]: + """Normalize position to cell.""" return int(x / self._cell_size), int(y / self._cell_size) - def add_widget(self, widget): + def add_widget(self, widget: WidgetBase) -> None: """Add a Widget to the spatial hash.""" min_vec, max_vec = self._hash(*widget.aabb[0:2]), self._hash(*widget.aabb[2:4]) for i in range(min_vec[0], max_vec[0] + 1): @@ -43,64 +52,66 @@ def add_widget(self, widget): self._cells.setdefault((i, j), set()).add(widget) widget.update_groups(self._order) - def remove_widget(self, widget): + def remove_widget(self, widget: WidgetBase) -> None: """Remove a Widget from the spatial hash.""" min_vec, max_vec = self._hash(*widget.aabb[0:2]), self._hash(*widget.aabb[2:4]) for i in range(min_vec[0], max_vec[0] + 1): for j in range(min_vec[1], max_vec[1] + 1): self._cells.get((i, j)).remove(widget) - def on_key_press(self, symbol, modifiers): - """Pass the event to any widgets within range of the mouse""" + # Handlers + + def on_key_press(self, symbol: int, modifiers: int) -> None: + """Pass the event to any widgets within range of the mouse.""" for widget in self._cells.get(self._hash(*self._mouse_pos), set()): widget.on_key_press(symbol, modifiers) - def on_key_release(self, symbol, modifiers): - """Pass the event to any widgets within range of the mouse""" + def on_key_release(self, symbol: int, modifiers: int) -> None: + """Pass the event to any widgets within range of the mouse.""" for widget in self._cells.get(self._hash(*self._mouse_pos), set()): widget.on_key_release(symbol, modifiers) - def on_mouse_press(self, x, y, buttons, modifiers): - """Pass the event to any widgets within range of the mouse""" + def on_mouse_press(self, x: int, y: int, buttons: int, modifiers: int) -> None: + """Pass the event to any widgets within range of the mouse.""" for widget in self._cells.get(self._hash(x, y), set()): widget.on_mouse_press(x, y, buttons, modifiers) self._active_widgets.add(widget) - def on_mouse_release(self, x, y, buttons, modifiers): - """Pass the event to any widgets that are currently active""" + def on_mouse_release(self, x: int, y: int, buttons: int, modifiers: int) -> None: + """Pass the event to any widgets that are currently active.""" for widget in self._active_widgets: widget.on_mouse_release(x, y, buttons, modifiers) self._active_widgets.clear() - def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): - """Pass the event to any widgets that are currently active""" + def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int) -> None: + """Pass the event to any widgets that are currently active.""" for widget in self._active_widgets: widget.on_mouse_drag(x, y, dx, dy, buttons, modifiers) self._mouse_pos = x, y - def on_mouse_scroll(self, x, y, scroll_x, scroll_y): - """Pass the event to any widgets within range of the mouse""" + def on_mouse_scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None: + """Pass the event to any widgets within range of the mouse.""" for widget in self._cells.get(self._hash(x, y), set()): widget.on_mouse_scroll(x, y, scroll_x, scroll_y) - def on_mouse_motion(self, x, y, dx, dy): - """Pass the event to any widgets within range of the mouse""" + def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None: + """Pass the event to any widgets within range of the mouse.""" for widget in self._cells.get(self._hash(x, y), set()): widget.on_mouse_motion(x, y, dx, dy) self._mouse_pos = x, y - def on_text(self, text): - """Pass the event to any widgets within range of the mouse""" + def on_text(self, text: str) -> None: + """Pass the event to any widgets within range of the mouse.""" for widget in self._cells.get(self._hash(*self._mouse_pos), set()): widget.on_text(text) - def on_text_motion(self, motion): - """Pass the event to any widgets within range of the mouse""" + def on_text_motion(self, motion: int) -> None: + """Pass the event to any widgets within range of the mouse.""" for widget in self._cells.get(self._hash(*self._mouse_pos), set()): widget.on_text_motion(motion) - def on_text_motion_select(self, motion): - """Pass the event to any widgets within range of the mouse""" + def on_text_motion_select(self, motion: int) -> None: + """Pass the event to any widgets within range of the mouse.""" for widget in self._cells.get(self._hash(*self._mouse_pos), set()): widget.on_text_motion_select(motion) @@ -121,28 +132,44 @@ class MovableFrame(Frame): API documentation. """ - def __init__(self, window, order=0, modifier=0): + def __init__(self, window: BaseWindow, order: int = 0, modifier: int = 0) -> None: + """Create an instance of a MovableFrame. + + This is a similar to the standard Frame class, except that + you can specify a modifer key. When this key is held down, + Widgets can be re-positioned by drag-and-dropping. + + Args: + window: + The SpatialHash will recieve events from this Window. + Appropriate events will be passed on to all added Widgets. + order: + Widgets use internal OrderedGroups for draw sorting. + This is the base value for these Groups. + modifier: + A key modifier, such as `pyglet.window.key.MOD_CTRL` + """ super().__init__(window, order=order) self._modifier = modifier self._moving_widgets = set() - def on_mouse_press(self, x, y, buttons, modifiers): + def on_mouse_press(self, x: int, y: int, buttons: int, modifiers: int) -> None: if self._modifier & modifiers > 0: for widget in self._cells.get(self._hash(x, y), set()): - if widget._check_hit(x, y): + if widget._check_hit(x, y): # noqa: SLF001 self._moving_widgets.add(widget) for widget in self._moving_widgets: self.remove_widget(widget) else: super().on_mouse_press(x, y, buttons, modifiers) - def on_mouse_release(self, x, y, buttons, modifiers): + def on_mouse_release(self, x: int, y: int, buttons: int, modifiers: int) -> None: for widget in self._moving_widgets: self.add_widget(widget) self._moving_widgets.clear() super().on_mouse_release(x, y, buttons, modifiers) - def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): + def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int) -> None: for widget in self._moving_widgets: wx, wy = widget.position widget.position = wx + dx, wy + dy diff --git a/pyglet/gui/widgets.py b/pyglet/gui/widgets.py index e41f6926f..e8133f9f9 100644 --- a/pyglet/gui/widgets.py +++ b/pyglet/gui/widgets.py @@ -1,17 +1,22 @@ -"""Display different types of interactive widgets. -""" +"""Display different types of interactive widgets.""" -import pyglet +from __future__ import annotations + +from typing import TYPE_CHECKING +import pyglet from pyglet.event import EventDispatcher from pyglet.graphics import Group from pyglet.text.caret import Caret from pyglet.text.layout import IncrementalTextLayout +if TYPE_CHECKING: + from pyglet.graphics import Batch, Group + from pyglet.image import AbstractImage -class WidgetBase(EventDispatcher): - def __init__(self, x, y, width, height): +class WidgetBase(EventDispatcher): + def __init__(self, x: int, y: int, width: int, height: int) -> None: self._x = x self._y = y self._width = width @@ -26,7 +31,6 @@ def _set_enabled(self, enabled: bool) -> None: Override this in subclasses to perform effects when a widget is enabled or disabled. """ - pass @property def enabled(self) -> bool: @@ -36,10 +40,9 @@ def enabled(self) -> bool: :py:meth:`._set_enabled` on widgets. For example, you may want to cue the user by: - * Playing an animation and/or sound - * Setting a highlight color - * Displaying a toast or notification - + - Playing an animation and/or sound + - Setting a highlight color + - Displaying a toast or notification """ return self._enabled @@ -50,124 +53,110 @@ def enabled(self, new_enabled: bool) -> None: self._enabled = new_enabled self._set_enabled(new_enabled) - def update_groups(self, order): + def update_groups(self, order: float) -> None: pass @property - def x(self): - """X coordinate of the widget. - - :type: int - """ + def x(self) -> int: + """X coordinate of the widget.""" return self._x @x.setter - def x(self, value): + def x(self, value: int) -> None: self._x = value self._update_position() @property - def y(self): - """Y coordinate of the widget. - - :type: int - """ + def y(self) -> int: + """Y coordinate of the widget.""" return self._y @y.setter - def y(self, value): + def y(self, value: int) -> None: self._y = value self._update_position() @property - def position(self): - """The x, y coordinate of the widget as a tuple. - - :type: tuple(int, int) - """ + def position(self) -> tuple[int, int]: + """The x, y coordinate of the widget as a tuple.""" return self._x, self._y @position.setter - def position(self, values): + def position(self, values: tuple[int, int]) -> None: self._x, self._y = values self._update_position() @property - def width(self): - """Width of the widget. - - :type: int - """ + def width(self) -> int: + """Width of the widget.""" return self._width @property - def height(self): - """Height of the widget. - - :type: int - """ + def height(self) -> int: + """Height of the widget.""" return self._height @property - def aabb(self): + def aabb(self) -> tuple[int, int, int, int]: """Bounding box of the widget. - Expressed as (x, y, x + width, y + height) - - :type: (int, int, int, int) + The "left", "bottom", "right", and "top" coordinates of the + widget. This is expressed as (x, y, x + width, y + height). """ return self._x, self._y, self._x + self._width, self._y + self._height @property - def value(self): + def value(self) -> int | float | bool: """Query or set the Widget's value. - + This property allows you to set the value of a Widget directly, without any user input. This could be used, for example, to restore Widgets to a previous state, or if some event in your program is meant to naturally change the same value that the Widget controls. Note that events are not - dispatched when changing this property. + dispatched when changing this property directly. """ raise NotImplementedError("Value depends on control type!") - + @value.setter - def value(self, value): + def value(self, value: int | float | bool): raise NotImplementedError("Value depends on control type!") - def _check_hit(self, x, y): + def _check_hit(self, x: int, y: int) -> bool: return self._x < x < self._x + self._width and self._y < y < self._y + self._height - def _update_position(self): + def _update_position(self) -> None: raise NotImplementedError("Unable to reposition this Widget") - def on_key_press(self, symbol, modifiers): + # Handlers + + def on_key_press(self, symbol: int, modifiers: int) -> None: pass - def on_key_release(self, symbol, modifiers): + def on_key_release(self, symbol: int, modifiers: int) -> None: pass - def on_mouse_press(self, x, y, buttons, modifiers): + def on_mouse_press(self, x: int, y: int, buttons: int, modifiers: int) -> None: pass - def on_mouse_release(self, x, y, buttons, modifiers): + def on_mouse_release(self, x: int, y: int, buttons: int, modifiers: int) -> None: pass - def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): + def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int) -> None: pass - def on_mouse_motion(self, x, y, dx, dy): + def on_mouse_scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None: pass - def on_mouse_scroll(self, x, y, scroll_x, scroll_y): + def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None: pass - def on_text(self, text): + def on_text(self, text: str) -> None: pass - def on_text_motion(self, motion): + def on_text_motion(self, motion: int) -> None: pass - def on_text_motion_select(self, motion): + def on_text_motion_select(self, motion: int) -> None: pass @@ -178,23 +167,28 @@ class PushButton(WidgetBase): Triggers the event 'on_release' when the mouse is released. """ - def __init__(self, x, y, pressed, depressed, hover=None, batch=None, group=None): + def __init__(self, x: int, y: int, + pressed: AbstractImage, + depressed: AbstractImage, + hover: AbstractImage | None = None, + batch: Batch | None = None, + group: Group | None = None) -> None: """Create a push button. - :Parameters: - `x` : int + Args: + x X coordinate of the push button. - `y` : int + y: Y coordinate of the push button. - `pressed` : `~pyglet.image.AbstractImage` + pressed: Image to display when the button is pressed. - `depresseed` : `~pyglet.image.AbstractImage` + depressed: Image to display when the button isn't pressed. - `hover` : `~pyglet.image.AbstractImage` + hover: Image to display when the button is being hovered over. - `batch` : `~pyglet.graphics.Batch` + batch: Optional batch to add the push button to. - `group` : `~pyglet.graphics.Group` + group: Optional parent group of the push button. """ super().__init__(x, y, depressed.width, depressed.height) @@ -209,50 +203,50 @@ def __init__(self, x, y, pressed, depressed, hover=None, batch=None, group=None) self._pressed = False - def _update_position(self): + def _update_position(self) -> None: self._sprite.position = self._x, self._y, 0 @property def value(self): return self._pressed - + @value.setter - def value(self, value): + def value(self, value: bool) -> None: assert type(value) is bool, "This Widget's value must be True or False." self._pressed = value self._sprite.image = self._pressed_img if self._pressed else self._depressed_img - def update_groups(self, order): + def update_groups(self, order: float) -> None: self._sprite.group = Group(order=order + 1, parent=self._user_group) - def on_mouse_press(self, x, y, buttons, modifiers): + def on_mouse_press(self, x: int, y: int, buttons: int, modifiers: int) -> None: if not self.enabled or not self._check_hit(x, y): return self._sprite.image = self._pressed_img self._pressed = True - self.dispatch_event('on_press') + self.dispatch_event("on_press") - def on_mouse_release(self, x, y, buttons, modifiers): + def on_mouse_release(self, x: int, y: int, buttons: int, modifiers: int) -> None: if not self.enabled or not self._pressed: return self._sprite.image = self._hover_img if self._check_hit(x, y) else self._depressed_img self._pressed = False - self.dispatch_event('on_release') + self.dispatch_event("on_release") - def on_mouse_motion(self, x, y, dx, dy): + def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None: if not self.enabled or self._pressed: return self._sprite.image = self._hover_img if self._check_hit(x, y) else self._depressed_img - def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): + def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int) -> None: if not self.enabled or self._pressed: return self._sprite.image = self._hover_img if self._check_hit(x, y) else self._depressed_img - def on_press(self): + def on_press(self) -> None: """Event: Dispatched when the button is clicked.""" - def on_release(self): + def on_release(self) -> None: """Event: Dispatched when the button is released.""" @@ -266,22 +260,22 @@ class ToggleButton(PushButton): Triggers the event 'on_toggle' when the mouse is pressed or released. """ - def _get_release_image(self, x, y): + def _get_release_image(self, x: int, y: int): return self._hover_img if self._check_hit(x, y) else self._depressed_img - def on_mouse_press(self, x, y, buttons, modifiers): + def on_mouse_press(self, x: int, y: int, buttons: int, modifiers: int) -> None: if not self.enabled or not self._check_hit(x, y): return self._pressed = not self._pressed self._sprite.image = self._pressed_img if self._pressed else self._get_release_image(x, y) self.dispatch_event('on_toggle', self._pressed) - def on_mouse_release(self, x, y, buttons, modifiers): + def on_mouse_release(self, x: int, y: int, buttons: int, modifiers: int) -> None: if not self.enabled or self._pressed: return self._sprite.image = self._get_release_image(x, y) - def on_toggle(self, value: bool): + def on_toggle(self, value: bool) -> None: """Event: returns True or False to indicate the current state.""" @@ -296,24 +290,28 @@ class Slider(WidgetBase): scrolling the mouse wheel. """ - def __init__(self, x, y, base, knob, edge=0, batch=None, group=None): + def __init__(self, x: int, y: int, + base: AbstractImage, knob: AbstractImage, + edge: int = 0, + batch: Batch | None = None, + group: Group | None = None) -> None: """Create a slider. - :Parameters: - `x` : int + Args: + x: X coordinate of the slider. - `y` : int + y: Y coordinate of the slider. - `base` : `~pyglet.image.AbstractImage` + base: Image to display as the background to the slider. - `knob` : `~pyglet.image.AbstractImage` + knob: Knob that moves to show the position of the slider. - `edge` : int + edge: Pixels from the maximum and minimum position of the slider, to the edge of the base image. - `batch` : `~pyglet.graphics.Batch` + batch: Optional batch to add the slider to. - `group` : `~pyglet.graphics.Group` + group: Optional parent group of the slider. """ super().__init__(x, y, base.width, knob.height) @@ -336,111 +334,114 @@ def __init__(self, x, y, base, knob, edge=0, batch=None, group=None): self._value = 0 self._in_update = False - def _update_position(self): + def _update_position(self) -> None: self._base_spr.position = self._x, self._y, 0 self._knob_spr.position = self._x + self._edge, self._y + self._base_img.height / 2, 0 @property - def value(self): + def value(self) -> float: return self._value @value.setter - def value(self, value): + def value(self, value: float) -> None: assert type(value) in (int, float), "This Widget's value must be an int or float." self._value = value x = (self._max_knob_x - self._min_knob_x) * value / 100 + self._min_knob_x + self._half_knob_width self._knob_spr.x = max(self._min_knob_x, min(x - self._half_knob_width, self._max_knob_x)) - def update_groups(self, order): + def update_groups(self, order: float) -> None: self._base_spr.group = Group(order=order + 1, parent=self._user_group) self._knob_spr.group = Group(order=order + 2, parent=self._user_group) @property - def _min_x(self): + def _min_x(self) -> int: return self._x + self._edge @property - def _max_x(self): + def _max_x(self) -> int: return self._x + self._width - self._edge @property - def _min_y(self): + def _min_y(self) -> int: return self._y - self._half_knob_height @property - def _max_y(self): + def _max_y(self) -> int: return self._y + self._half_knob_height + self._base_img.height / 2 - def _check_hit(self, x, y): + def _check_hit(self, x: int, y: int) -> bool: return self._min_x < x < self._max_x and self._min_y < y < self._max_y - def _update_knob(self, x): + def _update_knob(self, x: int) -> None: self._knob_spr.x = max(self._min_knob_x, min(x - self._half_knob_width, self._max_knob_x)) self._value = abs(((self._knob_spr.x - self._min_knob_x) * 100) / (self._min_knob_x - self._max_knob_x)) self.dispatch_event('on_change', self._value) - def on_mouse_press(self, x, y, buttons, modifiers): + def on_mouse_press(self, x: int, y: int, buttons: int, modifiers: int) -> None: if not self.enabled: return if self._check_hit(x, y): self._in_update = True self._update_knob(x) - def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): + def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int) -> None: if not self.enabled: return if self._in_update: self._update_knob(x) - def on_mouse_scroll(self, x, y, scroll_x, scroll_y): + def on_mouse_scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None: if not self.enabled: return if self._check_hit(x, y): self._update_knob(self._knob_spr.x + self._half_knob_width + scroll_y) - def on_mouse_release(self, x, y, buttons, modifiers): + def on_mouse_release(self, x: int, y: int, buttons: int, modifiers: int) -> None: if not self.enabled: return self._in_update = False - def on_change(self, value: float): + def on_change(self, value: float) -> None: """Event: Returns the current value when the slider is changed.""" -Slider.register_event_type('on_change') +Slider.register_event_type("on_change") class TextEntry(WidgetBase): """Instance of a text entry widget. Allows the user to enter and submit text. - + Triggers the event 'on_commit', when the user hits the Enter or Return key. The current text string is passed along with the event. """ - def __init__(self, text, x, y, width, - color=(255, 255, 255, 255), text_color=(0, 0, 0, 255), caret_color=(0, 0, 0, 255), - batch=None, group=None): + def __init__(self, text: str, + x: int, y: int, width: int, + color: tuple[int, int, int, int] = (255, 255, 255, 255), + text_color: tuple[int, int, int, int] = (0, 0, 0, 255), + caret_color: tuple[int, int, int, int] = (0, 0, 0, 255), + batch: Batch | None = None, + group: Group | None = None) -> None: """Create a text entry widget. - :Parameters: - `text` : str + Args: + text: Initial text to display. - `x` : int + x: X coordinate of the text entry widget. - `y` : int + y: Y coordinate of the text entry widget. - `width` : int + width: The width of the text entry widget. - `color` : (int, int, int, int) + color: The color of the outline box in RGBA format. - `text_color` : (int, int, int, int) + text_color: The color of the text in RGBA format. - `caret_color` : (int, int, int, int) - The color of the caret when it is visible in RGBA or RGB - format. - `batch` : `~pyglet.graphics.Batch` + caret_color: + The color of the caret (when it is visible) in RGBA or RGB format. + batch: Optional batch to add the text entry widget to. - `group` : `~pyglet.graphics.Group` + group: Optional parent group of text entry widget. """ self._doc = pyglet.text.document.UnformattedDocument(text) @@ -454,8 +455,7 @@ def __init__(self, text, x, y, width, # Rectangular outline with 2-pixel pad: self._pad = p = 2 - self._outline = pyglet.shapes.Rectangle(x-p, y-p, width+p+p, height+p+p, color[:3], batch, bg_group) - self._outline.opacity = color[3] + self._outline = pyglet.shapes.Rectangle(x-p, y-p, width+p+p, height+p+p, color, batch, bg_group) # Text and Caret: self._layout = IncrementalTextLayout(self._doc, width, height, multiline=False, batch=batch, group=fg_group) @@ -468,39 +468,39 @@ def __init__(self, text, x, y, width, super().__init__(x, y, width, height) - def _update_position(self): + def _update_position(self) -> None: self._layout.position = self._x, self._y, 0 self._outline.position = self._x - self._pad, self._y - self._pad @property - def value(self): + def value(self) -> str: return self._doc.text @value.setter - def value(self, value): + def value(self, value: str) -> None: assert type(value) is str, "This Widget's value must be a string." self._doc.text = value @property - def width(self): + def width(self) -> int: return self._width @width.setter - def width(self, value): + def width(self, value: int) -> None: self._width = value self._layout.width = value self._outline.width = value @property - def height(self): + def height(self) -> int: return self._height @height.setter - def height(self, value): + def height(self, value: int) -> None: self._height = value self._layout.height = value self._outline.height = value - + @property def focus(self) -> bool: return self._focus @@ -509,29 +509,29 @@ def focus(self) -> bool: def focus(self, value: bool) -> None: self._set_focus(value) - def _check_hit(self, x, y): + def _check_hit(self, x: int, y: int) -> bool: return self._x < x < self._x + self._width and self._y < y < self._y + self._height - def _set_focus(self, value): + def _set_focus(self, value: bool) -> None: self._focus = value self._caret.visible = value self._caret.layout = self._layout - def update_groups(self, order): + def update_groups(self, order: float) -> None: self._outline.group = Group(order=order + 1, parent=self._user_group) self._layout.group = Group(order=order + 2, parent=self._user_group) - def on_mouse_motion(self, x, y, dx, dy): + def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None: if not self.enabled: return - def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): + def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int) -> None: if not self.enabled: return if self._focus: self._caret.on_mouse_drag(x, y, dx, dy, buttons, modifiers) - def on_mouse_press(self, x, y, buttons, modifiers): + def on_mouse_press(self, x: int, y: int, buttons: int, modifiers: int) -> None: if not self.enabled: return if self._check_hit(x, y): @@ -540,7 +540,7 @@ def on_mouse_press(self, x, y, buttons, modifiers): else: self._set_focus(False) - def on_text(self, text): + def on_text(self, text: str) -> None: if not self.enabled: return if self._focus: @@ -551,19 +551,19 @@ def on_text(self, text): return self._caret.on_text(text) - def on_text_motion(self, motion): + def on_text_motion(self, motion: int) -> None: if not self.enabled: return if self._focus: self._caret.on_text_motion(motion) - def on_text_motion_select(self, motion): + def on_text_motion_select(self, motion: int) -> None: if not self.enabled: return if self._focus: self._caret.on_text_motion_select(motion) - def on_commit(self, text: str): + def on_commit(self, text: str) -> None: """Event: dispatches the current text when commited via Enter/Return key.""" diff --git a/pyglet/shapes.py b/pyglet/shapes.py index 75b3314b0..d6d03270f 100644 --- a/pyglet/shapes.py +++ b/pyglet/shapes.py @@ -149,7 +149,7 @@ def _sat(vertices, point): return True -def _get_segment(p0, p1, p2, p3, thickness=1, prev_miter=None, prev_scale=None): +def _get_segment(p0, p1, p2, p3, thickness=1.0, prev_miter=None, prev_scale=None): """Computes a line segment between the points p1 and p2. If points p0 or p3 are supplied then the segment p1->p2 will have the correct "miter" angle @@ -831,7 +831,7 @@ def __init__( *points: tuple[float, float], t: float = 1.0, segments: int = 100, - thickness: int =1, + thickness: int = 1.0, color: tuple[int, int, int, int] | tuple[int, int, int] = (255, 255, 255, 255), batch: Batch | None = None, group: Group | None = None