Optional support for extended ASCII

pull/3/merge
Peter Hinch 2017-01-12 15:48:26 +00:00
rodzic 5c37366451
commit 4c966bbdcd
4 zmienionych plików z 131 dodań i 50 usunięć

Wyświetl plik

@ -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'\

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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.')