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