1#!/usr/bin/env python3 2# 3# Copyright (c) 2018 Henrik Brix Andersen <henrik@brixandersen.dk> 4# 5# SPDX-License-Identifier: Apache-2.0 6 7import argparse 8import sys 9 10from PIL import ImageFont 11from PIL import Image 12from PIL import ImageDraw 13 14PRINTABLE_MIN = 32 15PRINTABLE_MAX = 126 16 17def generate_element(image, charcode): 18 """Generate CFB font element for a given character code from an image""" 19 blackwhite = image.convert("1", dither=Image.NONE) 20 pixels = blackwhite.load() 21 22 width, height = image.size 23 if args.dump: 24 blackwhite.save("{}_{}.png".format(args.name, charcode)) 25 26 if PRINTABLE_MIN <= charcode <= PRINTABLE_MAX: 27 char = " ({:c})".format(charcode) 28 else: 29 char = "" 30 31 args.output.write("""\t/* {:d}{} */\n\t{{\n""".format(charcode, char)) 32 33 glyph = [] 34 if args.hpack: 35 for row in range(0, height): 36 packed = [] 37 for octet in range(0, int(width / 8)): 38 value = "" 39 for bit in range(0, 8): 40 col = octet * 8 + bit 41 if pixels[col, row]: 42 value = value + "0" 43 else: 44 value = value + "1" 45 packed.append(value) 46 glyph.append(packed) 47 else: 48 for col in range(0, width): 49 packed = [] 50 for octet in range(0, int(height / 8)): 51 value = "" 52 for bit in range(0, 8): 53 row = octet * 8 + bit 54 if pixels[col, row]: 55 value = value + "0" 56 else: 57 value = value + "1" 58 packed.append(value) 59 glyph.append(packed) 60 for packed in glyph: 61 args.output.write("\t\t") 62 bits = [] 63 for value in packed: 64 bits.append(value) 65 if not args.msb_first: 66 value = value[::-1] 67 args.output.write("0x{:02x},".format(int(value, 2))) 68 args.output.write(" /* {} */\n".format(''.join(bits).replace('0', ' ').replace('1', '#'))) 69 args.output.write("\t},\n") 70 71def extract_font_glyphs(): 72 """Extract font glyphs from a TrueType/OpenType font file""" 73 font = ImageFont.truetype(args.input, args.size) 74 75 # Figure out the bounding box for the desired glyphs 76 fw_max = 0 77 fh_max = 0 78 for i in range(args.first, args.last + 1): 79 fw, fh = font.getsize(chr(i)) 80 if fw > fw_max: 81 fw_max = fw 82 if fh > fh_max: 83 fh_max = fh 84 85 # Round the packed length up to pack into bytes. 86 if args.hpack: 87 width = 8 * int((fw_max + 7) / 8) 88 height = fh_max + args.y_offset 89 else: 90 width = fw_max 91 height = 8 * int((fh_max + args.y_offset + 7) / 8) 92 93 # Diagnose inconsistencies with arguments 94 if width != args.width: 95 raise Exception('text width {} mismatch with -x {}'.format(width, args.width)) 96 if height != args.height: 97 raise Exception('text height {} mismatch with -y {}'.format(height, args.height)) 98 99 for i in range(args.first, args.last + 1): 100 image = Image.new('1', (width, height), 'white') 101 draw = ImageDraw.Draw(image) 102 103 fw, fh = draw.textsize(chr(i), font=font) 104 105 xpos = 0 106 if args.center_x: 107 xpos = (width - fw) / 2 + 1 108 ypos = args.y_offset 109 110 draw.text((xpos, ypos), chr(i), font=font) 111 generate_element(image, i) 112 113def extract_image_glyphs(): 114 """Extract font glyphs from an image file""" 115 image = Image.open(args.input) 116 117 x_offset = 0 118 for i in range(args.first, args.last + 1): 119 glyph = image.crop((x_offset, 0, x_offset + args.width, args.height)) 120 generate_element(glyph, i) 121 x_offset += args.width 122 123def generate_header(): 124 """Generate CFB font header file""" 125 126 caps = [] 127 if args.hpack: 128 caps.append('MONO_HPACKED') 129 else: 130 caps.append('MONO_VPACKED') 131 if args.msb_first: 132 caps.append('MSB_FIRST') 133 caps = ' | '.join(['CFB_FONT_' + f for f in caps]) 134 135 clean_cmd = [] 136 for arg in sys.argv: 137 if arg.startswith("--bindir"): 138 # Drop. Assumes --bindir= was passed with '=' sign. 139 continue 140 if args.bindir and arg.startswith(args.bindir): 141 # +1 to also strip '/' or '\' separator 142 striplen = min(len(args.bindir)+1, len(arg)) 143 clean_cmd.append(arg[striplen:]) 144 continue 145 146 if args.zephyr_base is not None: 147 clean_cmd.append(arg.replace(args.zephyr_base, '"${ZEPHYR_BASE}"')) 148 else: 149 clean_cmd.append(arg) 150 151 152 args.output.write("""/* 153 * This file was automatically generated using the following command: 154 * {cmd} 155 * 156 */ 157 158#include <zephyr.h> 159#include <display/cfb.h> 160 161static const uint8_t cfb_font_{name:s}_{width:d}{height:d}[{elem:d}][{b:.0f}] = {{\n""" 162 .format(cmd=" ".join(clean_cmd), 163 name=args.name, 164 width=args.width, 165 height=args.height, 166 elem=args.last - args.first + 1, 167 b=args.width / 8 * args.height)) 168 169 if args.type == "font": 170 extract_font_glyphs() 171 elif args.type == "image": 172 extract_image_glyphs() 173 elif args.input.name.lower().endswith((".otf", ".otc", ".ttf", ".ttc")): 174 extract_font_glyphs() 175 else: 176 extract_image_glyphs() 177 178 args.output.write(""" 179}}; 180 181FONT_ENTRY_DEFINE({name}_{width}{height}, 182 {width}, 183 {height}, 184 {caps}, 185 cfb_font_{name}_{width}{height}, 186 {first}, 187 {last} 188); 189""" .format(name=args.name, width=args.width, height=args.height, 190 caps=caps, first=args.first, last=args.last)) 191 192def parse_args(): 193 """Parse arguments""" 194 global args 195 parser = argparse.ArgumentParser( 196 description="Character Frame Buffer (CFB) font header file generator", 197 formatter_class=argparse.RawDescriptionHelpFormatter) 198 199 parser.add_argument( 200 "-z", "--zephyr-base", 201 help="Zephyr base directory") 202 203 parser.add_argument( 204 "-d", "--dump", action="store_true", 205 help="dump generated CFB font elements as images for preview") 206 207 group = parser.add_argument_group("input arguments") 208 group.add_argument( 209 "-i", "--input", required=True, type=argparse.FileType('rb'), metavar="FILE", 210 help="TrueType/OpenType file or image input file") 211 group.add_argument( 212 "-t", "--type", default="auto", choices=["auto", "font", "image"], 213 help="Input file type (default: %(default)s)") 214 215 group = parser.add_argument_group("font arguments") 216 group.add_argument( 217 "-s", "--size", type=int, default=10, metavar="POINTS", 218 help="TrueType/OpenType font size in points (default: %(default)s)") 219 220 group = parser.add_argument_group("output arguments") 221 group.add_argument( 222 "-o", "--output", type=argparse.FileType('w'), default="-", metavar="FILE", 223 help="CFB font header file (default: stdout)") 224 group.add_argument( 225 "--bindir", type=str, 226 help="CMAKE_BINARY_DIR for pure logging purposes. No trailing slash.") 227 group.add_argument( 228 "-x", "--width", required=True, type=int, 229 help="width of the CFB font elements in pixels") 230 group.add_argument( 231 "-y", "--height", required=True, type=int, 232 help="height of the CFB font elements in pixels") 233 group.add_argument( 234 "-n", "--name", default="custom", 235 help="name of the CFB font entry (default: %(default)s)") 236 group.add_argument( 237 "--first", type=int, default=PRINTABLE_MIN, metavar="CHARCODE", 238 help="character code mapped to the first CFB font element (default: %(default)s)") 239 group.add_argument( 240 "--last", type=int, default=PRINTABLE_MAX, metavar="CHARCODE", 241 help="character code mapped to the last CFB font element (default: %(default)s)") 242 group.add_argument( 243 "--center-x", action='store_true', 244 help="center character glyphs horizontally") 245 group.add_argument( 246 "--y-offset", type=int, default=0, 247 help="vertical offset for character glyphs (default: %(default)s)") 248 group.add_argument( 249 "--hpack", dest='hpack', default=False, action='store_true', 250 help="generate bytes encoding row data rather than column data (default: %(default)s)") 251 group.add_argument( 252 "--msb-first", action='store_true', 253 help="packed content starts at high bit of each byte (default: lsb-first)") 254 255 args = parser.parse_args() 256 257def main(): 258 """Parse arguments and generate CFB font header file""" 259 parse_args() 260 generate_header() 261 262if __name__ == "__main__": 263 main() 264