238 lines
7.6 KiB
Python
238 lines
7.6 KiB
Python
|
|
#!python3
|
|||
|
|
import freetype
|
|||
|
|
import zlib
|
|||
|
|
import sys
|
|||
|
|
import re
|
|||
|
|
import math
|
|||
|
|
import argparse
|
|||
|
|
from collections import namedtuple
|
|||
|
|
|
|||
|
|
# From: https://github.com/vroland/epdiy
|
|||
|
|
|
|||
|
|
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("--compress", dest="compress", action="store_true", help="compress glyph bitmaps.")
|
|||
|
|
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()
|
|||
|
|
|
|||
|
|
GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "compressed_size", "data_offset", "code_point"])
|
|||
|
|
|
|||
|
|
font_stack = [freetype.Face(f) for f in args.fontstack]
|
|||
|
|
compress = args.compress
|
|||
|
|
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:
|
|||
|
|
# shift by 6 bytes, because sizes are given as 6-bit fractions
|
|||
|
|
# the display has about 150 dpi.
|
|||
|
|
face.set_char_size(size << 6, size << 6, 150, 150)
|
|||
|
|
|
|||
|
|
total_size = 0
|
|||
|
|
total_packed = 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
|
|||
|
|
pixels = []
|
|||
|
|
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)
|
|||
|
|
pixels.append(px);
|
|||
|
|
px = 0
|
|||
|
|
# eol
|
|||
|
|
if x == bitmap.width - 1 and bitmap.width % 2 > 0:
|
|||
|
|
pixels.append(px)
|
|||
|
|
px = 0
|
|||
|
|
|
|||
|
|
packed = bytes(pixels);
|
|||
|
|
total_packed += len(packed)
|
|||
|
|
compressed = packed
|
|||
|
|
if compress:
|
|||
|
|
compressed = zlib.compress(packed)
|
|||
|
|
|
|||
|
|
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,
|
|||
|
|
compressed_size = len(compressed),
|
|||
|
|
data_offset = total_size,
|
|||
|
|
code_point = code_point,
|
|||
|
|
)
|
|||
|
|
total_size += len(compressed)
|
|||
|
|
all_glyphs.append((glyph, compressed))
|
|||
|
|
|
|||
|
|
# 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):
|
|||
|
|
props, compressed = glyph
|
|||
|
|
glyph_data.extend([b for b in compressed])
|
|||
|
|
glyph_props.append(props)
|
|||
|
|
|
|||
|
|
print(f"/**\n * generated by fontconvert.py\n * name: {font_name}\n * size: {size}\n * compressed: {compress}\n */")
|
|||
|
|
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" {1 if compress else 0},")
|
|||
|
|
print(f" {norm_ceil(face.size.height)},")
|
|||
|
|
print(f" {norm_ceil(face.size.ascender)},")
|
|||
|
|
print(f" {norm_floor(face.size.descender)},")
|
|||
|
|
print("};")
|