Compare commits

...

23 commits
master ... pcf

Author SHA1 Message Date
6e57a8b9e9 bitmap_font: Remove use of f-string, not compatible with 5.x mpy-cross 2020-12-08 14:03:33 -06:00
d379f1962e pcf: Remove use of f-string, not compatible with 5.x mpy-cross 2020-12-08 14:02:53 -06:00
383fc039c5 ttf: fix lint error 2020-12-08 13:47:58 -06:00
ceed4c2a88 pcf: black & lint 2020-12-08 11:16:37 -06:00
Jeff Epler
6ae6e07484 bdf: Add ascent, descent properties
This will be used by a future version of adafruit_display_text to avoid
needing to load glyphs from the font to guess these values.
2020-12-08 11:16:37 -06:00
e15c7d1f5c pcf: Optimize memory allocations 2020-12-08 10:59:08 -06:00
84bf5208bb Optimize bitmap fill 2020-12-08 10:49:18 -06:00
e1634e1a59 Fix off by 1 error in encoding index calculation 2020-12-08 10:49:18 -06:00
7dcd40828e Allow alternate specimens 2020-12-08 10:49:18 -06:00
e5d0839aca Add font with kana glyphs for testing 2020-12-08 10:49:18 -06:00
7869ef43be pcf: handle missing glyphs (mapped like code point 0xffff) 2020-12-07 21:57:02 -06:00
3c31cff165 run black 2020-12-07 19:08:24 -06:00
7658511786 Fix number of characters in this font
This font was probably manually trimmed from one with a larger repertoire of code points.

This was fine, except that bdftopcf didn't like it.
2020-12-07 19:07:12 -06:00
2bef623710 Implement loading pcf fonts
A pcf font can be generated from a bdf font using `bdftopcf` from debian/ubuntu package fonts-utils
2020-12-07 19:06:25 -06:00
27648e7ab7 bitmap_font: fix imports, give better error 2020-12-07 19:05:24 -06:00
Scott Shawcroft
51e9a1fa2e
Merge pull request #30 from FoamyGuy/magtag_example
adding MagTag example
2020-11-23 15:16:04 -08:00
foamyguy
8c1fa9b684 a few comments in magtag example 2020-11-22 15:04:12 -06:00
foamyguy
c38baeb932 adding magtag example 2020-11-22 15:01:06 -06:00
foamyguy
7e153547f5
Merge pull request #29 from adafruit/tannewt-patch-1
Fix 2.6.0 lint
2020-09-03 19:33:27 -05:00
Scott Shawcroft
ca48d4afde
Merge pull request #28 from ronfischler/set-changed-during-iteration-fix
Make a copy of the set to iterate over, not the original set we will …
2020-09-02 16:57:41 -07:00
Scott Shawcroft
1f46cc3a68
reorder imports 2020-09-02 16:46:51 -07:00
Scott Shawcroft
977edcbd88
Reorder imports 2020-09-02 16:43:09 -07:00
ronfischler
2501072843 Make a copy of the set to iterate over, not the original set we will be removing items from 2020-09-02 12:06:32 -07:00
12 changed files with 30803 additions and 118 deletions

1
.gitignore vendored
View file

@ -9,5 +9,4 @@ bundles
.eggs
dist
**/*.egg-info
*.pcf
*.ttf

View file

@ -63,6 +63,41 @@ class BDF(GlyphCache):
self.point_size = None
self.x_resolution = None
self.y_resolution = None
self._ascent = None
self._descent = None
@property
def descent(self):
"""The number of pixels below the baseline of a typical descender"""
if self._descent is None:
self.file.seek(0)
while True:
line = self.file.readline()
if not line:
break
if line.startswith(b"FONT_DESCENT "):
self._descent = int(line.split()[1])
break
return self._descent
@property
def ascent(self):
"""The number of pixels above the baseline of a typical ascender"""
if self._ascent is None:
self.file.seek(0)
while True:
line = self.file.readline()
line = str(line, "utf-8")
if not line:
break
if line.startswith("FONT_ASCENT "):
self._ascent = int(line.split()[1])
break
return self._ascent
def get_bounding_box(self):
"""Return the maximum glyph size as a 4-tuple of: width, height, x_offset, y_offset"""
@ -97,7 +132,7 @@ class BDF(GlyphCache):
remaining = code_points
else:
remaining = set(code_points)
for code_point in remaining:
for code_point in remaining.copy():
if code_point in self._glyphs and self._glyphs[code_point]:
remaining.remove(code_point)
if not remaining:

View file

@ -57,11 +57,12 @@ def load_font(filename, bitmap=None):
return bdf.BDF(font_file, bitmap)
if filename.endswith("pcf") and first_four == b"\x01fcp":
import pcf
from . import pcf
return pcf.PCF(font_file)
return pcf.PCF(font_file, bitmap)
if filename.endswith("ttf") and first_four == b"\x00\x01\x00\x00":
import ttf
from . import ttf
return ttf.TTF(font_file)
return None
return ttf.TTF(font_file, bitmap)
raise ValueError("Unknown magic number %r" % first_four)

View file

@ -1,10 +1,51 @@
# pylint: skip-file
# Remove the above when PCF is actually supported.
# The MIT License (MIT)
#
# Copyright © 2020 Jeff Epler for Adafruit Industries LLC
#
# 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.
"""
`adafruit_bitmap_font.pcf`
====================================================
from .glyph_cache import GlyphCache
import displayio
Loads PCF format fonts.
* Author(s): Jeff Epler
Implementation Notes
--------------------
**Hardware:**
**Software and Dependencies:**
* Adafruit CircuitPython firmware for the supported boards:
https://github.com/adafruit/circuitpython/releases
"""
from collections import namedtuple
import gc
import struct
from fontio import Glyph
from .glyph_cache import GlyphCache
_PCF_PROPERTIES = 1 << 0
_PCF_ACCELERATORS = 1 << 1
_PCF_METRICS = 1 << 2
@ -25,139 +66,345 @@ _PCF_BYTE_MASK = 1 << 2 # If set then Most Sig Byte First */
_PCF_BIT_MASK = 1 << 3 # If set then Most Sig Bit First */
_PCF_SCAN_UNIT_MASK = 3 << 4
# https://fontforge.github.io/en-US/documentation/reference/pcf-format/
# https://fontforge.org/docs/techref/pcf-format.html
Table = namedtuple("Table", ("format", "size", "offset"))
Metrics = namedtuple(
"Metrics",
(
"left_side_bearing",
"right_side_bearing",
"character_width",
"character_ascent",
"character_descent",
"character_attributes",
),
)
Accelerators = namedtuple(
"Accelerators",
(
"no_overlap",
"constant_metrics",
"terminal_font",
"constant_width",
"ink_inside",
"ink_metrics",
"draw_direction",
"font_ascent",
"font_descent",
"max_overlap",
"minbounds",
"maxbounds",
"ink_minbounds",
"ink_maxbounds",
),
)
Encoding = namedtuple(
"Encoding", ("min_byte2", "max_byte2", "min_byte1", "max_byte1", "default_char")
)
Bitmap = namedtuple("Bitmap", ("glyph_count", "bitmap_sizes"))
class PCF(GlyphCache):
def __init__(self, f):
"""Loads glyphs from a PCF file in the given bitmap_class."""
def __init__(self, f, bitmap_class):
super().__init__()
self.file = f
self.name = f
f.seek(0)
header, table_count = self.read("<4sI")
self.buffer = bytearray(1)
self.bitmap_class = bitmap_class
_, table_count = self._read("<4sI")
self.tables = {}
for _ in range(table_count):
type, format, size, offset = self.read("<IIII")
self.tables[type] = {"format": format, "size": size, "offset": offset}
print(type)
type_, format_, size, offset = self._read("<IIII")
self.tables[type_] = Table(format_, size, offset)
def read(self, format):
s = struct.calcsize(format)
return struct.unpack_from(format, self.file.read(s))
bitmap_format = self.tables[_PCF_BITMAPS].format
if bitmap_format != 0xE:
raise NotImplementedError("Unsupported format %s" % bitmap_format)
self._accel = self._read_accelerator_tables()
self._encoding = self._read_encoding_table()
self._bitmaps = self._read_bitmap_table()
self._ascent = self._accel.font_ascent
self._descent = self._accel.font_descent
minbounds = self._accel.ink_minbounds
maxbounds = self._accel.ink_maxbounds
width = maxbounds.right_side_bearing - minbounds.left_side_bearing
height = maxbounds.character_ascent + maxbounds.character_descent
self._bounding_box = (
width,
height,
minbounds.left_side_bearing,
-maxbounds.character_descent,
)
@property
def ascent(self):
"""The number of pixels above the baseline of a typical ascender"""
return self._ascent
@property
def descent(self):
"""The number of pixels below the baseline of a typical descender"""
return self._descent
def get_bounding_box(self):
"""Return the maximum glyph size as a 4-tuple of: width, height, x_offset, y_offset"""
return self._bounding_box
def _read(self, format_):
size = struct.calcsize(format_)
if size != len(self.buffer):
self.buffer = bytearray(size)
self.file.readinto(self.buffer)
return struct.unpack_from(format_, self.buffer)
def _seek_table(self, table):
self.file.seek(table.offset)
(format_,) = self._read("<I")
if format_ & _PCF_BYTE_MASK == 0:
raise RuntimeError("Only big endian supported")
return format_
def _read_encoding_table(self):
encoding = self.tables[_PCF_BDF_ENCODINGS]
self._seek_table(encoding)
return Encoding(*self._read(">hhhhh"))
def _read_bitmap_table(self):
bitmaps = self.tables[_PCF_BITMAPS]
format_ = self._seek_table(bitmaps)
(glyph_count,) = self._read(">I")
self.file.seek(bitmaps.offset + 8 + 4 * glyph_count)
bitmap_sizes = self._read(">4I")
return Bitmap(glyph_count, bitmap_sizes[format_ & 3])
def _read_metrics(self, compressed_metrics):
if compressed_metrics:
(
left_side_bearing,
right_side_bearing,
character_width,
character_ascent,
character_descent,
) = self._read("5B")
left_side_bearing -= 0x80
right_side_bearing -= 0x80
character_width -= 0x80
character_ascent -= 0x80
character_descent -= 0x80
attributes = 0
else:
(
left_side_bearing,
right_side_bearing,
character_width,
character_ascent,
character_descent,
attributes,
) = self._read(">5hH")
return Metrics(
left_side_bearing,
right_side_bearing,
character_width,
character_ascent,
character_descent,
attributes,
)
def _read_accelerator_tables(self):
# pylint: disable=too-many-locals
accelerators = self.tables.get(_PCF_BDF_ACCELERATORS)
if not accelerators:
accelerators = self.tables.get(_PCF_ACCELERATORS)
if not accelerators:
raise RuntimeError("Accelerator table missing")
format_ = self._seek_table(accelerators)
has_inkbounds = format_ & _PCF_ACCEL_W_INKBOUNDS
compressed_metrics = False # format_ & _PCF_COMPRESSED_METRICS
(
no_overlap,
constant_metrics,
terminal_font,
constant_width,
ink_inside,
ink_metrics,
draw_direction,
_,
font_ascent,
font_descent,
max_overlap,
) = self._read(">BBBBBBBBIII")
minbounds = self._read_metrics(compressed_metrics)
maxbounds = self._read_metrics(compressed_metrics)
if has_inkbounds:
ink_minbounds = self._read_metrics(compressed_metrics)
ink_maxbounds = self._read_metrics(compressed_metrics)
else:
ink_minbounds = minbounds
ink_maxbounds = maxbounds
return Accelerators(
no_overlap,
constant_metrics,
terminal_font,
constant_width,
ink_inside,
ink_metrics,
draw_direction,
font_ascent,
font_descent,
max_overlap,
minbounds,
maxbounds,
ink_minbounds,
ink_maxbounds,
)
def _read_properties(self):
property_table_offset = self.tables[_PCF_PROPERTIES]["offset"]
self.file.seek(property_table_offset)
(format,) = self.read("<I")
(format_,) = self._read("<I")
if format & _PCF_BYTE_MASK == 0:
if format_ & _PCF_BYTE_MASK == 0:
raise RuntimeError("Only big endian supported")
(nprops,) = self.read(">I")
(nprops,) = self._read(">I")
self.file.seek(property_table_offset + 8 + 9 * nprops)
pos = self.file.tell()
if pos % 4 > 0:
self.file.read(4 - pos % 4)
(string_size,) = self.read(">I")
(string_size,) = self._read(">I")
strings = self.file.read(string_size)
string_map = {}
i = 0
for s in strings.split(b"\x00"):
string_map[i] = s
i += len(s) + 1
for value in strings.split(b"\x00"):
string_map[i] = value
i += len(value) + 1
self.file.seek(property_table_offset + 8)
for _ in range(nprops):
name_offset, isStringProp, value = self.read(">IBI")
name_offset, is_string_prop, value = self._read(">IBI")
if isStringProp:
print(string_map[name_offset], string_map[value])
if is_string_prop:
yield (string_map[name_offset], string_map[value])
else:
print(string_map[name_offset], value)
return None
yield (string_map[name_offset], value)
def load_glyphs(self, code_points):
metadata = True
character = False
code_point = None
rounded_x = 1
bytes_per_row = 1
desired_character = False
current_info = None
current_y = 0
total_remaining = len(code_points)
# pylint: disable=too-many-statements,too-many-branches,too-many-nested-blocks,too-many-locals
if isinstance(code_points, int):
code_points = (code_points,)
elif isinstance(code_points, str):
code_points = [ord(c) for c in code_points]
x, _, _, _ = self.get_bounding_box()
# create a scratch bytearray to load pixels into
scratch_row = memoryview(bytearray((((x - 1) // 32) + 1) * 4))
code_points = sorted(
c for c in code_points if self._glyphs.get(c, None) is None
)
if not code_points:
return
self.file.seek(0)
while True:
line = self.file.readline()
if not line:
break
if line.startswith(b"CHARS "):
metadata = False
elif line.startswith(b"SIZE"):
_, self.point_size, self.x_resolution, self.y_resolution = line.split()
elif line.startswith(b"COMMENT"):
pass
elif line.startswith(b"STARTCHAR"):
# print(lineno, line.strip())
# _, character_name = line.split()
character = True
elif line.startswith(b"ENDCHAR"):
character = False
if desired_character:
self._glyphs[code_point] = current_info
if total_remaining == 0:
return
desired_character = False
elif line.startswith(b"BBX"):
if desired_character:
_, x, y, dx, dy = line.split()
x = int(x)
y = int(y)
dx = int(dx)
dy = int(dy)
current_info["bounds"] = (x, y, dx, dy)
current_info["bitmap"] = displayio.Bitmap(x, y, 2)
elif line.startswith(b"BITMAP"):
if desired_character:
rounded_x = x // 8
if x % 8 > 0:
rounded_x += 1
bytes_per_row = rounded_x
if bytes_per_row % 4 > 0:
bytes_per_row += 4 - bytes_per_row % 4
current_y = 0
elif line.startswith(b"ENCODING"):
_, code_point = line.split()
code_point = int(code_point)
if code_point == code_points or code_point in code_points:
total_remaining -= 1
if code_point not in self._glyphs:
desired_character = True
current_info = {"bitmap": None, "bounds": None, "shift": None}
elif line.startswith(b"DWIDTH"):
if desired_character:
_, shift_x, shift_y = line.split()
shift_x = int(shift_x)
shift_y = int(shift_y)
current_info["shift"] = (shift_x, shift_y)
elif line.startswith(b"SWIDTH"):
pass
elif character:
if desired_character:
bits = int(line.strip(), 16)
for i in range(rounded_x):
val = (bits >> ((rounded_x - i - 1) * 8)) & 0xFF
scratch_row[i] = val
current_info["bitmap"]._load_row(
current_y, scratch_row[:bytes_per_row]
)
current_y += 1
elif metadata:
# print(lineno, line.strip())
pass
indices_offset = self.tables[_PCF_BDF_ENCODINGS].offset + 14
bitmap_offset_offsets = self.tables[_PCF_BITMAPS].offset + 8
first_bitmap_offset = self.tables[_PCF_BITMAPS].offset + 4 * (
6 + self._bitmaps.glyph_count
)
metrics_compressed = self.tables[_PCF_METRICS].format & _PCF_COMPRESSED_METRICS
first_metric_offset = (
self.tables[_PCF_METRICS].offset + 6 if metrics_compressed else 8
)
metrics_size = 5 if metrics_compressed else 12
# These will each _tend to be_ forward reads in the file, at least
# sometimes we'll benefit from oofatfs's 512 byte cache and avoid
# excess reads
indices = [None] * len(code_points)
for i, code_point in enumerate(code_points):
enc1 = (code_point >> 8) & 0xFF
enc2 = code_point & 0xFF
if enc1 < self._encoding.min_byte1 or enc1 > self._encoding.max_byte1:
continue
if enc2 < self._encoding.min_byte2 or enc2 > self._encoding.max_byte2:
continue
encoding_idx = (
(enc1 - self._encoding.min_byte1)
* (self._encoding.max_byte2 - self._encoding.min_byte2 + 1)
+ enc2
- self._encoding.min_byte2
)
self.file.seek(indices_offset + 2 * encoding_idx)
(glyph_idx,) = self._read(">H")
if glyph_idx != 65535:
indices[i] = glyph_idx
all_metrics = [None] * len(code_points)
for i, code_point in enumerate(code_points):
index = indices[i]
if index is None:
continue
self.file.seek(first_metric_offset + metrics_size * index)
all_metrics[i] = self._read_metrics(metrics_compressed)
bitmap_offsets = [None] * len(code_points)
for i, code_point in enumerate(code_points):
index = indices[i]
if index is None:
continue
self.file.seek(bitmap_offset_offsets + 4 * index)
(bitmap_offset,) = self._read(">I")
bitmap_offsets[i] = bitmap_offset
# Batch creation of glyphs and bitmaps so that we need only gc.collect
# once
gc.collect()
bitmaps = [None] * len(code_points)
for i in range(len(all_metrics)): # pylint: disable=consider-using-enumerate
metrics = all_metrics[i]
if metrics is not None:
width = metrics.right_side_bearing - metrics.left_side_bearing
height = metrics.character_ascent + metrics.character_descent
bitmap = bitmaps[i] = self.bitmap_class(width, height, 2)
self._glyphs[code_points[i]] = Glyph(
bitmap,
0,
width,
height,
metrics.left_side_bearing,
-metrics.character_descent,
metrics.character_width,
0,
)
for i, code_point in enumerate(code_points):
metrics = all_metrics[i]
if metrics is None:
continue
self.file.seek(first_bitmap_offset + bitmap_offsets[i])
width = metrics.right_side_bearing - metrics.left_side_bearing
height = metrics.character_ascent + metrics.character_descent
bitmap = bitmaps[i]
words_per_row = (width + 31) // 32
buf = bytearray(4 * words_per_row)
start = 0
for _ in range(height):
self.file.readinto(buf)
for k in range(width):
if buf[k // 8] & (128 >> (k % 8)):
bitmap[start + k] = 1
start += width

View file

@ -8,7 +8,7 @@ import struct
class TTF:
def __init__(self, f):
def __init__(self, f, bitmap):
f.seek(0)
self.file = f

View file

@ -8,8 +8,8 @@ bitmap with pixels matching glyphs from a given String
import board
from adafruit_bitmap_font import bitmap_font # pylint: disable=wrong-import-position
import displayio
from adafruit_bitmap_font import bitmap_font
font = bitmap_font.load_font("fonts/Arial-16.bdf")

View file

@ -0,0 +1,44 @@
"""
This example uses addfruit_display_text.label to display text using a custom font
loaded by adafruit_bitmap_font.
Adapted for use on MagTag
"""
import time
import board
from adafruit_display_text import label
from adafruit_bitmap_font import bitmap_font
# use built in display (PyPortal, PyGamer, PyBadge, CLUE, etc.)
# see guide for setting up external displays (TFT / OLED breakouts, RGB matrices, etc.)
# https://learn.adafruit.com/circuitpython-display-support-using-displayio/display-and-display-bus
display = board.DISPLAY
# wait until we can refresh the display
time.sleep(display.time_to_refresh)
# Set text, font, and color
text = "HELLO WORLD\nbitmap_font example"
font = bitmap_font.load_font("fonts/Arial-16.bdf")
color = 0xFFFFFF
background_color = 0x999999
# Create the tet label
text_area = label.Label(
font,
text=text,
color=color,
background_color=background_color,
padding_top=3,
padding_bottom=3,
padding_right=4,
padding_left=4,
)
text_area.line_spacing = 1.0
# Set the location
text_area.x = 20
text_area.y = 20
# Show it and refresh
display.show(text_area)
display.refresh()
while True:
pass

View file

@ -4,8 +4,8 @@ loaded by adafruit_bitmap_font
"""
import board
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text import label
from adafruit_bitmap_font import bitmap_font
display = board.DISPLAY

View file

@ -9,10 +9,13 @@ from adafruit_bitmap_font import bitmap_font # pylint: disable=wrong-import-pos
sys.path.append(os.path.join(sys.path[0], "../test"))
font = bitmap_font.load_font(sys.argv[1])
specimen = "Adafruit CircuitPython" if len(sys.argv) == 2 else sys.argv[2]
_, height, _, dy = font.get_bounding_box()
font.load_glyphs(specimen)
for y in range(height):
for c in "Adafruit CircuitPython":
for c in specimen:
glyph = font.get_glyph(ord(c))
if not glyph:
continue

View file

@ -26,7 +26,7 @@ COPYRIGHT "
_OTF_FONTFILE "arial.ttf"
_OTF_PSNAME "ArialMT"
ENDPROPERTIES
CHARS 3361
CHARS 318
STARTCHAR 0020
ENCODING 32
SWIDTH 270 0

30356
examples/fonts/yasashi24.bdf Normal file

File diff suppressed because it is too large Load diff

Binary file not shown.