V0.3 Sparse index implemented.

pull/28/head
Peter Hinch 2019-09-08 17:25:51 +01:00
rodzic dfb80d975d
commit 52d060737d
2 zmienionych plików z 61 dodań i 49 usunięć

Wyświetl plik

@ -1,18 +1,28 @@
# font_to_py.py # font_to_py.py
Convert a font file to Python source code. The principal reason for doing this Convert a font file to Python source code. Python font files provide a much
is to save RAM on resource-limited targets: the font file may be incorporated faster way to access glyphs than the principal alternative which is a random
into a firmware build such that it occupies flash memory rather than scarce access file on the filesystem.
RAM. Python code built into firmware is known as frozen bytecode.
Another benefit is that they can save large amounts of RAM on resource-limited
targets: the font file may be incorporated into a firmware build such that it
occupies flash memory rather than scarce RAM. Python code built into firmware
is known as frozen bytecode.
## V0.3 notes ## V0.3 notes
8 Sept 2019 8 Sept 2019
Remove redundancy from index file. Emit extra index for sparse fonts, reducing 1. Reduced output file size for sparse fonts. These result from large gaps
code size. Add comment field in the output file showing creation command line. between ordinal values of Unicode characters not in the standard ASCII set.
Repo includes the file `extended`. This facilitates creating fonts comprising 2. Output file has comment showing creation command line.
the printable ASCII set plus `°μπωϕθαβγδλΩ`. Improvements to `font_test.py`. 3. Repo includes the file `extended`. Using `-k extended` creates fonts
comprising the printable ASCII set plus `°μπωϕθαβγδλΩ`. Such a font has 95
chars having ordinal values from 32-981.
4. Improvements to `font_test.py`.
Python files produced are interchangeable with those from prior versions: the
API is unchanged.
###### [Main README](./README.md) ###### [Main README](./README.md)
@ -44,9 +54,11 @@ Further arguments ensure that the byte contents and layout are correct for the
target display hardware. Their usage should be specified in the documentation target display hardware. Their usage should be specified in the documentation
for the device driver. for the device driver.
Example usage to produce a file `myfont.py` with height of 23 pixels: Examples of usage to produce a file `myfont.py` with height of 23 pixels:
`font_to_py.py FreeSans.ttf 23 myfont.py` ```shell
$ font_to_py.py FreeSans.ttf 23 myfont.py
$ font_to_py.py -k extended FreeSans.ttf 23 my_extended_font.py
```
## Arguments ## Arguments
### Mandatory positional arguments: ### Mandatory positional arguments:
@ -72,9 +84,10 @@ Example usage to produce a file `myfont.py` with height of 23 pixels:
* -k or --charset_file Obtain the character set from a file. Typical use is * -k or --charset_file Obtain the character set from a file. Typical use is
for alternative character sets such as Cyrillic: the file must contain the for alternative character sets such as Cyrillic: the file must contain the
character set to be included. An example file is `cyrillic`. Another is character set to be included. An example file is `cyrillic`. Another is
`extended` which adds unicode characters "° μ π ω ϕ θ α β γ δ λ Ω" to those `extended` which adds unicode characters `°μπωϕθαβγδλΩ` to those in the
with `ord` values from 32-126. Such files will only produce useful results if original ASCII set of printable characters. At risk of stating the obvious
the source font file includes those glyphs. this will only produce useful results if the source font file includes all
specified glyphs.
The -c option may be used to reduce the size of the font file by limiting the The -c option may be used to reduce the size of the font file by limiting the
character set. If the font file is frozen as bytecode this will not reduce RAM character set. If the font file is frozen as bytecode this will not reduce RAM
@ -194,13 +207,15 @@ print(len(freesans20._font) + len(freesans20._index))
The memory used was 1712, 2032, 2384 and 2416 bytes. As increments over the The memory used was 1712, 2032, 2384 and 2416 bytes. As increments over the
prior state this corresponds to 320, 352 and 32 bytes. The `print` statement prior state this corresponds to 320, 352 and 32 bytes. The `print` statement
shows the RAM which would be consumed by the data arrays: this was 3956 bytes shows the RAM which would be consumed by the data arrays if they were not
for `freesans20`. frozen: this was 3956 bytes for `freesans20`.
The `foo()` function emulates the behaviour of a device driver in rendering a The `foo()` function emulates the behaviour of a device driver in rendering a
character to a display. The local variables constitute memory which is character to a display. The local variables constitute memory which is
reclaimed on exit from the function. Its additional RAM use was 16 bytes. reclaimed on exit from the function. Its additional RAM use was 16 bytes.
Similar figures were found in recent (2019) testing on a Pyboard D.
## Conclusion ## Conclusion
With a font of height 20 pixels RAM saving was an order of magnitude. The With a font of height 20 pixels RAM saving was an order of magnitude. The

Wyświetl plik

@ -36,8 +36,6 @@ import freetype
MINCHAR = 32 # Ordinal values of default printable ASCII set MINCHAR = 32 # Ordinal values of default printable ASCII set
MAXCHAR = 126 # 94 chars MAXCHAR = 126 # 94 chars
# By default there will be 94 ASCII characters + the default char in element[0]
ASSUME_SPARSE = MAXCHAR - MINCHAR + 1
# UTILITIES FOR WRITING PYTHON SOURCECODE TO A FILE # UTILITIES FOR WRITING PYTHON SOURCECODE TO A FILE
@ -356,23 +354,27 @@ class Font(dict):
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))
for n, char in enumerate(self.charset): # self.charset is contiguous with chars having ordinal values in the
# n = 1 + ord(char) - ord(smallest char in set) # inclusive range specified. Where the specified character set has gaps
# Build normal index for default char + 1st 94 chars. Efficient for # missing characters are empty strings.
# ASCII set. # Charset includes default char and both max and min chars, hence +2.
if n <= ASSUME_SPARSE: if len(self.charset) <= MAXCHAR - MINCHAR + 2:
# Build normal index. Efficient for ASCII set and smaller as
# entries are 2 bytes.
for char in self.charset:
if char == '': if char == '':
index += bytearray((0, 0)) index += bytearray((0, 0))
else: else:
index += (len(data)).to_bytes(2, byteorder='little') # Start index += (len(data)).to_bytes(2, byteorder='little') # Start
append_data(data, char) append_data(data, char)
elif char != '': index += (len(data)).to_bytes(2, byteorder='little') # End
# Build sparse index. Entries are 4 bytes but only populated if else:
# the char is in the charset. # Sparse index. Entries are 4 bytes but only populated if the char
# is in the charset.
for char in [c for c in self.charset if c]:
sparse += ord(char).to_bytes(2, byteorder='little') sparse += ord(char).to_bytes(2, byteorder='little')
sparse += (len(data)).to_bytes(2, byteorder='little') # Start sparse += (len(data)).to_bytes(2, byteorder='little') # Start
append_data(data, char) append_data(data, char)
index += (len(data)).to_bytes(2, byteorder='little') # End
return data, index, sparse return data, index, sparse
def build_binary_array(self, hmap, reverse, sig): def build_binary_array(self, hmap, reverse, sig):
@ -384,8 +386,8 @@ class Font(dict):
return data return data
# PYTHON FILE WRITING # PYTHON FILE WRITING
# Owing to sparse charsets and an index which only holds the atart of data, # The index only holds the start of data so can't read next_offset but must
# can't read next_offset but must calculate it # calculate it.
STR01 = """# Code generated by font-to-py.py. STR01 = """# Code generated by font-to-py.py.
# Font: {}{} # Font: {}{}
@ -394,49 +396,43 @@ version = '0.3'
""" """
# Code emitted for charsets comprising <= 95 chars (including default) # Code emitted for charsets spanning a small range of ordinal values
STR02 = """_mvfont = memoryview(_font) STR02 = """_mvfont = memoryview(_font)
def get_ch(ch): def get_ch(ch):
ordch = ord(ch) oc = ord(ch)
if ordch >= {0} and ordch <= {1}: idx_offs = 2 * (oc - {0} + 1) if oc >= {0} and oc <= {1} else 0
idx_offs = 2 * (ordch - {0} + 1)
else:
idx_offs = 0
offset = int.from_bytes(_index[idx_offs : idx_offs + 2], 'little') offset = int.from_bytes(_index[idx_offs : idx_offs + 2], 'little')
width = int.from_bytes(_font[offset:offset + 2], 'little') width = int.from_bytes(_font[offset:offset + 2], 'little')
""" """
# Code emiited for large charsets, assumed by build_arrays() to be sparse # Code emiited for large charsets, assumed by build_arrays() to be sparse
# Binary sort of sparse index.
STRSP = """_mvfont = memoryview(_font) STRSP = """_mvfont = memoryview(_font)
_mvsp = memoryview(_sparse) _mvsp = memoryview(_sparse)
def bins(lst, val): def bs(lst, val):
n = len(lst) // 4 n = len(lst) // 4
if n == 1: if n == 1:
v = int.from_bytes(lst[: 2], 'little') v = int.from_bytes(lst[: 2], 'little')
return int.from_bytes(lst[2 : 4], 'little') if v == val else 0 return int.from_bytes(lst[2 : 4], 'little') if v == val else 0
sp = (n // 2) * 4 sp = (n // 2) * 4
res = bins(lst[: sp], val) res = bs(lst[: sp], val)
return res if res else bins(lst[sp :], val) return res if res else bs(lst[sp :], val)
def get_ch(ch): def get_ch(ch):
ordch = ord(ch) offset = bs(_mvsp, ord(ch))
if ordch < {1}:
idx_offs = 2 * (ordch - {0} + 1) if ordch >= {0} else 0
offset = int.from_bytes(_index[idx_offs : idx_offs + 2], 'little')
else:
offset = bins(_mvsp, ordch)
width = int.from_bytes(_font[offset : offset + 2], 'little') width = int.from_bytes(_font[offset : offset + 2], 'little')
""" """
# Code emitted for horizontally mapped fonts.
STR02H =""" STR02H ="""
next_offs = offset + 2 + ((width - 1)//8 + 1) * {0} next_offs = offset + 2 + ((width - 1)//8 + 1) * {0}
return _mvfont[offset + 2:next_offs], {0}, width return _mvfont[offset + 2:next_offs], {0}, width
""" """
# Code emitted for vertically mapped fonts.
STR02V =""" STR02V ="""
next_offs = offset + 2 + (({0} - 1)//8 + 1) * width next_offs = offset + 2 + (({0} - 1)//8 + 1) * width
return _mvfont[offset + 2:next_offs], {0}, width return _mvfont[offset + 2:next_offs], {0}, width
@ -460,6 +456,7 @@ def write_font(op_path, font_path, height, monospaced, hmap, reverse, minchar, m
return False return False
return True return True
# Extra code emitted where -i is specified.
STR03 = ''' STR03 = '''
def glyphs(): def glyphs():
for c in """{}""": for c in """{}""":
@ -489,15 +486,15 @@ def write_data(stream, fnt, font_path, hmap, reverse, iterate):
bw_font = ByteWriter(stream, '_font') bw_font = ByteWriter(stream, '_font')
bw_font.odata(data) bw_font.odata(data)
bw_font.eot() bw_font.eot()
bw_index = ByteWriter(stream, '_index')
bw_index.odata(index)
bw_index.eot()
if sparse: # build_arrays() has returned a sparse index if sparse: # build_arrays() has returned a sparse index
bw_sparse = ByteWriter(stream, '_sparse') bw_sparse = ByteWriter(stream, '_sparse')
bw_sparse.odata(sparse) bw_sparse.odata(sparse)
bw_sparse.eot() bw_sparse.eot()
stream.write(STRSP.format(minchar, minchar + ASSUME_SPARSE, len(sparse))) stream.write(STRSP)
else: else:
bw_index = ByteWriter(stream, '_index')
bw_index.odata(index)
bw_index.eot()
stream.write(STR02.format(minchar, maxchar)) stream.write(STR02.format(minchar, maxchar))
if hmap: if hmap:
stream.write(STR02H.format(height)) stream.write(STR02H.format(height))