Difficult-Rocket/libs/pyglet/font/win32.py
2023-10-31 23:55:58 +08:00

615 lines
20 KiB
Python

# TODO Windows Vista: need to call SetProcessDPIAware? May affect GDI+ calls as well as font.
import math
import warnings
from sys import byteorder
from typing import Optional
import pyglet
from pyglet.font import base
from pyglet.font import win32query
import pyglet.image
from pyglet.libs.win32.constants import *
from pyglet.libs.win32.context_managers import device_context
from pyglet.libs.win32.types import *
from pyglet.libs.win32 import _gdi32 as gdi32, _user32 as user32
from pyglet.libs.win32 import _kernel32 as kernel32
from pyglet.util import asbytes
_debug_font = pyglet.options['debug_font']
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')
_debug_dir = 'debug_font'
def _debug_filename(base, extension):
import os
if not os.path.exists(_debug_dir):
os.makedirs(_debug_dir)
name = f'{os.path.join(_debug_dir, base)}-{{0}}.{{1}}'
num = 1
while os.path.exists(name.format(num, extension)):
num += 1
return name.format(num, extension)
def _debug_image(image, name):
filename = _debug_filename(name, 'png')
image.save(filename)
_debug(f'Saved image {image} to {filename}')
_debug_logfile = None
def _debug(msg):
global _debug_logfile
if not _debug_logfile:
_debug_logfile = open(_debug_filename('log', 'txt'), 'wt')
_debug_logfile.write(msg + '\n')
class Win32GlyphRenderer(base.GlyphRenderer):
def __init__(self, font):
self._bitmap = None
self._dc = None
self._bitmap_rect = None
super(Win32GlyphRenderer, self).__init__(font)
self.font = font
# Pessimistically round up width and height to 4 byte alignment
width = font.max_glyph_width
height = font.ascent - font.descent
width = (width | 0x3) + 1
height = (height | 0x3) + 1
self._create_bitmap(width, height)
gdi32.SelectObject(self._dc, self.font.hfont)
def _create_bitmap(self, width, height) -> None:
pass
def render(self, text: str) -> pyglet.font.base.Glyph:
raise NotImplementedError('abstract')
class GDIGlyphRenderer(Win32GlyphRenderer):
def __del__(self):
try:
if self._dc:
gdi32.DeleteDC(self._dc)
if self._bitmap:
gdi32.DeleteObject(self._bitmap)
except:
pass
def render(self, text: str) -> pyglet.font.base.Glyph:
# Attempt to get ABC widths (only for TrueType)
abc = ABC()
if gdi32.GetCharABCWidthsW(
self._dc,
ord(text), ord(text), byref(abc)
):
width = abc.abcB
lsb = abc.abcA
advance = abc.abcA + abc.abcB + abc.abcC
else:
width_buf = c_int()
gdi32.GetCharWidth32W(
self._dc, ord(text), ord(text), byref(width_buf))
width = width_buf.value
lsb = 0
advance = width
# Can't get glyph-specific dimensions, use whole line-height.
height = self._bitmap_height
image = self._get_image(text, width, height, lsb)
glyph = self.font.create_glyph(image)
glyph.set_bearings(-self.font.descent, lsb, advance)
if _debug_font:
_debug(f'{self}.render({text})')
_debug(f'abc.abcA = {abc.abcA}')
_debug(f'abc.abcB = {abc.abcB}')
_debug(f'abc.abcC = {abc.abcC}')
_debug(f'width = {width}')
_debug(f'height = {height}')
_debug(f'lsb = {lsb}')
_debug(f'advance = {advance}')
_debug_image(image, f'glyph_{text}')
_debug_image(self.font.textures[0], f'tex_{text}')
return glyph
def _get_image(self, text: str, width: int, height: int, lsb: int) -> pyglet.image.ImageData:
# There's no such thing as a greyscale bitmap format in GDI. We can
# create an 8-bit palette bitmap with 256 shades of grey, but
# unfortunately antialiasing will not work on such a bitmap. So, we
# use a 32-bit bitmap and use the red channel as OpenGL's alpha.
gdi32.SelectObject(self._dc, self._bitmap)
gdi32.SelectObject(self._dc, self.font.hfont)
gdi32.SetBkColor(self._dc, 0x0)
gdi32.SetTextColor(self._dc, 0x00ffffff)
gdi32.SetBkMode(self._dc, OPAQUE)
# Draw to DC
user32.FillRect(self._dc, byref(self._bitmap_rect), self._black)
gdi32.ExtTextOutA(
self._dc, -lsb, 0, 0, None, text,
len(text), None)
gdi32.GdiFlush()
# Create glyph object and copy bitmap data to texture
image = pyglet.image.ImageData(
width, height,
'AXXX', self._bitmap_data, self._bitmap_rect.right * 4)
return image
def _create_bitmap(self, width: int, height: int) -> None:
self._black = gdi32.GetStockObject(BLACK_BRUSH)
self._white = gdi32.GetStockObject(WHITE_BRUSH)
if self._dc:
gdi32.ReleaseDC(self._dc)
if self._bitmap:
gdi32.DeleteObject(self._bitmap)
pitch = width * 4
data = POINTER(c_byte * (height * pitch))()
info = BITMAPINFO()
info.bmiHeader.biSize = sizeof(info.bmiHeader)
info.bmiHeader.biWidth = width
info.bmiHeader.biHeight = height
info.bmiHeader.biPlanes = 1
info.bmiHeader.biBitCount = 32
info.bmiHeader.biCompression = BI_RGB
self._dc = gdi32.CreateCompatibleDC(None)
self._bitmap = gdi32.CreateDIBSection(
None,
byref(info), DIB_RGB_COLORS, byref(data), None,
0)
# Spookiness: the above line causes a "not enough storage" error,
# even though that error cannot be generated according to docs,
# and everything works fine anyway. Call SetLastError to clear it.
kernel32.SetLastError(0)
self._bitmap_data = data.contents
self._bitmap_rect = RECT()
self._bitmap_rect.left = 0
self._bitmap_rect.right = width
self._bitmap_rect.top = 0
self._bitmap_rect.bottom = height
self._bitmap_height = height
if _debug_font:
_debug(f'{self}._create_dc({width}, {height})')
_debug(f'_dc = {self._dc}')
_debug(f'_bitmap = {self._bitmap}')
_debug(f'pitch = {pitch}')
_debug(f'info.bmiHeader.biSize = {info.bmiHeader.biSize}')
class Win32Font(base.Font):
glyph_renderer_class = GDIGlyphRenderer
def __init__(
self,
name: str, size: int,
bold: bool = False, italic: bool = False, stretch: bool = False,
dpi: Optional[float] = None
):
super(Win32Font, self).__init__()
self.logfont = self.get_logfont(name, size, bold, italic, dpi)
self.hfont = gdi32.CreateFontIndirectW(byref(self.logfont))
# Create a dummy DC for coordinate mapping
with device_context(None) as dc:
metrics = TEXTMETRIC()
gdi32.SelectObject(dc, self.hfont)
gdi32.GetTextMetricsA(dc, byref(metrics))
self.ascent = metrics.tmAscent
self.descent = -metrics.tmDescent
self.max_glyph_width = metrics.tmMaxCharWidth
def __del__(self):
gdi32.DeleteObject(self.hfont)
@staticmethod
def get_logfont(name: str, size: float, bold: bool, italic: bool, dpi: Optional[float] = None) -> LOGFONTW:
"""Get a raw Win32 :py:class:`.LOGFONTW` struct for the given arguments.
Args:
name: The name of the font
size: The font size in points
bold: Whether to request the font as bold
italic: Whether to request the font as italic
dpi: The screen dpi
Returns:
LOGFONTW: a ctypes binding of a Win32 LOGFONTW struct
"""
# Create a dummy DC for coordinate mapping
with device_context(None) as dc:
# Default to 96 DPI unless otherwise specified
if dpi is None:
dpi = 96
log_pixels_y = dpi
# Create LOGFONTW font description struct
logfont = LOGFONTW()
# Convert point size to actual device pixels
logfont.lfHeight = int(-size * log_pixels_y // 72)
# Configure the LOGFONTW's font properties
if bold:
logfont.lfWeight = FW_BOLD
else:
logfont.lfWeight = FW_NORMAL
logfont.lfItalic = italic
logfont.lfFaceName = name
logfont.lfQuality = ANTIALIASED_QUALITY
return logfont
@classmethod
def have_font(cls, name) -> bool:
# [ ] add support for loading raster fonts
return win32query.have_font(name)
@classmethod
def add_font_data(cls, data):
numfonts = c_uint32()
_handle = gdi32.AddFontMemResourceEx(data, len(data), 0, byref(numfonts))
# None means a null handle was returned, ie something went wrong
if _handle is None:
raise ctypes.WinError()
# --- GDI+ font rendering ---
from pyglet.image.codecs.gdiplus import PixelFormat32bppARGB, gdiplus, Rect
from pyglet.image.codecs.gdiplus import ImageLockModeRead, BitmapData
DriverStringOptionsCmapLookup = 1
DriverStringOptionsRealizedAdvance = 4
TextRenderingHintAntiAlias = 4
TextRenderingHintAntiAliasGridFit = 3
StringFormatFlagsDirectionRightToLeft = 0x00000001
StringFormatFlagsDirectionVertical = 0x00000002
StringFormatFlagsNoFitBlackBox = 0x00000004
StringFormatFlagsDisplayFormatControl = 0x00000020
StringFormatFlagsNoFontFallback = 0x00000400
StringFormatFlagsMeasureTrailingSpaces = 0x00000800
StringFormatFlagsNoWrap = 0x00001000
StringFormatFlagsLineLimit = 0x00002000
StringFormatFlagsNoClip = 0x00004000
class Rectf(ctypes.Structure):
_fields_ = [
('x', ctypes.c_float),
('y', ctypes.c_float),
('width', ctypes.c_float),
('height', ctypes.c_float),
]
class GDIPlusGlyphRenderer(Win32GlyphRenderer):
def __del__(self):
try:
if self._matrix:
res = gdiplus.GdipDeleteMatrix(self._matrix)
if self._brush:
res = gdiplus.GdipDeleteBrush(self._brush)
if self._graphics:
res = gdiplus.GdipDeleteGraphics(self._graphics)
if self._bitmap:
res = gdiplus.GdipDisposeImage(self._bitmap)
if self._dc:
res = user32.ReleaseDC(0, self._dc)
except:
pass
def _create_bitmap(self, width, height):
self._data = (BYTE * (4 * width * height))()
self._bitmap = ctypes.c_void_p()
self._format = PixelFormat32bppARGB
gdiplus.GdipCreateBitmapFromScan0(width, height, width * 4,
self._format, self._data, ctypes.byref(self._bitmap))
self._graphics = ctypes.c_void_p()
gdiplus.GdipGetImageGraphicsContext(self._bitmap,
ctypes.byref(self._graphics))
gdiplus.GdipSetPageUnit(self._graphics, UnitPixel)
self._dc = user32.GetDC(0)
gdi32.SelectObject(self._dc, self.font.hfont)
gdiplus.GdipSetTextRenderingHint(self._graphics,
TextRenderingHintAntiAliasGridFit)
self._brush = ctypes.c_void_p()
gdiplus.GdipCreateSolidFill(0xffffffff, ctypes.byref(self._brush))
self._matrix = ctypes.c_void_p()
gdiplus.GdipCreateMatrix(ctypes.byref(self._matrix))
self._flags = (DriverStringOptionsCmapLookup |
DriverStringOptionsRealizedAdvance)
self._rect = Rect(0, 0, width, height)
self._bitmap_height = height
def render(self, text):
ch = ctypes.create_unicode_buffer(text)
len_ch = len(text)
# Layout rectangle; not clipped against so not terribly important.
width = 10000
height = self._bitmap_height
rect = Rectf(0, self._bitmap_height
- self.font.ascent + self.font.descent,
width, height)
# Set up GenericTypographic with 1 character measure range
generic = ctypes.c_void_p()
gdiplus.GdipStringFormatGetGenericTypographic(ctypes.byref(generic))
fmt = ctypes.c_void_p()
gdiplus.GdipCloneStringFormat(generic, ctypes.byref(fmt))
gdiplus.GdipDeleteStringFormat(generic)
# --- Measure advance
# XXX HACK HACK HACK
# Windows GDI+ is a filthy broken toy. No way to measure the bounding
# box of a string, or to obtain LSB. What a joke.
#
# For historical note, GDI cannot be used because it cannot composite
# into a bitmap with alpha.
#
# It looks like MS have abandoned GDI and GDI+ and are finally
# supporting accurate text measurement with alpha composition in .NET
# 2.0 (WinForms) via the TextRenderer class; this has no C interface
# though, so we're entirely screwed.
#
# So anyway, we first try to get the width with GdipMeasureString.
# Then if it's a TrueType font, we use GetCharABCWidthsW to get the
# correct LSB. If it's a negative LSB, we move the layoutRect `rect`
# to the right so that the whole glyph is rendered on the surface.
# For positive LSB, we let the renderer render the correct white
# space and we don't pass the LSB info to the Glyph.set_bearings
bbox = Rectf()
flags = (StringFormatFlagsMeasureTrailingSpaces |
StringFormatFlagsNoClip |
StringFormatFlagsNoFitBlackBox)
gdiplus.GdipSetStringFormatFlags(fmt, flags)
gdiplus.GdipMeasureString(self._graphics,
ch,
len_ch,
self.font._gdipfont,
ctypes.byref(rect),
fmt,
ctypes.byref(bbox),
None,
None)
# We only care about the advance from this whole thing.
advance = int(math.ceil(bbox.width))
# GDI functions only work for a single character so we transform
# grapheme \r\n into \r
if text == '\r\n':
text = '\r'
# XXX END HACK HACK HACK
abc = ABC()
width = 0
lsb = 0
ttf_font = True
# Use GDI to get code points for the text passed. This is almost always 1.
# For special unicode characters it may be comprised of 2+ codepoints. Get the width/lsb of each.
# Function only works on TTF fonts.
for codepoint in [ord(c) for c in text]:
if gdi32.GetCharABCWidthsW(self._dc, codepoint, codepoint, byref(abc)):
lsb += abc.abcA
width += abc.abcB
if lsb < 0:
# Negative LSB: we shift the layout rect to the right
# Otherwise we will cut the left part of the glyph
rect.x = -lsb
width -= lsb
else:
width += lsb
else:
ttf_font = False
break
# Almost always a TTF font. Haven't seen a modern font that GetCharABCWidthsW fails on.
# For safety, just use the advance as the width.
if not ttf_font:
width = advance
# This hack bumps up the width if the font is italic;
# this compensates for some common fonts. It's also a stupid
# waste of texture memory.
if self.font.italic:
width += width // 2
# Do not enlarge more than the _rect width.
width = min(width, self._rect.Width)
# Draw character to bitmap
gdiplus.GdipGraphicsClear(self._graphics, 0x00000000)
gdiplus.GdipDrawString(self._graphics,
ch,
len_ch,
self.font._gdipfont,
ctypes.byref(rect),
fmt,
self._brush)
gdiplus.GdipFlush(self._graphics, 1)
gdiplus.GdipDeleteStringFormat(fmt)
bitmap_data = BitmapData()
gdiplus.GdipBitmapLockBits(
self._bitmap,
byref(self._rect), ImageLockModeRead, self._format,
byref(bitmap_data))
# Create buffer for RawImage
buffer = create_string_buffer(
bitmap_data.Stride * bitmap_data.Height)
memmove(buffer, bitmap_data.Scan0, len(buffer))
# Unlock data
gdiplus.GdipBitmapUnlockBits(self._bitmap, byref(bitmap_data))
image = pyglet.image.ImageData(
width, height,
'BGRA', buffer, -bitmap_data.Stride)
glyph = self.font.create_glyph(image)
# Only pass negative LSB info
lsb = min(lsb, 0)
glyph.set_bearings(-self.font.descent, lsb, advance)
return glyph
FontStyleBold = 1
FontStyleItalic = 2
UnitPixel = 2
UnitPoint = 3
class GDIPlusFont(Win32Font):
glyph_renderer_class = GDIPlusGlyphRenderer
_private_fonts = None
_default_name = 'Arial'
def __init__(self, name, size, bold=False, italic=False, stretch=False, dpi=None):
if not name:
name = self._default_name
# 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, size, bold, italic, stretch, dpi)
self._name = name
family = ctypes.c_void_p()
# GDI will add @ in front of a localized font for some Asian languages. However, GDI will also not find it
# based on that name (???). Here we remove it before checking font collections.
if name[0] == "@":
name = name[1:]
name = ctypes.c_wchar_p(name)
# Look in private collection first:
if self._private_fonts:
gdiplus.GdipCreateFontFamilyFromName(name, self._private_fonts, ctypes.byref(family))
# Then in system collection:
if not family:
if _debug_font:
print(f"Warning: Font '{name}' was not found. Defaulting to: {self._default_name}")
gdiplus.GdipCreateFontFamilyFromName(name, None, ctypes.byref(family))
# Nothing found, use default font.
if not family:
self._name = self._default_name
gdiplus.GdipCreateFontFamilyFromName(ctypes.c_wchar_p(self._name), None, ctypes.byref(family))
if dpi is None:
unit = UnitPoint
self.dpi = 96
else:
unit = UnitPixel
size = (size * dpi) // 72
self.dpi = dpi
style = 0
if bold:
style |= FontStyleBold
if italic:
style |= FontStyleItalic
self._gdipfont = ctypes.c_void_p()
gdiplus.GdipCreateFont(family, ctypes.c_float(size), style, unit, ctypes.byref(self._gdipfont))
gdiplus.GdipDeleteFontFamily(family)
@property
def name(self):
return self._name
def __del__(self):
super(GDIPlusFont, self).__del__()
gdiplus.GdipDeleteFont(self._gdipfont)
@classmethod
def add_font_data(cls, data):
super(GDIPlusFont, cls).add_font_data(data)
if not cls._private_fonts:
cls._private_fonts = ctypes.c_void_p()
gdiplus.GdipNewPrivateFontCollection(
ctypes.byref(cls._private_fonts))
gdiplus.GdipPrivateAddMemoryFont(cls._private_fonts, data, len(data))
@classmethod
def have_font(cls, name):
family = ctypes.c_void_p()
# Look in private collection first:
num_count = ctypes.c_int()
gdiplus.GdipGetFontCollectionFamilyCount(
cls._private_fonts, ctypes.byref(num_count))
gpfamilies = (ctypes.c_void_p * num_count.value)()
numFound = ctypes.c_int()
gdiplus.GdipGetFontCollectionFamilyList(
cls._private_fonts, num_count, gpfamilies, ctypes.byref(numFound))
font_name = ctypes.create_unicode_buffer(32)
for gpfamily in gpfamilies:
gdiplus.GdipGetFamilyName(gpfamily, font_name, '\0')
if font_name.value == name:
return True
# Else call parent class for system fonts
return super(GDIPlusFont, cls).have_font(name)