blob: 788208cbd3b753c70285202fb407fb79108d64e1 [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright (c) 2018 Henrik Brix Andersen <henrik@brixandersen.dk>
#
# SPDX-License-Identifier: Apache-2.0
import argparse
import sys
from PIL import Image, ImageDraw, ImageFont
PRINTABLE_MIN = 32
PRINTABLE_MAX = 126
def generate_element(image, charcode):
"""Generate CFB font element for a given character code from an image"""
blackwhite = image.convert("1", dither=Image.NONE)
pixels = blackwhite.load()
width, height = image.size
if args.dump:
blackwhite.save(f"{args.name}_{charcode}.png")
if PRINTABLE_MIN <= charcode <= PRINTABLE_MAX:
char = f" ({charcode:c})"
else:
char = ""
args.output.write(f"""\t/* {charcode:d}{char} */\n\t{{\n""")
glyph = []
if args.hpack:
for row in range(0, height):
packed = []
for octet in range(0, int(width / 8)):
value = ""
for bit in range(0, 8):
col = octet * 8 + bit
if pixels[col, row]:
value = value + "0"
else:
value = value + "1"
packed.append(value)
glyph.append(packed)
else:
for col in range(0, width):
packed = []
for octet in range(0, int(height / 8)):
value = ""
for bit in range(0, 8):
row = octet * 8 + bit
if pixels[col, row]:
value = value + "0"
else:
value = value + "1"
packed.append(value)
glyph.append(packed)
for packed in glyph:
args.output.write("\t\t")
bits = []
for value in packed:
bits.append(value)
if not args.msb_first:
value = value[::-1]
args.output.write(f"0x{int(value, 2):02x},")
args.output.write(" /* {} */\n".format(''.join(bits).replace('0', ' ').replace('1', '#')))
args.output.write("\t},\n")
def extract_font_glyphs():
"""Extract font glyphs from a TrueType/OpenType font file"""
font = ImageFont.truetype(args.input, args.size)
# Figure out the bounding box for the desired glyphs
fw_max = 0
fh_max = 0
for i in range(args.first, args.last + 1):
# returns (left, top, right, bottom) bounding box
size = font.getbbox(chr(i))
# calculate width + height
fw = size[2] - size[0] # right - left
fh = size[3] - size[1] # bottom - top
if fw > fw_max:
fw_max = fw
if fh > fh_max:
fh_max = fh
# Round the packed length up to pack into bytes.
if args.hpack:
width = 8 * int((fw_max + 7) / 8)
height = fh_max + args.y_offset
else:
width = fw_max
height = 8 * int((fh_max + args.y_offset + 7) / 8)
# Diagnose inconsistencies with arguments
if width != args.width:
raise Exception(f'text width {width} mismatch with -x {args.width}')
if height != args.height:
raise Exception(f'text height {height} mismatch with -y {args.height}')
for i in range(args.first, args.last + 1):
image = Image.new('1', (width, height), 'white')
draw = ImageDraw.Draw(image)
# returns (left, top, right, bottom) bounding box
size = draw.textbbox((0, 0), chr(i), font=font)
# calculate width + height
fw = size[2] - size[0] # right - left
fh = size[3] - size[1] # bottom - top
xpos = 0
if args.center_x:
xpos = (width - fw) / 2 + 1
ypos = args.y_offset
draw.text((xpos, ypos), chr(i), font=font)
generate_element(image, i)
def extract_image_glyphs():
"""Extract font glyphs from an image file"""
image = Image.open(args.input)
x_offset = 0
for i in range(args.first, args.last + 1):
glyph = image.crop((x_offset, 0, x_offset + args.width, args.height))
generate_element(glyph, i)
x_offset += args.width
def generate_header():
"""Generate CFB font header file"""
caps = []
if args.hpack:
caps.append('MONO_HPACKED')
else:
caps.append('MONO_VPACKED')
if args.msb_first:
caps.append('MSB_FIRST')
caps = ' | '.join(['CFB_FONT_' + f for f in caps])
clean_cmd = []
for arg in sys.argv:
if arg.startswith("--bindir"):
# Drop. Assumes --bindir= was passed with '=' sign.
continue
if args.bindir and arg.startswith(args.bindir):
# +1 to also strip '/' or '\' separator
striplen = min(len(args.bindir) + 1, len(arg))
clean_cmd.append(arg[striplen:])
continue
if args.zephyr_base is not None:
clean_cmd.append(arg.replace(args.zephyr_base, '"${ZEPHYR_BASE}"'))
else:
clean_cmd.append(arg)
args.output.write(
"""/*
* This file was automatically generated using the following command:
* {cmd}
*
*/
#include <zephyr/kernel.h>
#include <zephyr/display/cfb.h>
static const uint8_t cfb_font_{name:s}_{width:d}{height:d}[{elem:d}][{b:.0f}] = {{\n""".format(
cmd=" ".join(clean_cmd),
name=args.name,
width=args.width,
height=args.height,
elem=args.last - args.first + 1,
b=args.width / 8 * args.height,
)
)
if args.type == "font":
extract_font_glyphs()
elif args.type == "image":
extract_image_glyphs()
elif args.input.name.lower().endswith((".otf", ".otc", ".ttf", ".ttc")):
extract_font_glyphs()
else:
extract_image_glyphs()
args.output.write(
f"""
}};
FONT_ENTRY_DEFINE({args.name}_{args.width}{args.height},
{args.width},
{args.height},
{caps},
cfb_font_{args.name}_{args.width}{args.height},
{args.first},
{args.last}
);
""" # noqa: E101
)
def parse_args():
"""Parse arguments"""
global args
parser = argparse.ArgumentParser(
description="Character Frame Buffer (CFB) font header file generator",
formatter_class=argparse.RawDescriptionHelpFormatter,
allow_abbrev=False,
)
parser.add_argument("-z", "--zephyr-base", help="Zephyr base directory")
parser.add_argument(
"-d",
"--dump",
action="store_true",
help="dump generated CFB font elements as images for preview",
)
group = parser.add_argument_group("input arguments")
group.add_argument(
"-i",
"--input",
required=True,
type=argparse.FileType('rb'),
metavar="FILE",
help="TrueType/OpenType file or image input file",
)
group.add_argument(
"-t",
"--type",
default="auto",
choices=["auto", "font", "image"],
help="Input file type (default: %(default)s)",
)
group = parser.add_argument_group("font arguments")
group.add_argument(
"-s",
"--size",
type=int,
default=10,
metavar="POINTS",
help="TrueType/OpenType font size in points (default: %(default)s)",
)
group = parser.add_argument_group("output arguments")
group.add_argument(
"-o",
"--output",
type=argparse.FileType('w'),
default="-",
metavar="FILE",
help="CFB font header file (default: stdout)",
)
group.add_argument(
"--bindir", type=str, help="CMAKE_BINARY_DIR for pure logging purposes. No trailing slash."
)
group.add_argument(
"-x", "--width", required=True, type=int, help="width of the CFB font elements in pixels"
)
group.add_argument(
"-y", "--height", required=True, type=int, help="height of the CFB font elements in pixels"
)
group.add_argument(
"-n", "--name", default="custom", help="name of the CFB font entry (default: %(default)s)"
)
group.add_argument(
"--first",
type=int,
default=PRINTABLE_MIN,
metavar="CHARCODE",
help="character code mapped to the first CFB font element (default: %(default)s)",
)
group.add_argument(
"--last",
type=int,
default=PRINTABLE_MAX,
metavar="CHARCODE",
help="character code mapped to the last CFB font element (default: %(default)s)",
)
group.add_argument(
"--center-x", action='store_true', help="center character glyphs horizontally"
)
group.add_argument(
"--y-offset",
type=int,
default=0,
help="vertical offset for character glyphs (default: %(default)s)",
)
group.add_argument(
"--hpack",
dest='hpack',
default=False,
action='store_true',
help="generate bytes encoding row data rather than column data (default: %(default)s)",
)
group.add_argument(
"--msb-first",
action='store_true',
help="packed content starts at high bit of each byte (default: lsb-first)",
)
args = parser.parse_args()
def main():
"""Parse arguments and generate CFB font header file"""
parse_args()
generate_header()
if __name__ == "__main__":
main()