Difficult-Rocket/libs/pyglet/font/quartz.py

266 lines
11 KiB
Python

# TODO Tiger and later: need to set kWindowApplicationScaledAttribute for DPI independence?
import math
import warnings
from ctypes import c_void_p, c_int32, byref, c_byte
from pyglet.font import base
import pyglet.image
from pyglet.libs.darwin import cocoapy
cf = cocoapy.cf
ct = cocoapy.ct
quartz = cocoapy.quartz
class QuartzGlyphRenderer(base.GlyphRenderer):
def __init__(self, font):
super().__init__(font)
self.font = font
def render(self, text):
# Using CTLineDraw seems to be the only way to make sure that the text
# is drawn with the specified font when that font is a graphics font loaded from
# memory. For whatever reason, [NSAttributedString drawAtPoint:] ignores
# the graphics font if it not registered with the font manager.
# So we just use CTLineDraw for both graphics fonts and installed fonts.
ctFont = self.font.ctFont
# Create an attributed string using text and font.
attributes = c_void_p(cf.CFDictionaryCreateMutable(None, 1, cf.kCFTypeDictionaryKeyCallBacks, cf.kCFTypeDictionaryValueCallBacks))
cf.CFDictionaryAddValue(attributes, cocoapy.kCTFontAttributeName, ctFont)
string = c_void_p(cf.CFAttributedStringCreate(None, cocoapy.CFSTR(text), attributes))
# Create a CTLine object to render the string.
line = c_void_p(ct.CTLineCreateWithAttributedString(string))
cf.CFRelease(string)
cf.CFRelease(attributes)
# Get a bounding rectangle for glyphs in string.
count = len(text)
chars = (cocoapy.UniChar * count)(*list(map(ord,str(text))))
glyphs = (cocoapy.CGGlyph * count)()
ct.CTFontGetGlyphsForCharacters(ctFont, chars, glyphs, count)
rect = ct.CTFontGetBoundingRectsForGlyphs(ctFont, 0, glyphs, None, count)
# Get advance for all glyphs in string.
advance = ct.CTFontGetAdvancesForGlyphs(ctFont, 0, glyphs, None, count)
# Set image parameters:
# We add 2 pixels to the bitmap width and height so that there will be a 1-pixel border
# around the glyph image when it is placed in the texture atlas. This prevents
# weird artifacts from showing up around the edges of the rendered glyph textures.
# We adjust the baseline and lsb of the glyph by 1 pixel accordingly.
width = max(int(math.ceil(rect.size.width) + 2), 1)
height = max(int(math.ceil(rect.size.height) + 2), 1)
baseline = -int(math.floor(rect.origin.y)) + 1
lsb = int(math.floor(rect.origin.x)) - 1
advance = int(round(advance))
# Create bitmap context.
bitsPerComponent = 8
bytesPerRow = 4*width
colorSpace = c_void_p(quartz.CGColorSpaceCreateDeviceRGB())
bitmap = c_void_p(quartz.CGBitmapContextCreate(
None,
width,
height,
bitsPerComponent,
bytesPerRow,
colorSpace,
cocoapy.kCGImageAlphaPremultipliedLast))
# Draw text to bitmap context.
quartz.CGContextSetShouldAntialias(bitmap, True)
quartz.CGContextSetTextPosition(bitmap, -lsb, baseline)
ct.CTLineDraw(line, bitmap)
cf.CFRelease(line)
# Create an image to get the data out.
imageRef = c_void_p(quartz.CGBitmapContextCreateImage(bitmap))
bytesPerRow = quartz.CGImageGetBytesPerRow(imageRef)
dataProvider = c_void_p(quartz.CGImageGetDataProvider(imageRef))
imageData = c_void_p(quartz.CGDataProviderCopyData(dataProvider))
buffersize = cf.CFDataGetLength(imageData)
buffer = (c_byte * buffersize)()
byteRange = cocoapy.CFRange(0, buffersize)
cf.CFDataGetBytes(imageData, byteRange, buffer)
quartz.CGImageRelease(imageRef)
quartz.CGDataProviderRelease(imageData)
cf.CFRelease(bitmap)
cf.CFRelease(colorSpace)
glyph_image = pyglet.image.ImageData(width, height, 'RGBA', buffer, bytesPerRow)
glyph = self.font.create_glyph(glyph_image)
glyph.set_bearings(baseline, lsb, advance)
t = list(glyph.tex_coords)
glyph.tex_coords = t[9:12] + t[6:9] + t[3:6] + t[:3]
return glyph
class QuartzFont(base.Font):
glyph_renderer_class = QuartzGlyphRenderer
_loaded_CGFont_table = {}
def _lookup_font_with_family_and_traits(self, family, traits):
# This method searches the _loaded_CGFont_table to find a loaded
# font of the given family with the desired traits. If it can't find
# anything with the exact traits, it tries to fall back to whatever
# we have loaded that's close. If it can't find anything in the
# given family at all, it returns None.
# Check if we've loaded the font with the specified family.
if family not in self._loaded_CGFont_table:
return None
# Grab a dictionary of all fonts in the family, keyed by traits.
fonts = self._loaded_CGFont_table[family]
if not fonts:
return None
# Return font with desired traits if it is available.
if traits in fonts:
return fonts[traits]
# Otherwise try to find a font with some of the traits.
for (t, f) in fonts.items():
if traits & t:
return f
# Otherwise try to return a regular font.
if 0 in fonts:
return fonts[0]
# Otherwise return whatever we have.
return list(fonts.values())[0]
def _create_font_descriptor(self, family_name, traits):
# Create an attribute dictionary.
attributes = c_void_p(cf.CFDictionaryCreateMutable(None, 0, cf.kCFTypeDictionaryKeyCallBacks, cf.kCFTypeDictionaryValueCallBacks))
# Add family name to attributes.
cfname = cocoapy.CFSTR(family_name)
cf.CFDictionaryAddValue(attributes, cocoapy.kCTFontFamilyNameAttribute, cfname)
cf.CFRelease(cfname)
# Construct a CFNumber to represent the traits.
itraits = c_int32(traits)
symTraits = c_void_p(cf.CFNumberCreate(None, cocoapy.kCFNumberSInt32Type, byref(itraits)))
if symTraits:
# Construct a dictionary to hold the traits values.
traitsDict = c_void_p(cf.CFDictionaryCreateMutable(None, 0, cf.kCFTypeDictionaryKeyCallBacks, cf.kCFTypeDictionaryValueCallBacks))
if traitsDict:
# Add CFNumber traits to traits dictionary.
cf.CFDictionaryAddValue(traitsDict, cocoapy.kCTFontSymbolicTrait, symTraits)
# Add traits dictionary to attributes.
cf.CFDictionaryAddValue(attributes, cocoapy.kCTFontTraitsAttribute, traitsDict)
cf.CFRelease(traitsDict)
cf.CFRelease(symTraits)
# Create font descriptor with attributes.
descriptor = c_void_p(ct.CTFontDescriptorCreateWithAttributes(attributes))
cf.CFRelease(attributes)
return descriptor
def __init__(self, name, size, bold=False, italic=False, stretch=False, dpi=None):
# assert type(bold) is bool, "Only a boolean value is supported for bold in the current font renderer."
# assert type(italic) is bool, "Only a boolean value is supported for bold in the current font renderer."
if stretch:
warnings.warn("The current font render does not support stretching.")
super().__init__()
name = name or 'Helvetica'
# I don't know what is the right thing to do here.
dpi = dpi or 96
size = size * dpi / 72.0
# Construct traits value.
traits = 0
if bold:
traits |= cocoapy.kCTFontBoldTrait
if italic:
traits |= cocoapy.kCTFontItalicTrait
name = str(name)
# First see if we can find an appropriate font from our table of loaded fonts.
cgFont = self._lookup_font_with_family_and_traits(name, traits)
if cgFont:
# Use cgFont from table to create a CTFont object with the specified size.
self.ctFont = c_void_p(ct.CTFontCreateWithGraphicsFont(cgFont, size, None, None))
else:
# Create a font descriptor for given name and traits and use it to create font.
descriptor = self._create_font_descriptor(name, traits)
self.ctFont = c_void_p(ct.CTFontCreateWithFontDescriptor(descriptor, size, None))
cf.CFRelease(descriptor)
assert self.ctFont, "Couldn't load font: " + name
string = c_void_p(ct.CTFontCopyFamilyName(self.ctFont))
self._family_name = str(cocoapy.cfstring_to_string(string))
cf.CFRelease(string)
self.ascent = int(math.ceil(ct.CTFontGetAscent(self.ctFont)))
self.descent = -int(math.ceil(ct.CTFontGetDescent(self.ctFont)))
@property
def name(self):
return self._family_name
def __del__(self):
cf.CFRelease(self.ctFont)
@classmethod
def have_font(cls, name):
name = str(name)
if name in cls._loaded_CGFont_table: return True
# Try to create the font to see if it exists.
# TODO: Find a better way to check.
cfstring = cocoapy.CFSTR(name)
cgfont = c_void_p(quartz.CGFontCreateWithFontName(cfstring))
cf.CFRelease(cfstring)
if cgfont:
cf.CFRelease(cgfont)
return True
return False
@classmethod
def add_font_data(cls, data):
# Create a cgFont with the data. There doesn't seem to be a way to
# register a font loaded from memory such that the operating system will
# find it later. So instead we just store the cgFont in a table where
# it can be found by our __init__ method.
# Note that the iOS CTFontManager *is* able to register graphics fonts,
# however this method is missing from CTFontManager on MacOS 10.6
dataRef = c_void_p(cf.CFDataCreate(None, data, len(data)))
provider = c_void_p(quartz.CGDataProviderCreateWithCFData(dataRef))
cgFont = c_void_p(quartz.CGFontCreateWithDataProvider(provider))
cf.CFRelease(dataRef)
quartz.CGDataProviderRelease(provider)
# Create a template CTFont from the graphics font so that we can get font info.
ctFont = c_void_p(ct.CTFontCreateWithGraphicsFont(cgFont, 1, None, None))
# Get info about the font to use as key in our font table.
string = c_void_p(ct.CTFontCopyFamilyName(ctFont))
familyName = str(cocoapy.cfstring_to_string(string))
cf.CFRelease(string)
string = c_void_p(ct.CTFontCopyFullName(ctFont))
fullName = str(cocoapy.cfstring_to_string(string))
cf.CFRelease(string)
traits = ct.CTFontGetSymbolicTraits(ctFont)
cf.CFRelease(ctFont)
# Store font in table. We store it under both its family name and its
# full name, since its not always clear which one will be looked up.
if familyName not in cls._loaded_CGFont_table:
cls._loaded_CGFont_table[familyName] = {}
cls._loaded_CGFont_table[familyName][traits] = cgFont
if fullName not in cls._loaded_CGFont_table:
cls._loaded_CGFont_table[fullName] = {}
cls._loaded_CGFont_table[fullName][traits] = cgFont