diff --git a/font_to_py/bin_writer.py b/font_to_py/bin_writer.py new file mode 100755 index 0000000..d1e9391 --- /dev/null +++ b/font_to_py/bin_writer.py @@ -0,0 +1,59 @@ +# 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 click +import freetype + +from .font import Font + + +# 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 diff --git a/font_to_py/cli.py b/font_to_py/cli.py index ec8c66d..daa1e84 100755 --- a/font_to_py/cli.py +++ b/font_to_py/cli.py @@ -34,7 +34,8 @@ from pathlib import Path import click import freetype -from .writer import write_binary_font, write_font +from .bin_writer import write_binary_font +from .py_writer import write_font if freetype.version()[0] < 1: click.echo("freetype version should be >= 1. Please see FONT_TO_PY.md") diff --git a/font_to_py/py_writer.py b/font_to_py/py_writer.py new file mode 100755 index 0000000..7d15839 --- /dev/null +++ b/font_to_py/py_writer.py @@ -0,0 +1,184 @@ +# 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))