pyglet 2.1.2__py3-none-any.whl → 2.1.4__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 +21 -9
- pyglet/__init__.pyi +3 -1
- pyglet/app/cocoa.py +6 -3
- pyglet/app/xlib.py +1 -1
- pyglet/display/cocoa.py +2 -2
- pyglet/display/win32.py +17 -18
- pyglet/display/xlib.py +2 -2
- pyglet/display/xlib_vidmoderestore.py +1 -1
- pyglet/extlibs/earcut.py +2 -2
- pyglet/font/__init__.py +3 -3
- pyglet/font/base.py +118 -51
- pyglet/font/dwrite/__init__.py +1381 -0
- pyglet/font/dwrite/d2d1_lib.py +637 -0
- pyglet/font/dwrite/d2d1_types_lib.py +60 -0
- pyglet/font/dwrite/dwrite_lib.py +1577 -0
- pyglet/font/fontconfig.py +79 -16
- pyglet/font/freetype.py +252 -77
- pyglet/font/freetype_lib.py +234 -125
- pyglet/font/harfbuzz/__init__.py +275 -0
- pyglet/font/harfbuzz/harfbuzz_lib.py +212 -0
- pyglet/font/quartz.py +432 -112
- pyglet/font/user.py +18 -11
- pyglet/font/win32.py +9 -1
- pyglet/gl/wgl.py +94 -87
- pyglet/gl/wglext_arb.py +472 -218
- pyglet/gl/wglext_nv.py +410 -188
- pyglet/gui/frame.py +4 -4
- pyglet/gui/widgets.py +6 -1
- pyglet/image/__init__.py +0 -2
- pyglet/image/codecs/bmp.py +3 -5
- pyglet/image/codecs/dds.py +1 -1
- pyglet/image/codecs/gdiplus.py +28 -9
- pyglet/image/codecs/wic.py +198 -489
- pyglet/image/codecs/wincodec_lib.py +413 -0
- pyglet/input/base.py +3 -2
- pyglet/input/linux/x11_xinput.py +3 -3
- pyglet/input/linux/x11_xinput_tablet.py +2 -2
- pyglet/input/macos/darwin_hid.py +28 -2
- pyglet/input/win32/directinput.py +3 -2
- pyglet/input/win32/wintab.py +1 -1
- pyglet/input/win32/xinput.py +10 -9
- pyglet/lib.py +14 -2
- pyglet/libs/darwin/cocoapy/cocoalibs.py +74 -3
- pyglet/libs/darwin/coreaudio.py +0 -2
- pyglet/libs/win32/__init__.py +4 -2
- pyglet/libs/win32/com.py +65 -12
- pyglet/libs/win32/constants.py +1 -0
- pyglet/libs/win32/dinput.py +1 -9
- pyglet/libs/win32/types.py +72 -8
- pyglet/math.py +5 -5
- pyglet/media/codecs/coreaudio.py +1 -0
- pyglet/media/codecs/wmf.py +93 -72
- pyglet/media/devices/win32.py +5 -4
- pyglet/media/drivers/directsound/lib_dsound.py +4 -4
- pyglet/media/drivers/xaudio2/interface.py +21 -17
- pyglet/media/drivers/xaudio2/lib_xaudio2.py +42 -25
- pyglet/model/__init__.py +78 -57
- pyglet/shapes.py +1 -1
- pyglet/text/document.py +7 -53
- pyglet/text/formats/attributed.py +3 -1
- pyglet/text/formats/plaintext.py +1 -1
- pyglet/text/formats/structured.py +1 -1
- pyglet/text/layout/base.py +76 -68
- pyglet/text/layout/incremental.py +38 -8
- pyglet/text/layout/scrolling.py +1 -1
- pyglet/text/runlist.py +2 -114
- pyglet/window/__init__.py +11 -8
- pyglet/window/win32/__init__.py +1 -3
- pyglet/window/xlib/__init__.py +2 -2
- {pyglet-2.1.2.dist-info → pyglet-2.1.4.dist-info}/METADATA +2 -3
- {pyglet-2.1.2.dist-info → pyglet-2.1.4.dist-info}/RECORD +73 -67
- pyglet/font/directwrite.py +0 -2798
- {pyglet-2.1.2.dist-info → pyglet-2.1.4.dist-info}/LICENSE +0 -0
- {pyglet-2.1.2.dist-info → pyglet-2.1.4.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,1381 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import pathlib
|
|
5
|
+
from ctypes import POINTER, Array, byref, c_void_p, cast, create_unicode_buffer, pointer, py_object, sizeof, string_at
|
|
6
|
+
from ctypes.wintypes import BOOL, FLOAT, UINT
|
|
7
|
+
from enum import Flag
|
|
8
|
+
from typing import TYPE_CHECKING, BinaryIO, Sequence
|
|
9
|
+
|
|
10
|
+
import pyglet
|
|
11
|
+
from pyglet.font import base
|
|
12
|
+
from pyglet.font.base import Glyph, GlyphPosition
|
|
13
|
+
from pyglet.font.dwrite.d2d1_lib import (
|
|
14
|
+
D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT,
|
|
15
|
+
D2D1_DRAW_TEXT_OPTIONS_NONE,
|
|
16
|
+
D2D1_FACTORY_TYPE_SINGLE_THREADED,
|
|
17
|
+
D2D1_TEXT_ANTIALIAS_MODE_ALIASED,
|
|
18
|
+
D2D1_TEXT_ANTIALIAS_MODE_DEFAULT,
|
|
19
|
+
D2D1CreateFactory,
|
|
20
|
+
ID2D1DeviceContext4,
|
|
21
|
+
ID2D1Factory,
|
|
22
|
+
ID2D1RenderTarget,
|
|
23
|
+
ID2D1SolidColorBrush,
|
|
24
|
+
IID_ID2D1DeviceContext4,
|
|
25
|
+
IID_ID2D1Factory,
|
|
26
|
+
default_target_properties,
|
|
27
|
+
)
|
|
28
|
+
from pyglet.font.dwrite.d2d1_types_lib import D2D1_COLOR_F, D2D_POINT_2F
|
|
29
|
+
from pyglet.font.dwrite.dwrite_lib import (
|
|
30
|
+
DWRITE_CLUSTER_METRICS,
|
|
31
|
+
DWRITE_COLOR_GLYPH_RUN1,
|
|
32
|
+
DWRITE_FACTORY_TYPE_SHARED,
|
|
33
|
+
DWRITE_FONT_METRICS,
|
|
34
|
+
DWRITE_FONT_STRETCH_CONDENSED,
|
|
35
|
+
DWRITE_FONT_STRETCH_EXPANDED,
|
|
36
|
+
DWRITE_FONT_STRETCH_EXTRA_CONDENSED,
|
|
37
|
+
DWRITE_FONT_STRETCH_EXTRA_EXPANDED,
|
|
38
|
+
DWRITE_FONT_STRETCH_MEDIUM,
|
|
39
|
+
DWRITE_FONT_STRETCH_NORMAL,
|
|
40
|
+
DWRITE_FONT_STRETCH_SEMI_CONDENSED,
|
|
41
|
+
DWRITE_FONT_STRETCH_SEMI_EXPANDED,
|
|
42
|
+
DWRITE_FONT_STRETCH_ULTRA_CONDENSED,
|
|
43
|
+
DWRITE_FONT_STRETCH_UNDEFINED,
|
|
44
|
+
DWRITE_FONT_STYLE_ITALIC,
|
|
45
|
+
DWRITE_FONT_STYLE_NORMAL,
|
|
46
|
+
DWRITE_FONT_STYLE_OBLIQUE,
|
|
47
|
+
DWRITE_FONT_WEIGHT_BLACK,
|
|
48
|
+
DWRITE_FONT_WEIGHT_BOLD,
|
|
49
|
+
DWRITE_FONT_WEIGHT_DEMI_BOLD,
|
|
50
|
+
DWRITE_FONT_WEIGHT_EXTRA_BLACK,
|
|
51
|
+
DWRITE_FONT_WEIGHT_EXTRA_BOLD,
|
|
52
|
+
DWRITE_FONT_WEIGHT_EXTRA_LIGHT,
|
|
53
|
+
DWRITE_FONT_WEIGHT_HEAVY,
|
|
54
|
+
DWRITE_FONT_WEIGHT_LIGHT,
|
|
55
|
+
DWRITE_FONT_WEIGHT_MEDIUM,
|
|
56
|
+
DWRITE_FONT_WEIGHT_NORMAL,
|
|
57
|
+
DWRITE_FONT_WEIGHT_REGULAR,
|
|
58
|
+
DWRITE_FONT_WEIGHT_SEMI_BOLD,
|
|
59
|
+
DWRITE_FONT_WEIGHT_SEMI_LIGHT,
|
|
60
|
+
DWRITE_FONT_WEIGHT_THIN,
|
|
61
|
+
DWRITE_FONT_WEIGHT_ULTRA_BOLD,
|
|
62
|
+
DWRITE_FONT_WEIGHT_ULTRA_LIGHT,
|
|
63
|
+
DWRITE_GLYPH_IMAGE_FORMATS_ALL,
|
|
64
|
+
DWRITE_GLYPH_IMAGE_FORMATS_BITMAP,
|
|
65
|
+
DWRITE_GLYPH_IMAGE_FORMATS_SVG,
|
|
66
|
+
DWRITE_GLYPH_METRICS,
|
|
67
|
+
DWRITE_GLYPH_OFFSET,
|
|
68
|
+
DWRITE_GLYPH_RUN,
|
|
69
|
+
DWRITE_GLYPH_RUN_DESCRIPTION,
|
|
70
|
+
DWRITE_INFORMATIONAL_STRING_WIN32_FAMILY_NAMES,
|
|
71
|
+
DWRITE_MATRIX,
|
|
72
|
+
DWRITE_MEASURING_MODE_NATURAL,
|
|
73
|
+
DWRITE_NO_PALETTE_INDEX,
|
|
74
|
+
DWRITE_TEXT_METRICS,
|
|
75
|
+
DWriteCreateFactory,
|
|
76
|
+
IDWriteColorGlyphRunEnumerator,
|
|
77
|
+
IDWriteColorGlyphRunEnumerator1,
|
|
78
|
+
IDWriteFactory,
|
|
79
|
+
IDWriteFactory2,
|
|
80
|
+
IDWriteFactory5,
|
|
81
|
+
IDWriteFactory7,
|
|
82
|
+
IDWriteFont,
|
|
83
|
+
IDWriteFontCollection,
|
|
84
|
+
IDWriteFontCollection1,
|
|
85
|
+
IDWriteFontFace,
|
|
86
|
+
IDWriteFontFamily,
|
|
87
|
+
IDWriteFontFamily1,
|
|
88
|
+
IDWriteFontFile,
|
|
89
|
+
IDWriteFontFileLoader,
|
|
90
|
+
IDWriteFontFileLoader_LI,
|
|
91
|
+
IDWriteFontFileStream,
|
|
92
|
+
IDWriteFontSet,
|
|
93
|
+
IDWriteFontSetBuilder1,
|
|
94
|
+
IDWriteInMemoryFontFileLoader,
|
|
95
|
+
IDWriteLocalFontFileLoader,
|
|
96
|
+
IDWriteLocalizedStrings,
|
|
97
|
+
IDWriteTextFormat,
|
|
98
|
+
IDWriteTextLayout,
|
|
99
|
+
IDWriteTextRenderer,
|
|
100
|
+
IID_IDWriteFactory,
|
|
101
|
+
IID_IDWriteFactory2,
|
|
102
|
+
IID_IDWriteFactory5,
|
|
103
|
+
IID_IDWriteFactory7,
|
|
104
|
+
IID_IDWriteLocalFontFileLoader,
|
|
105
|
+
LegacyCollectionLoader,
|
|
106
|
+
LegacyFontFileLoader,
|
|
107
|
+
)
|
|
108
|
+
from pyglet.font.harfbuzz import get_harfbuzz_shaped_glyphs, get_resource_from_dw_font, harfbuzz_available
|
|
109
|
+
from pyglet.image.codecs.wincodec_lib import GUID_WICPixelFormat32bppPBGRA
|
|
110
|
+
from pyglet.libs.win32 import UINT16, UINT32, UINT64, com
|
|
111
|
+
from pyglet.libs.win32 import _kernel32 as kernel32
|
|
112
|
+
from pyglet.libs.win32.constants import (
|
|
113
|
+
LOCALE_NAME_MAX_LENGTH,
|
|
114
|
+
WINDOWS_8_1_OR_GREATER,
|
|
115
|
+
WINDOWS_10_1809_OR_GREATER,
|
|
116
|
+
WINDOWS_10_CREATORS_UPDATE_OR_GREATER,
|
|
117
|
+
)
|
|
118
|
+
from pyglet.util import debug_print
|
|
119
|
+
|
|
120
|
+
if TYPE_CHECKING:
|
|
121
|
+
from pyglet.image import ImageData
|
|
122
|
+
|
|
123
|
+
_debug_font = pyglet.options["debug_font"]
|
|
124
|
+
_debug_print = debug_print("debug_font")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
from pyglet.image.codecs import wic
|
|
129
|
+
except ImportError as err:
|
|
130
|
+
msg = "Failed to initialize Windows Imaging Component module."
|
|
131
|
+
raise ImportError(msg) from err
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
name_to_weight = {
|
|
135
|
+
True: DWRITE_FONT_WEIGHT_BOLD, # Temporary alias for attributed text
|
|
136
|
+
False: DWRITE_FONT_WEIGHT_NORMAL, # Temporary alias for attributed text
|
|
137
|
+
None: DWRITE_FONT_WEIGHT_NORMAL, # Temporary alias for attributed text
|
|
138
|
+
"thin": DWRITE_FONT_WEIGHT_THIN,
|
|
139
|
+
"extralight": DWRITE_FONT_WEIGHT_EXTRA_LIGHT,
|
|
140
|
+
"ultralight": DWRITE_FONT_WEIGHT_ULTRA_LIGHT,
|
|
141
|
+
"light": DWRITE_FONT_WEIGHT_LIGHT,
|
|
142
|
+
"semilight": DWRITE_FONT_WEIGHT_SEMI_LIGHT,
|
|
143
|
+
"normal": DWRITE_FONT_WEIGHT_NORMAL,
|
|
144
|
+
"regular": DWRITE_FONT_WEIGHT_REGULAR,
|
|
145
|
+
"medium": DWRITE_FONT_WEIGHT_MEDIUM,
|
|
146
|
+
"demibold": DWRITE_FONT_WEIGHT_DEMI_BOLD,
|
|
147
|
+
"semibold": DWRITE_FONT_WEIGHT_SEMI_BOLD,
|
|
148
|
+
"bold": DWRITE_FONT_WEIGHT_BOLD,
|
|
149
|
+
"extrabold": DWRITE_FONT_WEIGHT_EXTRA_BOLD,
|
|
150
|
+
"ultrabold": DWRITE_FONT_WEIGHT_ULTRA_BOLD,
|
|
151
|
+
"black": DWRITE_FONT_WEIGHT_BLACK,
|
|
152
|
+
"heavy": DWRITE_FONT_WEIGHT_HEAVY,
|
|
153
|
+
"extrablack": DWRITE_FONT_WEIGHT_EXTRA_BLACK,
|
|
154
|
+
}
|
|
155
|
+
name_to_stretch = {
|
|
156
|
+
"undefined": DWRITE_FONT_STRETCH_UNDEFINED,
|
|
157
|
+
"ultracondensed": DWRITE_FONT_STRETCH_ULTRA_CONDENSED,
|
|
158
|
+
"extracondensed": DWRITE_FONT_STRETCH_EXTRA_CONDENSED,
|
|
159
|
+
"condensed": DWRITE_FONT_STRETCH_CONDENSED,
|
|
160
|
+
"semicondensed": DWRITE_FONT_STRETCH_SEMI_CONDENSED,
|
|
161
|
+
"normal": DWRITE_FONT_STRETCH_NORMAL,
|
|
162
|
+
"medium": DWRITE_FONT_STRETCH_MEDIUM,
|
|
163
|
+
"semiexpanded": DWRITE_FONT_STRETCH_SEMI_EXPANDED,
|
|
164
|
+
"expanded": DWRITE_FONT_STRETCH_EXPANDED,
|
|
165
|
+
"extraexpanded": DWRITE_FONT_STRETCH_EXTRA_EXPANDED,
|
|
166
|
+
"narrow": DWRITE_FONT_STRETCH_CONDENSED,
|
|
167
|
+
}
|
|
168
|
+
name_to_style = {
|
|
169
|
+
"normal": DWRITE_FONT_STYLE_NORMAL,
|
|
170
|
+
"oblique": DWRITE_FONT_STYLE_OBLIQUE,
|
|
171
|
+
"italic": DWRITE_FONT_STYLE_ITALIC,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class DWRITE_GLYPH_IMAGE_FORMAT_FLAG(Flag):
|
|
176
|
+
NONE = 0x00000000
|
|
177
|
+
TRUETYPE = 0x00000001
|
|
178
|
+
CFF = 0x00000002
|
|
179
|
+
COLR = 0x00000004
|
|
180
|
+
SVG = 0x00000008
|
|
181
|
+
PNG = 0x00000010
|
|
182
|
+
JPEG = 0x00000020
|
|
183
|
+
TIFF = 0x00000040
|
|
184
|
+
PREMULTIPLIED_B8G8R8A8 = 0x00000080
|
|
185
|
+
COLR_PAINT_TREE = 0x00000100
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def get_system_locale() -> str:
|
|
190
|
+
"""Retrieve the string representing the system locale."""
|
|
191
|
+
local_name = create_unicode_buffer(LOCALE_NAME_MAX_LENGTH)
|
|
192
|
+
kernel32.GetUserDefaultLocaleName(local_name, LOCALE_NAME_MAX_LENGTH)
|
|
193
|
+
return local_name.value
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class _DWriteTextRenderer(com.COMObject):
|
|
197
|
+
"""This implements a custom renderer for IDWriteTextLayout.
|
|
198
|
+
|
|
199
|
+
This allows the use of DirectWrite shaping to offload manual shaping, fallback detection, glyph combining, and
|
|
200
|
+
other complicated scenarios.
|
|
201
|
+
"""
|
|
202
|
+
_interfaces_ = [IDWriteTextRenderer] # noqa: RUF012
|
|
203
|
+
|
|
204
|
+
def __init__(self) -> None:
|
|
205
|
+
super().__init__()
|
|
206
|
+
self.pixel_snapping = False
|
|
207
|
+
self.pixels_per_dip = 1.0
|
|
208
|
+
self.dmatrix = DWRITE_MATRIX()
|
|
209
|
+
|
|
210
|
+
def _get_font_reference(self, font_face: IDWriteFontFace) -> tuple[c_void_p, int]:
|
|
211
|
+
"""Unique identifier for each font face."""
|
|
212
|
+
font_file = _get_font_file(font_face)
|
|
213
|
+
return _get_font_ref(font_file, release_file=True)
|
|
214
|
+
|
|
215
|
+
def DrawUnderline(self, *_args) -> int: # noqa: ANN002, N802
|
|
216
|
+
return com.E_NOTIMPL
|
|
217
|
+
|
|
218
|
+
def DrawStrikethrough(self, *_args)-> int: # noqa: ANN002, N802
|
|
219
|
+
return com.E_NOTIMPL
|
|
220
|
+
|
|
221
|
+
def DrawInlineObject(self, *_args) -> int: # noqa: ANN002, N802
|
|
222
|
+
return com.E_NOTIMPL
|
|
223
|
+
|
|
224
|
+
def IsPixelSnappingDisabled(self, _draw_ctx: c_void_p, is_disabled: POINTER(FLOAT)) -> int: # noqa: N802
|
|
225
|
+
is_disabled[0] = self.pixel_snapping
|
|
226
|
+
return 0
|
|
227
|
+
|
|
228
|
+
def GetPixelsPerDip(self, _draw_ctx: c_void_p, pixels_per_dip: POINTER(FLOAT)) -> int: # noqa: N802
|
|
229
|
+
pixels_per_dip[0] = self.pixels_per_dip
|
|
230
|
+
return 0
|
|
231
|
+
|
|
232
|
+
def GetCurrentTransform(self, _draw_ctx: c_void_p, transform: POINTER(DWRITE_MATRIX)) -> int: # noqa: N802
|
|
233
|
+
transform[0] = self.dmatrix
|
|
234
|
+
return 0
|
|
235
|
+
|
|
236
|
+
def DrawGlyphRun(self, drawing_context: c_void_p, # noqa: N802
|
|
237
|
+
_baseline_x: float, _baseline_y: float, mode: int,
|
|
238
|
+
glyph_run_ptr: POINTER(DWRITE_GLYPH_RUN),
|
|
239
|
+
_run_des: POINTER(DWRITE_GLYPH_RUN_DESCRIPTION),
|
|
240
|
+
_effect: c_void_p) -> int:
|
|
241
|
+
|
|
242
|
+
c_buf = create_unicode_buffer(_run_des.contents.text)
|
|
243
|
+
|
|
244
|
+
c_wchar_txt = c_buf[:_run_des.contents.textLength]
|
|
245
|
+
pystr_len = len(c_wchar_txt)
|
|
246
|
+
|
|
247
|
+
glyph_renderer: DirectWriteGlyphRenderer = cast(drawing_context, py_object).value
|
|
248
|
+
glyph_run = glyph_run_ptr.contents
|
|
249
|
+
|
|
250
|
+
if glyph_run.glyphCount == 0:
|
|
251
|
+
glyph = glyph_renderer.font._zero_glyph # noqa: SLF001
|
|
252
|
+
glyph_renderer.current_glyphs.append(glyph)
|
|
253
|
+
glyph_renderer.current_offsets.append(GlyphPosition(0, 0, 0, 0))
|
|
254
|
+
return 0
|
|
255
|
+
|
|
256
|
+
# Process any glyphs we haven't rendered.
|
|
257
|
+
missing = []
|
|
258
|
+
for i in range(glyph_run.glyphCount):
|
|
259
|
+
glyph_indice = glyph_run.glyphIndices[i]
|
|
260
|
+
if glyph_indice not in glyph_renderer.font.glyphs and glyph_indice not in missing:
|
|
261
|
+
missing.append(glyph_indice)
|
|
262
|
+
|
|
263
|
+
# Missing glyphs, get their info.
|
|
264
|
+
if missing:
|
|
265
|
+
metrics = get_glyph_metrics(glyph_run.fontFace, (UINT16 * len(missing))(*missing), len(missing))
|
|
266
|
+
|
|
267
|
+
for idx, glyph_indice in enumerate(missing):
|
|
268
|
+
glyph = glyph_renderer.render_single_glyph(glyph_run.fontFace, glyph_indice, metrics[idx], mode)
|
|
269
|
+
glyph_renderer.font.glyphs[glyph_indice] = glyph
|
|
270
|
+
|
|
271
|
+
# Set glyphs for run.
|
|
272
|
+
current = []
|
|
273
|
+
for i in range(glyph_run.glyphCount):
|
|
274
|
+
glyph_indice = glyph_run.glyphIndices[i]
|
|
275
|
+
glyph = glyph_renderer.font.glyphs[glyph_indice]
|
|
276
|
+
current.append(glyph)
|
|
277
|
+
# In some cases (italics) the offsets may be NULL.
|
|
278
|
+
if glyph_run.glyphOffsets:
|
|
279
|
+
offset = base.GlyphPosition(
|
|
280
|
+
(glyph_run.glyphAdvances[i] - glyph.advance),
|
|
281
|
+
0,
|
|
282
|
+
glyph_run.glyphOffsets[i].advanceOffset,
|
|
283
|
+
glyph_run.glyphOffsets[i].ascenderOffset
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
else:
|
|
287
|
+
offset = base.GlyphPosition(0, 0, 0, 0)
|
|
288
|
+
|
|
289
|
+
glyph_renderer.current_glyphs.append(glyph)
|
|
290
|
+
glyph_renderer.current_offsets.append(offset)
|
|
291
|
+
|
|
292
|
+
diff = pystr_len - glyph_run.glyphCount
|
|
293
|
+
if diff > 0:
|
|
294
|
+
for i in range(diff):
|
|
295
|
+
glyph = glyph_renderer.font._zero_glyph
|
|
296
|
+
glyph_renderer.current_glyphs.append(glyph)
|
|
297
|
+
glyph_renderer.current_offsets.append(GlyphPosition(0, 0, 0, 0))
|
|
298
|
+
|
|
299
|
+
return 0
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
_renderer = _DWriteTextRenderer()
|
|
303
|
+
|
|
304
|
+
def get_glyph_metrics(font_face: IDWriteFontFace, indices: Array[UINT16], count: int) -> list[
|
|
305
|
+
tuple[float, float, float, float, float]]:
|
|
306
|
+
"""Obtain metrics for the specific string.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
A list of tuples with the following metrics per indice:
|
|
310
|
+
. (glyph width, glyph height, left side bearing, advance width, bottom side bearing)
|
|
311
|
+
"""
|
|
312
|
+
glyph_metrics = (DWRITE_GLYPH_METRICS * count)()
|
|
313
|
+
font_face.GetDesignGlyphMetrics(indices, count, glyph_metrics, False)
|
|
314
|
+
|
|
315
|
+
metrics_out = []
|
|
316
|
+
for metric in glyph_metrics:
|
|
317
|
+
glyph_width = (metric.advanceWidth + abs(metric.leftSideBearing) + abs(metric.rightSideBearing))
|
|
318
|
+
glyph_height = (metric.advanceHeight - metric.topSideBearing - metric.bottomSideBearing)
|
|
319
|
+
|
|
320
|
+
lsb = metric.leftSideBearing
|
|
321
|
+
bsb = metric.bottomSideBearing
|
|
322
|
+
|
|
323
|
+
advance_width = metric.advanceWidth
|
|
324
|
+
|
|
325
|
+
metrics_out.append((glyph_width, glyph_height, lsb, advance_width, bsb))
|
|
326
|
+
|
|
327
|
+
return metrics_out
|
|
328
|
+
|
|
329
|
+
class DirectWriteGlyphRenderer(base.GlyphRenderer): # noqa: D101
|
|
330
|
+
current_run: list[DWRITE_GLYPH_RUN]
|
|
331
|
+
font: Win32DirectWriteFont
|
|
332
|
+
antialias_mode = D2D1_TEXT_ANTIALIAS_MODE_DEFAULT if pyglet.options.text_antialiasing is True else D2D1_TEXT_ANTIALIAS_MODE_ALIASED
|
|
333
|
+
draw_options = D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT if WINDOWS_8_1_OR_GREATER else D2D1_DRAW_TEXT_OPTIONS_NONE
|
|
334
|
+
measuring_mode = DWRITE_MEASURING_MODE_NATURAL
|
|
335
|
+
|
|
336
|
+
def __init__(self, font: Win32DirectWriteFont) -> None: # noqa: D107
|
|
337
|
+
self._render_target = None
|
|
338
|
+
self._ctx_supported = False
|
|
339
|
+
self._bitmap = None
|
|
340
|
+
self._brush = None
|
|
341
|
+
self._bitmap_dimensions = (0, 0)
|
|
342
|
+
self.current_glyphs = []
|
|
343
|
+
self.current_offsets = []
|
|
344
|
+
super().__init__(font)
|
|
345
|
+
|
|
346
|
+
def render(self, text: str) -> Glyph:
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
def render_to_image(self, text: str, width: int, height: int) -> ImageData:
|
|
350
|
+
"""This process takes Pyglet out of the equation and uses only DirectWrite to shape and render text.
|
|
351
|
+
|
|
352
|
+
This may allows more accurate fonts (bidi, rtl, etc) in very special circumstances.
|
|
353
|
+
"""
|
|
354
|
+
text_buffer = create_unicode_buffer(text)
|
|
355
|
+
|
|
356
|
+
text_layout = IDWriteTextLayout()
|
|
357
|
+
self.font._write_factory.CreateTextLayout(
|
|
358
|
+
text_buffer,
|
|
359
|
+
len(text_buffer),
|
|
360
|
+
self.font._text_format,
|
|
361
|
+
width, # Doesn't affect bitmap size.
|
|
362
|
+
height,
|
|
363
|
+
byref(text_layout),
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
layout_metrics = DWRITE_TEXT_METRICS()
|
|
367
|
+
text_layout.GetMetrics(byref(layout_metrics))
|
|
368
|
+
|
|
369
|
+
width, height = int(math.ceil(layout_metrics.width)), int(math.ceil(layout_metrics.height))
|
|
370
|
+
|
|
371
|
+
wic_fmt = GUID_WICPixelFormat32bppPBGRA
|
|
372
|
+
bitmap = wic.get_bitmap(width, height, wic_fmt)
|
|
373
|
+
|
|
374
|
+
rt = ID2D1RenderTarget()
|
|
375
|
+
d2d_factory.CreateWicBitmapRenderTarget(bitmap, default_target_properties, byref(rt))
|
|
376
|
+
|
|
377
|
+
# Font aliasing rendering quality.
|
|
378
|
+
rt.SetTextAntialiasMode(self.antialias_mode)
|
|
379
|
+
|
|
380
|
+
if not self._brush:
|
|
381
|
+
self._brush = ID2D1SolidColorBrush()
|
|
382
|
+
|
|
383
|
+
rt.CreateSolidColorBrush(white, None, byref(self._brush))
|
|
384
|
+
|
|
385
|
+
rt.BeginDraw()
|
|
386
|
+
|
|
387
|
+
rt.Clear(transparent)
|
|
388
|
+
|
|
389
|
+
rt.DrawTextLayout(no_offset,
|
|
390
|
+
text_layout,
|
|
391
|
+
self._brush,
|
|
392
|
+
self.draw_options)
|
|
393
|
+
|
|
394
|
+
rt.EndDraw(None, None)
|
|
395
|
+
|
|
396
|
+
rt.Release()
|
|
397
|
+
|
|
398
|
+
return wic.extract_image_data(bitmap, wic_fmt)
|
|
399
|
+
|
|
400
|
+
def render_single_glyph(self, font_face: IDWriteFontFace,
|
|
401
|
+
indice: int,
|
|
402
|
+
metrics: tuple[float, float, float, float, float],
|
|
403
|
+
mode: int) -> base.Glyph:
|
|
404
|
+
"""Renders a single glyph indice using Direct2D."""
|
|
405
|
+
glyph_width, glyph_height, glyph_lsb, glyph_advance, glyph_bsb = metrics
|
|
406
|
+
|
|
407
|
+
offset = DWRITE_GLYPH_OFFSET(0.0, 0.0)
|
|
408
|
+
|
|
409
|
+
run = DWRITE_GLYPH_RUN(
|
|
410
|
+
fontFace=font_face,
|
|
411
|
+
fontEmSize=self.font.pixel_size,
|
|
412
|
+
glyphCount=1,
|
|
413
|
+
glyphIndices=(UINT16 * 1)(indice), # indice,
|
|
414
|
+
glyphAdvances=(FLOAT * 1)(0.0), # advance,
|
|
415
|
+
glyphOffsets=pointer(offset), # offset,
|
|
416
|
+
isSideways=False,
|
|
417
|
+
bidiLevel=0,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
# If color drawing is enabled, get a color enumerator.
|
|
421
|
+
if self.draw_options & D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT:
|
|
422
|
+
enumerator = self._get_color_enumerator(run)
|
|
423
|
+
else:
|
|
424
|
+
enumerator = None
|
|
425
|
+
|
|
426
|
+
# Use the glyph's advance as a width as bitmap width.
|
|
427
|
+
# Some characters have no glyph width at all, just use a 1x1
|
|
428
|
+
if glyph_width == 0 and glyph_height == 0:
|
|
429
|
+
render_width = 1
|
|
430
|
+
render_height = 1
|
|
431
|
+
else:
|
|
432
|
+
# Use the glyph width, or if the advance is larger, use that instead.
|
|
433
|
+
# Diacritics usually have no proper sizing, but instead have an advance.
|
|
434
|
+
# Add 1, sometimes AA can add an extra pixel or so.
|
|
435
|
+
render_width = int(math.ceil(max(glyph_width, glyph_advance) * self.font.font_scale_ratio)) + 1
|
|
436
|
+
render_height = int(math.ceil(self.font.max_glyph_height)) + 1
|
|
437
|
+
|
|
438
|
+
render_offset_x = 0
|
|
439
|
+
if glyph_lsb < 0:
|
|
440
|
+
# Negative LSB: we shift the offset, otherwise the glyph will be cut off.
|
|
441
|
+
render_offset_x = glyph_lsb * self.font.font_scale_ratio
|
|
442
|
+
|
|
443
|
+
# Create new bitmap.
|
|
444
|
+
# TODO: We can probably adjust bitmap/baseline to reduce the whitespace and save a lot of texture space.
|
|
445
|
+
# Note: Floating point precision makes this a giant headache, will need to be solved for this approach.
|
|
446
|
+
self._create_bitmap(render_width, render_height)
|
|
447
|
+
|
|
448
|
+
# Glyphs are drawn at the baseline, and with LSB, so we need to offset it based on top left position.
|
|
449
|
+
baseline_offset = D2D_POINT_2F(-render_offset_x,
|
|
450
|
+
self.font.ascent)
|
|
451
|
+
|
|
452
|
+
self._render_target.BeginDraw()
|
|
453
|
+
|
|
454
|
+
self._render_target.Clear(transparent)
|
|
455
|
+
|
|
456
|
+
if enumerator:
|
|
457
|
+
temp_brush: None | ID2D1SolidColorBrush = None
|
|
458
|
+
while True:
|
|
459
|
+
has_run = BOOL(True)
|
|
460
|
+
enumerator.MoveNext(byref(has_run))
|
|
461
|
+
if not has_run.value:
|
|
462
|
+
break
|
|
463
|
+
|
|
464
|
+
color_run = POINTER(DWRITE_COLOR_GLYPH_RUN1)()
|
|
465
|
+
enumerator.GetCurrentRun1(byref(color_run))
|
|
466
|
+
|
|
467
|
+
# Uses current color.
|
|
468
|
+
if color_run.contents.paletteIndex == DWRITE_NO_PALETTE_INDEX:
|
|
469
|
+
brush = self._brush
|
|
470
|
+
else:
|
|
471
|
+
# Need a temp brush for separate colors.
|
|
472
|
+
if not temp_brush:
|
|
473
|
+
temp_brush = ID2D1SolidColorBrush()
|
|
474
|
+
self._render_target.CreateSolidColorBrush(color_run.contents.runColor, None, byref(temp_brush))
|
|
475
|
+
else:
|
|
476
|
+
temp_brush.SetColor(color_run.contents.runColor)
|
|
477
|
+
brush = temp_brush
|
|
478
|
+
|
|
479
|
+
glyph_image_fmt = color_run.contents.glyphImageFormat
|
|
480
|
+
if glyph_image_fmt == DWRITE_GLYPH_IMAGE_FORMATS_SVG:
|
|
481
|
+
if self._ctx_supported:
|
|
482
|
+
self._render_target.DrawSvgGlyphRun(
|
|
483
|
+
baseline_offset,
|
|
484
|
+
color_run.contents.glyphRun,
|
|
485
|
+
self._brush,
|
|
486
|
+
None,
|
|
487
|
+
0,
|
|
488
|
+
mode
|
|
489
|
+
)
|
|
490
|
+
elif glyph_image_fmt & DWRITE_GLYPH_IMAGE_FORMATS_BITMAP:
|
|
491
|
+
if self._ctx_supported:
|
|
492
|
+
self._render_target.DrawColorBitmapGlyphRun(
|
|
493
|
+
glyph_image_fmt,
|
|
494
|
+
baseline_offset,
|
|
495
|
+
color_run.contents.glyphRun,
|
|
496
|
+
self.measuring_mode
|
|
497
|
+
)
|
|
498
|
+
else:
|
|
499
|
+
glyph_run = color_run.contents.glyphRun
|
|
500
|
+
self._render_target.DrawGlyphRun(baseline_offset,
|
|
501
|
+
glyph_run,
|
|
502
|
+
brush,
|
|
503
|
+
mode)
|
|
504
|
+
enumerator.Release()
|
|
505
|
+
if temp_brush:
|
|
506
|
+
temp_brush.Release()
|
|
507
|
+
else:
|
|
508
|
+
self._render_target.DrawGlyphRun(baseline_offset,
|
|
509
|
+
run,
|
|
510
|
+
self._brush,
|
|
511
|
+
mode)
|
|
512
|
+
|
|
513
|
+
self._render_target.EndDraw(None, None)
|
|
514
|
+
|
|
515
|
+
image = wic.extract_image_data(self._bitmap)
|
|
516
|
+
|
|
517
|
+
glyph = self.font.create_glyph(image)
|
|
518
|
+
|
|
519
|
+
glyph.set_bearings(-self.font.descent, render_offset_x,
|
|
520
|
+
glyph_advance * self.font.font_scale_ratio)
|
|
521
|
+
|
|
522
|
+
return glyph
|
|
523
|
+
|
|
524
|
+
def _get_color_enumerator(self, dwrite_run: DWRITE_GLYPH_RUN) -> IDWriteColorGlyphRunEnumerator | IDWriteColorGlyphRunEnumerator1 | None:
|
|
525
|
+
"""Obtain a color enumerator if possible."""
|
|
526
|
+
try:
|
|
527
|
+
if WINDOWS_10_CREATORS_UPDATE_OR_GREATER:
|
|
528
|
+
enumerator = IDWriteColorGlyphRunEnumerator1()
|
|
529
|
+
|
|
530
|
+
self.font._write_factory.TranslateColorGlyphRun4(
|
|
531
|
+
no_offset,
|
|
532
|
+
dwrite_run,
|
|
533
|
+
None,
|
|
534
|
+
DWRITE_GLYPH_IMAGE_FORMATS_ALL,
|
|
535
|
+
self.measuring_mode,
|
|
536
|
+
None,
|
|
537
|
+
0,
|
|
538
|
+
byref(enumerator),
|
|
539
|
+
)
|
|
540
|
+
elif WINDOWS_8_1_OR_GREATER:
|
|
541
|
+
enumerator = IDWriteColorGlyphRunEnumerator()
|
|
542
|
+
self.font._write_factory.TranslateColorGlyphRun(
|
|
543
|
+
0.0, 0.0,
|
|
544
|
+
dwrite_run,
|
|
545
|
+
None,
|
|
546
|
+
self.measuring_mode,
|
|
547
|
+
None,
|
|
548
|
+
0,
|
|
549
|
+
byref(enumerator),
|
|
550
|
+
)
|
|
551
|
+
else:
|
|
552
|
+
return None
|
|
553
|
+
|
|
554
|
+
return enumerator
|
|
555
|
+
except OSError as dw_err:
|
|
556
|
+
# HRESULT returns -2003283956 (DWRITE_E_NOCOLOR) if no color run is detected. Anything else is unexpected.
|
|
557
|
+
if dw_err.winerror != -2003283956:
|
|
558
|
+
raise dw_err
|
|
559
|
+
|
|
560
|
+
return None
|
|
561
|
+
|
|
562
|
+
def render_using_layout(self, text: str) -> Glyph | None:
|
|
563
|
+
"""This will render text given the built-in DirectWrite layout.
|
|
564
|
+
|
|
565
|
+
This process allows us to take advantage of color glyphs and fallback handling that is built into DirectWrite.
|
|
566
|
+
This can also handle shaping and many other features if you want to render directly to a texture.
|
|
567
|
+
"""
|
|
568
|
+
text_layout = self.font.create_text_layout(text)
|
|
569
|
+
|
|
570
|
+
layout_metrics = DWRITE_TEXT_METRICS()
|
|
571
|
+
text_layout.GetMetrics(byref(layout_metrics))
|
|
572
|
+
|
|
573
|
+
width = int(math.ceil(layout_metrics.width))
|
|
574
|
+
height = int(math.ceil(layout_metrics.height))
|
|
575
|
+
|
|
576
|
+
if width == 0 or height == 0:
|
|
577
|
+
return None
|
|
578
|
+
|
|
579
|
+
self._create_bitmap(width, height)
|
|
580
|
+
|
|
581
|
+
# This offsets the characters if needed.
|
|
582
|
+
point = D2D_POINT_2F(0, 0)
|
|
583
|
+
|
|
584
|
+
self._render_target.BeginDraw()
|
|
585
|
+
|
|
586
|
+
self._render_target.Clear(transparent)
|
|
587
|
+
|
|
588
|
+
self._render_target.DrawTextLayout(point,
|
|
589
|
+
text_layout,
|
|
590
|
+
self._brush,
|
|
591
|
+
self.draw_options)
|
|
592
|
+
|
|
593
|
+
self._render_target.EndDraw(None, None)
|
|
594
|
+
|
|
595
|
+
image = wic.extract_image_data(self._bitmap)
|
|
596
|
+
|
|
597
|
+
glyph = self.font.create_glyph(image)
|
|
598
|
+
glyph.set_bearings(-self.font.descent, 0, int(math.ceil(layout_metrics.width)))
|
|
599
|
+
return glyph
|
|
600
|
+
|
|
601
|
+
def _create_bitmap(self, width: int, height: int) -> None:
|
|
602
|
+
"""Creates a bitmap using Direct2D and WIC."""
|
|
603
|
+
# Create a new bitmap, try to re-use the bitmap as much as we can to minimize creations.
|
|
604
|
+
if self._bitmap_dimensions[0] != width or self._bitmap_dimensions[1] != height:
|
|
605
|
+
# If dimensions aren't the same, release bitmap to create new ones.
|
|
606
|
+
if self._render_target:
|
|
607
|
+
self._render_target.Release()
|
|
608
|
+
|
|
609
|
+
self._bitmap = wic.get_bitmap(width, height, GUID_WICPixelFormat32bppPBGRA)
|
|
610
|
+
|
|
611
|
+
_render_target = ID2D1RenderTarget()
|
|
612
|
+
d2d_factory.CreateWicBitmapRenderTarget(self._bitmap, default_target_properties, byref(_render_target))
|
|
613
|
+
|
|
614
|
+
# Allows drawing SVG/Bitmap glyphs. Check if supported.
|
|
615
|
+
dev_ctx = ID2D1DeviceContext4()
|
|
616
|
+
if com.is_available(_render_target, IID_ID2D1DeviceContext4, dev_ctx):
|
|
617
|
+
_render_target.Release() # Release original.
|
|
618
|
+
self._render_target = dev_ctx
|
|
619
|
+
self._ctx_supported = True
|
|
620
|
+
else:
|
|
621
|
+
self._render_target = _render_target
|
|
622
|
+
|
|
623
|
+
# Font aliasing rendering quality.
|
|
624
|
+
self._render_target.SetTextAntialiasMode(self.antialias_mode)
|
|
625
|
+
|
|
626
|
+
if not self._brush:
|
|
627
|
+
self._brush = ID2D1SolidColorBrush()
|
|
628
|
+
self._render_target.CreateSolidColorBrush(white, None, byref(self._brush))
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def _get_font_file(font_face: IDWriteFontFace) -> IDWriteFontFile:
|
|
632
|
+
"""Get the font file associated with this face.
|
|
633
|
+
|
|
634
|
+
Seems to give something, even for memory loaded fonts.
|
|
635
|
+
|
|
636
|
+
.. note:: Caller is responsible for freeing the returned object.
|
|
637
|
+
"""
|
|
638
|
+
file_ct = UINT32()
|
|
639
|
+
font_face.GetFiles(byref(file_ct), None)
|
|
640
|
+
|
|
641
|
+
font_files = (IDWriteFontFile * file_ct.value)()
|
|
642
|
+
font_face.GetFiles(byref(file_ct), font_files)
|
|
643
|
+
|
|
644
|
+
return font_files[font_face.GetIndex()]
|
|
645
|
+
|
|
646
|
+
def _get_font_ref(font_file: IDWriteFontFile, release_file: bool=True) -> tuple[c_void_p, int]:
|
|
647
|
+
"""Get a unique font reference for the font face.
|
|
648
|
+
|
|
649
|
+
Callbacks will generate new addresses for the same IDWriteFontFace, so a unique value
|
|
650
|
+
needs to be established to cache glyphs.
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
font_file:
|
|
654
|
+
The target font file object to pull the unique key from.
|
|
655
|
+
release_file:
|
|
656
|
+
If ``True`` the font file will be released.
|
|
657
|
+
"""
|
|
658
|
+
key_data = c_void_p()
|
|
659
|
+
ff_key_size = UINT32()
|
|
660
|
+
|
|
661
|
+
font_file.GetReferenceKey(byref(key_data), byref(ff_key_size))
|
|
662
|
+
if release_file:
|
|
663
|
+
font_file.Release()
|
|
664
|
+
|
|
665
|
+
return key_data, ff_key_size.value
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
class Win32DirectWriteFont(base.Font):
|
|
669
|
+
"""DirectWrite Font object for Windows 7+."""
|
|
670
|
+
# To load fonts from files, we need to produce a custom collection.
|
|
671
|
+
_custom_collection = None
|
|
672
|
+
|
|
673
|
+
# Shared loader values
|
|
674
|
+
_write_factory = None # Factory required to run any DirectWrite interfaces.
|
|
675
|
+
_font_loader = None
|
|
676
|
+
|
|
677
|
+
# Windows 10 loader values.
|
|
678
|
+
_font_builder = None
|
|
679
|
+
_font_set = None
|
|
680
|
+
|
|
681
|
+
# Legacy loader values
|
|
682
|
+
_font_collection_loader = None
|
|
683
|
+
_font_cache = []
|
|
684
|
+
_font_loader_key = None
|
|
685
|
+
|
|
686
|
+
_default_name = "Segoe UI" # Default font for Windows 7+
|
|
687
|
+
|
|
688
|
+
_glyph_renderer = None
|
|
689
|
+
_empty_glyph = None
|
|
690
|
+
_zero_glyph = None
|
|
691
|
+
|
|
692
|
+
glyph_renderer_class = DirectWriteGlyphRenderer
|
|
693
|
+
texture_internalformat = pyglet.gl.GL_RGBA
|
|
694
|
+
|
|
695
|
+
def __init__(self, name: str, size: float, weight: str = "normal", italic: bool | str = False, # noqa: D107
|
|
696
|
+
stretch: bool | str = False, dpi: int | None = None, locale: str | None = None) -> None:
|
|
697
|
+
self._filename: str | None = None
|
|
698
|
+
|
|
699
|
+
super().__init__()
|
|
700
|
+
|
|
701
|
+
if not name:
|
|
702
|
+
name = self._default_name
|
|
703
|
+
|
|
704
|
+
self.buffers = []
|
|
705
|
+
self._name = name
|
|
706
|
+
self.weight = weight
|
|
707
|
+
self.size = size
|
|
708
|
+
self.italic = italic
|
|
709
|
+
self.stretch = stretch
|
|
710
|
+
self.dpi = dpi
|
|
711
|
+
self.locale = locale
|
|
712
|
+
|
|
713
|
+
if self.locale is None:
|
|
714
|
+
self.locale = ""
|
|
715
|
+
self.rtl = False # Right to left should be handled by pyglet?
|
|
716
|
+
# Use system locale string?
|
|
717
|
+
|
|
718
|
+
if self.dpi is None:
|
|
719
|
+
self.dpi = 96
|
|
720
|
+
|
|
721
|
+
# From DPI to DIP (Device Independent Pixels) which is what the fonts rely on.
|
|
722
|
+
self.pixel_size = (self.size * self.dpi) // 72
|
|
723
|
+
|
|
724
|
+
self._weight = name_to_weight[self.weight]
|
|
725
|
+
|
|
726
|
+
if self.italic:
|
|
727
|
+
if isinstance(self.italic, str):
|
|
728
|
+
self._style = name_to_style[self.italic]
|
|
729
|
+
else:
|
|
730
|
+
self._style = DWRITE_FONT_STYLE_ITALIC
|
|
731
|
+
else:
|
|
732
|
+
self._style = DWRITE_FONT_STYLE_NORMAL
|
|
733
|
+
|
|
734
|
+
if self.stretch:
|
|
735
|
+
if isinstance(self.stretch, str):
|
|
736
|
+
self._stretch = name_to_stretch[self.stretch]
|
|
737
|
+
else:
|
|
738
|
+
self._stretch = DWRITE_FONT_STRETCH_EXPANDED
|
|
739
|
+
else:
|
|
740
|
+
self._stretch = DWRITE_FONT_STRETCH_NORMAL
|
|
741
|
+
|
|
742
|
+
self._font_index, self._collection = self.get_collection(name)
|
|
743
|
+
write_font = None
|
|
744
|
+
# If not font found, search all collections for legacy GDI naming.
|
|
745
|
+
if pyglet.options["dw_legacy_naming"] and (self._font_index is None and self._collection is None):
|
|
746
|
+
write_font, self._collection = self.find_font_face(name, self._weight, self._style, self._stretch)
|
|
747
|
+
|
|
748
|
+
assert self._collection is not None, f"Font: '{name}' not found in loaded or system font collection."
|
|
749
|
+
|
|
750
|
+
if self._font_index is not None:
|
|
751
|
+
font_family = IDWriteFontFamily1()
|
|
752
|
+
self._collection.GetFontFamily(self._font_index, byref(font_family))
|
|
753
|
+
|
|
754
|
+
write_font = IDWriteFont()
|
|
755
|
+
font_family.GetFirstMatchingFont(
|
|
756
|
+
self._weight,
|
|
757
|
+
self._stretch,
|
|
758
|
+
self._style,
|
|
759
|
+
byref(write_font),
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
# Create the text format this font will use permanently.
|
|
763
|
+
# Could technically be recreated, but will keep to be inline with other font objects.
|
|
764
|
+
self._text_format = IDWriteTextFormat()
|
|
765
|
+
self._write_factory.CreateTextFormat(
|
|
766
|
+
self._name,
|
|
767
|
+
self._collection,
|
|
768
|
+
self._weight,
|
|
769
|
+
self._style,
|
|
770
|
+
self._stretch,
|
|
771
|
+
self.pixel_size,
|
|
772
|
+
create_unicode_buffer(self.locale),
|
|
773
|
+
byref(self._text_format),
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
font_face = IDWriteFontFace()
|
|
777
|
+
write_font.CreateFontFace(byref(font_face))
|
|
778
|
+
|
|
779
|
+
# font_face4 = IDWriteFontFace4()
|
|
780
|
+
# if com.is_available(font_face, IID_IDWriteFontFace4, font_face4):
|
|
781
|
+
# font_face = font_face4
|
|
782
|
+
# else:
|
|
783
|
+
# font_face2 = IDWriteFontFace2()
|
|
784
|
+
# if com.is_available(font_face, IID_IDWriteFontFace2, font_face2):
|
|
785
|
+
# font_face = font_face2
|
|
786
|
+
|
|
787
|
+
self.font_face = font_face
|
|
788
|
+
self._font_metrics = DWRITE_FONT_METRICS()
|
|
789
|
+
self.font_face.GetMetrics(byref(self._font_metrics))
|
|
790
|
+
|
|
791
|
+
self.font_scale_ratio = (self.pixel_size / self._font_metrics.designUnitsPerEm)
|
|
792
|
+
|
|
793
|
+
self.ascent = math.ceil(self._font_metrics.ascent * self.font_scale_ratio)
|
|
794
|
+
self.descent = -round(self._font_metrics.descent * self.font_scale_ratio)
|
|
795
|
+
self.max_glyph_height = (self._font_metrics.ascent + self._font_metrics.descent) * self.font_scale_ratio
|
|
796
|
+
|
|
797
|
+
self.line_gap = self._font_metrics.lineGap * self.font_scale_ratio
|
|
798
|
+
|
|
799
|
+
if pyglet.options.text_shaping == 'harfbuzz' and harfbuzz_available():
|
|
800
|
+
self.hb_resource = get_resource_from_dw_font(self)
|
|
801
|
+
|
|
802
|
+
def get_font_data(self) -> bytes | None:
|
|
803
|
+
ff = _get_font_file(self.font_face)
|
|
804
|
+
loader = IDWriteFontFileLoader()
|
|
805
|
+
ff.GetLoader(byref(loader))
|
|
806
|
+
|
|
807
|
+
key, size = _get_font_ref(ff, False)
|
|
808
|
+
|
|
809
|
+
font_filestream = POINTER(IDWriteFontFileStream)()
|
|
810
|
+
loader.CreateStreamFromKey(key, size, byref(font_filestream))
|
|
811
|
+
|
|
812
|
+
if font_filestream:
|
|
813
|
+
size = UINT64()
|
|
814
|
+
font_filestream.GetFileSize(byref(size))
|
|
815
|
+
void_data = c_void_p()
|
|
816
|
+
context = c_void_p()
|
|
817
|
+
font_filestream.ReadFileFragment(byref(void_data), 0, size.value, byref(context))
|
|
818
|
+
|
|
819
|
+
font_data = string_at(void_data, size.value)
|
|
820
|
+
|
|
821
|
+
if context:
|
|
822
|
+
font_filestream.ReleaseFileFragment(context)
|
|
823
|
+
|
|
824
|
+
return font_data
|
|
825
|
+
|
|
826
|
+
return None
|
|
827
|
+
|
|
828
|
+
@property
|
|
829
|
+
def filename(self) -> str:
|
|
830
|
+
"""Returns a filename associated with the font face.
|
|
831
|
+
|
|
832
|
+
Note: Capable of returning more than 1 file in the future, but will do just one for now.
|
|
833
|
+
"""
|
|
834
|
+
if self._filename is not None:
|
|
835
|
+
return self._filename
|
|
836
|
+
|
|
837
|
+
self._filename = "Not Available"
|
|
838
|
+
|
|
839
|
+
font_file = _get_font_file(self.font_face)
|
|
840
|
+
key_data, key_size = _get_font_ref(font_file, release_file=False)
|
|
841
|
+
|
|
842
|
+
loader = IDWriteFontFileLoader()
|
|
843
|
+
font_file.GetLoader(byref(loader))
|
|
844
|
+
|
|
845
|
+
try:
|
|
846
|
+
local_loader = IDWriteLocalFontFileLoader()
|
|
847
|
+
loader.QueryInterface(IID_IDWriteLocalFontFileLoader, byref(local_loader))
|
|
848
|
+
except OSError as e:
|
|
849
|
+
int_error = e.winerror & 0xFFFFFFFF
|
|
850
|
+
if int_error == com.E_NOTIMPL or int_error == com.E_NOINTERFACE:
|
|
851
|
+
loader.Release()
|
|
852
|
+
font_file.Release()
|
|
853
|
+
else:
|
|
854
|
+
raise e
|
|
855
|
+
return self._filename
|
|
856
|
+
|
|
857
|
+
path_len = UINT32()
|
|
858
|
+
local_loader.GetFilePathLengthFromKey(key_data, key_size, byref(path_len))
|
|
859
|
+
|
|
860
|
+
buffer = create_unicode_buffer(path_len.value + 1)
|
|
861
|
+
local_loader.GetFilePathFromKey(key_data, key_size, buffer, len(buffer))
|
|
862
|
+
|
|
863
|
+
loader.Release()
|
|
864
|
+
local_loader.Release()
|
|
865
|
+
font_file.Release()
|
|
866
|
+
|
|
867
|
+
self._filename = pathlib.PureWindowsPath(buffer.value).as_posix() # Convert to forward slashes.
|
|
868
|
+
return self._filename
|
|
869
|
+
|
|
870
|
+
@property
|
|
871
|
+
def name(self) -> str:
|
|
872
|
+
return self._name
|
|
873
|
+
|
|
874
|
+
def render_to_image(self, text: str, width: int=10000, height: int=80) -> ImageData:
|
|
875
|
+
"""This process uses only DirectWrite to shape and render text for layout and graphics.
|
|
876
|
+
|
|
877
|
+
This may allow more accurate fonts (bidi, rtl, etc) in very special circumstances at the cost of
|
|
878
|
+
additional texture space.
|
|
879
|
+
"""
|
|
880
|
+
self._initialize_renderer()
|
|
881
|
+
|
|
882
|
+
return self._glyph_renderer.render_to_image(text, width, height)
|
|
883
|
+
|
|
884
|
+
def get_glyph_indices(self, text: str) -> tuple[Sequence[int], dict[int, int]]:
|
|
885
|
+
codepoints = []
|
|
886
|
+
clusters = base.get_grapheme_clusters(text)
|
|
887
|
+
|
|
888
|
+
text_length = len(clusters)
|
|
889
|
+
for c in clusters:
|
|
890
|
+
if c == "\t":
|
|
891
|
+
c = " "
|
|
892
|
+
codepoints.append(ord(c[0]))
|
|
893
|
+
|
|
894
|
+
text_array = (UINT32 * text_length)()
|
|
895
|
+
text_array[:] = codepoints
|
|
896
|
+
|
|
897
|
+
glyph_indices = (UINT16 * text_length)()
|
|
898
|
+
|
|
899
|
+
self.font_face.GetGlyphIndices(text_array, text_length, glyph_indices)
|
|
900
|
+
|
|
901
|
+
missing = {}
|
|
902
|
+
for i, glyph_indice in enumerate(glyph_indices):
|
|
903
|
+
if glyph_indice == 0:
|
|
904
|
+
missing[i] = clusters[i]
|
|
905
|
+
|
|
906
|
+
print("MISSING", missing)
|
|
907
|
+
|
|
908
|
+
return glyph_indices, missing
|
|
909
|
+
|
|
910
|
+
def render_glyph_indices(self, indices: list[int]) -> None:
|
|
911
|
+
"""Given the indice list, ensure all glyphs are available."""
|
|
912
|
+
# Process any glyphs we haven't rendered.
|
|
913
|
+
self._initialize_renderer()
|
|
914
|
+
|
|
915
|
+
missing = set()
|
|
916
|
+
for i in range(len(indices)):
|
|
917
|
+
glyph_indice = indices[i]
|
|
918
|
+
if glyph_indice not in self.glyphs:
|
|
919
|
+
missing.add(glyph_indice)
|
|
920
|
+
|
|
921
|
+
# Missing glyphs, get their info.
|
|
922
|
+
if missing:
|
|
923
|
+
metrics = get_glyph_metrics(self.font_face, (UINT16 * len(missing))(*missing), len(missing))
|
|
924
|
+
|
|
925
|
+
for idx, glyph_indice in enumerate(missing):
|
|
926
|
+
glyph = self._glyph_renderer.render_single_glyph(self.font_face, glyph_indice, metrics[idx],
|
|
927
|
+
self._glyph_renderer.measuring_mode)
|
|
928
|
+
self.glyphs[glyph_indice] = glyph
|
|
929
|
+
|
|
930
|
+
def _get_fallback_glyph(self, text: str) -> base.Glyph:
|
|
931
|
+
for fallback in self.fallbacks:
|
|
932
|
+
indices, missing = fallback.get_glyph_indices(text)
|
|
933
|
+
# If the amount of indices match what's missing, nothing was retrieved.
|
|
934
|
+
if len(indices) == len(missing):
|
|
935
|
+
continue
|
|
936
|
+
# Fallback should render the glyphs it found.
|
|
937
|
+
fallback.render_glyph_indices(indices)
|
|
938
|
+
for indice in indices:
|
|
939
|
+
if indice != 0:
|
|
940
|
+
return fallback.glyphs[indice]
|
|
941
|
+
|
|
942
|
+
return self.glyphs[0]
|
|
943
|
+
|
|
944
|
+
def get_glyphs(self, text: str) -> tuple[list[Glyph], list[base.GlyphPosition]]:
|
|
945
|
+
self._initialize_renderer()
|
|
946
|
+
|
|
947
|
+
if pyglet.options.text_shaping == 'harfbuzz' and harfbuzz_available():
|
|
948
|
+
return get_harfbuzz_shaped_glyphs(self, text)
|
|
949
|
+
|
|
950
|
+
if pyglet.options.text_shaping == 'platform':
|
|
951
|
+
self._glyph_renderer.current_glyphs.clear()
|
|
952
|
+
self._glyph_renderer.current_offsets.clear()
|
|
953
|
+
text_layout = self.create_text_layout(text)
|
|
954
|
+
|
|
955
|
+
ptr = cast(id(self._glyph_renderer), c_void_p)
|
|
956
|
+
text_layout.Draw(ptr, _renderer.as_interface(IDWriteTextRenderer), 0, 0)
|
|
957
|
+
text_layout.Release()
|
|
958
|
+
|
|
959
|
+
return self._glyph_renderer.current_glyphs, self._glyph_renderer.current_offsets
|
|
960
|
+
|
|
961
|
+
glyphs = []
|
|
962
|
+
offsets = []
|
|
963
|
+
indices, missing = self.get_glyph_indices(text)
|
|
964
|
+
self.render_glyph_indices(indices)
|
|
965
|
+
for i, indice in enumerate(indices):
|
|
966
|
+
if indice == 0:
|
|
967
|
+
glyph = self._get_fallback_glyph(missing[i])
|
|
968
|
+
glyphs.append(glyph)
|
|
969
|
+
else:
|
|
970
|
+
glyphs.append(self.glyphs[indice])
|
|
971
|
+
offsets.append(GlyphPosition(0, 0, 0, 0))
|
|
972
|
+
|
|
973
|
+
return glyphs, offsets
|
|
974
|
+
|
|
975
|
+
def create_text_layout(self, text: str) -> IDWriteTextLayout:
|
|
976
|
+
"""Create a text layout that holds the specified text.
|
|
977
|
+
|
|
978
|
+
.. note:: Caller is responsible for calling ``Release`` when finished.
|
|
979
|
+
"""
|
|
980
|
+
text_buffer = create_unicode_buffer(text)
|
|
981
|
+
|
|
982
|
+
text_layout = IDWriteTextLayout()
|
|
983
|
+
self._write_factory.CreateTextLayout(
|
|
984
|
+
text_buffer, len(text_buffer)-1, self._text_format,
|
|
985
|
+
10000, 10000, # Doesn't affect glyph, bitmap size.
|
|
986
|
+
byref(text_layout),
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
return text_layout
|
|
990
|
+
|
|
991
|
+
@classmethod
|
|
992
|
+
def _initialize_direct_write(cls: type[Win32DirectWriteFont]) -> None:
|
|
993
|
+
"""All DirectWrite fonts needs factory access as well as the loaders."""
|
|
994
|
+
if WINDOWS_10_1809_OR_GREATER: # Added Bitmap based image glyphs.
|
|
995
|
+
cls._write_factory = IDWriteFactory7()
|
|
996
|
+
guid = IID_IDWriteFactory7
|
|
997
|
+
elif WINDOWS_10_CREATORS_UPDATE_OR_GREATER: # Added memory loader. Added SVG color glyphs.
|
|
998
|
+
cls._write_factory = IDWriteFactory5()
|
|
999
|
+
guid = IID_IDWriteFactory5
|
|
1000
|
+
elif WINDOWS_8_1_OR_GREATER: # Added COLOR/CPAL color glyphs.
|
|
1001
|
+
cls._write_factory = IDWriteFactory2()
|
|
1002
|
+
guid = IID_IDWriteFactory2
|
|
1003
|
+
else:
|
|
1004
|
+
cls._write_factory = IDWriteFactory()
|
|
1005
|
+
guid = IID_IDWriteFactory
|
|
1006
|
+
|
|
1007
|
+
DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, guid, byref(cls._write_factory))
|
|
1008
|
+
|
|
1009
|
+
@classmethod
|
|
1010
|
+
def _initialize_custom_loaders(cls: type[Win32DirectWriteFont]) -> None:
|
|
1011
|
+
"""Initialize the loaders needed to load custom fonts."""
|
|
1012
|
+
if WINDOWS_10_CREATORS_UPDATE_OR_GREATER:
|
|
1013
|
+
# Windows 10 finally has a built in loader that can take data and make a font out of it w/ COMs.
|
|
1014
|
+
cls._font_loader = IDWriteInMemoryFontFileLoader()
|
|
1015
|
+
cls._write_factory.CreateInMemoryFontFileLoader(byref(cls._font_loader))
|
|
1016
|
+
cls._write_factory.RegisterFontFileLoader(cls._font_loader)
|
|
1017
|
+
|
|
1018
|
+
# Used for grouping fonts together.
|
|
1019
|
+
cls._font_builder = IDWriteFontSetBuilder1()
|
|
1020
|
+
cls._write_factory.CreateFontSetBuilder5(byref(cls._font_builder))
|
|
1021
|
+
else:
|
|
1022
|
+
cls._font_loader = LegacyFontFileLoader()
|
|
1023
|
+
|
|
1024
|
+
# Note: RegisterFontLoader takes a pointer. However, for legacy we implement our own callback interface.
|
|
1025
|
+
# Therefore we need to pass to the actual pointer directly.
|
|
1026
|
+
cls._write_factory.RegisterFontFileLoader(cls._font_loader.as_interface(IDWriteFontFileLoader_LI))
|
|
1027
|
+
|
|
1028
|
+
cls._font_collection_loader = LegacyCollectionLoader(cls._write_factory, cls._font_loader)
|
|
1029
|
+
cls._write_factory.RegisterFontCollectionLoader(cls._font_collection_loader)
|
|
1030
|
+
|
|
1031
|
+
cls._font_loader_key = cast(create_unicode_buffer("legacy_font_loader"), c_void_p)
|
|
1032
|
+
|
|
1033
|
+
@classmethod
|
|
1034
|
+
def add_font_data(cls: type[Win32DirectWriteFont], data: BinaryIO) -> None:
|
|
1035
|
+
if not cls._write_factory:
|
|
1036
|
+
cls._initialize_direct_write()
|
|
1037
|
+
|
|
1038
|
+
if not cls._font_loader:
|
|
1039
|
+
cls._initialize_custom_loaders()
|
|
1040
|
+
|
|
1041
|
+
if WINDOWS_10_CREATORS_UPDATE_OR_GREATER:
|
|
1042
|
+
font_file = IDWriteFontFile()
|
|
1043
|
+
cls._font_loader.CreateInMemoryFontFileReference(cls._write_factory,
|
|
1044
|
+
data,
|
|
1045
|
+
len(data),
|
|
1046
|
+
None,
|
|
1047
|
+
byref(font_file))
|
|
1048
|
+
|
|
1049
|
+
hr = cls._font_builder.AddFontFile(font_file)
|
|
1050
|
+
if hr != 0:
|
|
1051
|
+
raise Exception("This font file data is not not a font or unsupported.")
|
|
1052
|
+
|
|
1053
|
+
# We have to rebuild collection everytime we add a font.
|
|
1054
|
+
# No way to add fonts to the collection once the FontSet and Collection are created.
|
|
1055
|
+
# Release old one and renew.
|
|
1056
|
+
if cls._custom_collection:
|
|
1057
|
+
cls._font_set.Release()
|
|
1058
|
+
cls._custom_collection.Release()
|
|
1059
|
+
|
|
1060
|
+
cls._font_set = IDWriteFontSet()
|
|
1061
|
+
cls._font_builder.CreateFontSet(byref(cls._font_set))
|
|
1062
|
+
|
|
1063
|
+
cls._custom_collection = IDWriteFontCollection1()
|
|
1064
|
+
cls._write_factory.CreateFontCollectionFromFontSet(cls._font_set, byref(cls._custom_collection))
|
|
1065
|
+
|
|
1066
|
+
else:
|
|
1067
|
+
cls._font_cache.append(data)
|
|
1068
|
+
|
|
1069
|
+
# If a collection exists, we need to completely remake the collection, delete everything and start over.
|
|
1070
|
+
if cls._custom_collection:
|
|
1071
|
+
cls._custom_collection = None
|
|
1072
|
+
|
|
1073
|
+
cls._write_factory.UnregisterFontCollectionLoader(cls._font_collection_loader)
|
|
1074
|
+
cls._write_factory.UnregisterFontFileLoader(cls._font_loader)
|
|
1075
|
+
|
|
1076
|
+
cls._font_loader = LegacyFontFileLoader()
|
|
1077
|
+
cls._font_collection_loader = LegacyCollectionLoader(cls._write_factory, cls._font_loader)
|
|
1078
|
+
|
|
1079
|
+
cls._write_factory.RegisterFontCollectionLoader(cls._font_collection_loader)
|
|
1080
|
+
cls._write_factory.RegisterFontFileLoader(cls._font_loader.as_interface(IDWriteFontFileLoader_LI))
|
|
1081
|
+
|
|
1082
|
+
cls._font_collection_loader.AddFontData(cls._font_cache)
|
|
1083
|
+
|
|
1084
|
+
cls._custom_collection = IDWriteFontCollection()
|
|
1085
|
+
|
|
1086
|
+
cls._write_factory.CreateCustomFontCollection(cls._font_collection_loader,
|
|
1087
|
+
cls._font_loader_key,
|
|
1088
|
+
sizeof(cls._font_loader_key),
|
|
1089
|
+
byref(cls._custom_collection))
|
|
1090
|
+
|
|
1091
|
+
@classmethod
|
|
1092
|
+
def get_collection(cls: type[Win32DirectWriteFont], font_name: str) -> tuple[int | None, IDWriteFontCollection1 | None]:
|
|
1093
|
+
"""Obtain a collection of fonts based on the font name.
|
|
1094
|
+
|
|
1095
|
+
Returns:
|
|
1096
|
+
Warnings collection this font belongs to (system or custom collection), as well as the index
|
|
1097
|
+
in the collection.
|
|
1098
|
+
"""
|
|
1099
|
+
if not cls._write_factory:
|
|
1100
|
+
cls._initialize_direct_write()
|
|
1101
|
+
|
|
1102
|
+
font_index = UINT()
|
|
1103
|
+
font_exists = BOOL()
|
|
1104
|
+
|
|
1105
|
+
# Check custom loaded font collections.
|
|
1106
|
+
if cls._custom_collection:
|
|
1107
|
+
cls._custom_collection.FindFamilyName(create_unicode_buffer(font_name),
|
|
1108
|
+
byref(font_index),
|
|
1109
|
+
byref(font_exists))
|
|
1110
|
+
|
|
1111
|
+
if font_exists.value:
|
|
1112
|
+
return font_index.value, cls._custom_collection
|
|
1113
|
+
|
|
1114
|
+
# Check if font is in the system collection.
|
|
1115
|
+
# Do not cache these values permanently as system font collection can be updated during runtime.
|
|
1116
|
+
sys_collection = IDWriteFontCollection()
|
|
1117
|
+
if not font_exists.value:
|
|
1118
|
+
cls._write_factory.GetSystemFontCollection(byref(sys_collection), 1)
|
|
1119
|
+
sys_collection.FindFamilyName(create_unicode_buffer(font_name),
|
|
1120
|
+
byref(font_index),
|
|
1121
|
+
byref(font_exists))
|
|
1122
|
+
|
|
1123
|
+
if font_exists.value:
|
|
1124
|
+
return font_index.value, sys_collection
|
|
1125
|
+
|
|
1126
|
+
return None, None
|
|
1127
|
+
|
|
1128
|
+
@classmethod
|
|
1129
|
+
def find_font_face(cls, font_name: str, weight: str, italic: bool | str, stretch: bool | str) -> tuple[
|
|
1130
|
+
IDWriteFont | None, IDWriteFontCollection | None]:
|
|
1131
|
+
"""Search font collections for legacy RBIZ names.
|
|
1132
|
+
|
|
1133
|
+
Matching to weight, italic, stretch is problematic in that there are many values. Attempt to parse the font
|
|
1134
|
+
name looking for matches to the name database, and pick the closest match.
|
|
1135
|
+
|
|
1136
|
+
This will search all font faces in the system and custom collections.
|
|
1137
|
+
|
|
1138
|
+
Returns:
|
|
1139
|
+
Returns a collection and IDWriteFont if successful.
|
|
1140
|
+
"""
|
|
1141
|
+
p_weight, p_italic, p_stretch = cls.parse_name(font_name, weight, italic, stretch)
|
|
1142
|
+
|
|
1143
|
+
_debug_print(f"directwrite: '{font_name}' not found. Attempting legacy name lookup in all collections.")
|
|
1144
|
+
if cls._custom_collection:
|
|
1145
|
+
collection_idx = cls.find_legacy_font(cls._custom_collection, font_name, p_weight, p_italic, p_stretch)
|
|
1146
|
+
if collection_idx is not None:
|
|
1147
|
+
return collection_idx, cls._custom_collection
|
|
1148
|
+
|
|
1149
|
+
sys_collection = IDWriteFontCollection()
|
|
1150
|
+
cls._write_factory.GetSystemFontCollection(byref(sys_collection), 1)
|
|
1151
|
+
|
|
1152
|
+
collection_idx = cls.find_legacy_font(sys_collection, font_name, p_weight, p_italic, p_stretch)
|
|
1153
|
+
if collection_idx is not None:
|
|
1154
|
+
return collection_idx, sys_collection
|
|
1155
|
+
|
|
1156
|
+
return None, None
|
|
1157
|
+
|
|
1158
|
+
def get_text_size(self, text: str) -> tuple[int, int]:
|
|
1159
|
+
layout = self.create_text_layout(text)
|
|
1160
|
+
metrics = DWRITE_TEXT_METRICS()
|
|
1161
|
+
layout.GetMetrics(byref(metrics))
|
|
1162
|
+
layout.Release()
|
|
1163
|
+
return round(metrics.width), round(metrics.height)
|
|
1164
|
+
|
|
1165
|
+
@classmethod
|
|
1166
|
+
def have_font(cls: type[Win32DirectWriteFont], name: str) -> bool:
|
|
1167
|
+
return cls.get_collection(name)[0] is not None
|
|
1168
|
+
|
|
1169
|
+
@staticmethod
|
|
1170
|
+
def parse_name(font_name: str, weight: str, style: int, stretch: int) -> tuple[str, int, int]:
|
|
1171
|
+
"""Attempt at parsing any special names in a font for legacy checks. Takes the first found."""
|
|
1172
|
+
font_name = font_name.lower()
|
|
1173
|
+
split_name = font_name.split(" ")
|
|
1174
|
+
|
|
1175
|
+
found_weight = weight
|
|
1176
|
+
found_style = style
|
|
1177
|
+
found_stretch = stretch
|
|
1178
|
+
|
|
1179
|
+
# Only search if name is split more than once.
|
|
1180
|
+
if len(split_name) > 1:
|
|
1181
|
+
for name, value in name_to_weight.items():
|
|
1182
|
+
if name in split_name:
|
|
1183
|
+
found_weight = value
|
|
1184
|
+
break
|
|
1185
|
+
|
|
1186
|
+
for name, value in name_to_style.items():
|
|
1187
|
+
if name in split_name:
|
|
1188
|
+
found_style = value
|
|
1189
|
+
break
|
|
1190
|
+
|
|
1191
|
+
for name, value in name_to_stretch.items():
|
|
1192
|
+
if name in split_name:
|
|
1193
|
+
found_stretch = value
|
|
1194
|
+
break
|
|
1195
|
+
|
|
1196
|
+
return found_weight, found_style, found_stretch
|
|
1197
|
+
|
|
1198
|
+
@staticmethod
|
|
1199
|
+
def find_legacy_font(collection: IDWriteFontCollection, font_name: str, weight: str, italic: bool | str, stretch: bool | str, full_debug: bool=False) -> IDWriteFont | None:
|
|
1200
|
+
coll_count = collection.GetFontFamilyCount()
|
|
1201
|
+
|
|
1202
|
+
assert _debug_print(f"directwrite: Found {coll_count} fonts in collection.")
|
|
1203
|
+
|
|
1204
|
+
locale = get_system_locale()
|
|
1205
|
+
|
|
1206
|
+
for i in range(coll_count):
|
|
1207
|
+
family = IDWriteFontFamily()
|
|
1208
|
+
collection.GetFontFamily(i, byref(family))
|
|
1209
|
+
|
|
1210
|
+
# Just check the first character in Family Names to reduce search time. Arial -> A's only.
|
|
1211
|
+
family_name_str = IDWriteLocalizedStrings()
|
|
1212
|
+
family.GetFamilyNames(byref(family_name_str))
|
|
1213
|
+
|
|
1214
|
+
family_names = Win32DirectWriteFont.unpack_localized_string(family_name_str, locale)
|
|
1215
|
+
family_name = family_names[0]
|
|
1216
|
+
|
|
1217
|
+
if family_name[0] != font_name[0]:
|
|
1218
|
+
family.Release()
|
|
1219
|
+
continue
|
|
1220
|
+
|
|
1221
|
+
assert _debug_print(f"directwrite: Inspecting family name: {family_name}")
|
|
1222
|
+
|
|
1223
|
+
# Fonts in the family. Full search to search all font faces, typically the first will be good enough to tell
|
|
1224
|
+
ft_ct = family.GetFontCount()
|
|
1225
|
+
|
|
1226
|
+
face_names = []
|
|
1227
|
+
matches = []
|
|
1228
|
+
for j in range(ft_ct):
|
|
1229
|
+
temp_ft = IDWriteFont()
|
|
1230
|
+
family.GetFont(j, byref(temp_ft))
|
|
1231
|
+
|
|
1232
|
+
if _debug_font and full_debug:
|
|
1233
|
+
fc_str = IDWriteLocalizedStrings()
|
|
1234
|
+
temp_ft.GetFaceNames(byref(fc_str))
|
|
1235
|
+
|
|
1236
|
+
strings = Win32DirectWriteFont.unpack_localized_string(fc_str, locale)
|
|
1237
|
+
face_names.extend(strings)
|
|
1238
|
+
|
|
1239
|
+
assert _debug_print(f"directwrite: Face names found: {strings}")
|
|
1240
|
+
|
|
1241
|
+
# Check for GDI compatibility name
|
|
1242
|
+
compat_names = IDWriteLocalizedStrings()
|
|
1243
|
+
exists = BOOL()
|
|
1244
|
+
temp_ft.GetInformationalStrings(DWRITE_INFORMATIONAL_STRING_WIN32_FAMILY_NAMES,
|
|
1245
|
+
byref(compat_names),
|
|
1246
|
+
byref(exists))
|
|
1247
|
+
|
|
1248
|
+
# Successful in finding GDI name.
|
|
1249
|
+
match_found = False
|
|
1250
|
+
if exists.value != 0:
|
|
1251
|
+
for compat_name in Win32DirectWriteFont.unpack_localized_string(compat_names, locale):
|
|
1252
|
+
if compat_name == font_name:
|
|
1253
|
+
assert _debug_print(
|
|
1254
|
+
f"Found legacy name '{font_name}' as '{family_name}' in font face '{j}' (collection "
|
|
1255
|
+
f"id #{i}).")
|
|
1256
|
+
|
|
1257
|
+
match_found = True
|
|
1258
|
+
matches.append((temp_ft.GetWeight(), temp_ft.GetStyle(), temp_ft.GetStretch(), temp_ft))
|
|
1259
|
+
break
|
|
1260
|
+
|
|
1261
|
+
# Release resource if not a match.
|
|
1262
|
+
if not match_found:
|
|
1263
|
+
temp_ft.Release()
|
|
1264
|
+
|
|
1265
|
+
family.Release()
|
|
1266
|
+
|
|
1267
|
+
# If we have matches, we've already parsed through the proper family. Now try to match.
|
|
1268
|
+
if matches:
|
|
1269
|
+
write_font = Win32DirectWriteFont.match_closest_font(matches, weight, italic, stretch)
|
|
1270
|
+
|
|
1271
|
+
# Cleanup other matches not used.
|
|
1272
|
+
for match in matches:
|
|
1273
|
+
if match[3] != write_font:
|
|
1274
|
+
match[3].Release() # Release all other matches.
|
|
1275
|
+
|
|
1276
|
+
return write_font
|
|
1277
|
+
|
|
1278
|
+
return None
|
|
1279
|
+
|
|
1280
|
+
@staticmethod
|
|
1281
|
+
def match_closest_font(font_list: list[tuple[int, int, int, IDWriteFont]], weight: str, italic: int, stretch: int) -> IDWriteFont | None:
|
|
1282
|
+
"""Match the closest font to the parameters specified.
|
|
1283
|
+
|
|
1284
|
+
If a full match is not found, a secondary match will be found based on similar features. This can probably
|
|
1285
|
+
be improved, but it is possible you could get a different font style than expected.
|
|
1286
|
+
"""
|
|
1287
|
+
closest = []
|
|
1288
|
+
for match in font_list:
|
|
1289
|
+
(f_weight, f_style, f_stretch, writefont) = match
|
|
1290
|
+
|
|
1291
|
+
# Found perfect match, no need for the rest.
|
|
1292
|
+
if f_weight == weight and f_style == italic and f_stretch == stretch:
|
|
1293
|
+
_debug_print(
|
|
1294
|
+
f"directwrite: full match found. (weight: {f_weight}, italic: {f_style}, stretch: {f_stretch})")
|
|
1295
|
+
return writefont
|
|
1296
|
+
|
|
1297
|
+
prop_match = 0
|
|
1298
|
+
similar_match = 0
|
|
1299
|
+
# Look for a full match, otherwise look for close enough.
|
|
1300
|
+
# For example, Arial Black only has Oblique, not Italic, but good enough if you want slanted text.
|
|
1301
|
+
if f_weight == weight:
|
|
1302
|
+
prop_match += 1
|
|
1303
|
+
elif weight != DWRITE_FONT_WEIGHT_NORMAL and f_weight != DWRITE_FONT_WEIGHT_NORMAL:
|
|
1304
|
+
similar_match += 1
|
|
1305
|
+
|
|
1306
|
+
if f_style == italic:
|
|
1307
|
+
prop_match += 1
|
|
1308
|
+
elif italic != DWRITE_FONT_STYLE_NORMAL and f_style != DWRITE_FONT_STYLE_NORMAL:
|
|
1309
|
+
similar_match += 1
|
|
1310
|
+
|
|
1311
|
+
if stretch == f_stretch:
|
|
1312
|
+
prop_match += 1
|
|
1313
|
+
elif stretch != DWRITE_FONT_STRETCH_NORMAL and f_stretch != DWRITE_FONT_STRETCH_NORMAL:
|
|
1314
|
+
similar_match += 1
|
|
1315
|
+
|
|
1316
|
+
closest.append((prop_match, similar_match, *match))
|
|
1317
|
+
|
|
1318
|
+
# If we get here, no perfect match, sort by highest perfect match, to secondary matches.
|
|
1319
|
+
closest.sort(key=lambda fts: (fts[0], fts[1]), reverse=True)
|
|
1320
|
+
|
|
1321
|
+
if closest:
|
|
1322
|
+
# Take the first match after sorting.
|
|
1323
|
+
closest_match = closest[0]
|
|
1324
|
+
_debug_print(f"directwrite: falling back to partial match. "
|
|
1325
|
+
f"(weight: {closest_match[2]}, italic: {closest_match[3]}, stretch: {closest_match[4]})")
|
|
1326
|
+
return closest_match[5]
|
|
1327
|
+
|
|
1328
|
+
return None
|
|
1329
|
+
|
|
1330
|
+
@staticmethod
|
|
1331
|
+
def unpack_localized_string(local_string: IDWriteLocalizedStrings, locale: str) -> list[str]:
|
|
1332
|
+
"""Takes IDWriteLocalizedStrings and unpacks the strings inside of it into a list."""
|
|
1333
|
+
str_array_len = local_string.GetCount()
|
|
1334
|
+
|
|
1335
|
+
strings = []
|
|
1336
|
+
for _ in range(str_array_len):
|
|
1337
|
+
string_size = UINT32()
|
|
1338
|
+
|
|
1339
|
+
idx = Win32DirectWriteFont.get_localized_index(local_string, locale)
|
|
1340
|
+
|
|
1341
|
+
local_string.GetStringLength(idx, byref(string_size))
|
|
1342
|
+
|
|
1343
|
+
buffer_size = string_size.value
|
|
1344
|
+
|
|
1345
|
+
buffer = create_unicode_buffer(buffer_size + 1)
|
|
1346
|
+
|
|
1347
|
+
local_string.GetString(idx, buffer, len(buffer))
|
|
1348
|
+
|
|
1349
|
+
strings.append(buffer.value)
|
|
1350
|
+
|
|
1351
|
+
local_string.Release()
|
|
1352
|
+
|
|
1353
|
+
return strings
|
|
1354
|
+
|
|
1355
|
+
@staticmethod
|
|
1356
|
+
def get_localized_index(strings: IDWriteLocalizedStrings, locale: str) -> int:
|
|
1357
|
+
idx = UINT32()
|
|
1358
|
+
exists = BOOL()
|
|
1359
|
+
|
|
1360
|
+
if locale:
|
|
1361
|
+
strings.FindLocaleName(locale, byref(idx), byref(exists))
|
|
1362
|
+
|
|
1363
|
+
if not exists.value:
|
|
1364
|
+
# fallback to english.
|
|
1365
|
+
strings.FindLocaleName("en-us", byref(idx), byref(exists))
|
|
1366
|
+
|
|
1367
|
+
if not exists:
|
|
1368
|
+
return 0
|
|
1369
|
+
|
|
1370
|
+
return idx.value
|
|
1371
|
+
|
|
1372
|
+
return 0
|
|
1373
|
+
|
|
1374
|
+
|
|
1375
|
+
d2d_factory = ID2D1Factory()
|
|
1376
|
+
hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, IID_ID2D1Factory, None, byref(d2d_factory))
|
|
1377
|
+
transparent = D2D1_COLOR_F(0.0, 0.0, 0.0, 0.0)
|
|
1378
|
+
white = D2D1_COLOR_F(1.0, 1.0, 1.0, 1.0)
|
|
1379
|
+
red = D2D1_COLOR_F(1.0, 0.0, 0.0, 1.0)
|
|
1380
|
+
no_offset = D2D_POINT_2F(0, 0)
|
|
1381
|
+
|