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.
Files changed (267) hide show
  1. pyglet/__init__.py +67 -61
  2. pyglet/__init__.pyi +15 -8
  3. pyglet/app/__init__.py +22 -13
  4. pyglet/app/async_app.py +212 -0
  5. pyglet/app/base.py +2 -1
  6. pyglet/app/{xlib.py → linux.py} +3 -3
  7. pyglet/config/__init__.py +101 -0
  8. pyglet/config/gl/__init__.py +30 -0
  9. pyglet/config/gl/egl.py +120 -0
  10. pyglet/config/gl/macos.py +262 -0
  11. pyglet/config/gl/windows.py +267 -0
  12. pyglet/config/gl/x11.py +142 -0
  13. pyglet/customtypes.py +43 -2
  14. pyglet/display/__init__.py +8 -6
  15. pyglet/display/base.py +3 -63
  16. pyglet/display/cocoa.py +12 -17
  17. pyglet/display/emscripten.py +39 -0
  18. pyglet/display/headless.py +23 -30
  19. pyglet/display/wayland.py +157 -0
  20. pyglet/display/win32.py +5 -21
  21. pyglet/display/xlib.py +19 -27
  22. pyglet/display/xlib_vidmoderestore.py +2 -2
  23. pyglet/enums.py +183 -0
  24. pyglet/event.py +0 -1
  25. pyglet/experimental/geoshader_sprite.py +15 -13
  26. pyglet/experimental/hidraw.py +6 -15
  27. pyglet/experimental/multitexture_sprite.py +31 -19
  28. pyglet/experimental/particles.py +13 -35
  29. pyglet/font/__init__.py +251 -85
  30. pyglet/font/base.py +116 -61
  31. pyglet/font/dwrite/__init__.py +349 -204
  32. pyglet/font/dwrite/dwrite_lib.py +27 -5
  33. pyglet/font/fontconfig.py +14 -6
  34. pyglet/font/freetype.py +138 -87
  35. pyglet/font/freetype_lib.py +19 -0
  36. pyglet/font/group.py +179 -0
  37. pyglet/font/harfbuzz/__init__.py +3 -3
  38. pyglet/font/pyodide_js.py +310 -0
  39. pyglet/font/quartz.py +319 -126
  40. pyglet/font/ttf.py +45 -3
  41. pyglet/font/user.py +14 -19
  42. pyglet/font/win32.py +45 -21
  43. pyglet/graphics/__init__.py +8 -787
  44. pyglet/graphics/allocation.py +115 -1
  45. pyglet/graphics/api/__init__.py +77 -0
  46. pyglet/graphics/api/base.py +299 -0
  47. pyglet/graphics/api/gl/__init__.py +58 -0
  48. pyglet/graphics/api/gl/base.py +24 -0
  49. pyglet/graphics/{vertexbuffer.py → api/gl/buffer.py} +104 -159
  50. pyglet/graphics/api/gl/cocoa/context.py +76 -0
  51. pyglet/graphics/api/gl/context.py +391 -0
  52. pyglet/graphics/api/gl/default_shaders.py +0 -0
  53. pyglet/graphics/api/gl/draw.py +627 -0
  54. pyglet/graphics/api/gl/egl/__init__.py +0 -0
  55. pyglet/graphics/api/gl/egl/context.py +92 -0
  56. pyglet/graphics/api/gl/enums.py +76 -0
  57. pyglet/graphics/api/gl/framebuffer.py +315 -0
  58. pyglet/graphics/api/gl/gl.py +5463 -0
  59. pyglet/graphics/api/gl/gl_info.py +188 -0
  60. pyglet/graphics/api/gl/global_opengl.py +226 -0
  61. pyglet/{gl → graphics/api/gl}/lib.py +34 -18
  62. pyglet/graphics/api/gl/shader.py +1476 -0
  63. pyglet/graphics/api/gl/shapes.py +55 -0
  64. pyglet/graphics/api/gl/sprite.py +102 -0
  65. pyglet/graphics/api/gl/state.py +219 -0
  66. pyglet/graphics/api/gl/text.py +190 -0
  67. pyglet/graphics/api/gl/texture.py +1526 -0
  68. pyglet/graphics/{vertexarray.py → api/gl/vertexarray.py} +11 -13
  69. pyglet/graphics/api/gl/vertexdomain.py +751 -0
  70. pyglet/graphics/api/gl/win32/__init__.py +0 -0
  71. pyglet/graphics/api/gl/win32/context.py +108 -0
  72. pyglet/graphics/api/gl/win32/wgl_info.py +24 -0
  73. pyglet/graphics/api/gl/xlib/__init__.py +0 -0
  74. pyglet/graphics/api/gl/xlib/context.py +174 -0
  75. pyglet/{gl → graphics/api/gl/xlib}/glx_info.py +26 -31
  76. pyglet/graphics/api/gl1/__init__.py +0 -0
  77. pyglet/{gl → graphics/api/gl1}/gl_compat.py +3 -2
  78. pyglet/graphics/api/gl2/__init__.py +0 -0
  79. pyglet/graphics/api/gl2/buffer.py +320 -0
  80. pyglet/graphics/api/gl2/draw.py +600 -0
  81. pyglet/graphics/api/gl2/global_opengl.py +122 -0
  82. pyglet/graphics/api/gl2/shader.py +200 -0
  83. pyglet/graphics/api/gl2/shapes.py +51 -0
  84. pyglet/graphics/api/gl2/sprite.py +79 -0
  85. pyglet/graphics/api/gl2/text.py +175 -0
  86. pyglet/graphics/api/gl2/vertexdomain.py +364 -0
  87. pyglet/graphics/api/webgl/__init__.py +233 -0
  88. pyglet/graphics/api/webgl/buffer.py +302 -0
  89. pyglet/graphics/api/webgl/context.py +234 -0
  90. pyglet/graphics/api/webgl/draw.py +590 -0
  91. pyglet/graphics/api/webgl/enums.py +76 -0
  92. pyglet/graphics/api/webgl/framebuffer.py +360 -0
  93. pyglet/graphics/api/webgl/gl.py +1537 -0
  94. pyglet/graphics/api/webgl/gl_info.py +130 -0
  95. pyglet/graphics/api/webgl/shader.py +1346 -0
  96. pyglet/graphics/api/webgl/shapes.py +92 -0
  97. pyglet/graphics/api/webgl/sprite.py +102 -0
  98. pyglet/graphics/api/webgl/state.py +227 -0
  99. pyglet/graphics/api/webgl/text.py +187 -0
  100. pyglet/graphics/api/webgl/texture.py +1227 -0
  101. pyglet/graphics/api/webgl/vertexarray.py +54 -0
  102. pyglet/graphics/api/webgl/vertexdomain.py +616 -0
  103. pyglet/graphics/api/webgl/webgl_js.pyi +307 -0
  104. pyglet/{image → graphics}/atlas.py +33 -32
  105. pyglet/graphics/base.py +10 -0
  106. pyglet/graphics/buffer.py +245 -0
  107. pyglet/graphics/draw.py +578 -0
  108. pyglet/graphics/framebuffer.py +26 -0
  109. pyglet/graphics/instance.py +178 -69
  110. pyglet/graphics/shader.py +267 -1553
  111. pyglet/graphics/state.py +83 -0
  112. pyglet/graphics/texture.py +703 -0
  113. pyglet/graphics/vertexdomain.py +695 -538
  114. pyglet/gui/ninepatch.py +10 -10
  115. pyglet/gui/widgets.py +120 -10
  116. pyglet/image/__init__.py +20 -1973
  117. pyglet/image/animation.py +12 -12
  118. pyglet/image/base.py +730 -0
  119. pyglet/image/codecs/__init__.py +9 -0
  120. pyglet/image/codecs/bmp.py +53 -30
  121. pyglet/image/codecs/dds.py +53 -31
  122. pyglet/image/codecs/gdiplus.py +38 -14
  123. pyglet/image/codecs/gdkpixbuf2.py +0 -2
  124. pyglet/image/codecs/js_image.py +99 -0
  125. pyglet/image/codecs/ktx2.py +161 -0
  126. pyglet/image/codecs/pil.py +1 -1
  127. pyglet/image/codecs/png.py +1 -1
  128. pyglet/image/codecs/wic.py +11 -2
  129. pyglet/info.py +26 -24
  130. pyglet/input/__init__.py +8 -0
  131. pyglet/input/base.py +163 -105
  132. pyglet/input/controller.py +13 -19
  133. pyglet/input/controller_db.py +39 -24
  134. pyglet/input/emscripten/__init__.py +18 -0
  135. pyglet/input/emscripten/gamepad_js.py +397 -0
  136. pyglet/input/linux/__init__.py +11 -5
  137. pyglet/input/linux/evdev.py +10 -11
  138. pyglet/input/linux/x11_xinput.py +2 -2
  139. pyglet/input/linux/x11_xinput_tablet.py +1 -1
  140. pyglet/input/macos/__init__.py +7 -2
  141. pyglet/input/macos/darwin_gc.py +559 -0
  142. pyglet/input/win32/__init__.py +1 -1
  143. pyglet/input/win32/directinput.py +34 -29
  144. pyglet/input/win32/xinput.py +11 -61
  145. pyglet/lib.py +3 -3
  146. pyglet/libs/__init__.py +1 -1
  147. pyglet/{gl → libs/darwin}/agl.py +1 -1
  148. pyglet/libs/darwin/cocoapy/__init__.py +2 -2
  149. pyglet/libs/darwin/cocoapy/cocoahelpers.py +181 -0
  150. pyglet/libs/darwin/cocoapy/cocoalibs.py +31 -0
  151. pyglet/libs/darwin/cocoapy/cocoatypes.py +27 -0
  152. pyglet/libs/darwin/cocoapy/runtime.py +81 -45
  153. pyglet/libs/darwin/coreaudio.py +4 -4
  154. pyglet/{gl → libs/darwin}/lib_agl.py +9 -8
  155. pyglet/libs/darwin/quartzkey.py +1 -3
  156. pyglet/libs/egl/__init__.py +2 -0
  157. pyglet/libs/egl/egl_lib.py +576 -0
  158. pyglet/libs/egl/eglext.py +51 -5
  159. pyglet/libs/linux/__init__.py +0 -0
  160. pyglet/libs/linux/egl/__init__.py +0 -0
  161. pyglet/libs/linux/egl/eglext.py +22 -0
  162. pyglet/libs/linux/glx/__init__.py +0 -0
  163. pyglet/{gl → libs/linux/glx}/glx.py +13 -14
  164. pyglet/{gl → libs/linux/glx}/glxext_arb.py +408 -192
  165. pyglet/{gl → libs/linux/glx}/glxext_mesa.py +1 -1
  166. pyglet/{gl → libs/linux/glx}/glxext_nv.py +345 -164
  167. pyglet/{gl → libs/linux/glx}/lib_glx.py +3 -2
  168. pyglet/libs/linux/wayland/__init__.py +0 -0
  169. pyglet/libs/linux/wayland/client.py +1068 -0
  170. pyglet/libs/linux/wayland/lib_wayland.py +207 -0
  171. pyglet/libs/linux/wayland/wayland_egl.py +38 -0
  172. pyglet/libs/{wayland → linux/wayland}/xkbcommon.py +26 -0
  173. pyglet/libs/{x11 → linux/x11}/xf86vmode.py +4 -4
  174. pyglet/libs/{x11 → linux/x11}/xinerama.py +2 -2
  175. pyglet/libs/{x11 → linux/x11}/xinput.py +10 -10
  176. pyglet/libs/linux/x11/xrandr.py +0 -0
  177. pyglet/libs/{x11 → linux/x11}/xrender.py +1 -1
  178. pyglet/libs/shared/__init__.py +0 -0
  179. pyglet/libs/shared/spirv/__init__.py +0 -0
  180. pyglet/libs/shared/spirv/lib_shaderc.py +85 -0
  181. pyglet/libs/shared/spirv/lib_spirv_cross.py +126 -0
  182. pyglet/libs/win32/__init__.py +28 -8
  183. pyglet/libs/win32/constants.py +59 -48
  184. pyglet/libs/win32/context_managers.py +20 -3
  185. pyglet/libs/win32/dinput.py +105 -88
  186. pyglet/{gl → libs/win32}/lib_wgl.py +52 -26
  187. pyglet/libs/win32/types.py +58 -23
  188. pyglet/{gl → libs/win32}/wgl.py +32 -25
  189. pyglet/{gl → libs/win32}/wglext_arb.py +364 -2
  190. pyglet/media/__init__.py +9 -10
  191. pyglet/media/codecs/__init__.py +12 -1
  192. pyglet/media/codecs/base.py +99 -96
  193. pyglet/media/codecs/ffmpeg.py +2 -2
  194. pyglet/media/codecs/ffmpeg_lib/libavformat.py +3 -8
  195. pyglet/media/codecs/webaudio_pyodide.py +111 -0
  196. pyglet/media/drivers/__init__.py +9 -4
  197. pyglet/media/drivers/base.py +4 -4
  198. pyglet/media/drivers/openal/__init__.py +1 -1
  199. pyglet/media/drivers/openal/adaptation.py +3 -3
  200. pyglet/media/drivers/pulse/__init__.py +1 -1
  201. pyglet/media/drivers/pulse/adaptation.py +3 -3
  202. pyglet/media/drivers/pyodide_js/__init__.py +8 -0
  203. pyglet/media/drivers/pyodide_js/adaptation.py +288 -0
  204. pyglet/media/drivers/xaudio2/adaptation.py +3 -3
  205. pyglet/media/player.py +276 -193
  206. pyglet/media/player_worker_thread.py +1 -1
  207. pyglet/model/__init__.py +39 -29
  208. pyglet/model/codecs/base.py +4 -4
  209. pyglet/model/codecs/gltf.py +3 -3
  210. pyglet/model/codecs/obj.py +71 -43
  211. pyglet/resource.py +129 -78
  212. pyglet/shapes.py +154 -194
  213. pyglet/sprite.py +47 -164
  214. pyglet/text/__init__.py +44 -54
  215. pyglet/text/caret.py +12 -7
  216. pyglet/text/document.py +19 -17
  217. pyglet/text/formats/html.py +2 -2
  218. pyglet/text/formats/structured.py +10 -40
  219. pyglet/text/layout/__init__.py +20 -13
  220. pyglet/text/layout/base.py +176 -287
  221. pyglet/text/layout/incremental.py +9 -10
  222. pyglet/text/layout/scrolling.py +7 -95
  223. pyglet/window/__init__.py +183 -172
  224. pyglet/window/cocoa/__init__.py +62 -51
  225. pyglet/window/cocoa/pyglet_delegate.py +2 -25
  226. pyglet/window/cocoa/pyglet_view.py +9 -8
  227. pyglet/window/dialog/__init__.py +184 -0
  228. pyglet/window/dialog/base.py +99 -0
  229. pyglet/window/dialog/darwin.py +121 -0
  230. pyglet/window/dialog/linux.py +72 -0
  231. pyglet/window/dialog/windows.py +194 -0
  232. pyglet/window/emscripten/__init__.py +779 -0
  233. pyglet/window/headless/__init__.py +44 -28
  234. pyglet/window/key.py +2 -0
  235. pyglet/window/mouse.py +2 -2
  236. pyglet/window/wayland/__init__.py +377 -0
  237. pyglet/window/win32/__init__.py +101 -46
  238. pyglet/window/xlib/__init__.py +104 -66
  239. {pyglet-2.1.13.dist-info → pyglet-3.0.dev1.dist-info}/METADATA +2 -3
  240. pyglet-3.0.dev1.dist-info/RECORD +322 -0
  241. {pyglet-2.1.13.dist-info → pyglet-3.0.dev1.dist-info}/WHEEL +1 -1
  242. pyglet/gl/__init__.py +0 -208
  243. pyglet/gl/base.py +0 -499
  244. pyglet/gl/cocoa.py +0 -309
  245. pyglet/gl/gl.py +0 -4625
  246. pyglet/gl/gl.pyi +0 -2320
  247. pyglet/gl/gl_compat.pyi +0 -3097
  248. pyglet/gl/gl_info.py +0 -190
  249. pyglet/gl/headless.py +0 -166
  250. pyglet/gl/wgl_info.py +0 -36
  251. pyglet/gl/wglext_nv.py +0 -1096
  252. pyglet/gl/win32.py +0 -268
  253. pyglet/gl/xlib.py +0 -295
  254. pyglet/image/buffer.py +0 -274
  255. pyglet/image/codecs/s3tc.py +0 -354
  256. pyglet/libs/x11/xrandr.py +0 -166
  257. pyglet-2.1.13.dist-info/RECORD +0 -234
  258. /pyglet/{libs/wayland → graphics/api/gl/cocoa}/__init__.py +0 -0
  259. /pyglet/libs/{egl → linux/egl}/egl.py +0 -0
  260. /pyglet/libs/{egl → linux/egl}/lib.py +0 -0
  261. /pyglet/libs/{ioctl.py → linux/ioctl.py} +0 -0
  262. /pyglet/libs/{wayland → linux/wayland}/gbm.py +0 -0
  263. /pyglet/libs/{x11 → linux/x11}/__init__.py +0 -0
  264. /pyglet/libs/{x11 → linux/x11}/cursorfont.py +0 -0
  265. /pyglet/libs/{x11 → linux/x11}/xlib.py +0 -0
  266. /pyglet/libs/{x11 → linux/x11}/xsync.py +0 -0
  267. {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)
@@ -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.italic, font.stretch)
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.italic, font.stretch)
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.italic, font.stretch)
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
+ }