Created
June 10, 2026 15:17
-
-
Save andy0130tw/163abd6296a5f0bfd4cb444c13244d13 to your computer and use it in GitHub Desktop.
FreeType rendering some chars
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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