# 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)