#!/usr/bin/env python3 # # Copyright (c) 2018 Henrik Brix Andersen # # 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 #include 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()