heroes-renaissance/pyglet/font/carbon.py

439 lines
15 KiB
Python
Raw Permalink Normal View History

2008-11-23 20:07:47 +00:00
# ----------------------------------------------------------------------------
# pyglet
# Copyright (c) 2006-2008 Alex Holkner
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
# * Neither the name of pyglet nor the names of its
# contributors may be used to endorse or promote products
# derived from this software without specific prior written
# permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ----------------------------------------------------------------------------
'''
'''
__docformat__ = 'restructuredtext'
__version__ = '$Id: $'
# TODO Tiger and later: need to set kWindowApplicationScaledAttribute for DPI
# independence?
from ctypes import *
import string
import math
from sys import byteorder
from pyglet.font import base
import pyglet.image
from pyglet.window.carbon import carbon, _oscheck
from pyglet.window.carbon import _create_cfstring
from pyglet.window.carbon.types import *
class FixedPoint(Structure):
_fields_ = [
('x', Fixed),
('y', Fixed)
]
class ATSTrapezoid(Structure):
_fields_ = [
('upperLeft', FixedPoint),
('upperRight', FixedPoint),
('lowerRight', FixedPoint),
('lowerLeft', FixedPoint)
]
# TODO: most of the ATS and CG here not used any more.
CGGlyph = c_ushort
ATSUFontID = c_uint32
RGBColor = c_short * 3
ATSURGBAlphaColor = c_float * 4
kCGImageAlphaNone = 0
kCGImageAlphaPremultipliedLast = 1
kCGTextFill = 0
kATSUInvalidFontErr = -8796
kATSFontContextUnspecified = 0
kATSFontContextGlobal = 1
kATSFontContextLocal = 2
kATSFontFilterSelectorUnspecified = 0
kATSFontFilterSelectorGeneration = 3
kATSFontFilterSelectorFontFamily = 7
kATSFontFilterSelectorFontFamilyApplierFunction = 8
kATSFontFilterSelectorFontApplierFunction = 9
kATSOptionFlagsDoNotNotify = 0x00000001 << 8
kATSOptionFlagsIterationScopeMask = 0x00000007 << 12
kATSOptionFlagsDefaultScope = 0x00000000 << 12
kATSOptionFlagsUnRestrictedScope = 0x00000001 << 12
kATSOptionFlagsRestrictedScope = 0x00000002 << 12
kATSOptionFlagsProcessSubdirectories = 0x00000001 << 6
kATSUFromTextBeginning = c_ulong(0xFFFFFFFF)
kATSUToTextEnd = c_ulong(0xFFFFFFFF)
kATSULineAscentTag = 8
kATSULineDescentTag = 9
ATSUTextMeasurement = Fixed
kATSUQDBoldfaceTag = 256
kATSUQDItalicTag = 257
kATSUFontTag = 261
kATSUSizeTag = 262
kATSUCGContextTag = 32767
kATSUColorTag = 263
kATSURGBAlphaColorTag = 288
kATSULineWidthTag = 1
kFontFullName = 4
kFontNoPlatformCode = c_ulong(-1)
kFontNoScriptCode = c_ulong(-1)
kFontNoLanguageCode = c_ulong(-1)
kATSUseDeviceOrigins = 1
kATSFontFormatUnspecified = 0
kATSFontContextLocal = 2
carbon.CGColorSpaceCreateWithName.restype = c_void_p
carbon.CGBitmapContextCreate.restype = POINTER(c_void_p)
UniCharArrayOffset = c_uint32
UniCharCount = c_uint32
kATSULayoutOperationJustification = 1
kATSULayoutOperationPostLayoutAdjustment = 0x20
kATSULayoutOperationCallbackStatusHandled = 0
kATSULayoutOperationCallbackStatusContinue = c_long(1)
kATSULayoutOperationOverrideTag = 15
kATSUDirectDataAdvanceDeltaFixedArray = 0
kATSUDirectDataDeviceDeltaSInt16Array = 2
kATSUDirectDataLayoutRecordATSLayoutRecordVersion1 = 100
ATSUDirectLayoutOperationOverrideUPP = CFUNCTYPE(c_int,
c_int, c_void_p, c_uint32, c_void_p, POINTER(c_int))
class ATSULayoutOperationOverrideSpecifier(Structure):
_fields_ = [
('operationSelector', c_uint32),
('overrideUPP', ATSUDirectLayoutOperationOverrideUPP)
]
class ATSLayoutRecord(Structure):
_pack_ = 2
_fields_ = [
('glyphID', c_uint16),
('flags', c_uint32),
('originalOffset', c_uint32),
('realPos', Fixed),
]
def fixed(value):
return c_int32(carbon.Long2Fix(c_long(int(value))))
carbon.Fix2X.restype = c_double
def fix2float(value):
return carbon.Fix2X(value)
def create_atsu_style(attributes):
# attributes is a dict of ATSUAttributeTag => ctypes value
tags, values = zip(*attributes.items())
tags = (c_int * len(tags))(*tags)
sizes = (c_uint * len(values))(*[sizeof(v) for v in values])
values = (c_void_p * len(values))(*[cast(pointer(v), c_void_p) \
for v in values])
style = c_void_p()
carbon.ATSUCreateStyle(byref(style))
carbon.ATSUSetAttributes(style, len(tags), tags, sizes, values)
return style
def set_layout_attributes(layout, attributes):
if attributes:
# attributes is a dict of ATSUAttributeTag => ctypes value
tags, values = zip(*attributes.items())
tags = (c_int * len(tags))(*tags)
sizes = (c_uint * len(values))(*[sizeof(v) for v in values])
values = (c_void_p * len(values))(*[cast(pointer(v), c_void_p) \
for v in values])
r = carbon.ATSUSetLayoutControls(layout, len(tags), tags, sizes, values)
_oscheck(r)
def str_ucs2(text):
if byteorder == 'big':
text = text.encode('utf_16_be')
else:
text = text.encode('utf_16_le') # explicit endian avoids BOM
return create_string_buffer(text + '\0')
class CarbonGlyphRenderer(base.GlyphRenderer):
_bitmap = None
_bitmap_context = None
_bitmap_rect = None
_glyph_advance = 0 # set through callback
def __init__(self, font):
super(CarbonGlyphRenderer, self).__init__(font)
self._create_bitmap_context(256, 256)
self.font = font
def __del__(self):
try:
if self._bitmap_context:
carbon.CGContextRelease(self._bitmap_context)
except:
pass
def _layout_callback(self, operation, line, ref, extra, callback_status):
records = c_void_p()
n_records = c_uint()
r = carbon.ATSUDirectGetLayoutDataArrayPtrFromLineRef(line,
kATSUDirectDataLayoutRecordATSLayoutRecordVersion1,
0,
byref(records),
byref(n_records))
_oscheck(r)
records = cast(records,
POINTER(ATSLayoutRecord * n_records.value)).contents
self._glyph_advance = fix2float(records[-1].realPos)
callback_status.contents = kATSULayoutOperationCallbackStatusContinue
return 0
def render(self, text):
# Convert text to UCS2
text_len = len(text)
text_ucs2 = str_ucs2(text)
# Create layout override handler to extract device advance value.
override_spec = ATSULayoutOperationOverrideSpecifier()
override_spec.operationSelector = \
kATSULayoutOperationPostLayoutAdjustment
override_spec.overrideUPP = \
ATSUDirectLayoutOperationOverrideUPP(self._layout_callback)
# Create ATSU text layout for this text and font
layout = c_void_p()
carbon.ATSUCreateTextLayout(byref(layout))
set_layout_attributes(layout, {
kATSUCGContextTag: self._bitmap_context,
kATSULayoutOperationOverrideTag: override_spec})
carbon.ATSUSetTextPointerLocation(layout,
text_ucs2,
kATSUFromTextBeginning,
kATSUToTextEnd,
text_len)
carbon.ATSUSetRunStyle(layout, self.font.atsu_style,
kATSUFromTextBeginning, kATSUToTextEnd)
# Turning on transient font matching screws up font layout
# predictability when strange fonts are installed
# <ah> Don't believe this. Can't get foreign/special characters
# without transient on.
carbon.ATSUSetTransientFontMatching(layout, True)
# Get bitmap dimensions required
rect = Rect()
carbon.ATSUMeasureTextImage(layout,
kATSUFromTextBeginning,
kATSUToTextEnd,
0, 0,
byref(rect))
image_width = rect.right - rect.left + 2
image_height = rect.bottom - rect.top + 2
baseline = rect.bottom + 1
lsb = rect.left
# Resize Quartz context if necessary
if (image_width > self._bitmap_rect.size.width or
image_height > self._bitmap_rect.size.height):
self._create_bitmap_context(
int(max(image_width, self._bitmap_rect.size.width)),
int(max(image_height, self._bitmap_rect.size.height)))
set_layout_attributes(layout, {
kATSUCGContextTag: self._bitmap_context})
# Draw to the bitmap
carbon.CGContextClearRect(self._bitmap_context, self._bitmap_rect)
carbon.ATSUDrawText(layout,
0,
kATSUToTextEnd,
fixed(-lsb + 1), fixed(baseline))
advance = self._glyph_advance
# Round advance to nearest int. It actually looks good with sub-pixel
# advance as well -- Helvetica at 12pt is more tightly spaced, but
# Times New Roman at 12pt is too light. With integer positioning
# overall look seems darker and perhaps more uniform. It's also more
# similar (programmatically) to Win32 and FreeType. Still, worth
# messing around with (comment out next line) if you're interested.
advance = int(round(advance))
# Fix advance for zero-width space
if text == u'\u200b':
advance = 0
# A negative pitch is required, but it is much faster to load the
# glyph upside-down and flip the tex_coords. Note region used
# to start at top of glyph image.
pitch = int(4 * self._bitmap_rect.size.width)
image = pyglet.image.ImageData(image_width,
self._bitmap_rect.size.height, 'RGBA', self._bitmap, pitch)
skip_rows = int(self._bitmap_rect.size.height - image_height)
image = image.get_region(0, skip_rows, image.width, image_height)
glyph = self.font.create_glyph(image)
glyph.set_bearings(baseline, lsb - 1, int(advance))
t = list(glyph.tex_coords)
glyph.tex_coords = t[9:12] + t[6:9] + t[3:6] + t[:3]
return glyph
def _create_bitmap_context(self, width, height):
'''Create or recreate bitmap and Quartz context.'''
if self._bitmap_context:
carbon.CGContextRelease(self._bitmap_context)
components = 4
pitch = width * components
self._bitmap = (c_ubyte * (pitch * height))()
color_space = carbon.CGColorSpaceCreateDeviceRGB()
context = carbon.CGBitmapContextCreate(self._bitmap,
width, height, 8, pitch,
color_space, kCGImageAlphaPremultipliedLast)
carbon.CGColorSpaceRelease(color_space)
# Disable RGB decimated antialiasing, use standard
# antialiasing which won't break alpha.
carbon.CGContextSetShouldSmoothFonts(context, False)
carbon.CGContextSetShouldAntialias(context, True)
self._bitmap_context = context
self._bitmap_rect = CGRect()
self._bitmap_rect.origin.x = 0
self._bitmap_rect.origin.y = 0
self._bitmap_rect.size.width = width
self._bitmap_rect.size.height = height
class CarbonFont(base.Font):
glyph_renderer_class = CarbonGlyphRenderer
def __init__(self, name, size, bold=False, italic=False, dpi=None):
super(CarbonFont, self).__init__()
if not name:
name = 'Helvetica'
if dpi is None:
dpi = 96 # pyglet 1.1; in pyglet 1.0 this was 72.
# If application is not DPI-aware, DPI is fixed at 72. Scale
# font size to emulate other DPI. This will need to be fixed if issue
# #87 is implemented.
size = size * dpi / 72.
name = name.encode('ascii', 'ignore')
font_id = ATSUFontID()
carbon.ATSUFindFontFromName(
name,
len(name),
kFontFullName,
kFontNoPlatformCode,
kFontNoScriptCode,
kFontNoLanguageCode,
byref(font_id))
attributes = {
kATSUSizeTag: fixed(size),
kATSUFontTag: font_id,
kATSURGBAlphaColorTag: ATSURGBAlphaColor(1, 1, 1, 1),
kATSUQDBoldfaceTag: c_byte(bold),
kATSUQDItalicTag: c_byte(italic)
}
self.atsu_style = create_atsu_style(attributes)
self.calculate_metrics()
@classmethod
def have_font(cls, name):
font_id = ATSUFontID()
name = name.encode('ascii', 'ignore')
r = carbon.ATSUFindFontFromName(
name,
len(name),
kFontFullName,
kFontNoPlatformCode,
kFontNoScriptCode,
kFontNoLanguageCode,
byref(font_id))
return r != kATSUInvalidFontErr
def calculate_metrics(self):
# It seems the only way to get the font's ascent and descent is to lay
# out some glyphs and measure them.
# fake ucs2 string
text = '\0a'
layout = c_void_p()
carbon.ATSUCreateTextLayout(byref(layout))
carbon.ATSUSetTextPointerLocation(layout, text,
kATSUFromTextBeginning, kATSUToTextEnd, 1)
carbon.ATSUSetRunStyle(layout, self.atsu_style,
kATSUFromTextBeginning, kATSUToTextEnd)
# determine the metrics for this font only
carbon.ATSUSetTransientFontMatching(layout, False)
value = ATSUTextMeasurement()
carbon.ATSUGetLineControl(layout, 0, kATSULineAscentTag,
sizeof(value), byref(value), None)
self.ascent = int(math.ceil(fix2float(value)))
carbon.ATSUGetLineControl(layout, 0, kATSULineDescentTag,
sizeof(value), byref(value), None)
self.descent = -int(math.ceil(fix2float(value)))
@classmethod
def add_font_data(cls, data):
container = c_void_p()
r = carbon.ATSFontActivateFromMemory(data, len(data),
kATSFontContextLocal, kATSFontFormatUnspecified, None, 0,
byref(container))
_oscheck(r)