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 # returns (left, top, right, bottom) bounding box 80 size = font.getbbox(chr(i)) 81 82 # calculate width + height 83 fw = size[2] - size[0] # right - left 84 fh = size[3] - size[1] # bottom - top 85 86 if fw > fw_max: 87 fw_max = fw 88 if fh > fh_max: 89 fh_max = fh 90 91 # Round the packed length up to pack into bytes. 92 if args.hpack: 93 width = 8 * int((fw_max + 7) / 8) 94 height = fh_max + args.y_offset 95 else: 96 width = fw_max 97 height = 8 * int((fh_max + args.y_offset + 7) / 8) 98 99 # Diagnose inconsistencies with arguments 100 if width != args.width: 101 raise Exception('text width {} mismatch with -x {}'.format(width, args.width)) 102 if height != args.height: 103 raise Exception('text height {} mismatch with -y {}'.format(height, args.height)) 104 105 for i in range(args.first, args.last + 1): 106 image = Image.new('1', (width, height), 'white') 107 draw = ImageDraw.Draw(image) 108 109 # returns (left, top, right, bottom) bounding box 110 size = draw.textbbox((0, 0), chr(i), font=font) 111 112 # calculate width + height 113 fw = size[2] - size[0] # right - left 114 fh = size[3] - size[1] # bottom - top 115 116 xpos = 0 117 if args.center_x: 118 xpos = (width - fw) / 2 + 1 119 ypos = args.y_offset 120 121 draw.text((xpos, ypos), chr(i), font=font) 122 generate_element(image, i) 123 124def extract_image_glyphs(): 125 """Extract font glyphs from an image file""" 126 image = Image.open(args.input) 127 128 x_offset = 0 129 for i in range(args.first, args.last + 1): 130 glyph = image.crop((x_offset, 0, x_offset + args.width, args.height)) 131 generate_element(glyph, i) 132 x_offset += args.width 133 134def generate_header(): 135 """Generate CFB font header file""" 136 137 caps = [] 138 if args.hpack: 139 caps.append('MONO_HPACKED') 140 else: 141 caps.append('MONO_VPACKED') 142 if args.msb_first: 143 caps.append('MSB_FIRST') 144 caps = ' | '.join(['CFB_FONT_' + f for f in caps]) 145 146 clean_cmd = [] 147 for arg in sys.argv: 148 if arg.startswith("--bindir"): 149 # Drop. Assumes --bindir= was passed with '=' sign. 150 continue 151 if args.bindir and arg.startswith(args.bindir): 152 # +1 to also strip '/' or '\' separator 153 striplen = min(len(args.bindir)+1, len(arg)) 154 clean_cmd.append(arg[striplen:]) 155 continue 156 157 if args.zephyr_base is not None: 158 clean_cmd.append(arg.replace(args.zephyr_base, '"${ZEPHYR_BASE}"')) 159 else: 160 clean_cmd.append(arg) 161 162 163 args.output.write("""/* 164 * This file was automatically generated using the following command: 165 * {cmd} 166 * 167 */ 168 169#include <zephyr/kernel.h> 170#include <zephyr/display/cfb.h> 171 172static const uint8_t cfb_font_{name:s}_{width:d}{height:d}[{elem:d}][{b:.0f}] = {{\n""" 173 .format(cmd=" ".join(clean_cmd), 174 name=args.name, 175 width=args.width, 176 height=args.height, 177 elem=args.last - args.first + 1, 178 b=args.width / 8 * args.height)) 179 180 if args.type == "font": 181 extract_font_glyphs() 182 elif args.type == "image": 183 extract_image_glyphs() 184 elif args.input.name.lower().endswith((".otf", ".otc", ".ttf", ".ttc")): 185 extract_font_glyphs() 186 else: 187 extract_image_glyphs() 188 189 args.output.write(""" 190}}; 191 192FONT_ENTRY_DEFINE({name}_{width}{height}, 193 {width}, 194 {height}, 195 {caps}, 196 cfb_font_{name}_{width}{height}, 197 {first}, 198 {last} 199); 200""" .format(name=args.name, width=args.width, height=args.height, 201 caps=caps, first=args.first, last=args.last)) 202 203def parse_args(): 204 """Parse arguments""" 205 global args 206 parser = argparse.ArgumentParser( 207 description="Character Frame Buffer (CFB) font header file generator", 208 formatter_class=argparse.RawDescriptionHelpFormatter, allow_abbrev=False) 209 210 parser.add_argument( 211 "-z", "--zephyr-base", 212 help="Zephyr base directory") 213 214 parser.add_argument( 215 "-d", "--dump", action="store_true", 216 help="dump generated CFB font elements as images for preview") 217 218 group = parser.add_argument_group("input arguments") 219 group.add_argument( 220 "-i", "--input", required=True, type=argparse.FileType('rb'), metavar="FILE", 221 help="TrueType/OpenType file or image input file") 222 group.add_argument( 223 "-t", "--type", default="auto", choices=["auto", "font", "image"], 224 help="Input file type (default: %(default)s)") 225 226 group = parser.add_argument_group("font arguments") 227 group.add_argument( 228 "-s", "--size", type=int, default=10, metavar="POINTS", 229 help="TrueType/OpenType font size in points (default: %(default)s)") 230 231 group = parser.add_argument_group("output arguments") 232 group.add_argument( 233 "-o", "--output", type=argparse.FileType('w'), default="-", metavar="FILE", 234 help="CFB font header file (default: stdout)") 235 group.add_argument( 236 "--bindir", type=str, 237 help="CMAKE_BINARY_DIR for pure logging purposes. No trailing slash.") 238 group.add_argument( 239 "-x", "--width", required=True, type=int, 240 help="width of the CFB font elements in pixels") 241 group.add_argument( 242 "-y", "--height", required=True, type=int, 243 help="height of the CFB font elements in pixels") 244 group.add_argument( 245 "-n", "--name", default="custom", 246 help="name of the CFB font entry (default: %(default)s)") 247 group.add_argument( 248 "--first", type=int, default=PRINTABLE_MIN, metavar="CHARCODE", 249 help="character code mapped to the first CFB font element (default: %(default)s)") 250 group.add_argument( 251 "--last", type=int, default=PRINTABLE_MAX, metavar="CHARCODE", 252 help="character code mapped to the last CFB font element (default: %(default)s)") 253 group.add_argument( 254 "--center-x", action='store_true', 255 help="center character glyphs horizontally") 256 group.add_argument( 257 "--y-offset", type=int, default=0, 258 help="vertical offset for character glyphs (default: %(default)s)") 259 group.add_argument( 260 "--hpack", dest='hpack', default=False, action='store_true', 261 help="generate bytes encoding row data rather than column data (default: %(default)s)") 262 group.add_argument( 263 "--msb-first", action='store_true', 264 help="packed content starts at high bit of each byte (default: lsb-first)") 265 266 args = parser.parse_args() 267 268def main(): 269 """Parse arguments and generate CFB font header file""" 270 parse_args() 271 generate_header() 272 273if __name__ == "__main__": 274 main() 275