1# ---------------------------------------------------------------------------- 2# pyglet 3# Copyright (c) 2006-2008 Alex Holkner 4# Copyright (c) 2008-2021 pyglet contributors 5# All rights reserved. 6# 7# Redistribution and use in source and binary forms, with or without 8# modification, are permitted provided that the following conditions 9# are met: 10# 11# * Redistributions of source code must retain the above copyright 12# notice, this list of conditions and the following disclaimer. 13# * Redistributions in binary form must reproduce the above copyright 14# notice, this list of conditions and the following disclaimer in 15# the documentation and/or other materials provided with the 16# distribution. 17# * Neither the name of pyglet nor the names of its 18# contributors may be used to endorse or promote products 19# derived from this software without specific prior written 20# permission. 21# 22# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33# POSSIBILITY OF SUCH DAMAGE. 34# ---------------------------------------------------------------------------- 35 36# TODO Windows Vista: need to call SetProcessDPIAware? May affect GDI+ calls as well as font. 37 38import math 39import warnings 40 41from sys import byteorder 42import pyglet 43from pyglet.font import base 44from pyglet.font import win32query 45import pyglet.image 46from pyglet.libs.win32.constants import * 47from pyglet.libs.win32.types import * 48from pyglet.libs.win32 import _gdi32 as gdi32, _user32 as user32 49from pyglet.libs.win32 import _kernel32 as kernel32 50from pyglet.util import asbytes 51 52_debug_font = pyglet.options['debug_font'] 53 54 55def str_ucs2(text): 56 if byteorder == 'big': 57 text = text.encode('utf_16_be') 58 else: 59 text = text.encode('utf_16_le') # explicit endian avoids BOM 60 return create_string_buffer(text + '\0') 61 62_debug_dir = 'debug_font' 63def _debug_filename(base, extension): 64 import os 65 if not os.path.exists(_debug_dir): 66 os.makedirs(_debug_dir) 67 name = '%s-%%d.%%s' % os.path.join(_debug_dir, base) 68 num = 1 69 while os.path.exists(name % (num, extension)): 70 num += 1 71 return name % (num, extension) 72 73def _debug_image(image, name): 74 filename = _debug_filename(name, 'png') 75 image.save(filename) 76 _debug('Saved image %r to %s' % (image, filename)) 77 78_debug_logfile = None 79def _debug(msg): 80 global _debug_logfile 81 if not _debug_logfile: 82 _debug_logfile = open(_debug_filename('log', 'txt'), 'wt') 83 _debug_logfile.write(msg + '\n') 84 85class Win32GlyphRenderer(base.GlyphRenderer): 86 87 88 def __init__(self, font): 89 self._bitmap = None 90 self._dc = None 91 self._bitmap_rect = None 92 super(Win32GlyphRenderer, self).__init__(font) 93 self.font = font 94 95 # Pessimistically round up width and height to 4 byte alignment 96 width = font.max_glyph_width 97 height = font.ascent - font.descent 98 width = (width | 0x3) + 1 99 height = (height | 0x3) + 1 100 self._create_bitmap(width, height) 101 102 gdi32.SelectObject(self._dc, self.font.hfont) 103 104 def _create_bitmap(self, width, height): 105 pass 106 107 def render(self, text): 108 raise NotImplementedError('abstract') 109 110class GDIGlyphRenderer(Win32GlyphRenderer): 111 def __del__(self): 112 try: 113 if self._dc: 114 gdi32.DeleteDC(self._dc) 115 if self._bitmap: 116 gdi32.DeleteObject(self._bitmap) 117 except: 118 pass 119 120 def render(self, text): 121 # Attempt to get ABC widths (only for TrueType) 122 abc = ABC() 123 if gdi32.GetCharABCWidthsW(self._dc, 124 ord(text), ord(text), byref(abc)): 125 width = abc.abcB 126 lsb = abc.abcA 127 advance = abc.abcA + abc.abcB + abc.abcC 128 else: 129 width_buf = c_int() 130 gdi32.GetCharWidth32W(self._dc, 131 ord(text), ord(text), byref(width_buf)) 132 width = width_buf.value 133 lsb = 0 134 advance = width 135 136 # Can't get glyph-specific dimensions, use whole line-height. 137 height = self._bitmap_height 138 image = self._get_image(text, width, height, lsb) 139 140 glyph = self.font.create_glyph(image) 141 glyph.set_bearings(-self.font.descent, lsb, advance) 142 143 if _debug_font: 144 _debug('%r.render(%s)' % (self, text)) 145 _debug('abc.abcA = %r' % abc.abcA) 146 _debug('abc.abcB = %r' % abc.abcB) 147 _debug('abc.abcC = %r' % abc.abcC) 148 _debug('width = %r' % width) 149 _debug('height = %r' % height) 150 _debug('lsb = %r' % lsb) 151 _debug('advance = %r' % advance) 152 _debug_image(image, 'glyph_%s' % text) 153 _debug_image(self.font.textures[0], 'tex_%s' % text) 154 155 return glyph 156 157 def _get_image(self, text, width, height, lsb): 158 # There's no such thing as a greyscale bitmap format in GDI. We can 159 # create an 8-bit palette bitmap with 256 shades of grey, but 160 # unfortunately antialiasing will not work on such a bitmap. So, we 161 # use a 32-bit bitmap and use the red channel as OpenGL's alpha. 162 163 gdi32.SelectObject(self._dc, self._bitmap) 164 gdi32.SelectObject(self._dc, self.font.hfont) 165 gdi32.SetBkColor(self._dc, 0x0) 166 gdi32.SetTextColor(self._dc, 0x00ffffff) 167 gdi32.SetBkMode(self._dc, OPAQUE) 168 169 # Draw to DC 170 user32.FillRect(self._dc, byref(self._bitmap_rect), self._black) 171 gdi32.ExtTextOutA(self._dc, -lsb, 0, 0, None, text, 172 len(text), None) 173 gdi32.GdiFlush() 174 175 # Create glyph object and copy bitmap data to texture 176 image = pyglet.image.ImageData(width, height, 177 'AXXX', self._bitmap_data, self._bitmap_rect.right * 4) 178 return image 179 180 def _create_bitmap(self, width, height): 181 self._black = gdi32.GetStockObject(BLACK_BRUSH) 182 self._white = gdi32.GetStockObject(WHITE_BRUSH) 183 184 if self._dc: 185 gdi32.ReleaseDC(self._dc) 186 if self._bitmap: 187 gdi32.DeleteObject(self._bitmap) 188 189 pitch = width * 4 190 data = POINTER(c_byte * (height * pitch))() 191 info = BITMAPINFO() 192 info.bmiHeader.biSize = sizeof(info.bmiHeader) 193 info.bmiHeader.biWidth = width 194 info.bmiHeader.biHeight = height 195 info.bmiHeader.biPlanes = 1 196 info.bmiHeader.biBitCount = 32 197 info.bmiHeader.biCompression = BI_RGB 198 199 self._dc = gdi32.CreateCompatibleDC(None) 200 self._bitmap = gdi32.CreateDIBSection(None, 201 byref(info), DIB_RGB_COLORS, byref(data), None, 202 0) 203 # Spookiness: the above line causes a "not enough storage" error, 204 # even though that error cannot be generated according to docs, 205 # and everything works fine anyway. Call SetLastError to clear it. 206 kernel32.SetLastError(0) 207 208 self._bitmap_data = data.contents 209 self._bitmap_rect = RECT() 210 self._bitmap_rect.left = 0 211 self._bitmap_rect.right = width 212 self._bitmap_rect.top = 0 213 self._bitmap_rect.bottom = height 214 self._bitmap_height = height 215 216 if _debug_font: 217 _debug('%r._create_dc(%d, %d)' % (self, width, height)) 218 _debug('_dc = %r' % self._dc) 219 _debug('_bitmap = %r' % self._bitmap) 220 _debug('pitch = %r' % pitch) 221 _debug('info.bmiHeader.biSize = %r' % info.bmiHeader.biSize) 222 223class Win32Font(base.Font): 224 glyph_renderer_class = GDIGlyphRenderer 225 226 def __init__(self, name, size, bold=False, italic=False, stretch=False, dpi=None): 227 super(Win32Font, self).__init__() 228 229 self.logfont = self.get_logfont(name, size, bold, italic, dpi) 230 self.hfont = gdi32.CreateFontIndirectA(byref(self.logfont)) 231 232 # Create a dummy DC for coordinate mapping 233 dc = user32.GetDC(0) 234 metrics = TEXTMETRIC() 235 gdi32.SelectObject(dc, self.hfont) 236 gdi32.GetTextMetricsA(dc, byref(metrics)) 237 self.ascent = metrics.tmAscent 238 self.descent = -metrics.tmDescent 239 self.max_glyph_width = metrics.tmMaxCharWidth 240 user32.ReleaseDC(0, dc) 241 242 def __del__(self): 243 gdi32.DeleteObject(self.hfont) 244 245 @staticmethod 246 def get_logfont(name, size, bold, italic, dpi): 247 # Create a dummy DC for coordinate mapping 248 dc = user32.GetDC(0) 249 if dpi is None: 250 dpi = 96 251 logpixelsy = dpi 252 253 logfont = LOGFONT() 254 # Conversion of point size to device pixels 255 logfont.lfHeight = int(-size * logpixelsy // 72) 256 if bold: 257 logfont.lfWeight = FW_BOLD 258 else: 259 logfont.lfWeight = FW_NORMAL 260 logfont.lfItalic = italic 261 logfont.lfFaceName = asbytes(name) 262 logfont.lfQuality = ANTIALIASED_QUALITY 263 user32.ReleaseDC(0, dc) 264 return logfont 265 266 @classmethod 267 def have_font(cls, name): 268 # [ ] add support for loading raster fonts 269 return win32query.have_font(name) 270 271 @classmethod 272 def add_font_data(cls, data): 273 numfonts = c_uint32() 274 gdi32.AddFontMemResourceEx(data, len(data), 0, byref(numfonts)) 275 276# --- GDI+ font rendering --- 277 278from pyglet.image.codecs.gdiplus import PixelFormat32bppARGB, gdiplus, Rect 279from pyglet.image.codecs.gdiplus import ImageLockModeRead, BitmapData 280 281DriverStringOptionsCmapLookup = 1 282DriverStringOptionsRealizedAdvance = 4 283TextRenderingHintAntiAlias = 4 284TextRenderingHintAntiAliasGridFit = 3 285 286StringFormatFlagsDirectionRightToLeft = 0x00000001 287StringFormatFlagsDirectionVertical = 0x00000002 288StringFormatFlagsNoFitBlackBox = 0x00000004 289StringFormatFlagsDisplayFormatControl = 0x00000020 290StringFormatFlagsNoFontFallback = 0x00000400 291StringFormatFlagsMeasureTrailingSpaces = 0x00000800 292StringFormatFlagsNoWrap = 0x00001000 293StringFormatFlagsLineLimit = 0x00002000 294StringFormatFlagsNoClip = 0x00004000 295 296class Rectf(ctypes.Structure): 297 _fields_ = [ 298 ('x', ctypes.c_float), 299 ('y', ctypes.c_float), 300 ('width', ctypes.c_float), 301 ('height', ctypes.c_float), 302 ] 303 304class GDIPlusGlyphRenderer(Win32GlyphRenderer): 305 def __del__(self): 306 try: 307 if self._matrix: 308 res = gdiplus.GdipDeleteMatrix(self._matrix) 309 if self._brush: 310 res = gdiplus.GdipDeleteBrush(self._brush) 311 if self._graphics: 312 res = gdiplus.GdipDeleteGraphics(self._graphics) 313 if self._bitmap: 314 res = gdiplus.GdipDisposeImage(self._bitmap) 315 if self._dc: 316 res = user32.ReleaseDC(0, self._dc) 317 except: 318 pass 319 320 def _create_bitmap(self, width, height): 321 self._data = (ctypes.c_byte * (4 * width * height))() 322 self._bitmap = ctypes.c_void_p() 323 self._format = PixelFormat32bppARGB 324 gdiplus.GdipCreateBitmapFromScan0(width, height, width * 4, 325 self._format, self._data, ctypes.byref(self._bitmap)) 326 327 self._graphics = ctypes.c_void_p() 328 gdiplus.GdipGetImageGraphicsContext(self._bitmap, 329 ctypes.byref(self._graphics)) 330 gdiplus.GdipSetPageUnit(self._graphics, UnitPixel) 331 332 self._dc = user32.GetDC(0) 333 gdi32.SelectObject(self._dc, self.font.hfont) 334 335 gdiplus.GdipSetTextRenderingHint(self._graphics, 336 TextRenderingHintAntiAliasGridFit) 337 338 339 self._brush = ctypes.c_void_p() 340 gdiplus.GdipCreateSolidFill(0xffffffff, ctypes.byref(self._brush)) 341 342 343 self._matrix = ctypes.c_void_p() 344 gdiplus.GdipCreateMatrix(ctypes.byref(self._matrix)) 345 346 self._flags = (DriverStringOptionsCmapLookup | 347 DriverStringOptionsRealizedAdvance) 348 349 self._rect = Rect(0, 0, width, height) 350 351 self._bitmap_height = height 352 353 def render(self, text): 354 355 ch = ctypes.create_unicode_buffer(text) 356 len_ch = len(text) 357 358 # Layout rectangle; not clipped against so not terribly important. 359 width = 10000 360 height = self._bitmap_height 361 rect = Rectf(0, self._bitmap_height 362 - self.font.ascent + self.font.descent, 363 width, height) 364 365 # Set up GenericTypographic with 1 character measure range 366 generic = ctypes.c_void_p() 367 gdiplus.GdipStringFormatGetGenericTypographic(ctypes.byref(generic)) 368 format = ctypes.c_void_p() 369 gdiplus.GdipCloneStringFormat(generic, ctypes.byref(format)) 370 gdiplus.GdipDeleteStringFormat(generic) 371 372 # Measure advance 373 374 # XXX HACK HACK HACK 375 # Windows GDI+ is a filthy broken toy. No way to measure the bounding 376 # box of a string, or to obtain LSB. What a joke. 377 # 378 # For historical note, GDI cannot be used because it cannot composite 379 # into a bitmap with alpha. 380 # 381 # It looks like MS have abandoned GDI and GDI+ and are finally 382 # supporting accurate text measurement with alpha composition in .NET 383 # 2.0 (WinForms) via the TextRenderer class; this has no C interface 384 # though, so we're entirely screwed. 385 # 386 # So anyway, we first try to get the width with GdipMeasureString. 387 # Then if it's a TrueType font, we use GetCharABCWidthsW to get the 388 # correct LSB. If it's a negative LSB, we move the layoutRect `rect` 389 # to the right so that the whole glyph is rendered on the surface. 390 # For positive LSB, we let the renderer render the correct white 391 # space and we don't pass the LSB info to the Glyph.set_bearings 392 393 bbox = Rectf() 394 flags = (StringFormatFlagsMeasureTrailingSpaces | 395 StringFormatFlagsNoClip | 396 StringFormatFlagsNoFitBlackBox) 397 gdiplus.GdipSetStringFormatFlags(format, flags) 398 gdiplus.GdipMeasureString(self._graphics, 399 ch, 400 len_ch, 401 self.font._gdipfont, 402 ctypes.byref(rect), 403 format, 404 ctypes.byref(bbox), 405 None, 406 None) 407 lsb = 0 408 advance = int(math.ceil(bbox.width)) 409 width = advance 410 411 # This hack bumps up the width if the font is italic; 412 # this compensates for some common fonts. It's also a stupid 413 # waste of texture memory. 414 if self.font.italic: 415 width += width // 2 416 # Do not enlarge more than the _rect width. 417 width = min(width, self._rect.Width) 418 419 # GDI functions only work for a single character so we transform 420 # grapheme \r\n into \r 421 if text == '\r\n': 422 text = '\r' 423 424 abc = ABC() 425 # Check if ttf font. 426 if gdi32.GetCharABCWidthsW(self._dc, 427 ord(text), ord(text), byref(abc)): 428 429 lsb = abc.abcA 430 width = abc.abcB 431 if lsb < 0: 432 # Negative LSB: we shift the layout rect to the right 433 # Otherwise we will cut the left part of the glyph 434 rect.x = -lsb 435 width -= lsb 436 else: 437 width += lsb 438 439 # XXX END HACK HACK HACK 440 441 # Draw character to bitmap 442 443 gdiplus.GdipGraphicsClear(self._graphics, 0x00000000) 444 gdiplus.GdipDrawString(self._graphics, 445 ch, 446 len_ch, 447 self.font._gdipfont, 448 ctypes.byref(rect), 449 format, 450 self._brush) 451 gdiplus.GdipFlush(self._graphics, 1) 452 gdiplus.GdipDeleteStringFormat(format) 453 454 bitmap_data = BitmapData() 455 gdiplus.GdipBitmapLockBits(self._bitmap, 456 byref(self._rect), ImageLockModeRead, self._format, 457 byref(bitmap_data)) 458 459 # Create buffer for RawImage 460 buffer = create_string_buffer( 461 bitmap_data.Stride * bitmap_data.Height) 462 memmove(buffer, bitmap_data.Scan0, len(buffer)) 463 464 # Unlock data 465 gdiplus.GdipBitmapUnlockBits(self._bitmap, byref(bitmap_data)) 466 467 image = pyglet.image.ImageData(width, height, 468 'BGRA', buffer, -bitmap_data.Stride) 469 470 glyph = self.font.create_glyph(image) 471 # Only pass negative LSB info 472 lsb = min(lsb, 0) 473 glyph.set_bearings(-self.font.descent, lsb, advance) 474 return glyph 475 476FontStyleBold = 1 477FontStyleItalic = 2 478UnitPixel = 2 479UnitPoint = 3 480 481class GDIPlusFont(Win32Font): 482 glyph_renderer_class = GDIPlusGlyphRenderer 483 484 _private_fonts = None 485 486 _default_name = 'Arial' 487 488 def __init__(self, name, size, bold=False, italic=False, stretch=False, dpi=None): 489 if not name: 490 name = self._default_name 491 492 # assert type(bold) is bool, "Only a boolean value is supported for bold in the current font renderer." 493 # assert type(italic) is bool, "Only a boolean value is supported for bold in the current font renderer." 494 495 if stretch: 496 warnings.warn("The current font render does not support stretching.") 497 498 super().__init__(name, size, bold, italic, stretch, dpi) 499 500 self._name = name 501 502 family = ctypes.c_void_p() 503 name = ctypes.c_wchar_p(name) 504 505 # Look in private collection first: 506 if self._private_fonts: 507 gdiplus.GdipCreateFontFamilyFromName(name, self._private_fonts, ctypes.byref(family)) 508 509 # Then in system collection: 510 if not family: 511 gdiplus.GdipCreateFontFamilyFromName(name, None, ctypes.byref(family)) 512 513 # Nothing found, use default font. 514 if not family: 515 self._name = self._default_name 516 gdiplus.GdipCreateFontFamilyFromName(ctypes.c_wchar_p(self._name), None, ctypes.byref(family)) 517 518 if dpi is None: 519 unit = UnitPoint 520 self.dpi = 96 521 else: 522 unit = UnitPixel 523 size = (size * dpi) // 72 524 self.dpi = dpi 525 526 style = 0 527 if bold: 528 style |= FontStyleBold 529 if italic: 530 style |= FontStyleItalic 531 self._gdipfont = ctypes.c_void_p() 532 gdiplus.GdipCreateFont(family, ctypes.c_float(size), style, unit, ctypes.byref(self._gdipfont)) 533 gdiplus.GdipDeleteFontFamily(family) 534 535 @property 536 def name(self): 537 return self._name 538 539 def __del__(self): 540 super(GDIPlusFont, self).__del__() 541 gdiplus.GdipDeleteFont(self._gdipfont) 542 543 @classmethod 544 def add_font_data(cls, data): 545 super(GDIPlusFont, cls).add_font_data(data) 546 547 if not cls._private_fonts: 548 cls._private_fonts = ctypes.c_void_p() 549 gdiplus.GdipNewPrivateFontCollection( 550 ctypes.byref(cls._private_fonts)) 551 gdiplus.GdipPrivateAddMemoryFont(cls._private_fonts, data, len(data)) 552 553 @classmethod 554 def have_font(cls, name): 555 family = ctypes.c_void_p() 556 557 # Look in private collection first: 558 num_count = ctypes.c_int() 559 gdiplus.GdipGetFontCollectionFamilyCount( 560 cls._private_fonts, ctypes.byref(num_count)) 561 gpfamilies = (ctypes.c_void_p * num_count.value)() 562 numFound = ctypes.c_int() 563 gdiplus.GdipGetFontCollectionFamilyList( 564 cls._private_fonts, num_count, gpfamilies, ctypes.byref(numFound)) 565 566 font_name = ctypes.create_unicode_buffer(32) 567 for gpfamily in gpfamilies: 568 gdiplus.GdipGetFamilyName(gpfamily, font_name, '\0') 569 if font_name.value == name: 570 return True 571 572 # Else call parent class for system fonts 573 return super(GDIPlusFont, cls).have_font(name) 574