Initial release. Added font_test.py

pull/3/merge
Peter Hinch 2016-10-29 11:35:13 +01:00
rodzic 3ba89d2e0d
commit ae49472e90
3 zmienionych plików z 305 dodań i 205 usunięć

Wyświetl plik

@ -1,17 +1,26 @@
# micropython-font-to-py # font_to_py.py
This is currently a work in progress. This document specifies a forthcoming This is a utility written in Python 3 and intended to be run on a PC. It takes
module. Compared to my previous implementations this has the following aims: as input a font file in ttf or otf form and a height and outputs a Python
source file containing the font data. The purpose is to enable font files to be
used on microcontrollers running MicroPython: Python source files may be frozen
as bytecode. In this form they can be accessed at speed while using very little
RAM. The design has the following aims:
* Independence of specific display hardware. * Independence of specific display hardware.
* The path from font file to Python code to be fully open source. * The path from font file to Python code to be fully open source.
The first is achieved by supplying hardware specific arguments to the utility.
These define horizontal or vertical mapping and the bit order for font data.
The second is achieved by using Freetype and the Freetype Python bindings.
# Rationale # Rationale
MicroPython platforms generally have limited RAM, but more abundant storage in MicroPython platforms generally have limited RAM, but more abundant storage in
the form of flash memory. Font files tend to be relatively large. The the form of flash memory. Font files tend to be relatively large. The
conventional technique of rendering strings to a device involves loading the conventional technique of rendering strings to a device involves loading the
entire font into RAM. This is fast but ram intensive. The alternative of storing entire font into RAM. This is fast but RAM intensive. The alternative of storing
the font as a random access file and loading individual characters into RAM on the font as a random access file and loading individual characters into RAM on
demand is too slow for reasonable performance on most display devices. demand is too slow for reasonable performance on most display devices.
@ -24,27 +33,27 @@ devices and drivers. These include:
1. A driver for the official ``framebuffer`` class. 1. A driver for the official ``framebuffer`` class.
2. Drivers using ``bytearray`` instances as frame buffers. 2. Drivers using ``bytearray`` instances as frame buffers.
3. Drivers for devices where the frame buffer is implemented in external 3. Drivers for displays where the frame buffer is implemented in the display
hardware. device hardware.
# Limitations # Limitations
Only the ASCII character set from ``chr(32)`` to ``chr(126)`` is supported. Only the ASCII character set from ``chr(32)`` to ``chr(126)`` is supported.
Kerning is not supported. Fonts are one bit per pixel. This does not rule out Kerning is not supported. Fonts are one bit per pixel. This does not rule out
colour displays: the device driver can add colour information at the rendering colour displays: the device driver can add colour information at the rendering
stage. stage. It does assume that all pixels of a character are rendered identically.
# Usage # Usage
``font_to_py.py`` is a command line utility written in Python 3. It is run on a ``font_to_py.py`` is a command line utility written in Python 3. It is run on a
PC. It takes as input a font file with a ``ttf`` or ``otf`` extension and a PC. It takes as input a font file with a ``ttf`` or ``otf`` extension and a
required height in pixels and outputs a Python 3 source file. The pixel layout required height in pixels and outputs a Python 3 source file. The pixel layout
is determined by command arguments. Arguments also define whether the font is to is determined by command arguments. By default fonts are stored in variable
be stored in proportional or fixed width form. pitch form. This may be overidden by a command line argument.
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 defined in the documentation for target display hardware. Their usage should be specified in the documentation
the device driver. for the device driver.
Example usage to produce a file ``myfont.py`` with height of 23 pixels: Example usage to produce a file ``myfont.py`` with height of 23 pixels:
``font_to_py.py FreeSans.ttf 23 myfont.py`` ``font_to_py.py FreeSans.ttf 23 myfont.py``
@ -63,7 +72,6 @@ Example usage to produce a file ``myfont.py`` with height of 23 pixels:
default fonts are assumed to be variable pitch. default fonts are assumed to be variable pitch.
* -x Specifies horizontal mapping (default is vertical). * -x Specifies horizontal mapping (default is vertical).
* -b Specifies bit reversal in each font byte. * -b Specifies bit reversal in each font byte.
* -t Specifies test mode: output file suitable for cPython test programs only.
Optional arguments other than the fixed pitch argument will be specified in the Optional arguments other than the fixed pitch argument will be specified in the
device driver documentation. Bit reversal is required by some display hardware. device driver documentation. Bit reversal is required by some display hardware.
@ -82,7 +90,7 @@ strings on demand.
# Dependencies, links and licence # Dependencies, links and licence
The code is released under the MIT licence. The code is released under the MIT licence. It requires Python 3.2 or later.
The module relies on [Freetype](https://www.freetype.org/) which is included in most Linux distributions. The module relies on [Freetype](https://www.freetype.org/) which is included in most Linux distributions.
It uses the [Freetype Python bindings](http://freetype-py.readthedocs.io/en/latest/index.html) It uses the [Freetype Python bindings](http://freetype-py.readthedocs.io/en/latest/index.html)
@ -119,32 +127,54 @@ Assume the user has run the utility to produce a file ``myfont.py`` This then
has the following outline definition (in practice the bytes objects are large): has the following outline definition (in practice the bytes objects are large):
```python ```python
# Code generated by font-to-py.py.
# Font: FreeSerif.ttf
version = '0.1' version = '0.1'
test = False
height = 23
width = 22
vmap = True
reversed = False
_font = b'\x00\x00'
_index = b'\x00\x00\x23\x00\'
from uctypes import addressof def height():
return 21
def _chr_addr(ordch): def max_width():
# use _index to return the offset into _font return 22
def hmap():
return False
def reverse():
return False
def monospaced():
return False
_font =\
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'\
_index =\
b'\x00\x00\x14\x00\x2e\x00\x4b\x00\x71\x00\x97\x00\xd2\x00\x0a\x01'\
b'\x1b\x01\x35\x01\x4f\x01\x75\x01\x9e\x01\xb2\x01\xcc\x01\xe0\x01'\
# Boilerplate code omitted
def get_ch(ch): def get_ch(ch):
# validate ch, if out of range use '?' # validate ch, if out of range use '?'
# get offset into _font and retrieve char width # get offset into _font and retrieve char width
# Return address of start of bitmap, height and width # Return: address of start of bitmap, height and width
return addressof(_font) + offset + 2, height, width return addressof(_font) + offset + 2, height, width
``` ```
``height`` and ``width`` are specified in bits (pixels). ``height`` and ``width`` are specified in bits (pixels).
Note that the module global ``width`` is relevant only to files created as In the case of monospaced fonts the ``max_width`` function returns the width of
fixed pitch. It is provided for information only, and will be zero for variable every character. For variable pitch fonts it returns the width of the widest
pitch fonts. This enbles such fonts to be identified at runtime. character. Device drivers can use this to rapidly determine whether a string
will fit the available space. If it will fit on the assumption that all chars
are maximum width, it can be rendered rapidly without doing a character by
character check.
There is a small amount of additional code designed to enable font files to be
tested under cPython: in this instance ``get_ch()`` is called with an optional
``test`` argument and returns a slice rather than a machine address.
## Mapping ## Mapping

176
font_test.py 100644
Wyświetl plik

@ -0,0 +1,176 @@
#! /usr/bin/python3
# -*- coding: utf-8 -*-
# The MIT License (MIT)
#
# Copyright (c) 2016 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.
# Test programs for the utility font_to_py and for font files created by it.
# The test of most general use is test_font which enables a string to be
# output to the REPL using a font file created by this utility.
import sys
import os
from importlib import import_module
from font_to_py import Font, write_font
# Utility functions
def validate_hmap(data, height, width):
bpr = (width - 1)//8 + 1
msg = 'Horizontal map, invalid data length'
assert len(data) == bpr * height, msg
def validate_vmap(data, height, width):
bpc = (height - 1)//8 + 1
msg = 'Vertical map, invalid data length'
assert len(data) == bpc * width, msg
# Routines to render to REPL
def render_row_hmap(data, row, height, width, reverse):
validate_hmap(data, height, width)
bytes_per_row = (width - 1)//8 + 1
for col in range(width):
byte = data[row * bytes_per_row + col // 8]
if reverse:
bit = (byte & (1 << (col % 8))) > 0
else:
bit = (byte & (1 << (7 - (col % 8)))) > 0
char = '#' if bit else '.'
print(char, end='')
def render_row_vmap(data, row, height, width, reverse):
validate_vmap(data, height, width)
bytes_per_col = (height - 1)//8 + 1
for col in range(width):
byte = data[col * bytes_per_col + row//8]
if reverse:
bit = (byte & (1 << (7 - (row % 8)))) > 0
else:
bit = (byte & (1 << (row % 8))) > 0
char = '#' if bit else '.'
print(char, end='')
def display(data, hmap, height, width, reverse):
bpr = (width - 1)//8 + 1
bpc = (height - 1)//8 + 1
print('Height: {} Width: {}'.format(height, width))
print('Bytes/row: {} Bytes/col: {}'.format(bpr, bpc))
print('Data length: {}'.format(len(data)))
# Check bytearray is the correct length
if hmap:
validate_hmap(data, height, width)
else:
validate_vmap(data, height, width)
for row in range(height):
if hmap:
render_row_hmap(data, row, height, width, reverse)
else:
render_row_vmap(data, row, height, width, reverse)
print()
# TESTS: in order of code coverage
# Basic test of Font class functionality
# Usage font_test.font_test()
def font_test():
fnt = Font('FreeSans.ttf', 20)
for char in 'WM_eg!.,':
fnt[char][0].display()
print(fnt.width)
# Font character streaming
# Usage font_test.test_stream('abc', 20, False, False, False)
def test_stream(string, height, monospaced, hmap, reverse):
fnt = Font("FreeSans.ttf", height, monospaced)
height = fnt.height
for char in string:
width = fnt[char][1]
data = bytearray(fnt.stream_char(char, hmap, reverse))
display(data, hmap, height, width, reverse)
def chr_addr(index, ordch):
offset = 2 * (ordch - 32)
return int.from_bytes(index[offset:offset + 2], 'little')
# Font.build_arrays
# Usage font_test.test_arrays('abc', 20, False, False, False)
def test_arrays(string, height, monospaced, hmap, reverse):
fnt = Font("FreeSans.ttf", height, monospaced)
height = fnt.height
data, index = fnt.build_arrays(hmap, reverse)
for char in string:
ordch = ord(char)
offset = chr_addr(index, ordch)
width = int.from_bytes(data[offset:offset + 2], 'little')
offset += 2
next_offs = chr_addr(index, ordch + 1)
display(data[offset:next_offs], hmap, height, width, reverse)
# Render a string to REPL using a specified Python font file
# usage font_test.test_font('freeserif', 'abc')
def test_font(fontfile, string):
if fontfile in sys.modules:
del sys.modules[fontfile] # force reload
myfont = import_module(fontfile)
height = myfont.height()
for row in range(height):
for char in string:
data, _, width = myfont.get_ch(char, True)
if myfont.hmap():
render_row_hmap(data, row, height, width, myfont.reverse())
else:
render_row_vmap(data, row, height, width, myfont.reverse())
print()
# Create font file, render a string to REPL using it
# usage font_test.test_file('FreeSans.ttf', 20, 'xyz')
def test_file(fontfile, height, string, fixed=False, hmap=False,
reverse=False):
if not write_font('myfont.py', fontfile, height, fixed,
hmap, reverse):
print('Failed to create font file.')
return
if 'myfont' in sys.modules:
del sys.modules['myfont'] # force reload
import myfont
height = myfont.height()
for row in range(height):
for char in string:
data, _, width = myfont.get_ch(char, True)
if myfont.hmap():
render_row_hmap(data, row, height, width, myfont.reverse())
else:
render_row_vmap(data, row, height, width, myfont.reverse())
print()
os.unlink('myfont.py')

Wyświetl plik

@ -27,10 +27,10 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # THE SOFTWARE.
import freetype
import argparse import argparse
import sys import sys
import os import os
import freetype
# UTILITIES FOR WRITING PYTHON SOURCECODE TO A FILE # UTILITIES FOR WRITING PYTHON SOURCECODE TO A FILE
@ -42,13 +42,12 @@ import os
class ByteWriter(object): class ByteWriter(object):
bytes_per_line = 8 bytes_per_line = 16
def __init__(self, stream, varname): def __init__(self, stream, varname):
self.stream = stream self.stream = stream
self.stream.write(''.join((varname, ' = '))) self.stream.write('{} =\\\n'.format(varname))
self.bytecount = 0 # For line breaks self.bytecount = 0 # For line breaks
self.total_bytes = 0
def _eol(self): def _eol(self):
self.stream.write("'\\\n") self.stream.write("'\\\n")
@ -64,7 +63,6 @@ class ByteWriter(object):
if not self.bytecount: if not self.bytecount:
self._bol() self._bol()
self.stream.write('\\x{:02x}'.format(data)) self.stream.write('\\x{:02x}'.format(data))
self.total_bytes += 1
self.bytecount += 1 self.bytecount += 1
self.bytecount %= self.bytes_per_line self.bytecount %= self.bytes_per_line
if not self.bytecount: if not self.bytecount:
@ -72,15 +70,8 @@ class ByteWriter(object):
# Output from a sequence # Output from a sequence
def odata(self, bytelist): def odata(self, bytelist):
for b in bytelist: for byt in bytelist:
self.obyte(b) self.obyte(byt)
# Output words of arbitrary length litle-endian
def owords(self, words, length=2):
for data in words:
for _ in range(length):
self.obyte(data & 0xff)
data >>= 8
# ensure a correct final line # ensure a correct final line
def eot(self): # User force EOL if one hasn't occurred def eot(self): # User force EOL if one hasn't occurred
@ -88,9 +79,6 @@ class ByteWriter(object):
self._eot() self._eot()
self.stream.write('\n') self.stream.write('\n')
def bytes_written(self):
return self.total_bytes
# Define a global # Define a global
def var_write(stream, name, value): def var_write(stream, name, value):
@ -112,21 +100,21 @@ class Bitmap(object):
def display(self): def display(self):
"""Print the bitmap's pixels.""" """Print the bitmap's pixels."""
for y in range(self.height): for row in range(self.height):
for x in range(self.width): for col in range(self.width):
ch = '#' if self.pixels[y * self.width + x] else '.' char = '#' if self.pixels[row * self.width + col] else '.'
print(ch, end='') print(char, end='')
print() print()
print() print()
def bitblt(self, src, y): def bitblt(self, src, row):
"""Copy all pixels from `src` into this bitmap""" """Copy all pixels from `src` into this bitmap"""
srcpixel = 0 srcpixel = 0
dstpixel = y * self.width dstpixel = row * self.width
row_offset = self.width - src.width row_offset = self.width - src.width
for sy in range(src.height): for _ in range(src.height):
for sx in range(src.width): for _ in range(src.width):
self.pixels[dstpixel] = src.pixels[srcpixel] self.pixels[dstpixel] = src.pixels[srcpixel]
srcpixel += 1 srcpixel += 1
dstpixel += 1 dstpixel += 1
@ -134,43 +122,43 @@ class Bitmap(object):
# Horizontal mapping generator function # Horizontal mapping generator function
def get_hbyte(self, reverse): def get_hbyte(self, reverse):
for y in range(self.height): for row in range(self.height):
x = 0 col = 0
while True: while True:
bit = x % 8 bit = col % 8
if bit == 0: if bit == 0:
if x >= self.width: if col >= self.width:
break break
byte = 0 byte = 0
if x < self.width: if col < self.width:
if reverse: if reverse:
byte |= self.pixels[y * self.width + x] << bit byte |= self.pixels[row * self.width + col] << bit
else: else:
# Normal map MSB of byte 0 is (0, 0) # Normal map MSB of byte 0 is (0, 0)
byte |= self.pixels[y * self.width + x] << (7 - bit) byte |= self.pixels[row * self.width + col] << (7 - bit)
if bit == 7: if bit == 7:
yield byte yield byte
x += 1 col += 1
# Vertical mapping # Vertical mapping
def get_vbyte(self, reverse): def get_vbyte(self, reverse):
for x in range(self.width): for col in range(self.width):
y = 0 row = 0
while True: while True:
bit = y % 8 bit = row % 8
if bit == 0: if bit == 0:
if y >= self.height: if row >= self.height:
break break
byte = 0 byte = 0
if y < self.height: if row < self.height:
if reverse: if reverse:
byte |= self.pixels[y * self.width + x] << (7 - bit) byte |= self.pixels[row * self.width + col] << (7 - bit)
else: else:
# Normal map MSB of byte 0 is (0, 7) # Normal map MSB of byte 0 is (0, 7)
byte |= self.pixels[y * self.width + x] << bit byte |= self.pixels[row * self.width + col] << bit
if bit == 7: if bit == 7:
yield byte yield byte
y += 1 row += 1
class Glyph(object): class Glyph(object):
@ -224,11 +212,11 @@ class Glyph(object):
# Iterate over every byte in the glyph bitmap. Note that we're not # Iterate over every byte in the glyph bitmap. Note that we're not
# iterating over every pixel in the resulting unpacked bitmap -- # iterating over every pixel in the resulting unpacked bitmap --
# we're iterating over the packed bytes in the input bitmap. # we're iterating over the packed bytes in the input bitmap.
for y in range(bitmap.rows): for row in range(bitmap.rows):
for byte_index in range(bitmap.pitch): for byte_index in range(bitmap.pitch):
# Read the byte that contains the packed pixel data. # Read the byte that contains the packed pixel data.
byte_value = bitmap.buffer[y * bitmap.pitch + byte_index] byte_value = bitmap.buffer[row * bitmap.pitch + byte_index]
# We've processed this many bits (=pixels) so far. This # We've processed this many bits (=pixels) so far. This
# determines where we'll read the next batch of pixels from. # determines where we'll read the next batch of pixels from.
@ -236,7 +224,7 @@ class Glyph(object):
# Pre-compute where to write the pixels that we're going # Pre-compute where to write the pixels that we're going
# to unpack from the current byte in the glyph bitmap. # to unpack from the current byte in the glyph bitmap.
rowstart = y * bitmap.width + byte_index * 8 rowstart = row * bitmap.width + byte_index * 8
# Iterate over every bit (=pixel) that's still a part of the # Iterate over every bit (=pixel) that's still a part of the
# output bitmap. Sometimes we're only unpacking a fraction of # output bitmap. Sometimes we're only unpacking a fraction of
@ -265,26 +253,28 @@ 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(x) for x in range(32, 127)] charset = [chr(char) for char in range(32, 127)]
def __init__(self, filename, size, monospaced=False): def __init__(self, filename, size, monospaced=False):
super().__init__()
self._face = freetype.Face(filename) self._face = freetype.Face(filename)
self._face.set_pixel_sizes(0, size) self._face.set_pixel_sizes(0, size)
self._max_descent = 0 self._max_descent = 0
# For each character in the charset string we get the glyph # For each character in the charset string we get the glyph
# and update the overall dimensions of the resulting bitmap. # and update the overall dimensions of the resulting bitmap.
max_width = 0 self.max_width = 0
max_ascent = 0 max_ascent = 0
for char in self.charset: for char in self.charset:
glyph = self._glyph_for_character(char) glyph = self._glyph_for_character(char)
max_ascent = max(max_ascent, int(glyph.ascent)) max_ascent = max(max_ascent, int(glyph.ascent))
self._max_descent = max(self._max_descent, int(glyph.descent)) self._max_descent = max(self._max_descent, int(glyph.descent))
# for a few chars e.g. _ glyph.width > glyph.advance_width # for a few chars e.g. _ glyph.width > glyph.advance_width
max_width = int(max(max_width, glyph.advance_width, glyph.width)) self.max_width = int(max(self.max_width, glyph.advance_width,
glyph.width))
self.height = max_ascent + self._max_descent self.height = max_ascent + self._max_descent
self.width = max_width if monospaced else 0 self.width = self.max_width if monospaced else 0
for char in self.charset: for char in self.charset:
self._render_char(char) self._render_char(char)
@ -305,12 +295,12 @@ class Font(dict):
# The vertical drawing position should place the glyph # The vertical drawing position should place the glyph
# on the baseline as intended. # on the baseline as intended.
y = self.height - int(glyph.ascent) - self._max_descent row = self.height - int(glyph.ascent) - self._max_descent
outbuffer.bitblt(glyph.bitmap, y) outbuffer.bitblt(glyph.bitmap, row)
self[char] = [outbuffer, width] self[char] = [outbuffer, width]
def _stream_char(self, char, hmap, reverse): def stream_char(self, char, hmap, reverse):
outbuffer, width = self[char] outbuffer, _ = self[char]
if hmap: if hmap:
gen = outbuffer.get_hbyte(reverse) gen = outbuffer.get_hbyte(reverse)
else: else:
@ -323,51 +313,44 @@ class Font(dict):
for char in self.charset: for char in self.charset:
width = self[char][1] width = self[char][1]
data += (width).to_bytes(2, byteorder='little') data += (width).to_bytes(2, byteorder='little')
data += bytearray(self._stream_char(char, hmap, reverse)) data += bytearray(self.stream_char(char, hmap, reverse))
index += (len(data)).to_bytes(2, byteorder='little') index += (len(data)).to_bytes(2, byteorder='little')
return data, index return data, index
# PYTHON FILE WRITING # PYTHON FILE WRITING
str01 = """# Code generated by font-to-py.py. STR01 = """# Code generated by font-to-py.py.
# Font: {} # Font: {}
version = '0.1' version = '0.1'
""" """
str02 = """ STR02 = """
try:
from uctypes import addressof from uctypes import addressof
except ImportError:
pass
def _chr_addr(ordch): def _chr_addr(ordch):
offset = 2 * (ordch - 32) offset = 2 * (ordch - 32)
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, test=False):
ordch = ord(ch)
ordch = ordch if ordch >= 32 and ordch <= 126 else ord('?')
offset = _chr_addr(ordch)
width = int.from_bytes(_font[offset:offset + 2], 'little')
return addressof(_font) + offset + 2, height, width
"""
# Test mode get_ch returns a slice rather than an address
str03 = """
def _chr_addr(ordch):
offset = 2 * (ordch - 32)
return int.from_bytes(_index[offset:offset + 2], 'little')
def get_ch(ch):
ordch = ord(ch) ordch = ord(ch)
ordch = ordch if ordch >= 32 and ordch <= 126 else ord('?') ordch = ordch if ordch >= 32 and ordch <= 126 else ord('?')
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')
if test:
next_offs = _chr_addr(ordch +1) next_offs = _chr_addr(ordch +1)
return _font[offset + 2:next_offs], height, width return _font[offset + 2:next_offs], {}, width
return addressof(_font) + offset + 2, {}, width
""" """
def write_func(stream, name, arg):
stream.write('def {}():\n return {}\n\n'.format(name, arg))
def write_font(op_path, font_path, height, monospaced, hmap, reverse, test):
def write_font(op_path, font_path, height, monospaced, hmap, reverse):
try: try:
fnt = Font(font_path, height, monospaced) fnt = Font(font_path, height, monospaced)
except freetype.ft_errors.FT_Exception: except freetype.ft_errors.FT_Exception:
@ -375,22 +358,22 @@ def write_font(op_path, font_path, height, monospaced, hmap, reverse, test):
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, height, write_data(stream, fnt, font_path, monospaced, hmap, reverse)
monospaced, hmap, reverse, test)
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, height, def write_data(stream, fnt, font_path, monospaced, hmap, reverse):
monospaced, hmap, reverse, test): 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]))
var_write(stream, 'test', test) stream.write('\n')
var_write(stream, 'height', height) write_func(stream, 'height', height)
var_write(stream, 'width', fnt.width) write_func(stream, 'max_width', fnt.max_width)
var_write(stream, 'vmap', not hmap) write_func(stream, 'hmap', hmap)
var_write(stream, 'reversed', reverse) write_func(stream, 'reverse', reverse)
write_func(stream, 'monospaced', monospaced)
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)
@ -398,99 +381,12 @@ def write_data(stream, fnt, font_path, height,
bw_index = ByteWriter(stream, '_index') bw_index = ByteWriter(stream, '_index')
bw_index.odata(index) bw_index.odata(index)
bw_index.eot() bw_index.eot()
strfinal = str03 if test else str02 stream.write(STR02.format(height, height))
stream.write(strfinal)
# ******************* TESTS *******************
def display_hmap(ba, height, width, reverse):
bytes_per_row = width // 8 + 1
for bitnum in range(height * width):
row, bn = divmod(bitnum, width)
if bn == 0:
print()
byte = ba[row * bytes_per_row + bn // 8]
if reverse:
bit = (byte & (1 << (bn % 8))) > 0
else:
bit = (byte & (1 << (7 - (bn % 8)))) > 0
ch = '#' if bit else '.'
print(ch, end='')
print()
print(height, width)
def display_vmap(ba, height, width, reverse):
bytes_per_col = height // 8 + 1
for row in range(height):
for col in range(width):
byte = ba[col * bytes_per_col + row // 8]
if reverse:
bit = (byte & (1 << (7 - (row % 8)))) > 0
else:
bit = (byte & (1 << (row % 8))) > 0
ch = '#' if bit else '.'
print(ch, end='')
print()
print(height, width)
def display(g, hmap, height, width, reverse):
if hmap:
display_hmap(g, height, width, reverse)
else:
display_vmap(g, height, width, reverse)
def test1(string, height, monospaced, hmap, reverse):
fnt = Font("FreeSans.ttf", height, monospaced)
height = fnt.height
for char in string:
width = fnt[char][1]
g = bytearray(fnt._stream_char(char, hmap, reverse))
display(g, hmap, height, width, reverse)
def chr_addr(index, ordch):
offset = 2 * (ordch - 32)
return int.from_bytes(index[offset:offset + 2], 'little')
def test(string, height, monospaced, hmap, reverse):
fnt = Font("FreeSans.ttf", height, monospaced)
height = fnt.height
data, index = fnt.build_arrays(hmap, reverse)
for char in string:
ordch = ord(char)
offset = chr_addr(index, ordch)
width = int.from_bytes(data[offset:offset + 2], 'little')
offset += 2
next_offs = chr_addr(index, ordch + 1)
display(data[offset:next_offs], hmap, height, width, reverse)
# usage testfile('FreeSans','xyz')
def testfile(fontfile, string):
import importlib
myfont = importlib.import_module(fontfile)
for ch in string:
data, height, width = myfont.get_ch(ch)
display(data, not myfont.vmap, height, width, myfont.reversed)
def bar():
# Number indicates height, in practice can be one less i.e. 36->35 rows
fnt = Font("FreeSans.ttf", 20)
for ch in 'WM_eg!.,':
fnt[ch][0].display()
print(fnt.width)
# test('|_g.AW', height = 20, monospaced = True, hmap = False, reverse = False)
# PARSE COMMAND LINE ARGUMENTS # PARSE COMMAND LINE ARGUMENTS
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
@ -500,7 +396,7 @@ font_to_py.py FreeSans.ttf 23 --fixed freesans.py
""" """
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')
@ -510,8 +406,6 @@ if __name__ == "__main__":
help='bit reversal') help='bit reversal')
parser.add_argument('-f', '--fixed', action='store_true', parser.add_argument('-f', '--fixed', action='store_true',
help='Fixed width (monospaced) font') help='Fixed width (monospaced) font')
parser.add_argument('-t', '--test', action='store_true',
help='Test file: import from cPython')
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')
args = parser.parse_args() args = parser.parse_args()
@ -527,7 +421,7 @@ if __name__ == "__main__":
if not os.path.splitext(args.outfile)[1].upper() == '.PY': if not os.path.splitext(args.outfile)[1].upper() == '.PY':
print("Output filename should have a .py extension.") print("Output filename should have a .py extension.")
sys.exit(1) sys.exit(1)
print(args.infile, args.outfile, args.reverse, args.xmap)
if not write_font(args.outfile, args.infile, args.height, args.fixed, if not write_font(args.outfile, args.infile, args.height, args.fixed,
args.xmap, args.reverse, args.test): args.xmap, args.reverse):
sys.exit(1) sys.exit(1)
print(args.outfile, 'written successfully.')