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. " )
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 ]
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.
# (0x0400, 0x04FF),
### Math Symbols (common subset) ###
# 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),
]
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-07 01:26:49 +11:00
# Downsample to 1-bit bitmap - treat any non-zero as black
pixelsbw = [ ]
px = 0
pitch = ( bitmap . width / / 2 ) + ( bitmap . width % 2 )
2025-12-07 12:25:10 +11:00
for y in range ( bitmap . rows ) :
for x in range ( bitmap . width ) :
2025-12-07 01:26:49 +11:00
px = px << 1
2025-12-07 12:25:10 +11:00
bm = pixels4g [ y * pitch + ( x / / 2 ) ]
px + = 1 if ( ( x & 1 ) == 0 and bm & 0xF > 0 ) or ( ( x & 1 ) == 1 and bm & 0xF0 > 0 ) else 0
if ( y * bitmap . width + x ) % 8 == 7 :
2025-12-07 01:26:49 +11:00
pixelsbw . append ( px )
px = 0
if ( bitmap . width * bitmap . rows ) % 8 != 0 :
2025-12-07 12:25:10 +11:00
px = px << ( 8 - ( bitmap . width * bitmap . rows ) % 8 )
2025-12-07 01:26:49 +11:00
pixelsbw . append ( px )
# Build output data
packed = bytes ( pixelsbw )
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 )
2025-12-07 01:26:49 +11:00
print ( f " /** \n * generated by fontconvert.py \n * name: { font_name } \n * size: { size } \n */ " )
2025-12-03 22:00:29 +11:00
print ( " #pragma once " )
print ( " #include \" EpdFontData.h \" \n " )
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 ) } , " )
print ( " }; " )