You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
274 lines
9.0 KiB
274 lines
9.0 KiB
#!/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()
|
|
|