# bdflib, a library for working with BDF font files
# Copyright (C) 2009-2022, Timothy Allen
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Classes that represent a bitmap font, with its glyphs and metadata.
.. testsetup::
from bdflib.model import *
"""
from typing import Union, Optional, Dict, List, Iterable, Tuple, overload
PropertyValue = Union[bytes, int]
Properties = Dict[bytes, PropertyValue]
[docs]
class GlyphExists(Exception):
"""
Raised when creating a new glyph for a codepoint that already has one.
"""
[docs]
class Glyph(object):
"""
Represents a font glyph and associated properties.
:param name: The name of this glyph, ASCII encoded.
:param data: If provided,
gives the initial bitmap for the glyph,
see the :attr:`data` attribute below.
:param bbX: The left-most edge of the glyph's bounding box, in
pixels.
:param bbY: The bottom-most edge of the glyph's bounding box, in pixels.
:param bbW: The glyph's bounding-box extends this many pixels right of
``bbX`` (must be >= 0).
If `data` is provided,
each integer should be at most this many bits wide.
:param bbH: The glyph's bounding-box extends this many pixels upward
from ``bbY`` (must be >= 0).
If `data` is provided,
it should yield this many rows.
:param advance: After drawing this glyph, the next glyph will be
drawn this many pixels to the right.
:param codepoint: The Unicode codepoint that this glyph represents.
"""
#: How far to the right the next glyph should be drawn, in pixels.
advance: int
#: Each item of the ``.data`` property
#: is a :class:`int` `bbW` bits wide,
#: representing the pixels of a single row.
#: the first item in ``.data`` is
#: the lowest row in the glyph,
#: so that list indices increase in the same
#: direction as pixel coordinates.
#:
#: >>> my_glyph = Glyph(
#: ... name=b"capital A",
#: ... data=[
#: ... 0b10001,
#: ... 0b11111,
#: ... 0b10001,
#: ... 0b01110,
#: ... ],
#: ... bbW=5,
#: ... bbH=4,
#: ... )
#: >>> for row in reversed(my_glyph.data):
#: ... print("{:05b}".format(row))
#: 01110
#: 10001
#: 11111
#: 10001
#:
#: If you want to get the actual coordinates
#: of the glyph's drawn pixels,
#: look at the :meth:`iter_pixels` method.
data: List[int]
def __init__(
self,
name: bytes,
data: Optional[List[int]] = None,
bbX: int = 0,
bbY: int = 0,
bbW: int = 0,
bbH: int = 0,
advance: int = 0,
codepoint: Optional[int] = None,
):
self.name = name
self.bbX = bbX
self.bbY = bbY
self.bbW = bbW
self.bbH = bbH
if data is None:
self.data = []
else:
self.data = data
self.advance = advance
if codepoint is None:
self.codepoint = -1
else:
self.codepoint = codepoint
def __eq__(self, other: object) -> bool:
return (
isinstance(other, Glyph)
and self.name == other.name
and self.bbX == other.bbX
and self.bbY == other.bbY
and self.bbW == other.bbW
and self.bbH == other.bbH
and self.data == other.data
and self.advance == other.advance
)
def __str__(self) -> str:
def padding_char(x: int, y: int) -> str:
if x == 0 and y == 0:
return "+"
elif x == 0:
return "|"
elif y == 0:
return "-"
else:
return "."
# What are the extents of this bitmap, given that we always want to
# include the origin?
bitmap_min_X = min(0, self.bbX)
bitmap_max_X = max(0, self.bbX + self.bbW - 1)
bitmap_min_Y = min(0, self.bbY)
bitmap_max_Y = max(0, self.bbY + self.bbH - 1)
res = []
for y in range(bitmap_max_Y, bitmap_min_Y - 1, -1):
res_row = []
# Find the data row associated with this output row.
if self.bbY <= y < self.bbY + self.bbH:
data_row = self.data[y - self.bbY]
else:
data_row = 0
for x in range(bitmap_min_X, bitmap_max_X + 1):
# Figure out which bit controls (x,y)
bit_number = self.bbW - (x - self.bbX) - 1
# If we're in a cell covered by the bitmap and this particular
# bit is set...
if self.bbX <= x < self.bbX + self.bbW and (
data_row >> bit_number & 1
):
res_row.append("#")
else:
res_row.append(padding_char(x, y))
res.append("".join(res_row))
return "\n".join(res)
[docs]
def get_bounding_box(self) -> Tuple[int, int, int, int]:
"""
Returns the position and dimensions of the glyph's bounding box.
:returns: The left, bottom, width and height of the bounding box, as
passed to the constructor.
"""
return (self.bbX, self.bbY, self.bbW, self.bbH)
[docs]
def merge_glyph(self, other: "Glyph", atX: int, atY: int) -> None:
"""
Draw another glyph onto this one at the given coordinates.
:param other: The other glyph to draw onto this one.
:param atX: The other glyph's origin will be placed at this X
offset in this glyph.
:param atY: The other glyph's origin will be placed at this Y
offset in this glyph.
This glyph's bounding box will be stretch to include the area of
the added glyph, but the :attr:`advance` will not be modified.
"""
# Calculate the new metrics
new_bbX = min(self.bbX, atX + other.bbX)
new_bbY = min(self.bbY, atY + other.bbY)
new_bbW = (
max(self.bbX + self.bbW, atX + other.bbX + other.bbW) - new_bbX
)
new_bbH = (
max(self.bbY + self.bbH, atY + other.bbY + other.bbH) - new_bbY
)
# Calculate the new data
new_data = []
for y in range(new_bbY, new_bbY + new_bbH):
# If the old glyph has a row here...
if self.bbY <= y < self.bbY + self.bbH:
old_row = self.data[y - self.bbY]
# If the right-hand edge of the bounding box has moved right,
# we'll need to left shift the old-data to get more empty space
# to draw the new glyph into.
right_edge_delta = (new_bbX + new_bbW) - (self.bbX + self.bbW)
if right_edge_delta > 0:
old_row <<= right_edge_delta
else:
old_row = 0
# If the new glyph has a row here...
if atY + other.bbY <= y < atY + other.bbY + other.bbH:
new_row = other.data[y - other.bbY - atY]
# If the new right-hand-edge ofthe bounding box
if atX + other.bbX + other.bbW < new_bbX + new_bbW:
new_row <<= (new_bbX + new_bbW) - (
atX + other.bbX + other.bbW
)
else:
new_row = 0
new_data.append(old_row | new_row)
# Update our properties with calculated values
self.bbX = new_bbX
self.bbY = new_bbY
self.bbW = new_bbW
self.bbH = new_bbH
self.data = new_data
[docs]
def get_ascent(self) -> int:
"""
Returns the distance from the Y axis to the highest point of the glyph.
This is zero if no part of the glyph is above the Y axis.
:returns: The ascent of this glyph.
"""
res = self.bbY + self.bbH
# Each empty row at the top of the bitmap should not be counted as part
# of the ascent.
for row in self.data[::-1]:
if row != 0:
break
else:
res -= 1
return res
[docs]
def get_descent(self) -> int:
"""
Returns the distance from the Y axis to the lowest point of the glyph.
This is zero if no part of the glyph is below the Y axis.
:returns: The descent of this glyph.
"""
res = -1 * self.bbY
# Each empty row at the bottom of the bitmap should not be counted as
# part of the descent.
for row in self.data:
if row != 0:
break
else:
res -= 1
return res
[docs]
def iter_pixels(self) -> Iterable[Iterable[bool]]:
"""
Yields the state of pixels within the bounding box.
This method returns an iterable of ``bbH`` rows,
from the top of the glyph (large X values)
to the bottom (small X values).
Each row is an iterable of ``bbW`` booleans,
from left to right.
Each boolean is ``True`` if that pixel should be drawn,
and otherwise ``False``.
Alternatively,
you can obtain the glyph data in BDF format with :meth:`get_data()`,
or access the raw bitmap via the :attr:`data` property.
:returns: the state of each pixel
"""
return (
(bool(row & (1 << self.bbW - x - 1)) for x in range(self.bbW))
for row in (self.data[self.bbH - y - 1] for y in range(self.bbH))
)
[docs]
class Font(object):
"""
Represents the entire font and font-global properties.
:param bytes name: The human-readable name of this font, ASCII encoded.
:param int ptSize: The nominal size of this font in PostScript points (1/72
of an inch).
:param int xdpi: The horizontal resolution of this font in dots-per-inch.
:param int ydpi: The vertical resolution of this font in dots-per-inch.
Instances of this class can be used like :class:`dict` instances.
:class:`bytes` keys refer to the font's properties and are associated with
:class:`bytes` instances, while :class:`int` keys refer to the code-points
the font supports, and are associated with :class:`Glyph` instances.
>>> myfont = Font(
... b"My Font",
... ptSize=12,
... xdpi=96,
... ydpi=96,
... )
>>> myfont.ptSize
12
>>> a_glyph = myfont.new_glyph_from_data(
... b"capital A",
... codepoint=65,
... )
>>> a_glyph == myfont[65]
True
.. note::
Some properties (the name, point-size and resolutions) are required,
and although they can be examined via the ``dict`` interface,
they cannot be removed with the ``del`` statement.
"""
#: All the glyphs in this font,
#: even the ones with no associated codepoint.
glyphs: List[Glyph]
#: The value of the FONT field in the BDF file
name: bytes = b""
#: The font's nominal size in PostScript points (1/72 of an inch),
#: the first value in the SIZE field in the BDF file
ptSize: int
#: The font's horizontal resolution in dots-per-inch,
#: the second value in the SIZE field in the BDF file
xdpi: int
#: The font's vertical resolution in dots-per-inch,
#: the third value in the SIZE field in the BDF file
ydpi: int
def __init__(self, name: bytes, ptSize: int, xdpi: int, ydpi: int) -> None:
"""
Initialise this font object.
"""
self.properties: Properties = {}
self.name = name
self.ptSize = ptSize
self.xdpi = xdpi
self.ydpi = ydpi
self.glyphs = []
self.glyphs_by_codepoint: Dict[int, Glyph] = {}
self.comments: List[bytes] = []
def __setitem__(self, name: bytes, value: PropertyValue) -> None:
self.properties[name] = value
@overload
def __getitem__(self, key: bytes) -> PropertyValue: ...
@overload
def __getitem__(self, key: int) -> Glyph: ...
def __getitem__(
self, key: Union[bytes, int]
) -> Union[PropertyValue, Glyph]:
if isinstance(key, bytes):
return self.properties[key]
elif isinstance(key, int):
return self.glyphs_by_codepoint[key]
def __delitem__(self, key: Union[bytes, int]) -> None:
if isinstance(key, bytes):
del self.properties[key]
elif isinstance(key, int):
g = self.glyphs_by_codepoint[key]
self.glyphs.remove(g)
del self.glyphs_by_codepoint[key]
def __contains__(self, key: object) -> bool:
if isinstance(key, bytes):
return key in self.properties
elif isinstance(key, int):
return key in self.glyphs_by_codepoint
else:
return False
@overload
def get(self, key: bytes) -> Optional[PropertyValue]: ...
@overload
def get(self, key: int) -> Optional[Glyph]: ...
[docs]
def get(
self, key: Union[bytes, int]
) -> Optional[Union[PropertyValue, Glyph]]:
if isinstance(key, bytes):
return self.properties.get(key)
elif isinstance(key, int):
return self.glyphs_by_codepoint.get(key)
[docs]
def new_glyph_from_data(
self,
name: bytes,
data: Optional[List[int]] = None,
bbX: int = 0,
bbY: int = 0,
bbW: int = 0,
bbH: int = 0,
advance: int = 0,
codepoint: Optional[int] = None,
) -> Glyph:
"""
Add a new :class:`Glyph` to this font.
This method's arguments are passed to the :class:`Glyph` constructor.
If you include the ``codepoint`` parameter, the codepoint will be
included in the result of :meth:`codepoints` and you will be able
to look up the glyph by codepoint later.
Otherwise, the glyph will only be available
via the :attr:`glyphs` property.
:returns: the newly-created Glyph
:raises GlyphExists: if an existing glyph is already associated with
the requested codepoint.
"""
g = Glyph(name, data, bbX, bbY, bbW, bbH, advance, codepoint)
if codepoint is not None and codepoint >= 0:
if codepoint in self.glyphs_by_codepoint:
old_glyph = self.glyphs_by_codepoint[codepoint]
if g == old_glyph:
# We've already got one! It's very nice.
return old_glyph
else:
# Two different glyphs with the same codepoint?
# Must be a mistake.
raise GlyphExists(
"A glyph already exists for codepoint %r" % codepoint
)
else:
# This is a genuinely new glyph, let's use it.
self.glyphs_by_codepoint[codepoint] = g
self.glyphs.append(g)
return g
[docs]
def copy(self) -> "Font":
"""
Returns a deep copy of this font.
The new font, along with all of its properties and glyphs, may be
modified without affecting this font.
:returns: A new, independent copy of this Font
"""
# Create a new font object.
res = Font(self.name, self.ptSize, self.xdpi, self.ydpi)
# Copy the comments across.
for c in self.comments:
res.add_comment(c)
# Copy the properties across.
for p in self.properties:
res[p] = self[p]
# Copy the glyphs across.
for g in self.glyphs:
res.new_glyph_from_data(
g.name,
g.data,
g.bbX,
g.bbY,
g.bbW,
g.bbH,
g.advance,
g.codepoint,
)
return res
[docs]
def property_names(self) -> Iterable[bytes]:
"""
Returns the names of this font's properties.
These names can be used with the regular dict syntax to retrieve the
associated value.
:returns: Property names
"""
return self.properties.keys()
[docs]
def codepoints(self) -> Iterable[int]:
"""
Returns the codepoints that this font has glyphs for.
These codepoints can be used with the regular dict syntax to retrieve
the associated glyphs
:returns: Supported codepoints
"""
return self.glyphs_by_codepoint.keys()