How to find out if a character is supported by a font

Viewed 311

I'm using pygame and I want to diplay the character ⊕, but when I ran the code it instead displayed a rectangle, and I tried many other fonts and they were all rectangles or rectangles with question marks. Other special characters like ± worked so I'm not sure what the problem is. I searched it up and there was another question similar to this on the site and it said python 3.x would have no problem with special characters as long as they're supported by the font. Then I checked fonts that support ⊕ and most of the fonts did.

tl;dr

how do you display ⊕ on pygame?

1 Answers

Ok, this question turned out to be difficult. When Linux renders a non-implemented glyph from a font (at least in PyGame), you get an empty "border rectangle". This rectangle sits at base-line of the font, with space on either side, but no space at the top. Probably it's similar on other operating environments. (Originally I assumed the border surrounded the glyph-image without space, and was easily detected.)

So now we can check the glyph renders to something by examining the interior quarter of the glyph to see if it contains any non-empty pixels.

Obviously this is a heuristic, and may fail on characters like , which is mostly a descender. However I tested it on a bunch of symbol and Greek letters, and it seems to work OK. It does work for the OP's test glyph.

Instead of intensively examining every pixel, the algorithm first tries to check the centre horizontal pixels, then the centre vertical pixels. If neither of these produce a result, then every interior pixel is checked. If ever a pixel is found, we stop checking and return.

def glyphInFont( glyph, font ):
    """ Given a glyph and a font, use a pixel-finding heuristic to determine
        if the glyph renders to something other than an "empty border" non-existant
        font symbol.  Returns True if it renders to something. """

    result = False
    WHITE  = ( 255, 255, 255 )   # can be any colour pair with constrast
    BLACK  = (   0,   0,   0 )

    try:
        text_image = font.render( glyph, True, WHITE, BLACK )
        text_rect  = text_image.get_rect()
        x_centre = text_rect.width // 2
        y_centre = text_rect.height // 2

        # On Linux at least, non-renderable glyphs have a border.
        # work out a 50% search box, centred inside the gluph
        box_top    = y_centre - ( text_rect.height // 4 )
        box_bottom = y_centre + ( text_rect.height // 4 )
        box_left   = x_centre - ( text_rect.width // 4 )
        box_right  = x_centre + ( text_rect.width // 4 )

        # Trace a Horizontal line through the middle of the bitmap 
        # looking for non-black pixels
        for x in range( box_left, box_right ):
            if ( text_image.get_at( ( x, y_centre ) ) != BLACK ):
                result = True
                break

        # If not found already, trace a line vertically
        if ( result == False ):
            for y in range( box_top, box_bottom ):
                if ( text_image.get_at( ( x_centre, y ) ) != BLACK ):
                    result = True
                    break

        # If still not found, check every pixel in the centre-box
        if ( result == False ):
            for y in range( box_top, box_bottom ):
                for x in range( box_left, box_right ):
                    if ( text_image.get_at( ( x, y ) ) != BLACK ):
                        result = True
                        break

    except UnicodeError as uce:
        # Glyph-ID not supported
        pass  # False goes through

    return result

Tested with the "Badboom" font, and whatever the Linux default SysFont is.

screenshot

This reference program checks to see if the glyph renders in the given fonts, drawing it if True, otherwise drawing an "x".

import pygame

# Window size
WINDOW_WIDTH    = 400
WINDOW_HEIGHT   = 400
WINDOW_SURFACE  = pygame.HWSURFACE|pygame.DOUBLEBUF|pygame.RESIZABLE

DARK_BLUE = (   3,   5,  54 )
YELLOW    = ( 255, 255,   0 )
RED       = ( 255,   0,   0 )

symbols = [ '⊕', 'Δ', 'Ψ', '' ]


def glyphInFont( glyph, font ):
    """ Given a glyph and a font, use a pixel-finding heuristic to determine
        if the glyph renders to something other than an "empty border" non-existant
        font symbol.  Returns True if it renders to something. """

    result = False
    WHITE  = ( 255, 255, 255 )   # can be any colour pair with constrast
    BLACK  = (   0,   0,   0 )

    try:
        text_image = font.render( glyph, True, WHITE, BLACK )
        text_rect  = text_image.get_rect()
        x_centre = text_rect.width // 2
        y_centre = text_rect.height // 2

        # On Linux at least, non-renderable glyphs have a border.
        # work out a 50% search box, centred inside the gluph
        box_top    = y_centre - ( text_rect.height // 4 )
        box_bottom = y_centre + ( text_rect.height // 4 )
        box_left   = x_centre - ( text_rect.width // 4 )
        box_right  = x_centre + ( text_rect.width // 4 )

        # Trace a Horizontal line through the middle of the bitmap 
        # looking for non-black pixels
        for x in range( box_left, box_right ):
            if ( text_image.get_at( ( x, y_centre ) ) != BLACK ):
                result = True
                break

        # If not found already, trace a line vertically
        if ( result == False ):
            for y in range( box_top, box_bottom ):
                if ( text_image.get_at( ( x_centre, y ) ) != BLACK ):
                    result = True
                    break

        # If still not found, check every pixel in the centre-box
        if ( result == False ):
            for y in range( box_top, box_bottom ):
                for x in range( box_left, box_right ):
                    if ( text_image.get_at( ( x, y ) ) != BLACK ):
                        result = True
                        break

    except UnicodeError as uce:
        # Glyph-ID not supported
        pass  # False goes through

    return result


### initialisation
pygame.init()
pygame.mixer.init()
window = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), WINDOW_SURFACE )
pygame.display.set_caption("Glyph Check")


### Make some fonts
font1 = pygame.font.Font( 'badaboom.ttf', 64 )   # ref: https://www.1001freefonts.com/badaboom.font
font2 = pygame.font.SysFont( None, 64 )

### Main Loop
clock = pygame.time.Clock()
done = False
while not done:

    # Handle user-input
    for event in pygame.event.get():
        if ( event.type == pygame.QUIT ):
            done = True

    # Update the window, but not more than 60fps
    window.fill( DARK_BLUE )

    # Simply to layout nicely on the screen
    cursor_x = 50
    cursor_y = (WINDOW_HEIGHT // 2 ) - 100

    # Loop through the symbol list, rendering the symbol if it exists
    # or an "x" otherwise
    #
    # NOTE: This is inefficiant demo code! 
    #       It's bad to keep rendering fonts every frame
    for glyph in symbols:
        cursor_x += 50
        
        if ( glyphInFont( glyph, font1 ) ):
            window.blit( font1.render( glyph, True, YELLOW ), ( cursor_x, cursor_y ) )
        else:
            # does not exist
            window.blit( font1.render( 'x', True, YELLOW ), ( cursor_x, cursor_y ) )

        if ( glyphInFont( glyph, font2 ) ):
            window.blit( font2.render( glyph, True, YELLOW ), ( cursor_x, cursor_y+100 ) )
        else:
            # does not exist
            window.blit( font2.render( 'x', True, YELLOW ), ( cursor_x, cursor_y+100 ) )

    pygame.display.flip()

    # Clamp FPS
    clock.tick( 3 ) # slow update

pygame.quit()

Note that the "happy poo" emoji glyph [] doesn't seem to work in either font, and causes an exception in font.render(), which is caught and considered a Fail.

Related