kopia lustrzana https://github.com/peterhinch/micropython-font-to-py
232 wiersze
10 KiB
Python
232 wiersze
10 KiB
Python
# Some code adapted from Daniel Bader's work at the following URL
|
|
# https://dbader.org/blog/monochrome-font-rendering-with-freetype-and-python
|
|
# With thanks to Stephen Irons @ironss for various improvements, also to
|
|
# @enigmaniac for ideas around handling `bdf` and `pcf` files.
|
|
|
|
# The MIT License (MIT)
|
|
#
|
|
# Copyright (c) 2016-2023 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.
|
|
|
|
import freetype
|
|
|
|
from .bitmap import Bitmap
|
|
from .glyph import Glyph
|
|
|
|
if freetype.version()[0] < 1:
|
|
print("freetype version should be >= 1. Please see FONT_TO_PY.md")
|
|
|
|
|
|
# A Font object is a dictionary of ASCII chars indexed by a character e.g.
|
|
# myfont['a']
|
|
# Each entry comprises a list
|
|
# [0] A Bitmap instance containing the character
|
|
# [1] The width of the character data including advance (actual data stored)
|
|
# Public attributes:
|
|
# height (in pixels) of all characters
|
|
# width (in pixels) for monospaced output (advance width of widest char)
|
|
class Font(dict):
|
|
def __init__( # noqa: PLR0913
|
|
self, filename, size, minchar, maxchar, monospaced, defchar, charset, bitmapped
|
|
):
|
|
super().__init__()
|
|
self._face = freetype.Face(filename)
|
|
# .crange is the inclusive range of ordinal values spanning the character set.
|
|
self.crange = range(minchar, maxchar + 1)
|
|
self.monospaced = monospaced
|
|
self.defchar = defchar
|
|
# .charset has all defined characters with '' for those in range but undefined.
|
|
# Sort order is increasing ordinal value of the character whether defined or not,
|
|
# except that item 0 is the default char.
|
|
if defchar is None: # Binary font
|
|
self.charset = [chr(ordv) for ordv in self.crange]
|
|
elif charset == "":
|
|
self.charset = [chr(defchar)] + [chr(ordv) for ordv in self.crange]
|
|
else:
|
|
cl = [
|
|
ord(x)
|
|
for x in chr(defchar) + charset
|
|
if self._face.get_char_index(x) != 0
|
|
]
|
|
self.crange = range(min(cl), max(cl) + 1) # Inclusive ordinal value range
|
|
cs = [
|
|
chr(ordv)
|
|
if chr(ordv) in charset and self._face.get_char_index(chr(ordv)) != 0
|
|
else ""
|
|
for ordv in self.crange
|
|
]
|
|
# .charset has an item for all chars in range. '' if unsupported.
|
|
# item 0 is the default char. Subsequent chars are in increasing ordinal value.
|
|
self.charset = [chr(defchar), *cs]
|
|
# Populate self with defined chars only
|
|
self.update(dict.fromkeys([c for c in self.charset if c]))
|
|
self.max_width = (
|
|
self.bmp_dimensions(size) if bitmapped else self.get_dimensions(size)
|
|
)
|
|
self.width = self.max_width if monospaced else 0
|
|
self._assign_values() # Assign values to existing keys
|
|
|
|
def bmp_dimensions(self, height):
|
|
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
|
|
max_ascent = 0
|
|
for char in self.keys():
|
|
glyph = self._glyph_for_character(char)
|
|
max_ascent = max(max_ascent, glyph.ascent)
|
|
max_descent = max(max_descent, 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.height = int(max_ascent + max_descent)
|
|
self._max_ascent = int(max_ascent)
|
|
self._max_descent = int(max_descent)
|
|
print("Requested height", height)
|
|
print("Actual height", self.height)
|
|
print("Max width", max_width)
|
|
print("Max descent", self._max_descent)
|
|
print("Max ascent", self._max_ascent)
|
|
return max_width
|
|
|
|
# n-pass solution to setting a precise height.
|
|
def get_dimensions(self, required_height):
|
|
error = 0
|
|
height = required_height
|
|
npass = 0
|
|
for _ in range(10):
|
|
npass += 1
|
|
height += error
|
|
self._face.set_pixel_sizes(0, height)
|
|
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
|
|
max_ascent = 0
|
|
for char in self.keys():
|
|
glyph = self._glyph_for_character(char)
|
|
max_ascent = max(max_ascent, glyph.ascent)
|
|
max_descent = max(max_descent, glyph.descent)
|
|
# for a few chars e.g. _ glyph.width > glyph.advance_width
|
|
max_width = int(max(max_width, glyph.advance_width, glyph.width))
|
|
|
|
new_error = required_height - (max_ascent + max_descent)
|
|
if (new_error == 0) or (abs(new_error) - abs(error) == 0):
|
|
break
|
|
error = new_error
|
|
self.height = int(max_ascent + max_descent)
|
|
st = "Height set in {} passes. Actual height {} pixels.\nMax character width {} pixels."
|
|
print(st.format(npass + 1, self.height, max_width))
|
|
self._max_ascent = int(max_ascent)
|
|
self._max_descent = int(max_descent)
|
|
return max_width
|
|
|
|
def _glyph_for_character(self, char):
|
|
# Let FreeType load the glyph for the given character and tell it to
|
|
# render a monochromatic bitmap representation.
|
|
assert char != ""
|
|
self._face.load_char(
|
|
char, freetype.FT_LOAD_RENDER | freetype.FT_LOAD_TARGET_MONO
|
|
)
|
|
return Glyph.from_glyphslot(self._face.glyph)
|
|
|
|
def _assign_values(self):
|
|
for char in self.keys():
|
|
glyph = self._glyph_for_character(char)
|
|
# https://github.com/peterhinch/micropython-font-to-py/issues/21
|
|
# Handle negative glyph.left correctly (capital J),
|
|
# also glyph.width > advance (capital K and R).
|
|
if glyph.left >= 0:
|
|
char_width = int(max(glyph.advance_width, glyph.width + glyph.left))
|
|
left = glyph.left
|
|
else:
|
|
char_width = int(max(glyph.advance_width - glyph.left, glyph.width))
|
|
left = 0
|
|
|
|
width = (
|
|
self.width if self.width else char_width
|
|
) # Space required if monospaced
|
|
outbuffer = Bitmap(width, self.height)
|
|
|
|
# The vertical drawing position should place the glyph
|
|
# on the baseline as intended.
|
|
row = self.height - int(glyph.ascent) - self._max_descent
|
|
outbuffer.bitblt(glyph.bitmap, row, left)
|
|
self[char] = [outbuffer, width, char_width]
|
|
|
|
def stream_char(self, char, hmap, reverse):
|
|
outbuffer, _, _ = self[char]
|
|
if hmap:
|
|
gen = outbuffer.get_hbyte(reverse)
|
|
else:
|
|
gen = outbuffer.get_vbyte(reverse)
|
|
yield from gen
|
|
|
|
def build_arrays(self, hmap, reverse):
|
|
data = bytearray()
|
|
index = bytearray()
|
|
sparse = bytearray()
|
|
|
|
def append_data(data, char):
|
|
width = self[char][1]
|
|
data += (width).to_bytes(2, byteorder="little")
|
|
data += bytearray(self.stream_char(char, hmap, reverse))
|
|
|
|
# self.charset is contiguous with chars having ordinal values in the
|
|
# inclusive range specified. Where the specified character set has gaps
|
|
# missing characters are empty strings.
|
|
# Charset includes default char and both max and min chars, hence +2.
|
|
if len(self.charset) <= len(self.crange) + 1:
|
|
# Build normal index. Efficient for ASCII set and smaller as
|
|
# entries are 2 bytes (-> data[0] for absent glyph)
|
|
for char in self.charset:
|
|
if char == "":
|
|
index += bytearray((0, 0))
|
|
else:
|
|
index += (len(data)).to_bytes(2, byteorder="little") # Start
|
|
append_data(data, char)
|
|
index += (len(data)).to_bytes(2, byteorder="little") # End
|
|
else:
|
|
# Sparse index. Entries are 4 bytes but only populated if the char
|
|
# has a defined glyph.
|
|
append_data(data, self.charset[0]) # data[0] is the default char
|
|
for char in sorted(self.keys()):
|
|
sparse += ord(char).to_bytes(2, byteorder="little")
|
|
pad = len(data) % 8
|
|
if pad: # Ensure len(data) % 8 == 0
|
|
data += bytearray(8 - pad)
|
|
try:
|
|
sparse += (len(data) >> 3).to_bytes(2, byteorder="little") # Start
|
|
except OverflowError as err:
|
|
raise ValueError(
|
|
"Total size of font bitmap exceeds 524287 bytes."
|
|
) from err
|
|
append_data(data, char)
|
|
return data, index, sparse
|
|
|
|
def build_binary_array(self, hmap, reverse, sig):
|
|
data = bytearray((0x3F + sig, 0xE7, self.max_width, self.height))
|
|
for char in self.charset:
|
|
width = self[char][2]
|
|
data += bytes((width,))
|
|
data += bytearray(self.stream_char(char, hmap, reverse))
|
|
return data
|