diff --git a/font_to_py/cli.py b/font_to_py/cli.py index 7d1f3cf..ec8c66d 100755 --- a/font_to_py/cli.py +++ b/font_to_py/cli.py @@ -34,8 +34,7 @@ from pathlib import Path import click import freetype -from .byte_writer import ByteWriter -from .font import Font +from .writer import write_binary_font, write_font if freetype.version()[0] < 1: click.echo("freetype version should be >= 1. Please see FONT_TO_PY.md") @@ -49,182 +48,6 @@ UNICODE_PRIVATE_USE_AREA_END = 0xF8FF VERSION = importlib.metadata.version("micropython-font-to-py") -# Define a global -def var_write(stream, name, value): - stream.write(f"{name} = {value}\n") - - -STR01 = """# Code generated by font_to_py. -# Font: {font:s}{charset:s} -# Cmd: {cmd:s} -version = '{version:s}' - -""" - -# Code emitted for charsets spanning a small range of ordinal values -STR02 = """_mvfont = memoryview(_font) -_mvi = memoryview(_index) -ifb = lambda l : l[0] | (l[1] << 8) - -def get_ch(ch): - oc = ord(ch) - ioff = 2 * (oc - {min:d} + 1) if oc >= {min:d} and oc <= {max:d} else 0 - doff = ifb(_mvi[ioff : ]) - width = ifb(_mvfont[doff : ]) -""" - -# Code emiited for large charsets, assumed by build_arrays() to be sparse. -# Binary search of sorted sparse index. -# Offset into data array is saved after dividing by 8 -STRSP = """_mvfont = memoryview(_font) -_mvsp = memoryview(_sparse) -ifb = lambda l : l[0] | (l[1] << 8) - -def bs(lst, val): - while True: - m = (len(lst) & ~ 7) >> 1 - v = ifb(lst[m:]) - if v == val: - return ifb(lst[m + 2:]) - if not m: - return 0 - lst = lst[m:] if v < val else lst[:m] - -def get_ch(ch): - doff = bs(_mvsp, ord(ch)) << 3 - width = ifb(_mvfont[doff : ]) -""" - -# Code emitted for horizontally mapped fonts. -STR02H = """ - next_offs = doff + 2 + ((width - 1)//8 + 1) * {height:d} - return _mvfont[doff + 2:next_offs], {height:d}, width - -""" - -# Code emitted for vertically mapped fonts. -STR02V = """ - next_offs = doff + 2 + (({height:d} - 1)//8 + 1) * width - return _mvfont[doff + 2:next_offs], {height:d}, width - -""" - -# Extra code emitted where -i is specified. -STR03 = ''' -def glyphs(): - for c in """{}""": - yield c, get_ch(c) - -''' - - -def write_func(stream, name, arg): - stream.write(f"def {name}():\n return {arg}\n\n") - - -def write_font( # noqa: PLR0913 - op_path, - font_path, - height, - monospaced, - hmap, - reverse, - minchar, - maxchar, - defchar, - charset, - iterate, - bitmapped, -): - try: - fnt = Font( - font_path, height, minchar, maxchar, monospaced, defchar, charset, bitmapped - ) - except freetype.ft_errors.FT_Exception: - click.echo(f"Can't open {font_path}") - return False - try: - with open(op_path, "w", encoding="utf-8") as stream: - write_data(stream, fnt, font_path, hmap, reverse, iterate, charset) - except OSError: - click.echo(f"Can't open {op_path} for writing") - return False - return True - - -def write_data( # noqa: PLR0913 - stream, fnt, font_path, hmap, reverse, iterate, charset -): - height = fnt.height # Actual height, not target height - minchar = min(fnt.crange) - maxchar = max(fnt.crange) - st = "" if charset == "" else f" Char set: {charset}" - cl = " ".join([str(Path(sys.argv[0]).stem), *sys.argv[1:]]) - stream.write( - STR01.format(font=Path(font_path).stem, charset=st, cmd=cl, version=VERSION) - ) - write_func(stream, "height", height) - write_func(stream, "baseline", fnt._max_ascent) - write_func(stream, "max_width", fnt.max_width) - write_func(stream, "hmap", hmap) - write_func(stream, "reverse", reverse) - write_func(stream, "monospaced", fnt.monospaced) - write_func(stream, "min_ch", minchar) - write_func(stream, "max_ch", maxchar) - if iterate: - stream.write(STR03.format("".join(sorted(fnt.keys())))) - data, index, sparse = fnt.build_arrays(hmap, reverse) - bw_font = ByteWriter(stream, "_font") - bw_font.odata(data) - bw_font.eot() - if sparse: # build_arrays() has returned a sparse index - bw_sparse = ByteWriter(stream, "_sparse") - bw_sparse.odata(sparse) - bw_sparse.eot() - stream.write(STRSP) - click.echo("Sparse") - else: - bw_index = ByteWriter(stream, "_index") - bw_index.odata(index) - bw_index.eot() - stream.write(STR02.format(min=minchar, max=maxchar)) - click.echo("Normal") - if hmap: - stream.write(STR02H.format(height=height)) - else: - stream.write(STR02V.format(height=height)) - - -# BINARY OUTPUT -# hmap reverse magic bytes -# 0 0 0x3f 0xe7 -# 1 0 0x40 0xe7 -# 0 1 0x41 0xe7 -# 1 1 0x42 0xe7 -def write_binary_font(op_path, font_path, height, hmap, reverse): - try: - fnt = Font( - font_path, height, 32, 126, True, None, "" - ) # All chars have same width - except freetype.ft_errors.FT_Exception: - click.echo(f"Can't open {font_path}") - return False - sig = 1 if hmap else 0 - if reverse: - sig += 2 - try: - with open(op_path, "wb") as stream: - data = fnt.build_binary_array(hmap, reverse, sig) - stream.write(data) - except OSError: - click.echo(f"Can't open {op_path} for writing") - return False - return True - - -# PARSE COMMAND LINE ARGUMENTS - - def quit(msg): click.echo(msg) sys.exit(1) diff --git a/font_to_py/writer.py b/font_to_py/writer.py new file mode 100755 index 0000000..6443a1c --- /dev/null +++ b/font_to_py/writer.py @@ -0,0 +1,211 @@ +# Some code adapted from Daniel Bader's work at the following URL +# https://dbader.org/blog/monochrome-font-rendering-with-freetype-and-python +# With thanks to Stephen Irons @ironss for various improvements, also to +# @enigmaniac for ideas around handling `bdf` and `pcf` files. + +# The MIT License (MIT) +# +# Copyright (c) 2016-2023 Peter Hinch +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import importlib.metadata +import sys +from pathlib import Path + +import click +import freetype + +from .byte_writer import ByteWriter +from .font import Font + +VERSION = importlib.metadata.version("micropython-font-to-py") + + +# Define a global +def var_write(stream, name, value): + stream.write(f"{name} = {value}\n") + + +STR01 = """# Code generated by font_to_py. +# Font: {font:s}{charset:s} +# Cmd: {cmd:s} +version = '{version:s}' + +""" + +# Code emitted for charsets spanning a small range of ordinal values +STR02 = """_mvfont = memoryview(_font) +_mvi = memoryview(_index) +ifb = lambda l : l[0] | (l[1] << 8) + +def get_ch(ch): + oc = ord(ch) + ioff = 2 * (oc - {min:d} + 1) if oc >= {min:d} and oc <= {max:d} else 0 + doff = ifb(_mvi[ioff : ]) + width = ifb(_mvfont[doff : ]) +""" + +# Code emiited for large charsets, assumed by build_arrays() to be sparse. +# Binary search of sorted sparse index. +# Offset into data array is saved after dividing by 8 +STRSP = """_mvfont = memoryview(_font) +_mvsp = memoryview(_sparse) +ifb = lambda l : l[0] | (l[1] << 8) + +def bs(lst, val): + while True: + m = (len(lst) & ~ 7) >> 1 + v = ifb(lst[m:]) + if v == val: + return ifb(lst[m + 2:]) + if not m: + return 0 + lst = lst[m:] if v < val else lst[:m] + +def get_ch(ch): + doff = bs(_mvsp, ord(ch)) << 3 + width = ifb(_mvfont[doff : ]) +""" + +# Code emitted for horizontally mapped fonts. +STR02H = """ + next_offs = doff + 2 + ((width - 1)//8 + 1) * {height:d} + return _mvfont[doff + 2:next_offs], {height:d}, width + +""" + +# Code emitted for vertically mapped fonts. +STR02V = """ + next_offs = doff + 2 + (({height:d} - 1)//8 + 1) * width + return _mvfont[doff + 2:next_offs], {height:d}, width + +""" + +# Extra code emitted where -i is specified. +STR03 = ''' +def glyphs(): + for c in """{}""": + yield c, get_ch(c) + +''' + + +def write_func(stream, name, arg): + stream.write(f"def {name}():\n return {arg}\n\n") + + +def write_font( # noqa: PLR0913 + op_path, + font_path, + height, + monospaced, + hmap, + reverse, + minchar, + maxchar, + defchar, + charset, + iterate, + bitmapped, +): + try: + fnt = Font( + font_path, height, minchar, maxchar, monospaced, defchar, charset, bitmapped + ) + except freetype.ft_errors.FT_Exception: + click.echo(f"Can't open {font_path}") + return False + try: + with open(op_path, "w", encoding="utf-8") as stream: + write_data(stream, fnt, font_path, hmap, reverse, iterate, charset) + except OSError: + click.echo(f"Can't open {op_path} for writing") + return False + return True + + +def write_data( # noqa: PLR0913 + stream, fnt, font_path, hmap, reverse, iterate, charset +): + height = fnt.height # Actual height, not target height + minchar = min(fnt.crange) + maxchar = max(fnt.crange) + st = "" if charset == "" else f" Char set: {charset}" + cl = " ".join([str(Path(sys.argv[0]).stem), *sys.argv[1:]]) + stream.write( + STR01.format(font=Path(font_path).stem, charset=st, cmd=cl, version=VERSION) + ) + write_func(stream, "height", height) + write_func(stream, "baseline", fnt._max_ascent) + write_func(stream, "max_width", fnt.max_width) + write_func(stream, "hmap", hmap) + write_func(stream, "reverse", reverse) + write_func(stream, "monospaced", fnt.monospaced) + write_func(stream, "min_ch", minchar) + write_func(stream, "max_ch", maxchar) + if iterate: + stream.write(STR03.format("".join(sorted(fnt.keys())))) + data, index, sparse = fnt.build_arrays(hmap, reverse) + bw_font = ByteWriter(stream, "_font") + bw_font.odata(data) + bw_font.eot() + if sparse: # build_arrays() has returned a sparse index + bw_sparse = ByteWriter(stream, "_sparse") + bw_sparse.odata(sparse) + bw_sparse.eot() + stream.write(STRSP) + click.echo("Sparse") + else: + bw_index = ByteWriter(stream, "_index") + bw_index.odata(index) + bw_index.eot() + stream.write(STR02.format(min=minchar, max=maxchar)) + click.echo("Normal") + if hmap: + stream.write(STR02H.format(height=height)) + else: + stream.write(STR02V.format(height=height)) + + +# BINARY OUTPUT +# hmap reverse magic bytes +# 0 0 0x3f 0xe7 +# 1 0 0x40 0xe7 +# 0 1 0x41 0xe7 +# 1 1 0x42 0xe7 +def write_binary_font(op_path, font_path, height, hmap, reverse): + try: + fnt = Font( + font_path, height, 32, 126, True, None, "" + ) # All chars have same width + except freetype.ft_errors.FT_Exception: + click.echo(f"Can't open {font_path}") + return False + sig = 1 if hmap else 0 + if reverse: + sig += 2 + try: + with open(op_path, "wb") as stream: + data = fnt.build_binary_array(hmap, reverse, sig) + stream.write(data) + except OSError: + click.echo(f"Can't open {op_path} for writing") + return False + return True