kopia lustrzana https://github.com/peterhinch/micropython-font-to-py
Optional support for extended ASCII
rodzic
5c37366451
commit
4c966bbdcd
|
@ -137,7 +137,7 @@ has the following outline definition (in practice the bytes objects are large):
|
||||||
```python
|
```python
|
||||||
# Code generated by font-to-py.py.
|
# Code generated by font-to-py.py.
|
||||||
# Font: FreeSerif.ttf
|
# Font: FreeSerif.ttf
|
||||||
version = '0.1'
|
version = '0.2'
|
||||||
|
|
||||||
def height():
|
def height():
|
||||||
return 21
|
return 21
|
||||||
|
@ -154,6 +154,12 @@ def reverse():
|
||||||
def monospaced():
|
def monospaced():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def min_ch():
|
||||||
|
return 32
|
||||||
|
|
||||||
|
def max_ch():
|
||||||
|
return 126
|
||||||
|
|
||||||
_font =\
|
_font =\
|
||||||
b'\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
|
b'\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
|
||||||
b'\x00\x00\x00\x00\x08\x00\xfe\xc7\x00\x7e\xc0\x00\x00\x00\x00\x00'\
|
b'\x00\x00\x00\x00\x08\x00\xfe\xc7\x00\x7e\xc0\x00\x00\x00\x00\x00'\
|
||||||
|
|
|
@ -10,6 +10,10 @@ required height in pixels and outputs a Python 3 source file. The pixel layout
|
||||||
is determined by command arguments. By default fonts are stored in variable
|
is determined by command arguments. By default fonts are stored in variable
|
||||||
pitch form. This may be overidden by a command line argument.
|
pitch form. This may be overidden by a command line argument.
|
||||||
|
|
||||||
|
By default the ASCII character set (ordinal values 32 to 126 inclusive) is
|
||||||
|
supported. Command line arguments can modify this range as required, if
|
||||||
|
necessary to include extended ASCII characters up to 255.
|
||||||
|
|
||||||
Further arguments ensure that the byte contents and layout are correct for the
|
Further arguments ensure that the byte contents and layout are correct for the
|
||||||
target display hardware. Their usage should be specified in the documentation
|
target display hardware. Their usage should be specified in the documentation
|
||||||
for the device driver.
|
for the device driver.
|
||||||
|
@ -23,24 +27,28 @@ Example usage to produce a file ``myfont.py`` with height of 23 pixels:
|
||||||
|
|
||||||
1. Font file path. Must be a ttf or otf file.
|
1. Font file path. Must be a ttf or otf file.
|
||||||
2. Height in pixels.
|
2. Height in pixels.
|
||||||
3. Output file path. Must have a .py extension otherwise a binary font file
|
3. Output file path. Filename must have a .py extension.
|
||||||
will be created.
|
|
||||||
|
|
||||||
### Optional arguments:
|
### Optional arguments:
|
||||||
|
|
||||||
* -f or --fixed If specified, all characters will have the same width. By
|
* -f or --fixed If specified, all characters will have the same width. By
|
||||||
default fonts are assumed to be variable pitch.
|
default fonts are assumed to be variable pitch.
|
||||||
* -x Specifies horizontal mapping (default is vertical).
|
* -x or --xmap Specifies horizontal mapping (default is vertical).
|
||||||
* -b Specifies bit reversal in each font byte.
|
* -r or --reverse Specifies bit reversal in each font byte.
|
||||||
|
* -s or --smallest Ordinal value of smallest character to be stored. Default
|
||||||
|
32 (ASCII space).
|
||||||
|
* -l or --largest Ordinal value of largest character to be stored. Default 126.
|
||||||
|
* -e or --errchar Ordinal value of character to be rendered if an attempt is
|
||||||
|
made to display an out-of-range character. Default 63 (ASCII "?").
|
||||||
|
|
||||||
Optional arguments other than the fixed pitch argument will be specified in the
|
Any requirement for arguments -xr will be specified in the device driver
|
||||||
device driver documentation. Bit reversal is required by some display hardware.
|
documentation. Bit reversal is required by some display hardware.
|
||||||
|
|
||||||
### Output
|
### Output
|
||||||
|
|
||||||
The specified height is a target. The algorithm gets as close to the target
|
The specified height is a target. The algorithm gets as close to the target
|
||||||
height as possible (usually within one pixel). The actual height achieved is
|
height as possible (usually within one pixel). The actual height achieved is
|
||||||
displayed on completion.
|
displayed on completion, along with the width of the widest character.
|
||||||
|
|
||||||
A warning is output if the output filename does not have a .py extension as the
|
A warning is output if the output filename does not have a .py extension as the
|
||||||
creation of a binary font file may not be intended.
|
creation of a binary font file may not be intended.
|
||||||
|
@ -61,13 +69,23 @@ The detailed layout of the Python file may be seen [here](./DRIVERS.md).
|
||||||
|
|
||||||
### Binary font files
|
### Binary font files
|
||||||
|
|
||||||
If the output filename does not have a ``.py`` extension a binary font file is
|
There is an option to create a binary font file, specified with a ``-b`` or
|
||||||
created. This is primarily intended for the e-paper driver. Specifically in
|
``--binary`` command line argument. In this instance the output filename must
|
||||||
applications where the file is to be stored on the display's internal flash
|
not have a ``.py`` extension. This is primarily intended for the e-paper driver
|
||||||
|
in applications where the file is to be stored on the display's internal flash
|
||||||
memory rather than using frozen Python modules.
|
memory rather than using frozen Python modules.
|
||||||
|
|
||||||
The technique of accessing character data from a random access file is only
|
The technique of accessing character data from a random access file is slow
|
||||||
applicable to devices such as e-paper where the update time is slow.
|
and thus probably only applicable to devices such as e-paper where the update
|
||||||
|
time is slow.
|
||||||
|
|
||||||
|
Binary files currently support only the standard ASCII character set. There is
|
||||||
|
no error character: the device driver must ensure that seeks are within range.
|
||||||
|
Consequently the following arguments are invalid:
|
||||||
|
|
||||||
|
* -s or --smallest
|
||||||
|
* -l or --largest
|
||||||
|
* -e or --errchar
|
||||||
|
|
||||||
# Dependencies, links and licence
|
# Dependencies, links and licence
|
||||||
|
|
||||||
|
|
|
@ -153,10 +153,10 @@ def test_font(fontfile, string):
|
||||||
|
|
||||||
# Create font file, render a string to REPL using it
|
# Create font file, render a string to REPL using it
|
||||||
# usage font_test.test_file('FreeSans.ttf', 20, 'xyz')
|
# usage font_test.test_file('FreeSans.ttf', 20, 'xyz')
|
||||||
def test_file(fontfile, height, string, fixed=False, hmap=False,
|
def test_file(fontfile, height, string, *, minchar=32, maxchar=126, defchar=ord('?'),
|
||||||
reverse=False):
|
fixed=False, hmap=False, reverse=False):
|
||||||
if not write_font('myfont.py', fontfile, height, fixed,
|
if not write_font('myfont.py', fontfile, height, fixed,
|
||||||
hmap, reverse):
|
hmap, reverse, minchar, maxchar, defchar):
|
||||||
print('Failed to create font file.')
|
print('Failed to create font file.')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
125
font_to_py.py
125
font_to_py.py
|
@ -255,11 +255,10 @@ class Glyph(object):
|
||||||
# height (in pixels) of all characters
|
# height (in pixels) of all characters
|
||||||
# width (in pixels) for monospaced output (advance width of widest char)
|
# width (in pixels) for monospaced output (advance width of widest char)
|
||||||
class Font(dict):
|
class Font(dict):
|
||||||
charset = [chr(char) for char in range(32, 127)]
|
def __init__(self, filename, size, minchar=32, maxchar=126, monospaced=False, defchar=ord('?')):
|
||||||
|
|
||||||
def __init__(self, filename, size, monospaced=False):
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._face = freetype.Face(filename)
|
self._face = freetype.Face(filename)
|
||||||
|
self.charset = [chr(defchar)] + [chr(char) for char in range(minchar, maxchar + 1)]
|
||||||
self.max_width = self.get_dimensions(size)
|
self.max_width = self.get_dimensions(size)
|
||||||
self.width = self.max_width if monospaced else 0
|
self.width = self.max_width if monospaced else 0
|
||||||
for char in self.charset: # Populate dictionary
|
for char in self.charset: # Populate dictionary
|
||||||
|
@ -291,7 +290,8 @@ class Font(dict):
|
||||||
break
|
break
|
||||||
error = new_error
|
error = new_error
|
||||||
self.height = int(max_ascent + max_descent)
|
self.height = int(max_ascent + max_descent)
|
||||||
print('Height set in {} passes. Actual height {} pixels'.format(npass + 1, self.height))
|
st = 'Height set in {} passes. Actual height {} pixels.\nMax character width {} pixels.'
|
||||||
|
print(st.format(npass + 1, self.height, max_width))
|
||||||
self._max_descent = int(max_descent)
|
self._max_descent = int(max_descent)
|
||||||
return max_width
|
return max_width
|
||||||
|
|
||||||
|
@ -345,18 +345,18 @@ class Font(dict):
|
||||||
|
|
||||||
STR01 = """# Code generated by font-to-py.py.
|
STR01 = """# Code generated by font-to-py.py.
|
||||||
# Font: {}
|
# Font: {}
|
||||||
version = '0.1'
|
version = '0.2'
|
||||||
"""
|
"""
|
||||||
|
|
||||||
STR02 = """_mvfont = memoryview(_font)
|
STR02 = """_mvfont = memoryview(_font)
|
||||||
|
|
||||||
def _chr_addr(ordch):
|
def _chr_addr(ordch):
|
||||||
offset = 2 * (ordch - 32)
|
offset = 2 * (ordch - {})
|
||||||
return int.from_bytes(_index[offset:offset + 2], 'little')
|
return int.from_bytes(_index[offset:offset + 2], 'little')
|
||||||
|
|
||||||
def get_ch(ch):
|
def get_ch(ch):
|
||||||
ordch = ord(ch)
|
ordch = ord(ch)
|
||||||
ordch = ordch if ordch >= 32 and ordch <= 126 else ord('?')
|
ordch = ordch + 1 if ordch >= {} and ordch <= {} else {}
|
||||||
offset = _chr_addr(ordch)
|
offset = _chr_addr(ordch)
|
||||||
width = int.from_bytes(_font[offset:offset + 2], 'little')
|
width = int.from_bytes(_font[offset:offset + 2], 'little')
|
||||||
next_offs = _chr_addr(ordch +1)
|
next_offs = _chr_addr(ordch +1)
|
||||||
|
@ -367,23 +367,24 @@ def get_ch(ch):
|
||||||
def write_func(stream, name, arg):
|
def write_func(stream, name, arg):
|
||||||
stream.write('def {}():\n return {}\n\n'.format(name, arg))
|
stream.write('def {}():\n return {}\n\n'.format(name, arg))
|
||||||
|
|
||||||
|
# filename, size, minchar=32, maxchar=126, monospaced=False, defchar=ord('?'):
|
||||||
|
|
||||||
def write_font(op_path, font_path, height, monospaced, hmap, reverse):
|
def write_font(op_path, font_path, height, monospaced, hmap, reverse, minchar, maxchar, defchar):
|
||||||
try:
|
try:
|
||||||
fnt = Font(font_path, height, monospaced)
|
fnt = Font(font_path, height, minchar, maxchar, monospaced, defchar)
|
||||||
except freetype.ft_errors.FT_Exception:
|
except freetype.ft_errors.FT_Exception:
|
||||||
print("Can't open", font_path)
|
print("Can't open", font_path)
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
with open(op_path, 'w') as stream:
|
with open(op_path, 'w') as stream:
|
||||||
write_data(stream, fnt, font_path, monospaced, hmap, reverse)
|
write_data(stream, fnt, font_path, monospaced, hmap, reverse, minchar, maxchar)
|
||||||
except OSError:
|
except OSError:
|
||||||
print("Can't open", op_path, 'for writing')
|
print("Can't open", op_path, 'for writing')
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def write_data(stream, fnt, font_path, monospaced, hmap, reverse):
|
def write_data(stream, fnt, font_path, monospaced, hmap, reverse, minchar, maxchar):
|
||||||
height = fnt.height # Actual height, not target height
|
height = fnt.height # Actual height, not target height
|
||||||
stream.write(STR01.format(os.path.split(font_path)[1]))
|
stream.write(STR01.format(os.path.split(font_path)[1]))
|
||||||
stream.write('\n')
|
stream.write('\n')
|
||||||
|
@ -392,6 +393,8 @@ def write_data(stream, fnt, font_path, monospaced, hmap, reverse):
|
||||||
write_func(stream, 'hmap', hmap)
|
write_func(stream, 'hmap', hmap)
|
||||||
write_func(stream, 'reverse', reverse)
|
write_func(stream, 'reverse', reverse)
|
||||||
write_func(stream, 'monospaced', monospaced)
|
write_func(stream, 'monospaced', monospaced)
|
||||||
|
write_func(stream, 'min_ch', minchar)
|
||||||
|
write_func(stream, 'max_ch', maxchar)
|
||||||
data, index = fnt.build_arrays(hmap, reverse)
|
data, index = fnt.build_arrays(hmap, reverse)
|
||||||
bw_font = ByteWriter(stream, '_font')
|
bw_font = ByteWriter(stream, '_font')
|
||||||
bw_font.odata(data)
|
bw_font.odata(data)
|
||||||
|
@ -399,7 +402,7 @@ def write_data(stream, fnt, font_path, monospaced, hmap, reverse):
|
||||||
bw_index = ByteWriter(stream, '_index')
|
bw_index = ByteWriter(stream, '_index')
|
||||||
bw_index.odata(index)
|
bw_index.odata(index)
|
||||||
bw_index.eot()
|
bw_index.eot()
|
||||||
stream.write(STR02.format(height))
|
stream.write(STR02.format(minchar, minchar, maxchar, minchar, height))
|
||||||
|
|
||||||
# BINARY OUTPUT
|
# BINARY OUTPUT
|
||||||
# hmap reverse magic bytes
|
# hmap reverse magic bytes
|
||||||
|
@ -427,45 +430,99 @@ def write_binary_font(op_path, font_path, height, hmap, reverse):
|
||||||
|
|
||||||
# PARSE COMMAND LINE ARGUMENTS
|
# PARSE COMMAND LINE ARGUMENTS
|
||||||
|
|
||||||
|
def quit(msg):
|
||||||
|
print(msg)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
DESC = """font_to_py.py
|
DESC = """font_to_py.py
|
||||||
Utility to convert ttf or otf font files to Python source.
|
Utility to convert ttf or otf font files to Python source.
|
||||||
Sample usage:
|
Sample usage:
|
||||||
font_to_py.py FreeSans.ttf 23 freesans.py
|
font_to_py.py FreeSans.ttf 23 freesans.py
|
||||||
This creates a font with nominal height 23 pixels. To specify monospaced
|
|
||||||
rendering issue
|
This creates a font with nominal height 23 pixels with these defaults:
|
||||||
|
Mapping is vertical, pitch variable, character set 32-126 inclusive.
|
||||||
|
Illegal characters will be rendered as "?".
|
||||||
|
|
||||||
|
To specify monospaced rendering issue:
|
||||||
font_to_py.py FreeSans.ttf 23 --fixed freesans.py
|
font_to_py.py FreeSans.ttf 23 --fixed freesans.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
BINARY = """Invalid arguments. Binary (random access) font files support the standard ASCII
|
||||||
|
character set (from 32 to 126 inclusive). This range cannot be overridden.
|
||||||
|
Random access font files don't support an error character.
|
||||||
|
"""
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(__file__, description=DESC,
|
parser = argparse.ArgumentParser(__file__, description=DESC,
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
parser.add_argument('infile', type=str, help='input file path')
|
parser.add_argument('infile', type=str, help='Input file path')
|
||||||
parser.add_argument('height', type=int, help='font height in pixels')
|
parser.add_argument('height', type=int, help='Font height in pixels')
|
||||||
parser.add_argument('-x', '--xmap', action='store_true',
|
|
||||||
help='horizontal (x) mapping')
|
|
||||||
parser.add_argument('-r', '--reverse', action='store_true',
|
|
||||||
help='bit reversal')
|
|
||||||
parser.add_argument('-f', '--fixed', action='store_true',
|
|
||||||
help='Fixed width (monospaced) font')
|
|
||||||
parser.add_argument('outfile', type=str,
|
parser.add_argument('outfile', type=str,
|
||||||
help='Path and name of output file')
|
help='Path and name of output file')
|
||||||
|
|
||||||
|
parser.add_argument('-x', '--xmap', action='store_true',
|
||||||
|
help='Horizontal (x) mapping')
|
||||||
|
parser.add_argument('-r', '--reverse', action='store_true',
|
||||||
|
help='Bit reversal')
|
||||||
|
parser.add_argument('-f', '--fixed', action='store_true',
|
||||||
|
help='Fixed width (monospaced) font')
|
||||||
|
parser.add_argument('-b', '--binary', action='store_true',
|
||||||
|
help='Produce binary (random access) font file.')
|
||||||
|
|
||||||
|
parser.add_argument('-s', '--smallest',
|
||||||
|
type = int,
|
||||||
|
default = 32,
|
||||||
|
help = 'Ordinal value of smallest character default %(default)i')
|
||||||
|
|
||||||
|
parser.add_argument('-l', '--largest',
|
||||||
|
type = int,
|
||||||
|
help = 'Ordinal value of largest character default %(default)i',
|
||||||
|
default = 126)
|
||||||
|
|
||||||
|
parser.add_argument('-e', '--errchar',
|
||||||
|
type = int,
|
||||||
|
help = 'Ordinal value of error character default %(default)i ("?")',
|
||||||
|
default = 63)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if not args.infile[0].isalpha():
|
if not args.infile[0].isalpha():
|
||||||
print('Font filenames must be valid Python variable names.')
|
quit('Font filenames must be valid Python variable names.')
|
||||||
sys.exit(1)
|
|
||||||
if not os.path.isfile(args.infile):
|
if not os.path.isfile(args.infile):
|
||||||
print("Font filename does not exist")
|
quit("Font filename does not exist")
|
||||||
sys.exit(1)
|
|
||||||
if not os.path.splitext(args.infile)[1].upper() in ('.TTF', '.OTF'):
|
if not os.path.splitext(args.infile)[1].upper() in ('.TTF', '.OTF'):
|
||||||
print("Font file should be a ttf or otf file.")
|
quit("Font file should be a ttf or otf file.")
|
||||||
sys.exit(1)
|
|
||||||
if os.path.splitext(args.outfile)[1].upper() == '.PY': # Emit Python
|
if args.binary:
|
||||||
if not write_font(args.outfile, args.infile, args.height, args.fixed,
|
if os.path.splitext(args.outfile)[1].upper() == '.PY':
|
||||||
args.xmap, args.reverse):
|
quit('Binary file must not have a .py extension.')
|
||||||
sys.exit(1)
|
|
||||||
else:
|
if args.smallest != 32 or args.largest != 126 or args.default != ord('?'):
|
||||||
print('WARNING: output filename lacks .py extension. Writing binary font file.')
|
quit(BINARY)
|
||||||
|
|
||||||
|
print('Writing binary font file.')
|
||||||
if not write_binary_font(args.outfile, args.infile, args.height,
|
if not write_binary_font(args.outfile, args.infile, args.height,
|
||||||
args.xmap, args.reverse):
|
args.xmap, args.reverse):
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
if not os.path.splitext(args.outfile)[1].upper() == '.PY':
|
||||||
|
quit('Output filename must have a .py extension.')
|
||||||
|
|
||||||
|
if args.smallest < 0:
|
||||||
|
quit('--smallest must be >= 0')
|
||||||
|
|
||||||
|
if args.largest > 255:
|
||||||
|
quit('--largest must be < 256')
|
||||||
|
|
||||||
|
if args.errchar < 0 or args.errchar > 255:
|
||||||
|
quit('--errchar must be between 0 and 255')
|
||||||
|
|
||||||
|
print('Writing Python font file.')
|
||||||
|
if not write_font(args.outfile, args.infile, args.height, args.fixed,
|
||||||
|
args.xmap, args.reverse, args.smallest, args.largest,
|
||||||
|
args.errchar):
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
print(args.outfile, 'written successfully.')
|
print(args.outfile, 'written successfully.')
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue