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
|
||||
module. Compared to my previous implementations this has the following aims:
|
||||
This is a utility written in Python 3 and intended to be run on a PC. It takes
|
||||
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.
|
||||
* 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
|
||||
|
||||
MicroPython platforms generally have limited RAM, but more abundant storage in
|
||||
the form of flash memory. Font files tend to be relatively large. 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
|
||||
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.
|
||||
2. Drivers using ``bytearray`` instances as frame buffers.
|
||||
3. Drivers for devices where the frame buffer is implemented in external
|
||||
hardware.
|
||||
3. Drivers for displays where the frame buffer is implemented in the display
|
||||
device hardware.
|
||||
|
||||
# Limitations
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
``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
|
||||
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
|
||||
be stored in proportional or fixed width form.
|
||||
is determined by command arguments. By default fonts are stored in variable
|
||||
pitch form. This may be overidden by a command line argument.
|
||||
|
||||
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
|
||||
the device driver.
|
||||
target display hardware. Their usage should be specified in the documentation
|
||||
for the device driver.
|
||||
|
||||
Example usage to produce a file ``myfont.py`` with height of 23 pixels:
|
||||
``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.
|
||||
* -x Specifies horizontal mapping (default is vertical).
|
||||
* -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
|
||||
device driver documentation. Bit reversal is required by some display hardware.
|
||||
|
@ -82,7 +90,7 @@ strings on demand.
|
|||
|
||||
# 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.
|
||||
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):
|
||||
|
||||
```python
|
||||
# Code generated by font-to-py.py.
|
||||
# Font: FreeSerif.ttf
|
||||
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):
|
||||
# use _index to return the offset into _font
|
||||
def max_width():
|
||||
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):
|
||||
# validate ch, if out of range use '?'
|
||||
# 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
|
||||
```
|
||||
|
||||
``height`` and ``width`` are specified in bits (pixels).
|
||||
|
||||
Note that the module global ``width`` is relevant only to files created as
|
||||
fixed pitch. It is provided for information only, and will be zero for variable
|
||||
pitch fonts. This enbles such fonts to be identified at runtime.
|
||||
In the case of monospaced fonts the ``max_width`` function returns the width of
|
||||
every character. For variable pitch fonts it returns the width of the widest
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
# THE SOFTWARE.
|
||||
|
||||
import freetype
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
import freetype
|
||||
|
||||
# UTILITIES FOR WRITING PYTHON SOURCECODE TO A FILE
|
||||
|
||||
|
@ -42,13 +42,12 @@ import os
|
|||
|
||||
|
||||
class ByteWriter(object):
|
||||
bytes_per_line = 8
|
||||
bytes_per_line = 16
|
||||
|
||||
def __init__(self, stream, varname):
|
||||
self.stream = stream
|
||||
self.stream.write(''.join((varname, ' = ')))
|
||||
self.stream.write('{} =\\\n'.format(varname))
|
||||
self.bytecount = 0 # For line breaks
|
||||
self.total_bytes = 0
|
||||
|
||||
def _eol(self):
|
||||
self.stream.write("'\\\n")
|
||||
|
@ -64,7 +63,6 @@ class ByteWriter(object):
|
|||
if not self.bytecount:
|
||||
self._bol()
|
||||
self.stream.write('\\x{:02x}'.format(data))
|
||||
self.total_bytes += 1
|
||||
self.bytecount += 1
|
||||
self.bytecount %= self.bytes_per_line
|
||||
if not self.bytecount:
|
||||
|
@ -72,15 +70,8 @@ class ByteWriter(object):
|
|||
|
||||
# Output from a sequence
|
||||
def odata(self, bytelist):
|
||||
for b in bytelist:
|
||||
self.obyte(b)
|
||||
|
||||
# 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
|
||||
for byt in bytelist:
|
||||
self.obyte(byt)
|
||||
|
||||
# ensure a correct final line
|
||||
def eot(self): # User force EOL if one hasn't occurred
|
||||
|
@ -88,9 +79,6 @@ class ByteWriter(object):
|
|||
self._eot()
|
||||
self.stream.write('\n')
|
||||
|
||||
def bytes_written(self):
|
||||
return self.total_bytes
|
||||
|
||||
|
||||
# Define a global
|
||||
def var_write(stream, name, value):
|
||||
|
@ -112,21 +100,21 @@ class Bitmap(object):
|
|||
|
||||
def display(self):
|
||||
"""Print the bitmap's pixels."""
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
ch = '#' if self.pixels[y * self.width + x] else '.'
|
||||
print(ch, end='')
|
||||
for row in range(self.height):
|
||||
for col in range(self.width):
|
||||
char = '#' if self.pixels[row * self.width + col] else '.'
|
||||
print(char, end='')
|
||||
print()
|
||||
print()
|
||||
|
||||
def bitblt(self, src, y):
|
||||
def bitblt(self, src, row):
|
||||
"""Copy all pixels from `src` into this bitmap"""
|
||||
srcpixel = 0
|
||||
dstpixel = y * self.width
|
||||
dstpixel = row * self.width
|
||||
row_offset = self.width - src.width
|
||||
|
||||
for sy in range(src.height):
|
||||
for sx in range(src.width):
|
||||
for _ in range(src.height):
|
||||
for _ in range(src.width):
|
||||
self.pixels[dstpixel] = src.pixels[srcpixel]
|
||||
srcpixel += 1
|
||||
dstpixel += 1
|
||||
|
@ -134,43 +122,43 @@ class Bitmap(object):
|
|||
|
||||
# Horizontal mapping generator function
|
||||
def get_hbyte(self, reverse):
|
||||
for y in range(self.height):
|
||||
x = 0
|
||||
for row in range(self.height):
|
||||
col = 0
|
||||
while True:
|
||||
bit = x % 8
|
||||
bit = col % 8
|
||||
if bit == 0:
|
||||
if x >= self.width:
|
||||
if col >= self.width:
|
||||
break
|
||||
byte = 0
|
||||
if x < self.width:
|
||||
if col < self.width:
|
||||
if reverse:
|
||||
byte |= self.pixels[y * self.width + x] << bit
|
||||
byte |= self.pixels[row * self.width + col] << bit
|
||||
else:
|
||||
# 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:
|
||||
yield byte
|
||||
x += 1
|
||||
col += 1
|
||||
|
||||
# Vertical mapping
|
||||
def get_vbyte(self, reverse):
|
||||
for x in range(self.width):
|
||||
y = 0
|
||||
for col in range(self.width):
|
||||
row = 0
|
||||
while True:
|
||||
bit = y % 8
|
||||
bit = row % 8
|
||||
if bit == 0:
|
||||
if y >= self.height:
|
||||
if row >= self.height:
|
||||
break
|
||||
byte = 0
|
||||
if y < self.height:
|
||||
if row < self.height:
|
||||
if reverse:
|
||||
byte |= self.pixels[y * self.width + x] << (7 - bit)
|
||||
byte |= self.pixels[row * self.width + col] << (7 - bit)
|
||||
else:
|
||||
# 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:
|
||||
yield byte
|
||||
y += 1
|
||||
row += 1
|
||||
|
||||
|
||||
class Glyph(object):
|
||||
|
@ -224,11 +212,11 @@ class Glyph(object):
|
|||
# Iterate over every byte in the glyph bitmap. Note that we're not
|
||||
# iterating over every pixel in the resulting unpacked 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):
|
||||
|
||||
# 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
|
||||
# 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
|
||||
# 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
|
||||
# output bitmap. Sometimes we're only unpacking a fraction of
|
||||
|
@ -265,26 +253,28 @@ class Glyph(object):
|
|||
# height (in pixels) of all characters
|
||||
# width (in pixels) for monospaced output (advance width of widest char)
|
||||
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):
|
||||
super().__init__()
|
||||
self._face = freetype.Face(filename)
|
||||
self._face.set_pixel_sizes(0, size)
|
||||
self._max_descent = 0
|
||||
|
||||
# For each character in the charset string we get the glyph
|
||||
# and update the overall dimensions of the resulting bitmap.
|
||||
max_width = 0
|
||||
self.max_width = 0
|
||||
max_ascent = 0
|
||||
for char in self.charset:
|
||||
glyph = self._glyph_for_character(char)
|
||||
max_ascent = max(max_ascent, int(glyph.ascent))
|
||||
self._max_descent = max(self._max_descent, int(glyph.descent))
|
||||
# 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.width = max_width if monospaced else 0
|
||||
self.width = self.max_width if monospaced else 0
|
||||
for char in self.charset:
|
||||
self._render_char(char)
|
||||
|
||||
|
@ -305,12 +295,12 @@ class Font(dict):
|
|||
|
||||
# The vertical drawing position should place the glyph
|
||||
# on the baseline as intended.
|
||||
y = self.height - int(glyph.ascent) - self._max_descent
|
||||
outbuffer.bitblt(glyph.bitmap, y)
|
||||
row = self.height - int(glyph.ascent) - self._max_descent
|
||||
outbuffer.bitblt(glyph.bitmap, row)
|
||||
self[char] = [outbuffer, width]
|
||||
|
||||
def _stream_char(self, char, hmap, reverse):
|
||||
outbuffer, width = self[char]
|
||||
def stream_char(self, char, hmap, reverse):
|
||||
outbuffer, _ = self[char]
|
||||
if hmap:
|
||||
gen = outbuffer.get_hbyte(reverse)
|
||||
else:
|
||||
|
@ -323,51 +313,44 @@ class Font(dict):
|
|||
for char in self.charset:
|
||||
width = self[char][1]
|
||||
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')
|
||||
return data, index
|
||||
|
||||
# PYTHON FILE WRITING
|
||||
|
||||
str01 = """# Code generated by font-to-py.py.
|
||||
STR01 = """# Code generated by font-to-py.py.
|
||||
# Font: {}
|
||||
version = '0.1'
|
||||
"""
|
||||
|
||||
str02 = """
|
||||
STR02 = """
|
||||
try:
|
||||
from uctypes import addressof
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
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 = 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):
|
||||
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')
|
||||
if test:
|
||||
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:
|
||||
fnt = Font(font_path, height, monospaced)
|
||||
except freetype.ft_errors.FT_Exception:
|
||||
|
@ -375,22 +358,22 @@ def write_font(op_path, font_path, height, monospaced, hmap, reverse, test):
|
|||
return False
|
||||
try:
|
||||
with open(op_path, 'w') as stream:
|
||||
write_data(stream, fnt, font_path, height,
|
||||
monospaced, hmap, reverse, test)
|
||||
write_data(stream, fnt, font_path, monospaced, hmap, reverse)
|
||||
except OSError:
|
||||
print("Can't open", op_path, 'for writing')
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def write_data(stream, fnt, font_path, height,
|
||||
monospaced, hmap, reverse, test):
|
||||
stream.write(str01.format(os.path.split(font_path)[1]))
|
||||
var_write(stream, 'test', test)
|
||||
var_write(stream, 'height', height)
|
||||
var_write(stream, 'width', fnt.width)
|
||||
var_write(stream, 'vmap', not hmap)
|
||||
var_write(stream, 'reversed', reverse)
|
||||
def write_data(stream, fnt, font_path, monospaced, hmap, reverse):
|
||||
height = fnt.height # Actual height, not target height
|
||||
stream.write(STR01.format(os.path.split(font_path)[1]))
|
||||
stream.write('\n')
|
||||
write_func(stream, 'height', height)
|
||||
write_func(stream, 'max_width', fnt.max_width)
|
||||
write_func(stream, 'hmap', hmap)
|
||||
write_func(stream, 'reverse', reverse)
|
||||
write_func(stream, 'monospaced', monospaced)
|
||||
data, index = fnt.build_arrays(hmap, reverse)
|
||||
bw_font = ByteWriter(stream, '_font')
|
||||
bw_font.odata(data)
|
||||
|
@ -398,99 +381,12 @@ def write_data(stream, fnt, font_path, height,
|
|||
bw_index = ByteWriter(stream, '_index')
|
||||
bw_index.odata(index)
|
||||
bw_index.eot()
|
||||
strfinal = str03 if test else str02
|
||||
stream.write(strfinal)
|
||||
stream.write(STR02.format(height, height))
|
||||
|
||||
|
||||
# ******************* 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
|
||||
|
||||
desc = """font_to_py.py
|
||||
DESC = """font_to_py.py
|
||||
Utility to convert ttf or otf font files to Python source.
|
||||
Sample usage:
|
||||
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__":
|
||||
parser = argparse.ArgumentParser(__file__, description=desc,
|
||||
parser = argparse.ArgumentParser(__file__, description=DESC,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument('infile', type=str, help='input file path')
|
||||
parser.add_argument('height', type=int, help='font height in pixels')
|
||||
|
@ -510,8 +406,6 @@ if __name__ == "__main__":
|
|||
help='bit reversal')
|
||||
parser.add_argument('-f', '--fixed', action='store_true',
|
||||
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,
|
||||
help='Path and name of output file')
|
||||
args = parser.parse_args()
|
||||
|
@ -527,7 +421,7 @@ if __name__ == "__main__":
|
|||
if not os.path.splitext(args.outfile)[1].upper() == '.PY':
|
||||
print("Output filename should have a .py extension.")
|
||||
sys.exit(1)
|
||||
print(args.infile, args.outfile, args.reverse, args.xmap)
|
||||
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)
|
||||
print(args.outfile, 'written successfully.')
|
||||
|
|
Ładowanie…
Reference in New Issue