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