2025-12-03 22:00:29 +11:00
#!python3
import freetype
import zlib
import sys
import re
import math
import argparse
from collections import namedtuple
2025-12-07 01:26:49 +11:00
# Originally from https://github.com/vroland/epdiy
2025-12-03 22:00:29 +11:00
parser = argparse . ArgumentParser ( description = "Generate a header file from a font to be used with epdiy." )
parser . add_argument ( "name" , action = "store" , help = "name of the font." )
parser . add_argument ( "size" , type = int , help = "font size to use." )
parser . add_argument ( "fontstack" , action = "store" , nargs = '+' , help = "list of font files, ordered by descending priority." )
2025-12-08 19:48:49 +11:00
parser . add_argument ( "--2bit" , dest = "is2Bit" , action = "store_true" , help = "generate 2-bit greyscale bitmap instead of 1-bit black and white." )
2025-12-03 22:00:29 +11:00
parser . add_argument ( "--additional-intervals" , dest = "additional_intervals" , action = "append" , help = "Additional code point intervals to export as min,max. This argument can be repeated." )
args = parser . parse_args ()
2025-12-07 01:26:49 +11:00
GlyphProps = namedtuple ( "GlyphProps" , [ "width" , "height" , "advance_x" , "left" , "top" , "data_length" , "data_offset" , "code_point" ])
2025-12-03 22:00:29 +11:00
font_stack = [ freetype . Face ( f ) for f in args . fontstack ]
2025-12-08 19:48:49 +11:00
is2Bit = args . is2Bit
2025-12-03 22:00:29 +11:00
size = args . size
font_name = args . name
# inclusive unicode code point intervals
# must not overlap and be in ascending order
intervals = [
### Basic Latin ###
# ASCII letters, digits, punctuation, control characters
( 0x0000 , 0x007F ),
### Latin-1 Supplement ###
# Accented characters for Western European languages
( 0x0080 , 0x00FF ),
### Latin Extended-A ###
# Eastern European and Baltic languages
( 0x0100 , 0x017F ),
### General Punctuation (core subset) ###
# Smart quotes, en dash, em dash, ellipsis, NO-BREAK SPACE
( 0x2000 , 0x206F ),
### Basic Symbols From "Latin-1 + Misc" ###
# dashes, quotes, prime marks
( 0x2010 , 0x203A ),
# misc punctuation
( 0x2040 , 0x205F ),
# common currency symbols
( 0x20A0 , 0x20CF ),
### Combining Diacritical Marks (minimal subset) ###
# Needed for proper rendering of many extended Latin languages
( 0x0300 , 0x036F ),
### Greek & Coptic ###
# Used in science, maths, philosophy, some academic texts
# (0x0370, 0x03FF),
### Cyrillic ###
# Russian, Ukrainian, Bulgarian, etc.
2025-12-16 14:52:49 +03:00
( 0x0400 , 0x04FF ),
2025-12-03 22:00:29 +11:00
### Math Symbols (common subset) ###
2026-01-21 22:42:41 +11:00
# Superscripts and Subscripts
( 0x2070 , 0x209F ),
2025-12-03 22:00:29 +11:00
# General math operators
( 0x2200 , 0x22FF ),
# Arrows
( 0x2190 , 0x21FF ),
### CJK ###
# Core Unified Ideographs
# (0x4E00, 0x9FFF),
# # Extension A
# (0x3400, 0x4DBF),
# # Extension B
# (0x20000, 0x2A6DF),
# # Extension C– F
# (0x2A700, 0x2EBEF),
# # Extension G
# (0x30000, 0x3134F),
# # Hiragana
# (0x3040, 0x309F),
# # Katakana
# (0x30A0, 0x30FF),
# # Katakana Phonetic Extensions
# (0x31F0, 0x31FF),
# # Halfwidth Katakana
# (0xFF60, 0xFF9F),
# # Hangul Syllables
# (0xAC00, 0xD7AF),
# # Hangul Jamo
# (0x1100, 0x11FF),
# # Hangul Compatibility Jamo
# (0x3130, 0x318F),
# # Hangul Jamo Extended-A
# (0xA960, 0xA97F),
# # Hangul Jamo Extended-B
# (0xD7B0, 0xD7FF),
# # CJK Radicals Supplement
# (0x2E80, 0x2EFF),
# # Kangxi Radicals
# (0x2F00, 0x2FDF),
# # CJK Symbols and Punctuation
# (0x3000, 0x303F),
# # CJK Compatibility Forms
# (0xFE30, 0xFE4F),
# # CJK Compatibility Ideographs
# (0xF900, 0xFAFF),
2026-01-19 05:58:43 -06:00
### Specials
# Replacement Character
( 0xFFFD , 0xFFFD ),
2025-12-03 22:00:29 +11:00
]
add_ints = []
if args . additional_intervals :
add_ints = [ tuple ([ int ( n , base = 0 ) for n in i . split ( "," )]) for i in args . additional_intervals ]
def norm_floor ( val ):
return int ( math . floor ( val / ( 1 << 6 )))
def norm_ceil ( val ):
return int ( math . ceil ( val / ( 1 << 6 )))
def chunks ( l , n ):
for i in range ( 0 , len ( l ), n ):
yield l [ i : i + n ]
def load_glyph ( code_point ):
face_index = 0
while face_index < len ( font_stack ):
face = font_stack [ face_index ]
glyph_index = face . get_char_index ( code_point )
if glyph_index > 0 :
face . load_glyph ( glyph_index , freetype . FT_LOAD_RENDER )
return face
face_index += 1
print ( f "code point { code_point } ( { hex ( code_point ) } ) not found in font stack!" , file = sys . stderr )
return None
unmerged_intervals = sorted ( intervals + add_ints )
intervals = []
unvalidated_intervals = []
for i_start , i_end in unmerged_intervals :
if len ( unvalidated_intervals ) > 0 and i_start + 1 <= unvalidated_intervals [ - 1 ][ 1 ]:
unvalidated_intervals [ - 1 ] = ( unvalidated_intervals [ - 1 ][ 0 ], max ( unvalidated_intervals [ - 1 ][ 1 ], i_end ))
continue
unvalidated_intervals . append (( i_start , i_end ))
for i_start , i_end in unvalidated_intervals :
start = i_start
for code_point in range ( i_start , i_end + 1 ):
face = load_glyph ( code_point )
if face is None :
if start < code_point :
intervals . append (( start , code_point - 1 ))
start = code_point + 1
if start != i_end + 1 :
intervals . append (( start , i_end ))
for face in font_stack :
face . set_char_size ( size << 6 , size << 6 , 150 , 150 )
total_size = 0
all_glyphs = []
for i_start , i_end in intervals :
for code_point in range ( i_start , i_end + 1 ):
face = load_glyph ( code_point )
bitmap = face . glyph . bitmap
2025-12-07 01:26:49 +11:00
# Build out 4-bit greyscale bitmap
pixels4g = []
2025-12-03 22:00:29 +11:00
px = 0
for i , v in enumerate ( bitmap . buffer ):
y = i / bitmap . width
x = i % bitmap . width
if x % 2 == 0 :
px = ( v >> 4 )
else :
px = px | ( v & 0xF0 )
2025-12-07 01:26:49 +11:00
pixels4g . append ( px );
2025-12-03 22:00:29 +11:00
px = 0
# eol
if x == bitmap . width - 1 and bitmap . width % 2 > 0 :
2025-12-07 01:26:49 +11:00
pixels4g . append ( px )
2025-12-03 22:00:29 +11:00
px = 0
2025-12-08 19:48:49 +11:00
if is2Bit :
2025-12-18 21:39:13 +11:00
# 0-3 white, 4-7 light grey, 8-11 dark grey, 12-15 black
2025-12-08 19:48:49 +11:00
# Downsample to 2-bit bitmap
pixels2b = []
px = 0
pitch = ( bitmap . width // 2 ) + ( bitmap . width % 2 )
for y in range ( bitmap . rows ):
for x in range ( bitmap . width ):
px = px << 2
bm = pixels4g [ y * pitch + ( x // 2 )]
bm = ( bm >> (( x % 2 ) * 4 )) & 0xF
2025-12-03 22:00:29 +11:00
2025-12-18 21:39:13 +11:00
if bm >= 12 :
2025-12-08 19:48:49 +11:00
px += 3
elif bm >= 8 :
px += 2
2025-12-18 21:39:13 +11:00
elif bm >= 4 :
2025-12-08 19:48:49 +11:00
px += 1
2025-12-07 01:26:49 +11:00
2025-12-08 19:48:49 +11:00
if ( y * bitmap . width + x ) % 4 == 3 :
pixels2b . append ( px )
px = 0
if ( bitmap . width * bitmap . rows ) % 4 != 0 :
px = px << ( 4 - ( bitmap . width * bitmap . rows ) % 4 ) * 2
pixels2b . append ( px )
# for y in range(bitmap.rows):
# line = ''
# for x in range(bitmap.width):
# pixelPosition = y * bitmap.width + x
# byte = pixels2b[pixelPosition // 4]
# bit_index = (3 - (pixelPosition % 4)) * 2
# line += '#' if ((byte >> bit_index) & 3) > 0 else '.'
# print(line)
# print('')
else :
2025-12-18 21:39:13 +11:00
# Downsample to 1-bit bitmap - treat any 2+ as black
2025-12-08 19:48:49 +11:00
pixelsbw = []
px = 0
pitch = ( bitmap . width // 2 ) + ( bitmap . width % 2 )
for y in range ( bitmap . rows ):
for x in range ( bitmap . width ):
px = px << 1
bm = pixels4g [ y * pitch + ( x // 2 )]
2025-12-18 21:39:13 +11:00
px += 1 if (( x & 1 ) == 0 and bm & 0xE > 0 ) or (( x & 1 ) == 1 and bm & 0xE0 > 0 ) else 0
2025-12-08 19:48:49 +11:00
if ( y * bitmap . width + x ) % 8 == 7 :
pixelsbw . append ( px )
px = 0
if ( bitmap . width * bitmap . rows ) % 8 != 0 :
px = px << ( 8 - ( bitmap . width * bitmap . rows ) % 8 )
pixelsbw . append ( px )
# for y in range(bitmap.rows):
# line = ''
# for x in range(bitmap.width):
# pixelPosition = y * bitmap.width + x
# byte = pixelsbw[pixelPosition // 8]
# bit_index = 7 - (pixelPosition % 8)
# line += '#' if (byte >> bit_index) & 1 else '.'
# print(line)
# print('')
pixels = pixels2b if is2Bit else pixelsbw
2025-12-07 01:26:49 +11:00
# Build output data
2025-12-08 19:48:49 +11:00
packed = bytes ( pixels )
2025-12-03 22:00:29 +11:00
glyph = GlyphProps (
width = bitmap . width ,
height = bitmap . rows ,
advance_x = norm_floor ( face . glyph . advance . x ),
left = face . glyph . bitmap_left ,
top = face . glyph . bitmap_top ,
2025-12-07 01:26:49 +11:00
data_length = len ( packed ),
2025-12-03 22:00:29 +11:00
data_offset = total_size ,
code_point = code_point ,
)
2025-12-07 01:26:49 +11:00
total_size += len ( packed )
all_glyphs . append (( glyph , packed ))
2025-12-03 22:00:29 +11:00
# pipe seems to be a good heuristic for the "real" descender
face = load_glyph ( ord ( '|' ))
glyph_data = []
glyph_props = []
for index , glyph in enumerate ( all_glyphs ):
2025-12-07 01:26:49 +11:00
props , packed = glyph
glyph_data . extend ([ b for b in packed ])
2025-12-03 22:00:29 +11:00
glyph_props . append ( props )
2026-01-27 12:19:19 +01:00
print ( f """/**
* generated by fontconvert.py
* name: { font_name }
* size: { size }
* mode: { '2-bit' if is2Bit else '1-bit' }
* Command used: { ' ' . join ( sys . argv ) }
*/
#pragma once
#include "EpdFontData.h"
""" )
2025-12-03 22:00:29 +11:00
print ( f "static const uint8_t { font_name } Bitmaps[ { len ( glyph_data ) } ] = {{ " )
for c in chunks ( glyph_data , 16 ):
print ( " " + " " . join ( f "0x { b : 02X } ," for b in c ))
print ( "}; \n " );
print ( f "static const EpdGlyph { font_name } Glyphs[] = {{ " )
for i , g in enumerate ( glyph_props ):
print ( " { " + ", " . join ([ f " { a } " for a in list ( g [: - 1 ])]), "}," , f "// { chr ( g . code_point ) if g . code_point != 92 else '<backslash>' } " )
print ( "}; \n " );
print ( f "static const EpdUnicodeInterval { font_name } Intervals[] = {{ " )
offset = 0
for i_start , i_end in intervals :
print ( f " {{ 0x { i_start : X } , 0x { i_end : X } , 0x { offset : X } }} ," )
offset += i_end - i_start + 1
print ( "}; \n " );
print ( f "static const EpdFontData { font_name } = {{ " )
print ( f " { font_name } Bitmaps," )
print ( f " { font_name } Glyphs," )
print ( f " { font_name } Intervals," )
print ( f " { len ( intervals ) } ," )
print ( f " { norm_ceil ( face . size . height ) } ," )
print ( f " { norm_ceil ( face . size . ascender ) } ," )
print ( f " { norm_floor ( face . size . descender ) } ," )
2025-12-08 19:48:49 +11:00
print ( f " { 'true' if is2Bit else 'false' } ," )
2025-12-03 22:00:29 +11:00
print ( "};" )