Skip to content

Instantly share code, notes, and snippets.

@bblurock
Forked from medicalwei/fontgen.py
Created February 6, 2016 03:28
Show Gist options
  • Select an option

  • Save bblurock/6ea6118800ff50241916 to your computer and use it in GitHub Desktop.

Select an option

Save bblurock/6ea6118800ff50241916 to your computer and use it in GitHub Desktop.

Revisions

  1. @medicalwei medicalwei revised this gist Jul 6, 2015. 1 changed file with 8 additions and 17 deletions.
    25 changes: 8 additions & 17 deletions fontgen.py
    Original file line number Diff line number Diff line change
    @@ -82,18 +82,6 @@ def bits(x):
    x = x >> 1
    return data

    def fauxbold_bits(x, previousbit):
    data = []
    nextbit = int((x & 1) == 1)
    x = x | (x >> 1)
    for i in range(7):
    data.insert(0, int((x & 1) == 1))
    x = x >> 1
    # MSB | previousbit
    bit = int((x & 1) == 1)
    data.insert(0, int(bit | previousbit))
    return data, nextbit

    class Font:
    def __init__(self, ttf_path, height, max_glyphs, legacy):
    self.version = FONT_VERSION_2
    @@ -175,13 +163,16 @@ def glyph_bits(self, gindex):
    if pixel_mode == 1 and self.fauxbold: # faux bold monochrome font, 1 bit per pixel
    for i in range(bitmap.rows):
    row = []
    previousbit = 0
    previousbyte = 0
    for j in range(bitmap.pitch):
    data, previousbit = fauxbold_bits(bitmap.buffer[i*bitmap.pitch+j], previousbit)
    row.extend(data)
    byte = bitmap.buffer[i*bitmap.pitch+j] | previousbyte
    fauxboldbyte = byte | byte >> 1
    row.extend(bits(fauxboldbyte))
    previousbyte = byte << 8 # shift 8 bits for next
    if fauxbold_additional_byte:
    data, previousbit = fauxbold_bits(0, previousbit)
    row.extend(data)
    byte = previousbyte
    fauxboldbyte = byte | byte >> 1
    row.extend(bits(fauxboldbyte))
    glyph_bitmap.extend(row[:width])
    elif pixel_mode == 1: # monochrome font, 1 bit per pixel
    for i in range(bitmap.rows):
  2. @medicalwei medicalwei created this gist Jul 4, 2015.
    407 changes: 407 additions & 0 deletions fontgen.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,407 @@
    #!/usr/bin/env python

    import argparse
    import freetype
    import os
    import re
    import struct
    import sys
    import itertools
    import json
    from math import ceil

    sys.path.append(os.path.join(os.path.dirname(__file__), '../'))
    import generate_c_byte_array

    # Font
    # FontInfo
    # (uint8_t) version
    # (uint8_t) max_height
    # (uint16_t) number_of_glyphs
    # (uint16_t) wildcard_codepoint
    # (uint8_t) hash_table_size
    # (uint8_t) codepoint_bytes
    #
    # (uint32_t) hash_table[]
    # this hash table contains offsets to each glyph offset table. each offset is counted in
    # 32 bit blocks from the start of the offset tables block. Each entry in the hash table is
    # as follow: (uint8_t) hash value
    # (uint8_t) offset_table_size
    # (uint16_t) offset
    #
    # (uint32_t) offset_tables[][]
    # this list of tables contains offsets into the glyph_table for characters 0x20 to 0xff
    # each offset is counted in 32-bit blocks from the start of the glyph
    # each individual offset table contains ~10 sorted glyphs
    # table. 16-bit offsets are keyed by 16-bit codepoints.
    # packed: (codepoint_bytes [uint16_t | uint32_t]) codepoint
    # (uint_16) offset
    #
    # (uint32_t) glyph_table[]
    # [0]: the 32-bit block for offset 0 is used to indicate that a glyph is not supported
    # then for each glyph:
    # [offset + 0] packed: (int_8) offset_top
    # (int_8) offset_left,
    # (uint_8) bitmap_height,
    # (uint_8) bitmap_width (LSB)
    #
    # [offset + 1] (int_8) horizontal_advance
    # (24 bits) zero padding
    # [offset + 2] bitmap data (unaligned rows of bits), padded with 0's at
    # the end to make the bitmap data as a whole use multiples of 32-bit
    # blocks

    MIN_CODEPOINT = 0x20
    MAX_2_BYTES_CODEPOINT = 0xffff
    MAX_EXTENDED_CODEPOINT = 0x10ffff
    FONT_VERSION_1 = 1
    FONT_VERSION_2 = 2
    # Set a codepoint that the font doesn't know how to render
    # The watch will use this glyph as the wildcard character
    WILDCARD_CODEPOINT = 0x25AF # White vertical rectangle
    ELLIPSIS_CODEPOINT = 0x2026

    HASH_TABLE_SIZE = 255
    OFFSET_TABLE_MAX_SIZE = 128
    MAX_GLYPHS_EXTENDED = HASH_TABLE_SIZE * OFFSET_TABLE_MAX_SIZE
    MAX_GLYPHS = 256
    OFFSET_SIZE_BYTES = 4

    def grouper(n, iterable, fillvalue=None):
    """grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"""
    args = [iter(iterable)] * n
    return itertools.izip_longest(fillvalue=fillvalue, *args)

    def hasher(codepoint, num_glyphs):
    return (codepoint % num_glyphs)

    def bits(x):
    data = []
    for i in range(8):
    data.insert(0, int((x & 1) == 1))
    x = x >> 1
    return data

    def fauxbold_bits(x, previousbit):
    data = []
    nextbit = int((x & 1) == 1)
    x = x | (x >> 1)
    for i in range(7):
    data.insert(0, int((x & 1) == 1))
    x = x >> 1
    # MSB | previousbit
    bit = int((x & 1) == 1)
    data.insert(0, int(bit | previousbit))
    return data, nextbit

    class Font:
    def __init__(self, ttf_path, height, max_glyphs, legacy):
    self.version = FONT_VERSION_2
    self.ttf_path = ttf_path
    self.max_height = int(height)
    self.legacy = legacy
    self.face = freetype.Face(self.ttf_path)
    self.face.set_pixel_sizes(0, self.max_height)
    self.name = self.face.family_name + "_" + self.face.style_name
    self.wildcard_codepoint = WILDCARD_CODEPOINT
    self.number_of_glyphs = 0
    self.table_size = HASH_TABLE_SIZE
    self.tracking_adjust = 0
    self.regex = None
    self.codepoints = range(MIN_CODEPOINT, MAX_EXTENDED_CODEPOINT)
    self.codepoint_bytes = 2
    self.max_glyphs = max_glyphs
    self.glyph_table = []
    self.hash_table = [0] * self.table_size
    self.offset_tables = [[] for i in range(self.table_size)]
    self.heightoffset = 0
    self.fauxbold = False
    return

    def set_tracking_adjust(self, adjust):
    self.tracking_adjust = adjust

    def set_heightoffset(self, offset):
    self.heightoffset = offset

    def set_fauxbold(self, fauxbold):
    self.fauxbold = fauxbold

    def set_regex_filter(self, regex_string):
    if regex_string != ".*":
    try:
    self.regex = re.compile(unicode(regex_string, 'utf8'), re.UNICODE)
    except Exception, e:
    raise Exception("Supplied filter argument was not a valid regular expression.")
    else:
    self.regex = None

    def set_codepoint_list(self, list_path):
    codepoints_file = open(list_path)
    codepoints_json = json.load(codepoints_file)
    self.codepoints = [int(cp) for cp in codepoints_json["codepoints"]]

    def is_supported_glyph(self, codepoint):
    return (self.face.get_char_index(codepoint) > 0 or (codepoint == unichr(self.wildcard_codepoint)))

    def glyph_bits(self, gindex):
    flags = (freetype.FT_LOAD_RENDER if self.legacy else
    freetype.FT_LOAD_RENDER | freetype.FT_LOAD_MONOCHROME | freetype.FT_LOAD_TARGET_MONO)
    self.face.load_glyph(gindex, flags)
    # Font metrics
    bitmap = self.face.glyph.bitmap
    advance = self.face.glyph.advance.x / 64 # Convert 26.6 fixed float format to px
    advance += self.tracking_adjust
    width = bitmap.width
    if self.fauxbold:
    width += 1
    fauxbold_additional_byte = (bitmap.width % 8 == 0)
    height = bitmap.rows
    left = self.face.glyph.bitmap_left
    bottom = self.max_height - self.face.glyph.bitmap_top + self.heightoffset
    pixel_mode = self.face.glyph.bitmap.pixel_mode

    glyph_structure = ''.join((
    '<', #little_endian
    'B', #bitmap_width
    'B', #bitmap_height
    'b', #offset_left
    'b', #offset_top
    'b' #horizontal_advance
    ))
    glyph_header = struct.pack(glyph_structure, width, height, left, bottom, advance)

    glyph_bitmap = []
    if pixel_mode == 1 and self.fauxbold: # faux bold monochrome font, 1 bit per pixel
    for i in range(bitmap.rows):
    row = []
    previousbit = 0
    for j in range(bitmap.pitch):
    data, previousbit = fauxbold_bits(bitmap.buffer[i*bitmap.pitch+j], previousbit)
    row.extend(data)
    if fauxbold_additional_byte:
    data, previousbit = fauxbold_bits(0, previousbit)
    row.extend(data)
    glyph_bitmap.extend(row[:width])
    elif pixel_mode == 1: # monochrome font, 1 bit per pixel
    for i in range(bitmap.rows):
    row = []
    for j in range(bitmap.pitch):
    row.extend(bits(bitmap.buffer[i*bitmap.pitch+j]))
    glyph_bitmap.extend(row[:bitmap.width])
    elif pixel_mode == 2: # grey font, 255 bits per pixel
    for val in bitmap.buffer:
    glyph_bitmap.extend([1 if val > 127 else 0])
    else:
    # freetype-py should never give us a value not in (1,2)
    raise Exception("Unsupported pixel mode: {}".format(pixel_mode))

    glyph_packed = []
    for word in grouper(32, glyph_bitmap, 0):
    w = 0
    for index, bit in enumerate(word):
    w |= bit << index
    glyph_packed.append(struct.pack('<I', w))

    return glyph_header + ''.join(glyph_packed)

    def fontinfo_bits(self):
    return struct.pack('<BBHHBB',
    self.version,
    self.max_height,
    self.number_of_glyphs,
    self.wildcard_codepoint,
    self.table_size,
    self.codepoint_bytes)

    def build_tables(self):
    def build_hash_table(bucket_sizes):
    acc = 0
    for i in range(self.table_size):
    bucket_size = bucket_sizes[i]
    self.hash_table[i] = (struct.pack('<BBH', i, bucket_size, acc))
    acc += bucket_size * (OFFSET_SIZE_BYTES + self.codepoint_bytes)

    def build_offset_tables(glyph_entries):
    offset_table_format = '<LL' if self.codepoint_bytes == 4 else '<HL'
    bucket_sizes = [0] * self.table_size
    for entry in glyph_entries:
    codepoint, offset = entry
    glyph_hash = hasher(codepoint, self.table_size)
    self.offset_tables[glyph_hash].append(struct.pack(offset_table_format, codepoint, offset))
    bucket_sizes[glyph_hash] = bucket_sizes[glyph_hash] + 1
    if bucket_sizes[glyph_hash] > OFFSET_TABLE_MAX_SIZE:
    print "error: %d > 127" % bucket_sizes[glyph_hash]
    return bucket_sizes

    def add_glyph(codepoint, next_offset, gindex, glyph_indices_lookup):
    offset = next_offset
    if gindex not in glyph_indices_lookup:
    glyph_bits = self.glyph_bits(gindex)
    glyph_indices_lookup[gindex] = offset
    self.glyph_table.append(glyph_bits)
    next_offset += len(glyph_bits)
    else:
    offset = glyph_indices_lookup[gindex]

    if (codepoint > MAX_2_BYTES_CODEPOINT):
    self.codepoint_bytes = 4

    self.number_of_glyphs += 1
    return offset, next_offset, glyph_indices_lookup

    def codepoint_is_in_subset(codepoint):
    if (codepoint not in (WILDCARD_CODEPOINT, ELLIPSIS_CODEPOINT)):
    if self.regex is not None:
    if self.regex.match(unichr(codepoint)) is None:
    return False
    if codepoint not in self.codepoints:
    return False
    return True

    glyph_entries = []
    # MJZ: The 0th offset of the glyph table is 32-bits of
    # padding, no idea why.
    self.glyph_table.append(struct.pack('<I', 0))
    self.number_of_glyphs = 0
    glyph_indices_lookup = dict()
    next_offset = 4
    codepoint, gindex = self.face.get_first_char()

    # add wildcard_glyph
    offset, next_offset, glyph_indices_lookup = add_glyph(WILDCARD_CODEPOINT, next_offset, 0, glyph_indices_lookup)
    glyph_entries.append((WILDCARD_CODEPOINT, offset))

    while gindex:
    # Hard limit on the number of glyphs in a font
    if (self.number_of_glyphs > self.max_glyphs):
    break

    if (codepoint is WILDCARD_CODEPOINT):
    raise Exception('Wildcard codepoint is used for something else in this font')

    if (gindex is 0):
    raise Exception('0 index is reused by a non wildcard glyph')

    if (codepoint_is_in_subset(codepoint)):
    offset, next_offset, glyph_indices_lookup = add_glyph(codepoint, next_offset, gindex, glyph_indices_lookup)
    glyph_entries.append((codepoint, offset))

    codepoint, gindex = self.face.get_next_char(codepoint, gindex)

    # Make sure the entries are sorted by codepoint
    sorted_entries = sorted(glyph_entries, key=lambda entry: entry[0])
    hash_bucket_sizes = build_offset_tables(sorted_entries)
    build_hash_table(hash_bucket_sizes)

    def bitstring(self):
    btstr = self.fontinfo_bits()
    btstr += ''.join(self.hash_table)
    for table in self.offset_tables:
    btstr += ''.join(table)
    btstr += ''.join(self.glyph_table)

    return btstr

    def convert_to_h(self):
    to_file = os.path.splitext(self.ttf_path)[0] + '.h'
    f = open(to_file, 'wb')
    f.write("#pragma once\n\n")
    f.write("#include <stdint.h>\n\n")
    f.write("// TODO: Load font from flash...\n\n")
    self.build_tables()
    bytes = self.bitstring()
    generate_c_byte_array.write(f, bytes, self.name)
    f.close()
    return to_file

    def convert_to_pfo(self, pfo_path=None):
    to_file = pfo_path if pfo_path else (os.path.splitext(self.ttf_path)[0] + '.pfo')
    with open(to_file, 'wb') as f:
    self.build_tables()
    f.write(self.bitstring())
    return to_file

    def cmd_pfo(args):
    max_glyphs = MAX_GLYPHS_EXTENDED if args.extended else MAX_GLYPHS
    f = Font(args.input_ttf, args.height, max_glyphs, args.legacy)
    if (args.tracking):
    f.set_tracking_adjust(args.tracking)
    if (args.heightoffset):
    f.set_heightoffset(args.heightoffset)
    if (args.fauxbold):
    f.set_fauxbold(args.fauxbold)
    if (args.filter):
    f.set_regex_filter(args.filter)
    if (args.list):
    f.set_codepoint_list(args.list)
    f.convert_to_pfo(args.output_pfo)

    def cmd_header(args):
    f = Font(args.input_ttf, args.height, MAX_GLYPHS, args.legacy)
    if (args.filter):
    f.set_regex_filter(args.filter)
    f.convert_to_h()

    def process_all_fonts():
    font_directory = "ttf"
    font_paths = []
    for _, _, filenames in os.walk(font_directory):
    for filename in filenames:
    if os.path.splitext(filename)[1] == '.ttf':
    font_paths.append(os.path.join(font_directory, filename))

    header_paths = []
    for font_path in font_paths:
    f = Font(font_path, 14)
    print "Rendering {0}...".format(f.name)
    f.convert_to_pfo()
    to_file = f.convert_to_h()
    header_paths.append(os.path.basename(to_file))

    f = open(os.path.join(font_directory, 'fonts.h'), 'w')
    print>>f, '#pragma once'
    for h in header_paths:
    print>>f, "#include \"{0}\"".format(h)
    f.close()

    def process_cmd_line_args():
    parser = argparse.ArgumentParser(description="Generate pebble-usable fonts from ttf files")
    subparsers = parser.add_subparsers(help="commands", dest='which')

    pbi_parser = subparsers.add_parser('pfo', help="make a .pfo (pebble font) file")
    pbi_parser.add_argument('--extended', action='store_true', help="Whether or not to store > 256 glyphs")
    pbi_parser.add_argument('height', metavar='HEIGHT', help="Height at which to render the font")
    pbi_parser.add_argument('--tracking', type=int, help="Optional tracking adjustment of the font's horizontal advance")
    pbi_parser.add_argument('--filter', help="Regex to match the characters that should be included in the output")
    pbi_parser.add_argument('--list', help="json list of characters to include")
    pbi_parser.add_argument('--legacy', action='store_true', help="use legacy rasterizer (non-mono) to preserve font dimensions")
    pbi_parser.add_argument('--fauxbold', action='store_true', help="generate faux bold font")
    pbi_parser.add_argument('--heightoffset', type=int, help="height offset")
    pbi_parser.add_argument('input_ttf', metavar='INPUT_TTF', help="The ttf to process")
    pbi_parser.add_argument('output_pfo', metavar='OUTPUT_PFO', help="The pfo output file")
    pbi_parser.set_defaults(func=cmd_pfo)

    pbh_parser = subparsers.add_parser('header', help="make a .h (pebble fallback font) file")
    pbh_parser.add_argument('height', metavar='HEIGHT', help="Height at which to render the font")
    pbh_parser.add_argument('input_ttf', metavar='INPUT_TTF', help="The ttf to process")
    pbh_parser.add_argument('output_header', metavar='OUTPUT_HEADER', help="The pfo output file")
    pbh_parser.add_argument('--filter', help="Regex to match the characters that should be included in the output")

    pbi_parser.set_defaults(func=cmd_pfo)
    pbh_parser.set_defaults(func=cmd_header)

    args = parser.parse_args()
    args.func(args)

    def main():
    if len(sys.argv) < 2:
    # process all the fonts in the ttf folder
    process_all_fonts()
    else:
    # process an individual file
    process_cmd_line_args()


    if __name__ == "__main__":
    main()