pyglet 2.1.3__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.
Files changed (61) hide show
  1. pyglet/__init__.py +21 -9
  2. pyglet/__init__.pyi +3 -1
  3. pyglet/app/cocoa.py +6 -3
  4. pyglet/display/win32.py +14 -15
  5. pyglet/display/xlib_vidmoderestore.py +1 -1
  6. pyglet/extlibs/earcut.py +2 -2
  7. pyglet/font/__init__.py +3 -3
  8. pyglet/font/base.py +118 -51
  9. pyglet/font/dwrite/__init__.py +1381 -0
  10. pyglet/font/dwrite/d2d1_lib.py +637 -0
  11. pyglet/font/dwrite/d2d1_types_lib.py +60 -0
  12. pyglet/font/dwrite/dwrite_lib.py +1577 -0
  13. pyglet/font/fontconfig.py +79 -16
  14. pyglet/font/freetype.py +252 -77
  15. pyglet/font/freetype_lib.py +234 -125
  16. pyglet/font/harfbuzz/__init__.py +275 -0
  17. pyglet/font/harfbuzz/harfbuzz_lib.py +212 -0
  18. pyglet/font/quartz.py +432 -112
  19. pyglet/font/user.py +18 -11
  20. pyglet/font/win32.py +9 -1
  21. pyglet/gl/wgl.py +94 -87
  22. pyglet/gl/wglext_arb.py +472 -218
  23. pyglet/gl/wglext_nv.py +410 -188
  24. pyglet/gui/widgets.py +6 -1
  25. pyglet/image/codecs/bmp.py +3 -5
  26. pyglet/image/codecs/gdiplus.py +28 -9
  27. pyglet/image/codecs/wic.py +198 -489
  28. pyglet/image/codecs/wincodec_lib.py +413 -0
  29. pyglet/input/base.py +3 -2
  30. pyglet/input/macos/darwin_hid.py +28 -2
  31. pyglet/input/win32/directinput.py +2 -1
  32. pyglet/input/win32/xinput.py +10 -9
  33. pyglet/lib.py +14 -2
  34. pyglet/libs/darwin/cocoapy/cocoalibs.py +74 -3
  35. pyglet/libs/darwin/coreaudio.py +0 -2
  36. pyglet/libs/win32/__init__.py +4 -2
  37. pyglet/libs/win32/com.py +65 -12
  38. pyglet/libs/win32/constants.py +1 -0
  39. pyglet/libs/win32/dinput.py +1 -9
  40. pyglet/libs/win32/types.py +72 -8
  41. pyglet/media/codecs/coreaudio.py +1 -0
  42. pyglet/media/codecs/wmf.py +93 -72
  43. pyglet/media/devices/win32.py +5 -4
  44. pyglet/media/drivers/directsound/lib_dsound.py +4 -4
  45. pyglet/media/drivers/xaudio2/interface.py +21 -17
  46. pyglet/media/drivers/xaudio2/lib_xaudio2.py +42 -25
  47. pyglet/shapes.py +1 -1
  48. pyglet/text/document.py +7 -53
  49. pyglet/text/formats/attributed.py +3 -1
  50. pyglet/text/formats/plaintext.py +1 -1
  51. pyglet/text/formats/structured.py +1 -1
  52. pyglet/text/layout/base.py +76 -68
  53. pyglet/text/layout/incremental.py +38 -8
  54. pyglet/text/layout/scrolling.py +1 -1
  55. pyglet/text/runlist.py +2 -114
  56. pyglet/window/win32/__init__.py +1 -3
  57. {pyglet-2.1.3.dist-info → pyglet-2.1.4.dist-info}/METADATA +1 -1
  58. {pyglet-2.1.3.dist-info → pyglet-2.1.4.dist-info}/RECORD +60 -54
  59. pyglet/font/directwrite.py +0 -2798
  60. {pyglet-2.1.3.dist-info → pyglet-2.1.4.dist-info}/LICENSE +0 -0
  61. {pyglet-2.1.3.dist-info → pyglet-2.1.4.dist-info}/WHEEL +0 -0
pyglet/font/quartz.py CHANGED
@@ -3,17 +3,114 @@ from __future__ import annotations
3
3
 
4
4
  import math
5
5
  import warnings
6
- from ctypes import byref, c_byte, c_int32, c_void_p
6
+ from ctypes import byref, c_int32, c_void_p, string_at, cast, c_char_p, c_float
7
7
  from typing import BinaryIO
8
8
 
9
9
  import pyglet.image
10
10
  from pyglet.font import base
11
- from pyglet.libs.darwin import CGFloat, cocoapy, kCTFontURLAttribute
11
+ from pyglet.font.base import Glyph, GlyphPosition
12
+ from pyglet.libs.darwin import CGFloat, cocoapy, kCTFontURLAttribute, cfnumber_to_number, \
13
+ kCTFontWeightTrait
14
+ from pyglet.font.harfbuzz import harfbuzz_available, get_resource_from_ct_font, \
15
+ get_harfbuzz_shaped_glyphs
16
+
12
17
 
13
18
  cf = cocoapy.cf
14
19
  ct = cocoapy.ct
15
20
  quartz = cocoapy.quartz
16
21
 
22
+ UIFontWeightUltraLight = -0.8
23
+ UIFontWeightThin = -0.6
24
+ UIFontWeightLight = -0.4
25
+ UIFontWeightRegular = 0.0
26
+ UIFontWeightMedium = 0.23
27
+ UIFontWeightSemibold = 0.3
28
+ UIFontWeightBold = 0.4
29
+ UIFontWeightHeavy = 0.56
30
+ UIFontWeightBlack = 0.62
31
+
32
+ name_to_weight = {
33
+ True: UIFontWeightBold, # Bold as default for True
34
+ False: UIFontWeightRegular, # Regular for False
35
+ None: UIFontWeightRegular, # Regular if no weight provided
36
+ "thin": UIFontWeightThin,
37
+ "extralight": UIFontWeightUltraLight,
38
+ "ultralight": UIFontWeightUltraLight,
39
+ "light": UIFontWeightLight,
40
+ "semilight": UIFontWeightLight,
41
+ "normal": UIFontWeightRegular,
42
+ "regular": UIFontWeightRegular,
43
+ "medium": UIFontWeightMedium,
44
+ "demibold": UIFontWeightSemibold,
45
+ "semibold": UIFontWeightSemibold,
46
+ "bold": UIFontWeightBold,
47
+ "extrabold": UIFontWeightBold,
48
+ "ultrabold": UIFontWeightBold,
49
+ "black": UIFontWeightBlack,
50
+ "heavy": UIFontWeightHeavy,
51
+ "extrablack": UIFontWeightBlack,
52
+ }
53
+
54
+ name_to_stretch = {
55
+ None: 1.0,
56
+ False: 1.0,
57
+ "undefined": 1.0,
58
+ "ultracondensed": -0.4,
59
+ "extracondensed": -0.3,
60
+ "condensed": -0.2,
61
+ "semicondensed": -0.1,
62
+ "normal": 0.0,
63
+ "medium": 0.0,
64
+ "semiexpanded": 0.1,
65
+ "expanded": 0.2,
66
+ "extraexpanded": 0.3,
67
+ "ultraexpanded": 0.4,
68
+ }
69
+
70
+
71
+ if harfbuzz_available():
72
+ """Build the callbacks and information needed for Harfbuzz to work with CoreText Fonts.
73
+
74
+ Getting the font data is not always reliable, and since no other way exists to
75
+ retrieve the full font bytes from memory, we must construct callbacks for harfbuzz
76
+ to retrieve the tag tables.
77
+ """
78
+ from pyglet.font.harfbuzz.harfbuzz_lib import hb_lib, hb_destroy_func_t, hb_reference_table_func_t, HB_MEMORY_MODE_READONLY
79
+
80
+ def py_coretext_table_data_destroy(user_data: c_void_p):
81
+ """Release the table resources once harfbuzz is done."""
82
+ if user_data:
83
+ cf.CFRelease(user_data)
84
+
85
+ py_coretext_table_data_destroy_c = hb_destroy_func_t(py_coretext_table_data_destroy)
86
+
87
+ @hb_reference_table_func_t
88
+ def py_coretext_table_callback(face: c_void_p, tag: int, user_data: c_void_p):
89
+ """This callback is invoked by HarfBuzz for each table it needs.
90
+
91
+ user_data is a pointer to the CGFont.
92
+ """
93
+ # Use Quartz to get the table data for the given tag.
94
+ table_data = quartz.CGFontCopyTableForTag(user_data, tag)
95
+ if table_data is None:
96
+ return None
97
+
98
+ # Get the length and pointer to the raw data.
99
+ length = cf.CFDataGetLength(table_data)
100
+ data_ptr = cf.CFDataGetBytePtr(table_data)
101
+ if not data_ptr:
102
+ # Release the table_data and return empty blob.
103
+ cf.CFRelease(table_data)
104
+ return None
105
+
106
+ # Create a blob that references this table data.
107
+ data_ptr_char = cast(data_ptr, c_char_p)
108
+ blob = hb_lib.hb_blob_create(data_ptr_char, length, HB_MEMORY_MODE_READONLY,
109
+ table_data, py_coretext_table_data_destroy_c)
110
+ return blob
111
+
112
+ # Null callback, so harfbuzz cannot destroy our CGFont.
113
+ _destroy_callback_null = cast(None, hb_destroy_func_t)
17
114
 
18
115
  class QuartzGlyphRenderer(base.GlyphRenderer):
19
116
  font: QuartzFont
@@ -22,6 +119,81 @@ class QuartzGlyphRenderer(base.GlyphRenderer):
22
119
  super().__init__(font)
23
120
  self.font = font
24
121
 
122
+ def render_index(self, glyph_index: int):
123
+ ctFont = self.font.ctFont
124
+
125
+ # Create an attributed string using text and font.
126
+ # Determine the glyphs involved for the text (if any)
127
+ # Get a bounding rectangle for glyphs in string.
128
+ count = 1
129
+ glyphs = (cocoapy.CGGlyph * count)(glyph_index)
130
+
131
+ rect = ct.CTFontGetBoundingRectsForGlyphs(ctFont, 0, glyphs, None, count)
132
+
133
+ # Get advance for all glyphs in string.
134
+ advance = ct.CTFontGetAdvancesForGlyphs(ctFont, 0, glyphs, None, count)
135
+
136
+ # Set image parameters:
137
+ # We add 2 pixels to the bitmap width and height so that there will be a 1-pixel border
138
+ # around the glyph image when it is placed in the texture atlas. This prevents
139
+ # weird artifacts from showing up around the edges of the rendered glyph textures.
140
+ # We adjust the baseline and lsb of the glyph by 1 pixel accordingly.
141
+ width = max(int(math.ceil(rect.size.width) + 2), 1)
142
+ height = max(int(math.ceil(rect.size.height) + 2), 1)
143
+ baseline = -int(math.floor(rect.origin.y)) + 1
144
+ lsb = int(math.ceil(rect.origin.x)) - 1
145
+ advance = int(round(advance))
146
+
147
+ # Create bitmap context.
148
+ bits_per_components = 8
149
+ bytes_per_row = 4 * width
150
+ colorSpace = c_void_p(quartz.CGColorSpaceCreateDeviceRGB())
151
+ bitmap_context = c_void_p(quartz.CGBitmapContextCreate(
152
+ None,
153
+ width,
154
+ height,
155
+ bits_per_components,
156
+ bytes_per_row,
157
+ colorSpace,
158
+ cocoapy.kCGImageAlphaPremultipliedLast))
159
+
160
+ # Draw text to bitmap context.
161
+ quartz.CGContextSetShouldAntialias(bitmap_context, pyglet.options.text_antialiasing)
162
+ quartz.CGContextSetTextPosition(bitmap_context, -lsb, baseline)
163
+ quartz.CGContextSetRGBFillColor(bitmap_context, 1, 1, 1, 1)
164
+ quartz.CGContextSetFont(bitmap_context, ctFont)
165
+ quartz.CGContextSetFontSize(bitmap_context, self.font.pixel_size)
166
+ quartz.CGContextTranslateCTM(bitmap_context, 0, height) # Move origin to top-left
167
+ quartz.CGContextScaleCTM(bitmap_context, 1, -1) # Flip vertically
168
+
169
+ positions = (cocoapy.CGPoint * 1)(*[cocoapy.CGPoint(0, 0)])
170
+ quartz.CTFontDrawGlyphs(ctFont, glyphs, positions, 1, bitmap_context)
171
+
172
+ # Create an image to get the data out.
173
+ image_ref = c_void_p(quartz.CGBitmapContextCreateImage(bitmap_context))
174
+
175
+ bytes_per_row = quartz.CGImageGetBytesPerRow(image_ref)
176
+ data_provider = c_void_p(quartz.CGImageGetDataProvider(image_ref))
177
+ image_data = c_void_p(quartz.CGDataProviderCopyData(data_provider))
178
+ buffer_size = cf.CFDataGetLength(image_data)
179
+ buffer_ptr = cf.CFDataGetBytePtr(image_data)
180
+ if buffer_ptr:
181
+ buffer = string_at(buffer_ptr, buffer_size)
182
+
183
+ quartz.CGImageRelease(image_ref)
184
+ quartz.CGDataProviderRelease(image_data)
185
+ cf.CFRelease(bitmap_context)
186
+ cf.CFRelease(colorSpace)
187
+
188
+ glyph_image = pyglet.image.ImageData(width, height, "RGBA", buffer, bytes_per_row)
189
+
190
+ glyph = self.font.create_glyph(glyph_image)
191
+ glyph.set_bearings(baseline, lsb, advance)
192
+
193
+ return glyph
194
+
195
+ raise Exception("CG Image buffer could not be read.")
196
+
25
197
  def render(self, text: str) -> base.Glyph:
26
198
  # Using CTLineDraw seems to be the only way to make sure that the text
27
199
  # is drawn with the specified font when that font is a graphics font loaded from
@@ -33,14 +205,18 @@ class QuartzGlyphRenderer(base.GlyphRenderer):
33
205
 
34
206
  # Create an attributed string using text and font.
35
207
  attributes = c_void_p(
36
- cf.CFDictionaryCreateMutable(None, 1, cf.kCFTypeDictionaryKeyCallBacks, cf.kCFTypeDictionaryValueCallBacks))
208
+ cf.CFDictionaryCreateMutable(None, 2, cf.kCFTypeDictionaryKeyCallBacks, cf.kCFTypeDictionaryValueCallBacks)
209
+ )
37
210
  cf.CFDictionaryAddValue(attributes, cocoapy.kCTFontAttributeName, ctFont)
38
- string = c_void_p(cf.CFAttributedStringCreate(None, cocoapy.CFSTR(text), attributes))
211
+ cf.CFDictionaryAddValue(attributes, cocoapy.kCTForegroundColorFromContextAttributeName , cocoapy.kCFBooleanTrue)
212
+ cf_str = cocoapy.CFSTR(text)
213
+ string = c_void_p(cf.CFAttributedStringCreate(None, cf_str, attributes))
39
214
 
40
215
  # Create a CTLine object to render the string.
41
216
  line = c_void_p(ct.CTLineCreateWithAttributedString(string))
42
217
  cf.CFRelease(string)
43
218
  cf.CFRelease(attributes)
219
+ cf.CFRelease(cf_str)
44
220
 
45
221
  # Determine the glyphs involved for the text (if any)
46
222
  count = len(text)
@@ -77,55 +253,170 @@ class QuartzGlyphRenderer(base.GlyphRenderer):
77
253
  advance = int(round(advance))
78
254
 
79
255
  # Create bitmap context.
80
- bitsPerComponent = 8
81
- bytesPerRow = 4 * width
256
+ bits_per_components = 8
257
+ bytes_per_row = 4 * width
82
258
  colorSpace = c_void_p(quartz.CGColorSpaceCreateDeviceRGB())
83
- bitmap = c_void_p(quartz.CGBitmapContextCreate(
259
+ bitmap_context = c_void_p(quartz.CGBitmapContextCreate(
84
260
  None,
85
261
  width,
86
262
  height,
87
- bitsPerComponent,
88
- bytesPerRow,
263
+ bits_per_components,
264
+ bytes_per_row,
89
265
  colorSpace,
90
266
  cocoapy.kCGImageAlphaPremultipliedLast))
91
267
 
92
268
  # Draw text to bitmap context.
93
- quartz.CGContextSetShouldAntialias(bitmap, True)
94
- quartz.CGContextSetTextPosition(bitmap, -lsb, baseline)
95
- ct.CTLineDraw(line, bitmap)
96
- cf.CFRelease(line)
269
+ quartz.CGContextSetShouldAntialias(bitmap_context, pyglet.options.text_antialiasing)
270
+ quartz.CGContextSetTextPosition(bitmap_context, -lsb, baseline)
271
+ quartz.CGContextSetRGBFillColor(bitmap_context, 1, 1, 1, 1) # Render white for multiplying.
272
+ quartz.CGContextTranslateCTM(bitmap_context, 0, height) # Move origin to top-left
273
+ quartz.CGContextScaleCTM(bitmap_context, 1, -1) # Flip vertically
97
274
 
275
+ ct.CTLineDraw(line, bitmap_context)
276
+ cf.CFRelease(line)
98
277
  # Create an image to get the data out.
99
- imageRef = c_void_p(quartz.CGBitmapContextCreateImage(bitmap))
278
+ image_ref = c_void_p(quartz.CGBitmapContextCreateImage(bitmap_context))
100
279
 
101
- bytesPerRow = quartz.CGImageGetBytesPerRow(imageRef)
102
- dataProvider = c_void_p(quartz.CGImageGetDataProvider(imageRef))
103
- imageData = c_void_p(quartz.CGDataProviderCopyData(dataProvider))
104
- buffersize = cf.CFDataGetLength(imageData)
105
- buffer = (c_byte * buffersize)()
106
- byteRange = cocoapy.CFRange(0, buffersize)
107
- cf.CFDataGetBytes(imageData, byteRange, buffer)
280
+ bytes_per_row = quartz.CGImageGetBytesPerRow(image_ref)
281
+ data_provider = c_void_p(quartz.CGImageGetDataProvider(image_ref))
282
+ image_data = c_void_p(quartz.CGDataProviderCopyData(data_provider))
283
+ buffer_size = cf.CFDataGetLength(image_data)
284
+ buffer_ptr = cf.CFDataGetBytePtr(image_data)
285
+ if buffer_ptr:
286
+ buffer = string_at(buffer_ptr, buffer_size)
108
287
 
109
- quartz.CGImageRelease(imageRef)
110
- quartz.CGDataProviderRelease(imageData)
111
- cf.CFRelease(bitmap)
112
- cf.CFRelease(colorSpace)
288
+ quartz.CGImageRelease(image_ref)
289
+ quartz.CGDataProviderRelease(image_data)
290
+ cf.CFRelease(bitmap_context)
291
+ cf.CFRelease(colorSpace)
113
292
 
114
- glyph_image = pyglet.image.ImageData(width, height, "RGBA", buffer, bytesPerRow)
293
+ glyph_image = pyglet.image.ImageData(width, height, "RGBA", buffer, bytes_per_row)
115
294
 
116
- glyph = self.font.create_glyph(glyph_image)
117
- glyph.set_bearings(baseline, lsb, advance)
118
- t = list(glyph.tex_coords)
119
- glyph.tex_coords = t[9:12] + t[6:9] + t[3:6] + t[:3]
295
+ glyph = self.font.create_glyph(glyph_image)
296
+ glyph.set_bearings(baseline, lsb, advance)
120
297
 
121
- return glyph
298
+ return glyph
299
+
300
+ raise Exception("CG Image buffer could not be read.")
122
301
 
123
302
 
124
303
  class QuartzFont(base.Font):
125
304
  glyph_renderer_class: type[base.GlyphRenderer] = QuartzGlyphRenderer
126
- _loaded_CGFont_table: dict[str, dict[int, c_void_p]] = {}
305
+ _loaded_CGFont_table: dict[str, dict[int, tuple[c_void_p, bytes]]] = {}
306
+
307
+ def __init__(self, name: str, size: float, weight: str = "normal", italic: bool = False, stretch: bool = False,
308
+ dpi: int | None = None) -> None:
309
+
310
+ super().__init__()
311
+
312
+ name = name or "Helvetica"
313
+
314
+ self.dpi = dpi or 96
315
+ self.size = size
316
+ self.pixel_size = size * dpi / 72.0
317
+ self.italic = italic
318
+ self.stretch = stretch
319
+ self.weight = weight
320
+
321
+ if isinstance(weight, str):
322
+ self.weight_value = name_to_weight[weight]
323
+ elif weight is True:
324
+ self.weight_value = name_to_weight["bold"]
325
+ else:
326
+ self.weight_value = None
327
+
328
+ self.italic = italic
329
+
330
+ # Construct traits value.
331
+ traits = 0
332
+ if italic:
333
+ traits |= cocoapy.kCTFontItalicTrait
334
+
335
+ if isinstance(stretch, str):
336
+ self.stretch_value = name_to_stretch[stretch]
337
+ else:
338
+ self.stretch_value = None
339
+
340
+ name = str(name)
341
+ self.traits = traits
342
+ # First see if we can find an appropriate font from our table of loaded fonts.
343
+ result = self._lookup_font_with_family_and_traits(name, traits)
344
+ if result:
345
+ cgFont = result[0]
346
+ # Use cgFont from table to create a CTFont object with the specified size.
347
+ self.ctFont = c_void_p(ct.CTFontCreateWithGraphicsFont(cgFont, self.pixel_size, None, None))
348
+ else:
349
+ # Create a font descriptor for given name and traits and use it to create font.
350
+ descriptor = self._create_font_descriptor(name, traits, self.weight_value, self.stretch_value)
351
+ self.ctFont = c_void_p(ct.CTFontCreateWithFontDescriptor(descriptor, self.pixel_size, None))
352
+ cf.CFRelease(descriptor)
353
+ assert self.ctFont, "Couldn't load font: " + name
354
+
355
+ string = c_void_p(ct.CTFontCopyFamilyName(self.ctFont))
356
+ self._family_name = str(cocoapy.cfstring_to_string(string))
357
+ cf.CFRelease(string)
358
+
359
+ self.ascent = int(math.ceil(ct.CTFontGetAscent(self.ctFont)))
360
+ self.descent = -int(math.ceil(ct.CTFontGetDescent(self.ctFont)))
361
+
362
+ if pyglet.options.text_shaping == 'harfbuzz' and harfbuzz_available():
363
+ self._cg_font = None
364
+ self.hb_resource = get_resource_from_ct_font(self)
365
+
366
+
367
+ def _get_hb_face(self):
368
+ assert self._cg_font is None
369
+
370
+ # Create a CGFont from the CTFont for the face.
371
+ self._cg_font = quartz.CTFontCopyGraphicsFont(self.ctFont, None)
372
+ if self._cg_font is None:
373
+ raise ValueError("Could not get CGFont from CTFont")
374
+
375
+ # Create the HarfBuzz face using our table callback.
376
+ return hb_lib.hb_face_create_for_tables(py_coretext_table_callback, self._cg_font, _destroy_callback_null)
377
+
378
+ def get_font_data(self) -> bytes:
379
+ """Get the font file in bytes if possible.
380
+
381
+ Unfortunately CoreText doesn't have a good way to retrieve directly from a font object. Attempt to get the
382
+ filename from the system. If the filename is unknown, it most likely was loaded from memory. In which case,
383
+ the data was added and cached at some point with `add_font_data`.
384
+ """
385
+ filename = self.filename
386
+ if filename == "Unknown":
387
+ result = self._lookup_font_with_family_and_traits(self.name, self.traits)
388
+ if result:
389
+ _, data = result
390
+ else:
391
+ raise Exception("Couldn't load font data by name and traits. Report as a bug with the font file.")
392
+ else:
393
+ with open(filename, "rb") as f:
394
+ data = f.read()
395
+
396
+ return data
397
+
398
+ @property
399
+ def filename(self) -> str:
400
+ descriptor = self._create_font_descriptor(self.name, self.traits, self.weight_value, self.stretch_value)
401
+ ref = c_void_p(ct.CTFontDescriptorCopyAttribute(descriptor, kCTFontURLAttribute))
402
+ if ref:
403
+ url = cocoapy.ObjCInstance(ref) # NSURL
404
+ filepath = url.fileSystemRepresentation().decode()
405
+ cf.CFRelease(ref)
406
+ cf.CFRelease(descriptor)
407
+ return filepath
408
+
409
+ cf.CFRelease(descriptor)
410
+ return "Unknown"
127
411
 
128
- def _lookup_font_with_family_and_traits(self, family: str, traits: int) -> c_void_p | None:
412
+ @property
413
+ def name(self) -> str:
414
+ return self._family_name
415
+
416
+ def __del__(self) -> None:
417
+ cf.CFRelease(self.ctFont)
418
+
419
+ def _lookup_font_with_family_and_traits(self, family: str, traits: int) -> tuple[c_void_p, bytes] | None:
129
420
  # This method searches the _loaded_CGFont_table to find a loaded
130
421
  # font of the given family with the desired traits. If it can't find
131
422
  # anything with the exact traits, it tries to fall back to whatever
@@ -152,7 +443,9 @@ class QuartzFont(base.Font):
152
443
  # Otherwise return whatever we have.
153
444
  return list(fonts.values())[0]
154
445
 
155
- def _create_font_descriptor(self, family_name: str, traits: int) -> c_void_p:
446
+ def _create_font_descriptor(self, family_name: str, traits: int,
447
+ weight: float | None = None,
448
+ stretch: float | None = None) -> c_void_p:
156
449
  # Create an attribute dictionary.
157
450
  attributes = c_void_p(
158
451
  cf.CFDictionaryCreateMutable(None, 0, cf.kCFTypeDictionaryKeyCallBacks, cf.kCFTypeDictionaryValueCallBacks))
@@ -160,6 +453,7 @@ class QuartzFont(base.Font):
160
453
  cfname = cocoapy.CFSTR(family_name)
161
454
  cf.CFDictionaryAddValue(attributes, cocoapy.kCTFontFamilyNameAttribute, cfname)
162
455
  cf.CFRelease(cfname)
456
+
163
457
  # Construct a CFNumber to represent the traits.
164
458
  itraits = c_int32(traits)
165
459
  symTraits = c_void_p(cf.CFNumberCreate(None, cocoapy.kCFNumberSInt32Type, byref(itraits)))
@@ -168,99 +462,51 @@ class QuartzFont(base.Font):
168
462
  traitsDict = c_void_p(cf.CFDictionaryCreateMutable(None, 0, cf.kCFTypeDictionaryKeyCallBacks,
169
463
  cf.kCFTypeDictionaryValueCallBacks))
170
464
  if traitsDict:
465
+ if weight is not None:
466
+ weight_value = c_float(weight)
467
+ cfWeight = c_void_p(cf.CFNumberCreate(None, cocoapy.kCFNumberFloatType, byref(weight_value)))
468
+ if cfWeight:
469
+ cf.CFDictionaryAddValue(traitsDict, cocoapy.kCTFontWeightTrait, cfWeight)
470
+ cf.CFRelease(cfWeight)
471
+
472
+ if stretch is not None:
473
+ stretch_value = c_float(stretch)
474
+ cfWidth = c_void_p(cf.CFNumberCreate(None, cocoapy.kCFNumberFloatType, byref(stretch_value)))
475
+ if cfWidth:
476
+ cf.CFDictionaryAddValue(traitsDict, cocoapy.kCTFontWidthTrait, cfWidth)
477
+ cf.CFRelease(cfWidth)
478
+
171
479
  # Add CFNumber traits to traits dictionary.
172
480
  cf.CFDictionaryAddValue(traitsDict, cocoapy.kCTFontSymbolicTrait, symTraits)
173
481
  # Add traits dictionary to attributes.
174
482
  cf.CFDictionaryAddValue(attributes, cocoapy.kCTFontTraitsAttribute, traitsDict)
175
483
  cf.CFRelease(traitsDict)
484
+
176
485
  cf.CFRelease(symTraits)
177
486
  # Create font descriptor with attributes.
178
487
  descriptor = c_void_p(ct.CTFontDescriptorCreateWithAttributes(attributes))
179
488
  cf.CFRelease(attributes)
180
489
  return descriptor
181
490
 
182
- def __init__(self, name: str, size: float, weight: str = "normal", italic: bool = False, stretch: bool = False,
183
- dpi: int | None = None) -> None:
184
-
185
- if stretch:
186
- warnings.warn("The current font render does not support stretching.") # noqa: B028
187
-
188
- super().__init__()
189
-
190
- name = name or "Helvetica"
191
-
192
- # I don't know what is the right thing to do here.
193
- dpi = dpi or 96
194
- size = size * dpi / 72.0
195
-
196
- # Construct traits value.
197
- traits = 0
198
-
199
- # TODO: Use kCTFontWeightTrait instead, and
200
- # translate to the correct weight values.
201
- if isinstance(weight, str) and "bold" in weight:
202
- traits |= cocoapy.kCTFontBoldTrait
203
- elif weight is True:
204
- traits |= cocoapy.kCTFontBoldTrait
205
-
206
- if italic:
207
- traits |= cocoapy.kCTFontItalicTrait
208
-
491
+ @classmethod
492
+ def have_font(cls: type[QuartzFont], name: str) -> bool:
209
493
  name = str(name)
210
- self.traits = traits
211
- # First see if we can find an appropriate font from our table of loaded fonts.
212
- cgFont = self._lookup_font_with_family_and_traits(name, traits)
213
- if cgFont:
214
- # Use cgFont from table to create a CTFont object with the specified size.
215
- self.ctFont = c_void_p(ct.CTFontCreateWithGraphicsFont(cgFont, size, None, None))
216
- else:
217
- # Create a font descriptor for given name and traits and use it to create font.
218
- descriptor = self._create_font_descriptor(name, traits)
219
- self.ctFont = c_void_p(ct.CTFontCreateWithFontDescriptor(descriptor, size, None))
220
- cf.CFRelease(descriptor)
221
- assert self.ctFont, "Couldn't load font: " + name
494
+ if name in cls._loaded_CGFont_table:
495
+ return True
222
496
 
223
- string = c_void_p(ct.CTFontCopyFamilyName(self.ctFont))
224
- self._family_name = str(cocoapy.cfstring_to_string(string))
225
- cf.CFRelease(string)
497
+ # Query CoreText to see if the font exists.
498
+ descriptor = ct.CTFontDescriptorCreateWithNameAndSize(cocoapy.CFSTR(name), 0.0)
499
+ matched = ct.CTFontDescriptorCreateMatchingFontDescriptor(descriptor, None)
226
500
 
227
- self.ascent = int(math.ceil(ct.CTFontGetAscent(self.ctFont)))
228
- self.descent = -int(math.ceil(ct.CTFontGetDescent(self.ctFont)))
501
+ exists = True if matched else False
229
502
 
230
- @property
231
- def filename(self) -> str:
232
- descriptor = self._create_font_descriptor(self.name, self.traits)
233
- ref = c_void_p(ct.CTFontDescriptorCopyAttribute(descriptor, kCTFontURLAttribute))
234
- if ref:
235
- url = cocoapy.ObjCInstance(ref, cache=False) # NSURL
236
- filepath = url.fileSystemRepresentation().decode()
237
- cf.CFRelease(ref)
238
- return filepath
239
-
240
- cf.CFRelease(descriptor)
241
- return "Unknown"
242
-
243
- @property
244
- def name(self) -> str:
245
- return self._family_name
503
+ if descriptor:
504
+ cf.CFRelease(descriptor)
246
505
 
247
- def __del__(self) -> None:
248
- cf.CFRelease(self.ctFont)
506
+ if matched:
507
+ cf.CFRelease(matched)
249
508
 
250
- @classmethod
251
- def have_font(cls: type[QuartzFont], name: str) -> bool:
252
- name = str(name)
253
- if name in cls._loaded_CGFont_table:
254
- return True
255
- # Try to create the font to see if it exists.
256
- # TODO: Find a better way to check.
257
- cfstring = cocoapy.CFSTR(name)
258
- cgfont = c_void_p(quartz.CGFontCreateWithFontName(cfstring))
259
- cf.CFRelease(cfstring)
260
- if cgfont:
261
- cf.CFRelease(cgfont)
262
- return True
263
- return False
509
+ return exists
264
510
 
265
511
  @classmethod
266
512
  def add_font_data(cls: type[QuartzFont], data: BinaryIO) -> None:
@@ -296,8 +542,82 @@ class QuartzFont(base.Font):
296
542
  # full name, since its not always clear which one will be looked up.
297
543
  if familyName not in cls._loaded_CGFont_table:
298
544
  cls._loaded_CGFont_table[familyName] = {}
299
- cls._loaded_CGFont_table[familyName][traits] = cgFont
545
+ cls._loaded_CGFont_table[familyName][traits] = (cgFont, data)
300
546
 
301
547
  if fullName not in cls._loaded_CGFont_table:
302
548
  cls._loaded_CGFont_table[fullName] = {}
303
- cls._loaded_CGFont_table[fullName][traits] = cgFont
549
+ cls._loaded_CGFont_table[fullName][traits] = (cgFont, data)
550
+
551
+ def get_text_size(self, text: str) -> tuple[int, int]:
552
+ ctFont = self.ctFont
553
+
554
+ attributes = c_void_p(
555
+ cf.CFDictionaryCreateMutable(None, 1, cf.kCFTypeDictionaryKeyCallBacks, cf.kCFTypeDictionaryValueCallBacks)
556
+ )
557
+ cf.CFDictionaryAddValue(attributes, cocoapy.kCTFontAttributeName, ctFont)
558
+ cf_str = cocoapy.CFSTR(text)
559
+ string = c_void_p(cf.CFAttributedStringCreate(None, cf_str, attributes))
560
+
561
+ line = c_void_p(ct.CTLineCreateWithAttributedString(string))
562
+
563
+ ascent, descent = CGFloat(), CGFloat()
564
+ width = ct.CTLineGetTypographicBounds(line, byref(ascent), byref(descent), None)
565
+ height = ascent.value + descent.value
566
+
567
+ cf.CFRelease(string)
568
+ cf.CFRelease(attributes)
569
+ cf.CFRelease(line)
570
+ cf.CFRelease(cf_str)
571
+ return round(width), round(height)
572
+
573
+ def _get_font_weight(self):
574
+ traits = ct.CTFontCopyTraits(self.ctFont)
575
+ font_weight = cfnumber_to_number(c_void_p(cf.CFDictionaryGetValue(traits, kCTFontWeightTrait)))
576
+ cf.CFRelease(traits)
577
+ return font_weight
578
+
579
+ def _get_font_stretch(self):
580
+ traits = ct.CTFontCopyTraits(self.ctFont)
581
+ font_width = cfnumber_to_number(c_void_p(cf.CFDictionaryGetValue(traits, cocoapy.kCTFontWidthTrait)))
582
+ cf.CFRelease(traits)
583
+ return font_width
584
+
585
+ def render_glyph_indices(self, indices: list[int]) -> None:
586
+ # Process any glyphs that have not been rendered.
587
+ self._initialize_renderer()
588
+
589
+ missing = set()
590
+ for glyph_indice in set(indices):
591
+ if glyph_indice not in self.glyphs:
592
+ missing.add(glyph_indice)
593
+
594
+ # Missing glyphs, get their info.
595
+ for glyph_indice in missing:
596
+ self.glyphs[glyph_indice] = self._glyph_renderer.render_index(glyph_indice)
597
+
598
+ def get_glyphs(self, text: str) -> tuple[list[Glyph], list[GlyphPosition]]:
599
+ """Create and return a list of Glyphs for `text`.
600
+
601
+ If any characters do not have a known glyph representation in this
602
+ font, a substitution will be made.
603
+
604
+ Args:
605
+ text:
606
+ Text to render.
607
+ """
608
+ self._initialize_renderer()
609
+
610
+ if pyglet.options.text_shaping == "harfbuzz" and harfbuzz_available():
611
+ return get_harfbuzz_shaped_glyphs(self, text)
612
+
613
+ glyphs = [] # glyphs that are committed.
614
+ offsets = []
615
+ for c in base.get_grapheme_clusters(str(text)):
616
+ if c == "\t":
617
+ c = " " # noqa: PLW2901
618
+ if c not in self.glyphs:
619
+ self.glyphs[c] = self._glyph_renderer.render(c)
620
+ glyphs.append(self.glyphs[c])
621
+ offsets.append(base.GlyphPosition(0, 0, 0, 0))
622
+
623
+ return glyphs, offsets