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 Image, ImageDraw, ImageFont 11 12PRINTABLE_MIN = 32 13PRINTABLE_MAX = 126 14 15 16def generate_element(image, charcode): 17 """Generate CFB font element for a given character code from an image""" 18 blackwhite = image.convert("1", dither=Image.NONE) 19 pixels = blackwhite.load() 20 21 width, height = image.size 22 if args.dump: 23 blackwhite.save(f"{args.name}_{charcode}.png") 24 25 if PRINTABLE_MIN <= charcode <= PRINTABLE_MAX: 26 char = f" ({charcode:c})" 27 else: 28 char = "" 29 30 args.output.write(f"""\t/* {charcode:d}{char} */\n\t{{\n""") 31 32 glyph = [] 33 if args.hpack: 34 for row in range(0, height): 35 packed = [] 36 for octet in range(0, int(width / 8)): 37 value = "" 38 for bit in range(0, 8): 39 col = octet * 8 + bit 40 if pixels[col, row]: 41 value = value + "0" 42 else: 43 value = value + "1" 44 packed.append(value) 45 glyph.append(packed) 46 else: 47 for col in range(0, width): 48 packed = [] 49 for octet in range(0, int(height / 8)): 50 value = "" 51 for bit in range(0, 8): 52 row = octet * 8 + bit 53 if pixels[col, row]: 54 value = value + "0" 55 else: 56 value = value + "1" 57 packed.append(value) 58 glyph.append(packed) 59 for packed in glyph: 60 args.output.write("\t\t") 61 bits = [] 62 for value in packed: 63 bits.append(value) 64 if not args.msb_first: 65 value = value[::-1] 66 args.output.write(f"0x{int(value, 2):02x},") 67 args.output.write(" /* {} */\n".format(''.join(bits).replace('0', ' ').replace('1', '#'))) 68 args.output.write("\t},\n") 69 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(f'text width {width} mismatch with -x {args.width}') 102 if height != args.height: 103 raise Exception(f'text height {height} mismatch with -y {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 124 125def extract_image_glyphs(): 126 """Extract font glyphs from an image file""" 127 image = Image.open(args.input) 128 129 x_offset = 0 130 for i in range(args.first, args.last + 1): 131 glyph = image.crop((x_offset, 0, x_offset + args.width, args.height)) 132 generate_element(glyph, i) 133 x_offset += args.width 134 135 136def generate_header(): 137 """Generate CFB font header file""" 138 139 caps = [] 140 if args.hpack: 141 caps.append('MONO_HPACKED') 142 else: 143 caps.append('MONO_VPACKED') 144 if args.msb_first: 145 caps.append('MSB_FIRST') 146 caps = ' | '.join(['CFB_FONT_' + f for f in caps]) 147 148 clean_cmd = [] 149 for arg in sys.argv: 150 if arg.startswith("--bindir"): 151 # Drop. Assumes --bindir= was passed with '=' sign. 152 continue 153 if args.bindir and arg.startswith(args.bindir): 154 # +1 to also strip '/' or '\' separator 155 striplen = min(len(args.bindir) + 1, len(arg)) 156 clean_cmd.append(arg[striplen:]) 157 continue 158 159 if args.zephyr_base is not None: 160 clean_cmd.append(arg.replace(args.zephyr_base, '"${ZEPHYR_BASE}"')) 161 else: 162 clean_cmd.append(arg) 163 164 args.output.write( 165 """/* 166 * This file was automatically generated using the following command: 167 * {cmd} 168 * 169 */ 170 171#include <zephyr/kernel.h> 172#include <zephyr/display/cfb.h> 173 174static const uint8_t cfb_font_{name:s}_{width:d}{height:d}[{elem:d}][{b:.0f}] = {{\n""".format( 175 cmd=" ".join(clean_cmd), 176 name=args.name, 177 width=args.width, 178 height=args.height, 179 elem=args.last - args.first + 1, 180 b=args.width / 8 * args.height, 181 ) 182 ) 183 184 if args.type == "font": 185 extract_font_glyphs() 186 elif args.type == "image": 187 extract_image_glyphs() 188 elif args.input.name.lower().endswith((".otf", ".otc", ".ttf", ".ttc")): 189 extract_font_glyphs() 190 else: 191 extract_image_glyphs() 192 193 args.output.write( 194 f""" 195}}; 196 197FONT_ENTRY_DEFINE({args.name}_{args.width}{args.height}, 198 {args.width}, 199 {args.height}, 200 {caps}, 201 cfb_font_{args.name}_{args.width}{args.height}, 202 {args.first}, 203 {args.last} 204); 205""" # noqa: E101 206 ) 207 208 209def parse_args(): 210 """Parse arguments""" 211 global args 212 parser = argparse.ArgumentParser( 213 description="Character Frame Buffer (CFB) font header file generator", 214 formatter_class=argparse.RawDescriptionHelpFormatter, 215 allow_abbrev=False, 216 ) 217 218 parser.add_argument("-z", "--zephyr-base", help="Zephyr base directory") 219 220 parser.add_argument( 221 "-d", 222 "--dump", 223 action="store_true", 224 help="dump generated CFB font elements as images for preview", 225 ) 226 227 group = parser.add_argument_group("input arguments") 228 group.add_argument( 229 "-i", 230 "--input", 231 required=True, 232 type=argparse.FileType('rb'), 233 metavar="FILE", 234 help="TrueType/OpenType file or image input file", 235 ) 236 group.add_argument( 237 "-t", 238 "--type", 239 default="auto", 240 choices=["auto", "font", "image"], 241 help="Input file type (default: %(default)s)", 242 ) 243 244 group = parser.add_argument_group("font arguments") 245 group.add_argument( 246 "-s", 247 "--size", 248 type=int, 249 default=10, 250 metavar="POINTS", 251 help="TrueType/OpenType font size in points (default: %(default)s)", 252 ) 253 254 group = parser.add_argument_group("output arguments") 255 group.add_argument( 256 "-o", 257 "--output", 258 type=argparse.FileType('w'), 259 default="-", 260 metavar="FILE", 261 help="CFB font header file (default: stdout)", 262 ) 263 group.add_argument( 264 "--bindir", type=str, help="CMAKE_BINARY_DIR for pure logging purposes. No trailing slash." 265 ) 266 group.add_argument( 267 "-x", "--width", required=True, type=int, help="width of the CFB font elements in pixels" 268 ) 269 group.add_argument( 270 "-y", "--height", required=True, type=int, help="height of the CFB font elements in pixels" 271 ) 272 group.add_argument( 273 "-n", "--name", default="custom", help="name of the CFB font entry (default: %(default)s)" 274 ) 275 group.add_argument( 276 "--first", 277 type=int, 278 default=PRINTABLE_MIN, 279 metavar="CHARCODE", 280 help="character code mapped to the first CFB font element (default: %(default)s)", 281 ) 282 group.add_argument( 283 "--last", 284 type=int, 285 default=PRINTABLE_MAX, 286 metavar="CHARCODE", 287 help="character code mapped to the last CFB font element (default: %(default)s)", 288 ) 289 group.add_argument( 290 "--center-x", action='store_true', help="center character glyphs horizontally" 291 ) 292 group.add_argument( 293 "--y-offset", 294 type=int, 295 default=0, 296 help="vertical offset for character glyphs (default: %(default)s)", 297 ) 298 group.add_argument( 299 "--hpack", 300 dest='hpack', 301 default=False, 302 action='store_true', 303 help="generate bytes encoding row data rather than column data (default: %(default)s)", 304 ) 305 group.add_argument( 306 "--msb-first", 307 action='store_true', 308 help="packed content starts at high bit of each byte (default: lsb-first)", 309 ) 310 311 args = parser.parse_args() 312 313 314def main(): 315 """Parse arguments and generate CFB font header file""" 316 parse_args() 317 generate_header() 318 319 320if __name__ == "__main__": 321 main() 322