kopia lustrzana https://github.com/peterhinch/micropython-font-to-py
Initial release. Added font_test.py
rodzic
3ba89d2e0d
commit
ae49472e90
84
README.md
84
README.md
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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')
|
248
font_to_py.py
248
font_to_py.py
|
@ -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.')
|
||||||
|
|
Ładowanie…
Reference in New Issue