206 lines
6.3 KiB
Python
206 lines
6.3 KiB
Python
# Generate embedded 1-bit bitmap fonts from a TrueType/OpenType font.
|
|
#
|
|
# FontForge's direct monochrome XBM export is too thin for this display, especially
|
|
# at footer sizes. This exports antialiased BMP glyphs and packs 4-bit alpha.
|
|
#
|
|
# Run with FontForge, not python3:
|
|
# fontforge -lang=py -script scripts/generate-fonts.py \
|
|
# '/Volumes/Drawer/Fonts/iAFonts/iA Writer Quattro/Static/iAWriterQuattroS-Regular.ttf' \
|
|
# src/ui/generated_fonts.rs
|
|
|
|
import os
|
|
import struct
|
|
import sys
|
|
import tempfile
|
|
|
|
import fontforge
|
|
|
|
SIZES = [
|
|
("QUATTRO_SMALL", 24),
|
|
("QUATTRO_BODY", 30),
|
|
("QUATTRO_READER", 34),
|
|
("QUATTRO_TITLE", 38),
|
|
]
|
|
|
|
CODEPOINTS = list(range(32, 127)) + [
|
|
0x00A0, # no-break space
|
|
0x2018, # left single quotation mark
|
|
0x2019, # right single quotation mark
|
|
0x201C, # left double quotation mark
|
|
0x201D, # right double quotation mark
|
|
0x2013, # en dash
|
|
0x2014, # em dash
|
|
0x2026, # ellipsis
|
|
]
|
|
|
|
|
|
|
|
def parse_bmp(path):
|
|
with open(path, "rb") as file:
|
|
data = file.read()
|
|
|
|
if data[0:2] != b"BM":
|
|
raise RuntimeError("not a BMP file: {}".format(path))
|
|
|
|
pixel_offset = struct.unpack_from("<I", data, 10)[0]
|
|
header_size = struct.unpack_from("<I", data, 14)[0]
|
|
width = struct.unpack_from("<i", data, 18)[0]
|
|
height = struct.unpack_from("<i", data, 22)[0]
|
|
planes = struct.unpack_from("<H", data, 26)[0]
|
|
bits_per_pixel = struct.unpack_from("<H", data, 28)[0]
|
|
compression = struct.unpack_from("<I", data, 30)[0]
|
|
|
|
if width <= 0 or height == 0 or planes != 1 or bits_per_pixel != 8 or compression != 0:
|
|
raise RuntimeError("unsupported BMP format: {}".format(path))
|
|
|
|
palette_start = 14 + header_size
|
|
palette = []
|
|
for index in range(256):
|
|
offset = palette_start + index * 4
|
|
blue, green, red, _alpha = data[offset : offset + 4]
|
|
palette.append((int(red) + int(green) + int(blue)) // 3)
|
|
|
|
top_down = height < 0
|
|
height = abs(height)
|
|
bmp_row_bytes = ((width * bits_per_pixel + 31) // 32) * 4
|
|
packed = [0] * ((width * height + 1) // 2)
|
|
|
|
for row in range(height):
|
|
source_row = row if top_down else height - 1 - row
|
|
source_offset = pixel_offset + source_row * bmp_row_bytes
|
|
for column in range(width):
|
|
palette_index = data[source_offset + column]
|
|
alpha = (255 - palette[palette_index] + 8) // 17
|
|
alpha = min(15, max(0, alpha))
|
|
pixel_index = row * width + column
|
|
byte_index = pixel_index // 2
|
|
if pixel_index % 2 == 0:
|
|
packed[byte_index] |= alpha << 4
|
|
else:
|
|
packed[byte_index] |= alpha
|
|
|
|
return width, height, packed
|
|
|
|
|
|
def has_codepoint(font, codepoint):
|
|
try:
|
|
glyph = font[codepoint]
|
|
except Exception:
|
|
return False
|
|
|
|
return glyph is not None and glyph.isWorthOutputting()
|
|
|
|
|
|
def rust_array(values, indent=" ", per_line=16):
|
|
lines = []
|
|
for index in range(0, len(values), per_line):
|
|
chunk = values[index : index + per_line]
|
|
lines.append(indent + ", ".join("0x{:02x}".format(value) for value in chunk) + ",")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def generate_font(font, temp_dir, name, pixelsize):
|
|
glyphs = []
|
|
bitmap = []
|
|
cell_height = 0
|
|
|
|
for codepoint in CODEPOINTS:
|
|
if not has_codepoint(font, codepoint):
|
|
continue
|
|
|
|
glyph = font[codepoint]
|
|
path = os.path.join(temp_dir, "glyph_{:04x}_{}.bmp".format(codepoint, pixelsize))
|
|
glyph.export(path, pixelsize=pixelsize)
|
|
width, height, data = parse_bmp(path)
|
|
|
|
cell_height = max(cell_height, height)
|
|
|
|
glyphs.append((codepoint, width, height, len(bitmap)))
|
|
bitmap.extend(data)
|
|
|
|
if cell_height == 0:
|
|
raise RuntimeError("no glyphs generated for {}".format(name))
|
|
|
|
return {
|
|
"name": name,
|
|
"pixelsize": pixelsize,
|
|
"cell_height": cell_height,
|
|
"glyphs": glyphs,
|
|
"bitmap": bitmap,
|
|
}
|
|
|
|
|
|
def emit_font(output, generated):
|
|
name = generated["name"]
|
|
glyphs = generated["glyphs"]
|
|
bitmap = generated["bitmap"]
|
|
|
|
output.write(
|
|
"pub(crate) static {name}: BitmapFont = BitmapFont {{\n"
|
|
" cell_height: {cell_height},\n"
|
|
" glyphs: &{name}_GLYPHS,\n"
|
|
" bitmap: &{name}_BITMAP,\n"
|
|
"}};\n\n".format(name=name, cell_height=generated["cell_height"])
|
|
)
|
|
|
|
output.write("#[rustfmt::skip]\n")
|
|
output.write(
|
|
"static {name}_GLYPHS: [Glyph; {count}] = [\n".format(
|
|
name=name, count=len(glyphs)
|
|
)
|
|
)
|
|
for codepoint, width, height, offset in glyphs:
|
|
output.write(
|
|
" Glyph {{ codepoint: 0x{codepoint:04x}, width: {width}, height: {height}, bitmap_offset: {offset} }},\n".format(
|
|
codepoint=codepoint, width=width, height=height, offset=offset
|
|
)
|
|
)
|
|
output.write("];\n\n")
|
|
|
|
output.write("#[rustfmt::skip]\n")
|
|
output.write(
|
|
"static {name}_BITMAP: [u8; {count}] = [\n".format(
|
|
name=name, count=len(bitmap)
|
|
)
|
|
)
|
|
if bitmap:
|
|
output.write(rust_array(bitmap))
|
|
output.write("\n")
|
|
output.write("];\n\n")
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) != 3:
|
|
print("usage: fontforge -lang=py -script scripts/generate-fonts.py FONT.ttf OUTPUT.rs", file=sys.stderr)
|
|
return 2
|
|
|
|
font_path = sys.argv[1]
|
|
output_path = sys.argv[2]
|
|
|
|
font = fontforge.open(font_path)
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
generated_fonts = [
|
|
generate_font(font, temp_dir, name, pixelsize) for name, pixelsize in SIZES
|
|
]
|
|
|
|
with open(output_path, "w", encoding="utf-8") as output:
|
|
output.write("// @generated by scripts/generate-fonts.py. Do not edit by hand.\n")
|
|
output.write("// Source font: {}\n\n".format(font_path))
|
|
output.write("use crate::ui::typography::{BitmapFont, Glyph};\n\n")
|
|
for generated in generated_fonts:
|
|
output.write(
|
|
"// {} px; cell height {} px; {} glyphs; {} alpha bytes.\n".format(
|
|
generated["pixelsize"],
|
|
generated["cell_height"],
|
|
len(generated["glyphs"]),
|
|
len(generated["bitmap"]),
|
|
)
|
|
)
|
|
emit_font(output, generated)
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|