pyglet 2.1.13__py3-none-any.whl → 3.0.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyglet/__init__.py +67 -61
- pyglet/__init__.pyi +15 -8
- pyglet/app/__init__.py +22 -13
- pyglet/app/async_app.py +212 -0
- pyglet/app/base.py +2 -1
- pyglet/app/{xlib.py → linux.py} +3 -3
- pyglet/config/__init__.py +101 -0
- pyglet/config/gl/__init__.py +30 -0
- pyglet/config/gl/egl.py +120 -0
- pyglet/config/gl/macos.py +262 -0
- pyglet/config/gl/windows.py +267 -0
- pyglet/config/gl/x11.py +142 -0
- pyglet/customtypes.py +43 -2
- pyglet/display/__init__.py +8 -6
- pyglet/display/base.py +3 -63
- pyglet/display/cocoa.py +12 -17
- pyglet/display/emscripten.py +39 -0
- pyglet/display/headless.py +23 -30
- pyglet/display/wayland.py +157 -0
- pyglet/display/win32.py +5 -21
- pyglet/display/xlib.py +19 -27
- pyglet/display/xlib_vidmoderestore.py +2 -2
- pyglet/enums.py +183 -0
- pyglet/event.py +0 -1
- pyglet/experimental/geoshader_sprite.py +15 -13
- pyglet/experimental/hidraw.py +6 -15
- pyglet/experimental/multitexture_sprite.py +31 -19
- pyglet/experimental/particles.py +13 -35
- pyglet/font/__init__.py +251 -85
- pyglet/font/base.py +116 -61
- pyglet/font/dwrite/__init__.py +349 -204
- pyglet/font/dwrite/dwrite_lib.py +27 -5
- pyglet/font/fontconfig.py +14 -6
- pyglet/font/freetype.py +138 -87
- pyglet/font/freetype_lib.py +19 -0
- pyglet/font/group.py +179 -0
- pyglet/font/harfbuzz/__init__.py +3 -3
- pyglet/font/pyodide_js.py +310 -0
- pyglet/font/quartz.py +319 -126
- pyglet/font/ttf.py +45 -3
- pyglet/font/user.py +14 -19
- pyglet/font/win32.py +45 -21
- pyglet/graphics/__init__.py +8 -787
- pyglet/graphics/allocation.py +115 -1
- pyglet/graphics/api/__init__.py +77 -0
- pyglet/graphics/api/base.py +299 -0
- pyglet/graphics/api/gl/__init__.py +58 -0
- pyglet/graphics/api/gl/base.py +24 -0
- pyglet/graphics/{vertexbuffer.py → api/gl/buffer.py} +104 -159
- pyglet/graphics/api/gl/cocoa/context.py +76 -0
- pyglet/graphics/api/gl/context.py +391 -0
- pyglet/graphics/api/gl/default_shaders.py +0 -0
- pyglet/graphics/api/gl/draw.py +627 -0
- pyglet/graphics/api/gl/egl/__init__.py +0 -0
- pyglet/graphics/api/gl/egl/context.py +92 -0
- pyglet/graphics/api/gl/enums.py +76 -0
- pyglet/graphics/api/gl/framebuffer.py +315 -0
- pyglet/graphics/api/gl/gl.py +5463 -0
- pyglet/graphics/api/gl/gl_info.py +188 -0
- pyglet/graphics/api/gl/global_opengl.py +226 -0
- pyglet/{gl → graphics/api/gl}/lib.py +34 -18
- pyglet/graphics/api/gl/shader.py +1476 -0
- pyglet/graphics/api/gl/shapes.py +55 -0
- pyglet/graphics/api/gl/sprite.py +102 -0
- pyglet/graphics/api/gl/state.py +219 -0
- pyglet/graphics/api/gl/text.py +190 -0
- pyglet/graphics/api/gl/texture.py +1526 -0
- pyglet/graphics/{vertexarray.py → api/gl/vertexarray.py} +11 -13
- pyglet/graphics/api/gl/vertexdomain.py +751 -0
- pyglet/graphics/api/gl/win32/__init__.py +0 -0
- pyglet/graphics/api/gl/win32/context.py +108 -0
- pyglet/graphics/api/gl/win32/wgl_info.py +24 -0
- pyglet/graphics/api/gl/xlib/__init__.py +0 -0
- pyglet/graphics/api/gl/xlib/context.py +174 -0
- pyglet/{gl → graphics/api/gl/xlib}/glx_info.py +26 -31
- pyglet/graphics/api/gl1/__init__.py +0 -0
- pyglet/{gl → graphics/api/gl1}/gl_compat.py +3 -2
- pyglet/graphics/api/gl2/__init__.py +0 -0
- pyglet/graphics/api/gl2/buffer.py +320 -0
- pyglet/graphics/api/gl2/draw.py +600 -0
- pyglet/graphics/api/gl2/global_opengl.py +122 -0
- pyglet/graphics/api/gl2/shader.py +200 -0
- pyglet/graphics/api/gl2/shapes.py +51 -0
- pyglet/graphics/api/gl2/sprite.py +79 -0
- pyglet/graphics/api/gl2/text.py +175 -0
- pyglet/graphics/api/gl2/vertexdomain.py +364 -0
- pyglet/graphics/api/webgl/__init__.py +233 -0
- pyglet/graphics/api/webgl/buffer.py +302 -0
- pyglet/graphics/api/webgl/context.py +234 -0
- pyglet/graphics/api/webgl/draw.py +590 -0
- pyglet/graphics/api/webgl/enums.py +76 -0
- pyglet/graphics/api/webgl/framebuffer.py +360 -0
- pyglet/graphics/api/webgl/gl.py +1537 -0
- pyglet/graphics/api/webgl/gl_info.py +130 -0
- pyglet/graphics/api/webgl/shader.py +1346 -0
- pyglet/graphics/api/webgl/shapes.py +92 -0
- pyglet/graphics/api/webgl/sprite.py +102 -0
- pyglet/graphics/api/webgl/state.py +227 -0
- pyglet/graphics/api/webgl/text.py +187 -0
- pyglet/graphics/api/webgl/texture.py +1227 -0
- pyglet/graphics/api/webgl/vertexarray.py +54 -0
- pyglet/graphics/api/webgl/vertexdomain.py +616 -0
- pyglet/graphics/api/webgl/webgl_js.pyi +307 -0
- pyglet/{image → graphics}/atlas.py +33 -32
- pyglet/graphics/base.py +10 -0
- pyglet/graphics/buffer.py +245 -0
- pyglet/graphics/draw.py +578 -0
- pyglet/graphics/framebuffer.py +26 -0
- pyglet/graphics/instance.py +178 -69
- pyglet/graphics/shader.py +267 -1553
- pyglet/graphics/state.py +83 -0
- pyglet/graphics/texture.py +703 -0
- pyglet/graphics/vertexdomain.py +695 -538
- pyglet/gui/ninepatch.py +10 -10
- pyglet/gui/widgets.py +120 -10
- pyglet/image/__init__.py +20 -1973
- pyglet/image/animation.py +12 -12
- pyglet/image/base.py +730 -0
- pyglet/image/codecs/__init__.py +9 -0
- pyglet/image/codecs/bmp.py +53 -30
- pyglet/image/codecs/dds.py +53 -31
- pyglet/image/codecs/gdiplus.py +38 -14
- pyglet/image/codecs/gdkpixbuf2.py +0 -2
- pyglet/image/codecs/js_image.py +99 -0
- pyglet/image/codecs/ktx2.py +161 -0
- pyglet/image/codecs/pil.py +1 -1
- pyglet/image/codecs/png.py +1 -1
- pyglet/image/codecs/wic.py +11 -2
- pyglet/info.py +26 -24
- pyglet/input/__init__.py +8 -0
- pyglet/input/base.py +163 -105
- pyglet/input/controller.py +13 -19
- pyglet/input/controller_db.py +39 -24
- pyglet/input/emscripten/__init__.py +18 -0
- pyglet/input/emscripten/gamepad_js.py +397 -0
- pyglet/input/linux/__init__.py +11 -5
- pyglet/input/linux/evdev.py +10 -11
- pyglet/input/linux/x11_xinput.py +2 -2
- pyglet/input/linux/x11_xinput_tablet.py +1 -1
- pyglet/input/macos/__init__.py +7 -2
- pyglet/input/macos/darwin_gc.py +559 -0
- pyglet/input/win32/__init__.py +1 -1
- pyglet/input/win32/directinput.py +34 -29
- pyglet/input/win32/xinput.py +11 -61
- pyglet/lib.py +3 -3
- pyglet/libs/__init__.py +1 -1
- pyglet/{gl → libs/darwin}/agl.py +1 -1
- pyglet/libs/darwin/cocoapy/__init__.py +2 -2
- pyglet/libs/darwin/cocoapy/cocoahelpers.py +181 -0
- pyglet/libs/darwin/cocoapy/cocoalibs.py +31 -0
- pyglet/libs/darwin/cocoapy/cocoatypes.py +27 -0
- pyglet/libs/darwin/cocoapy/runtime.py +81 -45
- pyglet/libs/darwin/coreaudio.py +4 -4
- pyglet/{gl → libs/darwin}/lib_agl.py +9 -8
- pyglet/libs/darwin/quartzkey.py +1 -3
- pyglet/libs/egl/__init__.py +2 -0
- pyglet/libs/egl/egl_lib.py +576 -0
- pyglet/libs/egl/eglext.py +51 -5
- pyglet/libs/linux/__init__.py +0 -0
- pyglet/libs/linux/egl/__init__.py +0 -0
- pyglet/libs/linux/egl/eglext.py +22 -0
- pyglet/libs/linux/glx/__init__.py +0 -0
- pyglet/{gl → libs/linux/glx}/glx.py +13 -14
- pyglet/{gl → libs/linux/glx}/glxext_arb.py +408 -192
- pyglet/{gl → libs/linux/glx}/glxext_mesa.py +1 -1
- pyglet/{gl → libs/linux/glx}/glxext_nv.py +345 -164
- pyglet/{gl → libs/linux/glx}/lib_glx.py +3 -2
- pyglet/libs/linux/wayland/__init__.py +0 -0
- pyglet/libs/linux/wayland/client.py +1068 -0
- pyglet/libs/linux/wayland/lib_wayland.py +207 -0
- pyglet/libs/linux/wayland/wayland_egl.py +38 -0
- pyglet/libs/{wayland → linux/wayland}/xkbcommon.py +26 -0
- pyglet/libs/{x11 → linux/x11}/xf86vmode.py +4 -4
- pyglet/libs/{x11 → linux/x11}/xinerama.py +2 -2
- pyglet/libs/{x11 → linux/x11}/xinput.py +10 -10
- pyglet/libs/linux/x11/xrandr.py +0 -0
- pyglet/libs/{x11 → linux/x11}/xrender.py +1 -1
- pyglet/libs/shared/__init__.py +0 -0
- pyglet/libs/shared/spirv/__init__.py +0 -0
- pyglet/libs/shared/spirv/lib_shaderc.py +85 -0
- pyglet/libs/shared/spirv/lib_spirv_cross.py +126 -0
- pyglet/libs/win32/__init__.py +28 -8
- pyglet/libs/win32/constants.py +59 -48
- pyglet/libs/win32/context_managers.py +20 -3
- pyglet/libs/win32/dinput.py +105 -88
- pyglet/{gl → libs/win32}/lib_wgl.py +52 -26
- pyglet/libs/win32/types.py +58 -23
- pyglet/{gl → libs/win32}/wgl.py +32 -25
- pyglet/{gl → libs/win32}/wglext_arb.py +364 -2
- pyglet/media/__init__.py +9 -10
- pyglet/media/codecs/__init__.py +12 -1
- pyglet/media/codecs/base.py +99 -96
- pyglet/media/codecs/ffmpeg.py +2 -2
- pyglet/media/codecs/ffmpeg_lib/libavformat.py +3 -8
- pyglet/media/codecs/webaudio_pyodide.py +111 -0
- pyglet/media/drivers/__init__.py +9 -4
- pyglet/media/drivers/base.py +4 -4
- pyglet/media/drivers/openal/__init__.py +1 -1
- pyglet/media/drivers/openal/adaptation.py +3 -3
- pyglet/media/drivers/pulse/__init__.py +1 -1
- pyglet/media/drivers/pulse/adaptation.py +3 -3
- pyglet/media/drivers/pyodide_js/__init__.py +8 -0
- pyglet/media/drivers/pyodide_js/adaptation.py +288 -0
- pyglet/media/drivers/xaudio2/adaptation.py +3 -3
- pyglet/media/player.py +276 -193
- pyglet/media/player_worker_thread.py +1 -1
- pyglet/model/__init__.py +39 -29
- pyglet/model/codecs/base.py +4 -4
- pyglet/model/codecs/gltf.py +3 -3
- pyglet/model/codecs/obj.py +71 -43
- pyglet/resource.py +129 -78
- pyglet/shapes.py +154 -194
- pyglet/sprite.py +47 -164
- pyglet/text/__init__.py +44 -54
- pyglet/text/caret.py +12 -7
- pyglet/text/document.py +19 -17
- pyglet/text/formats/html.py +2 -2
- pyglet/text/formats/structured.py +10 -40
- pyglet/text/layout/__init__.py +20 -13
- pyglet/text/layout/base.py +176 -287
- pyglet/text/layout/incremental.py +9 -10
- pyglet/text/layout/scrolling.py +7 -95
- pyglet/window/__init__.py +183 -172
- pyglet/window/cocoa/__init__.py +62 -51
- pyglet/window/cocoa/pyglet_delegate.py +2 -25
- pyglet/window/cocoa/pyglet_view.py +9 -8
- pyglet/window/dialog/__init__.py +184 -0
- pyglet/window/dialog/base.py +99 -0
- pyglet/window/dialog/darwin.py +121 -0
- pyglet/window/dialog/linux.py +72 -0
- pyglet/window/dialog/windows.py +194 -0
- pyglet/window/emscripten/__init__.py +779 -0
- pyglet/window/headless/__init__.py +44 -28
- pyglet/window/key.py +2 -0
- pyglet/window/mouse.py +2 -2
- pyglet/window/wayland/__init__.py +377 -0
- pyglet/window/win32/__init__.py +101 -46
- pyglet/window/xlib/__init__.py +104 -66
- {pyglet-2.1.13.dist-info → pyglet-3.0.dev1.dist-info}/METADATA +2 -3
- pyglet-3.0.dev1.dist-info/RECORD +322 -0
- {pyglet-2.1.13.dist-info → pyglet-3.0.dev1.dist-info}/WHEEL +1 -1
- pyglet/gl/__init__.py +0 -208
- pyglet/gl/base.py +0 -499
- pyglet/gl/cocoa.py +0 -309
- pyglet/gl/gl.py +0 -4625
- pyglet/gl/gl.pyi +0 -2320
- pyglet/gl/gl_compat.pyi +0 -3097
- pyglet/gl/gl_info.py +0 -190
- pyglet/gl/headless.py +0 -166
- pyglet/gl/wgl_info.py +0 -36
- pyglet/gl/wglext_nv.py +0 -1096
- pyglet/gl/win32.py +0 -268
- pyglet/gl/xlib.py +0 -295
- pyglet/image/buffer.py +0 -274
- pyglet/image/codecs/s3tc.py +0 -354
- pyglet/libs/x11/xrandr.py +0 -166
- pyglet-2.1.13.dist-info/RECORD +0 -234
- /pyglet/{libs/wayland → graphics/api/gl/cocoa}/__init__.py +0 -0
- /pyglet/libs/{egl → linux/egl}/egl.py +0 -0
- /pyglet/libs/{egl → linux/egl}/lib.py +0 -0
- /pyglet/libs/{ioctl.py → linux/ioctl.py} +0 -0
- /pyglet/libs/{wayland → linux/wayland}/gbm.py +0 -0
- /pyglet/libs/{x11 → linux/x11}/__init__.py +0 -0
- /pyglet/libs/{x11 → linux/x11}/cursorfont.py +0 -0
- /pyglet/libs/{x11 → linux/x11}/xlib.py +0 -0
- /pyglet/libs/{x11 → linux/x11}/xsync.py +0 -0
- {pyglet-2.1.13.dist-info/licenses → pyglet-3.0.dev1.dist-info}/LICENSE +0 -0
pyglet/font/group.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import pyglet
|
|
6
|
+
from pyglet.font import base
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from pyglet.enums import Weight, Style, Stretch
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class _RangeEntry:
|
|
14
|
+
start: int
|
|
15
|
+
end: int
|
|
16
|
+
family: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FontGroup:
|
|
20
|
+
"""A collection of fonts that can be used like a single font.
|
|
21
|
+
|
|
22
|
+
Each font can be assigned a range of Unicode values that it is set to handle.
|
|
23
|
+
|
|
24
|
+
.. versionadded:: 3.0
|
|
25
|
+
"""
|
|
26
|
+
_instance_cache: dict[tuple[float, str | Weight, str | Style, str | Stretch, int], FontGroupInstance]
|
|
27
|
+
_ranges: list[_RangeEntry]
|
|
28
|
+
|
|
29
|
+
def __init__(self, name: str) -> None:
|
|
30
|
+
"""Create a new font group.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
name:
|
|
34
|
+
A unique name describing the grouping; must be unique enough to not collide with an existing font.
|
|
35
|
+
"""
|
|
36
|
+
self.name = name
|
|
37
|
+
self._ranges = []
|
|
38
|
+
self._instance_cache = {}
|
|
39
|
+
|
|
40
|
+
def add(self, family: str, start: int | str, end: int | str) -> FontGroup:
|
|
41
|
+
"""Add a font family responsible for a range of characters.
|
|
42
|
+
|
|
43
|
+
The first match will be used by layouts.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
family:
|
|
47
|
+
The name of the font family for this range.
|
|
48
|
+
start:
|
|
49
|
+
The start of the range. This may be a single-character string or an integer corresponding to a Unicode
|
|
50
|
+
code point.
|
|
51
|
+
end:
|
|
52
|
+
The end of the range. This may be a single-character string or an integer corresponding to a Unicode
|
|
53
|
+
code point.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
This existing font group instance.
|
|
57
|
+
"""
|
|
58
|
+
self._ranges.append(_RangeEntry(start, end, family))
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
def get_font(self,
|
|
62
|
+
size: float | None,
|
|
63
|
+
weight: str | None = "normal",
|
|
64
|
+
style: str | None = "normal",
|
|
65
|
+
stretch: str | None = "normal",
|
|
66
|
+
dpi: int | None = None,
|
|
67
|
+
) -> FontGroupInstance:
|
|
68
|
+
size = size or 12
|
|
69
|
+
dpi = dpi or 96
|
|
70
|
+
weight = weight or Weight.NORMAL
|
|
71
|
+
style = style or Style.NORMAL
|
|
72
|
+
stretch = stretch or Stretch.NORMAL
|
|
73
|
+
|
|
74
|
+
descriptor = (size, weight, style, stretch, dpi)
|
|
75
|
+
inst = self._instance_cache.get(descriptor)
|
|
76
|
+
if inst is None:
|
|
77
|
+
inst = FontGroupInstance(self, size, weight, style, stretch, dpi)
|
|
78
|
+
self._instance_cache[descriptor] = inst
|
|
79
|
+
return inst
|
|
80
|
+
|
|
81
|
+
def _family_for_char(self, ch: str) -> str | None:
|
|
82
|
+
cp = ord(ch)
|
|
83
|
+
for r in self._ranges:
|
|
84
|
+
if r.start <= cp <= r.end:
|
|
85
|
+
return r.family
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
class FontGroupInstance(base.Font):
|
|
89
|
+
"""A font instance based off the FontGroup."""
|
|
90
|
+
_child_cache: dict[str, base.Font]
|
|
91
|
+
|
|
92
|
+
def __init__(self, group: FontGroup, size: float, weight: str | Weight, style: str | Style, # noqa: D107
|
|
93
|
+
stretch: str | Stretch, dpi: int | None) -> None:
|
|
94
|
+
super().__init__("", size, weight, style, stretch, dpi)
|
|
95
|
+
self._name = self._get_name()
|
|
96
|
+
self._group = group
|
|
97
|
+
|
|
98
|
+
self._child_cache = {}
|
|
99
|
+
self.glyphs.clear() # This itself doesn't own glyphs
|
|
100
|
+
|
|
101
|
+
def _get_name(self) -> str:
|
|
102
|
+
"""Generates a unique descriptor name for this instance."""
|
|
103
|
+
ital = "Italic" if self.style else "Regular"
|
|
104
|
+
return f"{self.name} ({int(self.size)}px {ital} w{self.weight} s{self.stretch} @{self.dpi}dpi)"
|
|
105
|
+
|
|
106
|
+
def _resolve_child(self, family: str) -> base.Font:
|
|
107
|
+
f = self._child_cache.get(family)
|
|
108
|
+
if f is None:
|
|
109
|
+
f = pyglet.font.load(family,
|
|
110
|
+
size=self.size,
|
|
111
|
+
weight=self.weight,
|
|
112
|
+
style=self.style,
|
|
113
|
+
stretch=self.stretch,
|
|
114
|
+
dpi=self.dpi)
|
|
115
|
+
self._child_cache[family] = f
|
|
116
|
+
|
|
117
|
+
self.ascent = max(self.ascent, getattr(f, "ascent", 0))
|
|
118
|
+
self.descent = max(self.descent, getattr(f, "descent", 0))
|
|
119
|
+
return f
|
|
120
|
+
|
|
121
|
+
def _font_for_cluster(self, cluster: str) -> base.Font | None:
|
|
122
|
+
if not cluster:
|
|
123
|
+
return None
|
|
124
|
+
ft_fam = self._group._family_for_char(cluster[0]) # noqa: SLF001
|
|
125
|
+
if ft_fam is None and self._group._ranges: # noqa: SLF001
|
|
126
|
+
# Default to first font in the group if nothing matches
|
|
127
|
+
fam = self._group._ranges[0].family # noqa: SLF001
|
|
128
|
+
return self._resolve_child(ft_fam) if ft_fam else None
|
|
129
|
+
|
|
130
|
+
def get_glyphs(self, text: str, shaping: bool = False) -> tuple[list[base.Glyph], list[base.GlyphPosition]]:
|
|
131
|
+
glyphs: list[base.Glyph] = []
|
|
132
|
+
offsets: list[base.GlyphPosition] = []
|
|
133
|
+
|
|
134
|
+
for cluster in base.get_grapheme_clusters(str(text)):
|
|
135
|
+
c = " " if cluster == "\t" else cluster
|
|
136
|
+
fnt = self._font_for_cluster(c)
|
|
137
|
+
if fnt is None:
|
|
138
|
+
self._initialize_renderer()
|
|
139
|
+
gs = self._missing_glyph or self._glyph_renderer.render(" ")
|
|
140
|
+
gp = base.GlyphPosition(0, 0, 0, 0)
|
|
141
|
+
glyphs.append(gs)
|
|
142
|
+
offsets.append(gp)
|
|
143
|
+
else:
|
|
144
|
+
gs, gp = fnt.get_glyphs(c, shaping)
|
|
145
|
+
glyphs.extend(gs)
|
|
146
|
+
offsets.extend(gp)
|
|
147
|
+
|
|
148
|
+
return glyphs, offsets
|
|
149
|
+
|
|
150
|
+
def get_text_size(self, text: str) -> tuple[int, int]:
|
|
151
|
+
if not text:
|
|
152
|
+
return 0, 0
|
|
153
|
+
|
|
154
|
+
total_w = 0
|
|
155
|
+
max_ascent = self.ascent
|
|
156
|
+
max_descent = self.descent
|
|
157
|
+
|
|
158
|
+
run_font: base.Font | None = None
|
|
159
|
+
run_text: list[str] = []
|
|
160
|
+
|
|
161
|
+
def flush() -> None:
|
|
162
|
+
nonlocal total_w, max_ascent, max_descent, run_font, run_text
|
|
163
|
+
if run_font and run_text:
|
|
164
|
+
w, _ = run_font.get_text_size("".join(run_text))
|
|
165
|
+
total_w += w
|
|
166
|
+
max_ascent = max(max_ascent, getattr(run_font, "ascent", 0))
|
|
167
|
+
max_descent = max(max_descent, getattr(run_font, "descent", 0))
|
|
168
|
+
run_font = None
|
|
169
|
+
run_text = []
|
|
170
|
+
|
|
171
|
+
for cluster in base.get_grapheme_clusters(text):
|
|
172
|
+
f = self._font_for_cluster(cluster)
|
|
173
|
+
if f is not run_font:
|
|
174
|
+
flush()
|
|
175
|
+
run_font = f
|
|
176
|
+
run_text.append(" " if cluster == "\t" else cluster)
|
|
177
|
+
|
|
178
|
+
flush()
|
|
179
|
+
return (total_w, max_ascent + max_descent)
|
pyglet/font/harfbuzz/__init__.py
CHANGED
|
@@ -51,7 +51,7 @@ elif sys.maxunicode == 0xFFFF: # UTF-16
|
|
|
51
51
|
|
|
52
52
|
def get_resource_from_ct_font(font: QuartzFont):
|
|
53
53
|
"""Get a harfbuzz resource object from a CoreText (Mac) font."""
|
|
54
|
-
key = (font.name, font.weight, font.
|
|
54
|
+
key = (font.name, font.weight, font.style, font.stretch)
|
|
55
55
|
if key in _hb_cache:
|
|
56
56
|
return _hb_cache[key]
|
|
57
57
|
|
|
@@ -64,7 +64,7 @@ def get_resource_from_ct_font(font: QuartzFont):
|
|
|
64
64
|
|
|
65
65
|
def get_resource_from_dw_font(font: Win32DirectWriteFont) -> _HarfbuzzResources:
|
|
66
66
|
"""Get a harfbuzz resource object from a DirectWrite (Windows) font."""
|
|
67
|
-
key = (font.name, font.weight, font.
|
|
67
|
+
key = (font.name, font.weight, font.style, font.stretch)
|
|
68
68
|
if key in _hb_cache:
|
|
69
69
|
return _hb_cache[key]
|
|
70
70
|
|
|
@@ -79,7 +79,7 @@ def get_resource_from_dw_font(font: Win32DirectWriteFont) -> _HarfbuzzResources:
|
|
|
79
79
|
|
|
80
80
|
def get_resource_from_ft_font(font: FreeTypeFont) -> _HarfbuzzResources:
|
|
81
81
|
"""Get a harfbuzz resource object from a FreeType (Linux) font."""
|
|
82
|
-
key = (font.name, font.weight, font.
|
|
82
|
+
key = (font.name, font.weight, font.style, font.stretch)
|
|
83
83
|
if key in _hb_cache:
|
|
84
84
|
return _hb_cache[key]
|
|
85
85
|
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import math
|
|
5
|
+
from asyncio import Task
|
|
6
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
7
|
+
|
|
8
|
+
import pyglet
|
|
9
|
+
from pyglet.font.ttf import TruetypeInfoBytes
|
|
10
|
+
|
|
11
|
+
_debug = pyglet.options.debug_font
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import js
|
|
15
|
+
import pyodide.ffi
|
|
16
|
+
except ImportError:
|
|
17
|
+
raise ImportError
|
|
18
|
+
|
|
19
|
+
from pyglet.font import base, FontManager
|
|
20
|
+
from pyglet.font.base import Glyph, FontException, GlyphPosition
|
|
21
|
+
|
|
22
|
+
from pyglet.image import ImageData
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from pyglet.image import AbstractImage
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_font_canvas = js.document.createElement("canvas")
|
|
29
|
+
_font_canvas.id = "font_canvas"
|
|
30
|
+
# Added desynchronized for testing. Supposedly lower latency, but may introduce artifacts?
|
|
31
|
+
# Doesn't seem to affect quality since we are just using this to get pixel data. Remove if problem in the future.
|
|
32
|
+
_font_context = _font_canvas.getContext("2d", willReadFrequently=True, desynchronized=True, antialias=False)
|
|
33
|
+
|
|
34
|
+
class PyodideGlyphRenderer(base.GlyphRenderer):
|
|
35
|
+
font: JavascriptPyodideFont
|
|
36
|
+
def __init__(self, font: JavascriptPyodideFont) -> None: # noqa: D107
|
|
37
|
+
self.font = font
|
|
38
|
+
super().__init__(font)
|
|
39
|
+
self.temp_save = []
|
|
40
|
+
|
|
41
|
+
def render(self, text: str) -> Glyph:
|
|
42
|
+
_font_context.font = self.font.js_name
|
|
43
|
+
metrics = _font_context.measureText(text)
|
|
44
|
+
w = max(1, int(math.ceil(metrics.width)))
|
|
45
|
+
h = max(1, int(math.ceil(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent)))
|
|
46
|
+
|
|
47
|
+
# Setting the canvas size seems to reset the context settings?
|
|
48
|
+
_font_canvas.width = w
|
|
49
|
+
_font_canvas.height = h
|
|
50
|
+
_font_context.imageSmoothingEnabled = False # Doesn't seem to make a difference with antialiasing?
|
|
51
|
+
#_font_context.mozImageSmoothingEnabled = False
|
|
52
|
+
#_font_context.webkitImageSmoothingEnabled = False
|
|
53
|
+
#_font_context.msImageSmoothingEnabled = False
|
|
54
|
+
_font_context.font = self.font.js_name
|
|
55
|
+
_font_context.fillStyle = 'white'
|
|
56
|
+
|
|
57
|
+
_font_context.translate(0, h) # Move down
|
|
58
|
+
_font_context.scale(1, -1) # Flip vertically
|
|
59
|
+
|
|
60
|
+
# Draw to context
|
|
61
|
+
_font_context.fillText(text, 0, max(1, int(math.ceil(metrics.actualBoundingBoxAscent))))
|
|
62
|
+
|
|
63
|
+
image_data = _font_context.getImageData(0, 0, w, h)
|
|
64
|
+
pixel_data = image_data.data # Uint8Array
|
|
65
|
+
|
|
66
|
+
image = ImageData(w, h, 'RGBA', pixel_data)
|
|
67
|
+
|
|
68
|
+
glyph = self.font.create_glyph(image)
|
|
69
|
+
glyph.set_bearings(int(math.ceil(metrics.actualBoundingBoxDescent)), 0, w)
|
|
70
|
+
return glyph
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _measure_font_width(font_family: str) -> int:
|
|
74
|
+
"""Use a DOM element to measure the text width of a given string using a font family."""
|
|
75
|
+
_hidden_div.style.fontSize = "32px"
|
|
76
|
+
_hidden_div.style.fontFamily = font_family
|
|
77
|
+
return _hidden_div.offsetWidth
|
|
78
|
+
|
|
79
|
+
# DIV element used for measuring width for font fallback behavior. Do not remove.
|
|
80
|
+
_hidden_div = js.document.createElement("div")
|
|
81
|
+
_hidden_div.textContent = "PYGLET_FONT_WIDTH"
|
|
82
|
+
_hidden_div.style.visibility = "hidden"
|
|
83
|
+
_hidden_div.style.position = "absolute"
|
|
84
|
+
_hidden_div.id = "_font_resolver"
|
|
85
|
+
js.document.body.appendChild(_hidden_div)
|
|
86
|
+
|
|
87
|
+
class JavascriptPyodideFont(base.Font):
|
|
88
|
+
glyph_renderer_class = PyodideGlyphRenderer
|
|
89
|
+
_glyph_renderer: PyodideGlyphRenderer
|
|
90
|
+
|
|
91
|
+
_default_serif_width = _measure_font_width("serif")
|
|
92
|
+
_default_sans_serif_width = _measure_font_width("sans-serif")
|
|
93
|
+
|
|
94
|
+
# Cache font data by the loaded name dict.
|
|
95
|
+
_font_data_cache: ClassVar[dict] = {}
|
|
96
|
+
_name_font_cache: ClassVar[dict] = {}
|
|
97
|
+
|
|
98
|
+
def __init__(self, name: str, size: float, weight: str = "normal", style: str = "normal", stretch: str = "normal",
|
|
99
|
+
dpi: int | None = None) -> None:
|
|
100
|
+
self._glyph_renderer = None
|
|
101
|
+
super().__init__(name, size, weight, style, stretch, dpi)
|
|
102
|
+
|
|
103
|
+
if isinstance(weight, str):
|
|
104
|
+
self._weight = name_to_weight.get(weight.lower(), "normal")
|
|
105
|
+
else:
|
|
106
|
+
self._weight = "bold" if weight is True else "normal"
|
|
107
|
+
|
|
108
|
+
if isinstance(stretch, str):
|
|
109
|
+
self._stretch = _name_to_stretch.get(stretch.lower(), "normal")
|
|
110
|
+
else:
|
|
111
|
+
self._stretch = "normal"
|
|
112
|
+
|
|
113
|
+
self._italic = "italic" if style is True else "normal"
|
|
114
|
+
|
|
115
|
+
self.js_name = f"{self._italic} {self._weight} {self.pixel_size}px '{name}'"
|
|
116
|
+
|
|
117
|
+
_font_context.font = self.js_name
|
|
118
|
+
metrics = _font_context.measureText("A")
|
|
119
|
+
self.ascent = metrics.fontBoundingBoxAscent
|
|
120
|
+
self.descent = -metrics.fontBoundingBoxDescent
|
|
121
|
+
|
|
122
|
+
def get_text_size(self, text: str) -> tuple[int, int]:
|
|
123
|
+
_font_context.font = self.js_name
|
|
124
|
+
metrics = _font_context.measureText(text)
|
|
125
|
+
w = max(1, int(math.ceil(metrics.width)))
|
|
126
|
+
h = max(1, int(math.ceil(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent)))
|
|
127
|
+
return w, h
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def add_font_data(cls, data: bytes, manager: FontManager) -> Task:
|
|
131
|
+
ttf_info = TruetypeInfoBytes(data)
|
|
132
|
+
family = ttf_info.get_name("family") # Family Name
|
|
133
|
+
if family is None:
|
|
134
|
+
raise FontException("Could not read the font family name.")
|
|
135
|
+
|
|
136
|
+
subfamily = ttf_info.get_name("subfamily") # Contains words like Regular, Bold, etc.
|
|
137
|
+
if subfamily is None:
|
|
138
|
+
raise FontException("Could not read the font subfamily name.")
|
|
139
|
+
|
|
140
|
+
fullname = ttf_info.get_name("name") # Usually combines Family + Subfamily, but not always.
|
|
141
|
+
#if fullname is None:
|
|
142
|
+
# raise FontException("Could not read the font name.")
|
|
143
|
+
|
|
144
|
+
weight = ttf_info.get_weight_class() # TTF weight value like 700.
|
|
145
|
+
clamped_weight = min(max(weight, 100), 900) # clamp 100-900.
|
|
146
|
+
|
|
147
|
+
ttf_stretch_id = ttf_info.get_width_class()
|
|
148
|
+
italic = "italic" if ttf_info.is_italic() else "normal"
|
|
149
|
+
js_arr = js.Uint8Array.new(data)
|
|
150
|
+
|
|
151
|
+
weight_name = _ttf_weight_to_name.get(clamped_weight, "normal")
|
|
152
|
+
stretch_name = _width_class_to_pyglet_stretch.get(ttf_stretch_id, "normal")
|
|
153
|
+
|
|
154
|
+
# Specify family by the name and the weight.
|
|
155
|
+
fam_font = js.window.FontFace.new(family, js_arr.buffer,
|
|
156
|
+
weight=str(clamped_weight),
|
|
157
|
+
stretch=_width_class_to_js_stretch.get(ttf_stretch_id, "normal"),
|
|
158
|
+
style=italic,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if _debug:
|
|
162
|
+
js.console.log(f"Loaded custom font (family: {family}, subfamily: {subfamily}, full name: {fullname}, "
|
|
163
|
+
f"weight: {weight}, stretch_width={ttf_stretch_id})")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
#if family != fullname:
|
|
168
|
+
# Full font name may not always match the family name, add both to cover both.
|
|
169
|
+
# full_font = js.window.FontFace.new(fullname, js_arr.buffer)
|
|
170
|
+
|
|
171
|
+
async def _load_fonts() -> bool:
|
|
172
|
+
try:
|
|
173
|
+
await fam_font.load()
|
|
174
|
+
except Exception as e: # noqa: BLE001
|
|
175
|
+
print("Exception occurred loading Family Font:", e)
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
js.document.fonts.add(fam_font)
|
|
179
|
+
# js.document.body.style.fontFamily = family
|
|
180
|
+
# if _debug:
|
|
181
|
+
# js.console.log(f"Loaded Family Font: {family}")
|
|
182
|
+
|
|
183
|
+
manager._add_loaded_font({(family, weight_name, italic, stretch_name)}) # noqa: SLF001
|
|
184
|
+
|
|
185
|
+
# if family != fullname:
|
|
186
|
+
# try:
|
|
187
|
+
# await full_font.load()
|
|
188
|
+
# except Exception as e:
|
|
189
|
+
# print("Exception occurred loading Name Font:", e)
|
|
190
|
+
# return False
|
|
191
|
+
# js.document.fonts.add(full_font)
|
|
192
|
+
# if _debug:
|
|
193
|
+
# js.console.log(f"Loaded Named Font: {fullname}")
|
|
194
|
+
#
|
|
195
|
+
return True
|
|
196
|
+
|
|
197
|
+
return asyncio.create_task(_load_fonts())
|
|
198
|
+
|
|
199
|
+
def create_glyph(self, img: ImageData) -> Glyph:
|
|
200
|
+
return super().create_glyph(img)
|
|
201
|
+
|
|
202
|
+
def get_glyphs(self, text: str, shaping: bool) -> tuple[list[Glyph], list[GlyphPosition]]:
|
|
203
|
+
self._initialize_renderer()
|
|
204
|
+
|
|
205
|
+
glyphs = [] # glyphs that are committed.
|
|
206
|
+
offsets = []
|
|
207
|
+
for c in base.get_grapheme_clusters(str(text)):
|
|
208
|
+
# Get the glyph for 'c'. Hide tabs (Windows and Linux render boxes)
|
|
209
|
+
if c == "\t":
|
|
210
|
+
c = " " # noqa: PLW2901
|
|
211
|
+
if c not in self.glyphs:
|
|
212
|
+
self.glyphs[c] = self._glyph_renderer.render(c)
|
|
213
|
+
glyphs.append(self.glyphs[c])
|
|
214
|
+
offsets.append(GlyphPosition(0, 0, 0, 0))
|
|
215
|
+
return glyphs, offsets
|
|
216
|
+
|
|
217
|
+
def get_glyphs_for_width(self, text: str, width: int) -> list[Glyph]:
|
|
218
|
+
return super().get_glyphs_for_width(text, width)
|
|
219
|
+
|
|
220
|
+
@classmethod
|
|
221
|
+
def have_font(cls: type[JavascriptPyodideFont], name: str) -> bool:
|
|
222
|
+
"""A very round about way to determine if a font exists for JavaScript.
|
|
223
|
+
|
|
224
|
+
JavaScript does not have any way to query system or custom loaded fonts without experimental or
|
|
225
|
+
unreliable API's. Furthermore, you cannot determine what font is actually being used to render either.
|
|
226
|
+
|
|
227
|
+
According to docs, CSS should guarantee the font families of "serif" and "sans-serif".
|
|
228
|
+
|
|
229
|
+
Therefore, a hidden element will be used to measure a string to check for a size match between the above
|
|
230
|
+
font families. If the text matches, then a fallback font was used.
|
|
231
|
+
"""
|
|
232
|
+
match_serif_name = f"'{name}', serif"
|
|
233
|
+
match_sans_serif_name = f"'{name}', sans-serif"
|
|
234
|
+
|
|
235
|
+
# Check if the font matches our serif.
|
|
236
|
+
if (_measure_font_width(match_serif_name) == cls._default_serif_width and
|
|
237
|
+
# Font might actually be the fallback serif, check if it matches a sans serif.
|
|
238
|
+
_measure_font_width(match_sans_serif_name) == cls._default_sans_serif_width):
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
# The font should theoretically be available.
|
|
242
|
+
return True
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def name(self) -> str:
|
|
247
|
+
return self._name
|
|
248
|
+
|
|
249
|
+
# JavaScript/CSS naming, not Pyglet naming.
|
|
250
|
+
_width_class_to_js_stretch = {
|
|
251
|
+
1: "ultra-condensed",
|
|
252
|
+
2: "extra-condensed",
|
|
253
|
+
3: "condensed",
|
|
254
|
+
4: "semi-condensed",
|
|
255
|
+
5: "normal",
|
|
256
|
+
6: "semi-expanded",
|
|
257
|
+
7: "expanded",
|
|
258
|
+
8: "extra-expanded",
|
|
259
|
+
9: "ultra-expanded",
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
_width_class_to_pyglet_stretch = {
|
|
263
|
+
1: "ultracondensed",
|
|
264
|
+
2: "extracondensed",
|
|
265
|
+
3: "condensed",
|
|
266
|
+
4: "semicondensed",
|
|
267
|
+
5: "normal",
|
|
268
|
+
6: "semiexpanded",
|
|
269
|
+
7: "expanded",
|
|
270
|
+
8: "extraexpanded",
|
|
271
|
+
9: "ultraexpanded",
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
name_to_weight = {
|
|
275
|
+
'thin': 100,
|
|
276
|
+
'extralight': 200,
|
|
277
|
+
'light': 300,
|
|
278
|
+
'normal': 400,
|
|
279
|
+
'medium': 500,
|
|
280
|
+
'semibold': 600,
|
|
281
|
+
'bold': 700,
|
|
282
|
+
'extrabold': 800,
|
|
283
|
+
'black': 900,
|
|
284
|
+
}
|
|
285
|
+
_ttf_weight_to_name = {
|
|
286
|
+
100: 'thin',
|
|
287
|
+
200: 'extralight',
|
|
288
|
+
300: 'light',
|
|
289
|
+
400: 'normal',
|
|
290
|
+
500: 'medium',
|
|
291
|
+
600: 'semibold',
|
|
292
|
+
700: 'bold',
|
|
293
|
+
800: 'extrabold',
|
|
294
|
+
900: 'black',
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
_name_to_stretch = {
|
|
298
|
+
"undefined": "normal",
|
|
299
|
+
"ultracondensed": "ultra-condensed",
|
|
300
|
+
"extracondensed": "extra-condensed",
|
|
301
|
+
"condensed": "condensed",
|
|
302
|
+
"semicondensed": "semi-condensed",
|
|
303
|
+
"normal": "normal",
|
|
304
|
+
"medium": "normal",
|
|
305
|
+
"semiexpanded": "semi-expanded",
|
|
306
|
+
"expanded": "expanded",
|
|
307
|
+
"extraexpanded": "extra-expanded",
|
|
308
|
+
"narrow": "condensed",
|
|
309
|
+
"ultraexpanded": "ultra-expanded",
|
|
310
|
+
}
|