275 lines
9.0 KiB
Python
Executable File
275 lines
9.0 KiB
Python
Executable File
#!/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 ImageFont
|
|
from PIL import Image
|
|
from PIL import ImageDraw
|
|
|
|
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("{}_{}.png".format(args.name, charcode))
|
|
|
|
if PRINTABLE_MIN <= charcode <= PRINTABLE_MAX:
|
|
char = " ({:c})".format(charcode)
|
|
else:
|
|
char = ""
|
|
|
|
args.output.write("""\t/* {:d}{} */\n\t{{\n""".format(charcode, char))
|
|
|
|
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("0x{:02x},".format(int(value, 2)))
|
|
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('text width {} mismatch with -x {}'.format(width, args.width))
|
|
if height != args.height:
|
|
raise Exception('text height {} mismatch with -y {}'.format(height, 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("""
|
|
}};
|
|
|
|
FONT_ENTRY_DEFINE({name}_{width}{height},
|
|
{width},
|
|
{height},
|
|
{caps},
|
|
cfb_font_{name}_{width}{height},
|
|
{first},
|
|
{last}
|
|
);
|
|
""" .format(name=args.name, width=args.width, height=args.height,
|
|
caps=caps, first=args.first, last=args.last))
|
|
|
|
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()
|