Skip to content

Instantly share code, notes, and snippets.

@andy0130tw
Created June 10, 2026 15:17
Show Gist options
  • Select an option

  • Save andy0130tw/163abd6296a5f0bfd4cb444c13244d13 to your computer and use it in GitHub Desktop.

Select an option

Save andy0130tw/163abd6296a5f0bfd4cb444c13244d13 to your computer and use it in GitHub Desktop.
FreeType rendering some chars
import freetype
from PIL import Image
import math
def byte2bin(n):
assert 0 <= n < 256
return bin(n)[2:].rjust(8, '0')
ttf_path = 'Cubic_11.ttf'
pixel_size = 12
is_mono = True
char_to_render = '\u1E14'
should_fill_dark = False
face = freetype.Face(ttf_path)
face.set_pixel_sizes(0, pixel_size)
print(f'postscript_name: {face.postscript_name.decode()}', )
print(f'{{family,style}}_name: {face.family_name.decode()} {face.style_name.decode()}')
print(f'fixed sizes available = {face.available_sizes}')
# print(f'face flags = {face.face_flags}')
print()
# see: https://stackoverflow.com/a/70265636
max_ascent = face.ascender * face.size.y_scale / 65536 / 64
max_descent = -(face.descender * face.size.y_scale / 65536 / 64)
print('max_ascent', max_ascent, 'max_descent', max_descent)
# get the baseline offset from the top ascent, for normal fonts (A + D) should equal to pixel size
top_offset = math.ceil(max_ascent * pixel_size / (max_ascent + max_descent))
chars = '我能吞下玻璃而不傷身體'
canvas_dims = (600, pixel_size)
img = Image.new('L', canvas_dims, 96)
offs = 0
for c in chars:
print(f'loading char [{c}]')
codepoint = ord(c)
gindex = face.get_char_index(codepoint)
# important! FT_LOAD_TARGET_MONO specifies the hinter
face.load_glyph(gindex, freetype.FT_LOAD_TARGETS['FT_LOAD_TARGET_MONO'])
slot = face.glyph
print('slot w/h', slot.metrics.width / 64, slot.metrics.height / 64)
face.glyph.render(freetype.FT_RENDER_MODES['FT_RENDER_MODE_MONO'] if is_mono else 0)
assert slot.format == freetype.FT_GLYPH_FORMATS['FT_GLYPH_FORMAT_BITMAP']
bitmap = slot.bitmap
print('bitmap w/h', bitmap.width, bitmap.rows)
print('bitmap l/t', slot.bitmap_left, slot.bitmap_top)
print(f'buffer len {len(bitmap.buffer)} pitch {bitmap.pitch}')
if bitmap.pixel_mode == freetype.ft_pixel_mode_mono:
print('MONOCHROME FONT')
assert is_mono
elif bitmap.pixel_mode == freetype.ft_pixel_mode_grays:
print('GRAYSCALE FONT')
assert not is_mono
assert bitmap.num_grays == 256
else:
raise Exception('SHOULD NOT HAPPEN')
sx = offs + slot.bitmap_left
sy = top_offset - slot.bitmap_top
print('start x,y', sx, sy)
xx_overflowed = 0
yy_overflowed = 0
for i in range(bitmap.rows):
for j in range(bitmap.pitch):
yy = sy + i
if yy < 0:
yy_overflowed = -1
continue
elif yy >= canvas_dims[1]:
yy_overflowed = 1
continue
byte = bitmap.buffer[i * bitmap.pitch + j]
if is_mono:
bits = byte2bin(byte)
row = [*map(lambda x: x != '0', bits)]
takes = min(8, bitmap.width - j * 8)
for k in range(takes):
xx = sx + j * 8 + k
if xx < 0:
xx_overflowed = -1
elif xx >= canvas_dims[0]:
xx_overflowed = 1
break
if row[k]:
img.putpixel((xx, yy), 255)
elif should_fill_dark:
img.putpixel((xx, yy), 0)
else:
xx = sx + j
if xx < 0:
xx_overflowed = -1
if xx >= canvas_dims[0]:
xx_overflowed = 1
continue
if byte != 0 or should_fill_dark:
img.putpixel((xx, yy), byte)
if xx_overflowed < 0:
print('X overflowed to the left!')
if xx_overflowed > 0:
print('X overflowed to the right!')
if yy_overflowed < 0:
print('Y overflowed to the top!')
if yy_overflowed > 0:
print('Y overflowed to the bottom!')
offs += slot.advance.x // 64
print()
img = img.resize((canvas_dims[0] * 8, canvas_dims[1] * 8), Image.Resampling.NEAREST)
img.show()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment