Files
jameswitzeman.net/fonts/terminus-font/bin/bdf.py
T
2026-05-29 11:30:10 -07:00

310 lines
7.2 KiB
Python

#
# Copyright (C) 2017-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com>
#
# 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 2 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, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
import re
import codecs
from collections import OrderedDict
from enum import IntEnum, unique
import fnutil
# -- Width --
DPARSE_LIMIT = 512
SPARSE_LIMIT = 32000
class Width:
def __init__(self, x, y):
self.x = x
self.y = y
@staticmethod
def parse(name, value, limit):
words = fnutil.split_words(name, value, 2)
return Width(fnutil.parse_dec(name + '.x', words[0], -limit, limit),
fnutil.parse_dec(name + '.y', words[1], -limit, limit))
@staticmethod
def parse_s(name, value):
return Width.parse(name, value, SPARSE_LIMIT)
@staticmethod
def parse_d(name, value):
return Width.parse(name, value, DPARSE_LIMIT)
def __str__(self):
return '%d %d' % (self.x, self.y)
# -- BXX --
class BBX:
def __init__(self, width, height, xoff, yoff):
self.width = width
self.height = height
self.xoff = xoff
self.yoff = yoff
@staticmethod
def parse(name, value):
words = fnutil.split_words(name, value, 4)
return BBX(fnutil.parse_dec('width', words[0], 1, DPARSE_LIMIT),
fnutil.parse_dec('height', words[1], 1, DPARSE_LIMIT),
fnutil.parse_dec('bbxoff', words[2], -DPARSE_LIMIT, DPARSE_LIMIT),
fnutil.parse_dec('bbyoff', words[3], -DPARSE_LIMIT, DPARSE_LIMIT))
def row_size(self):
return (self.width + 7) >> 3
def __str__(self):
return '%d %d %d %d' % (self.width, self.height, self.xoff, self.yoff)
# -- Props --
def skip_comments(line):
return None if line[:7] == b'COMMENT' else line
class Props(OrderedDict):
def __iter__(self):
return self.items().__iter__()
def read(self, input, name, callback=None):
return self.parse(input.read_lines(skip_comments), name, callback)
def parse(self, line, name, callback=None):
if not line or not line.startswith(bytes(name, 'ascii')):
raise Exception(name + ' expected')
value = line[len(name):].lstrip()
self[name] = value
return value if callback is None else callback(name, value)
def set(self, name, value):
self[name] = value if isinstance(value, (bytes, bytearray)) else bytes(str(value), 'ascii')
# -- Base --
class Base:
def __init__(self):
self.props = Props()
self.bbx = None
# -- Char
HEX_BYTES = (48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70)
class Char(Base):
def __init__(self):
Base.__init__(self)
self.code = -1
self.swidth = None
self.dwidth = None
self.data = None
def bitmap(self):
bitmap = ''
row_size = self.bbx.row_size()
for index in range(0, len(self.data), row_size):
bitmap += self.data[index : index + row_size].hex() + '\n'
return bytes(bitmap, 'ascii').upper()
def _read(self, input):
# HEADER
self.props.read(input, 'STARTCHAR')
self.code = self.props.read(input, 'ENCODING', fnutil.parse_dec)
self.swidth = self.props.read(input, 'SWIDTH', Width.parse_s)
self.dwidth = self.props.read(input, 'DWIDTH', Width.parse_d)
self.bbx = self.props.read(input, 'BBX', BBX.parse)
line = input.read_lines(skip_comments)
if line and line.startswith(b'ATTRIBUTES'):
self.props.parse(line, 'ATTRIBUTES')
line = input.read_lines(skip_comments)
# BITMAP
if self.props.parse(line, 'BITMAP'):
raise Exception('BITMAP expected')
row_len = self.bbx.row_size() * 2
self.data = bytearray()
for _ in range(0, self.bbx.height):
line = input.read_lines(skip_comments)
if not line:
raise Exception('bitmap data expected')
if len(line) == row_len:
self.data += codecs.decode(line, 'hex')
else:
raise Exception('invalid bitmap length')
# FINAL
if input.read_lines(skip_comments) != b'ENDCHAR':
raise Exception('ENDCHAR expected')
return self
@staticmethod
def read(input):
return Char()._read(input) # pylint: disable=protected-access
def write(self, output):
for [name, value] in self.props:
output.write_prop(name, value)
output.write_line(self.bitmap() + b'ENDCHAR')
# -- Font --
@unique
class XLFD(IntEnum):
FOUNDRY = 1
FAMILY_NAME = 2
WEIGHT_NAME = 3
SLANT = 4
SETWIDTH_NAME = 5
ADD_STYLE_NAME = 6
PIXEL_SIZE = 7
POINT_SIZE = 8
RESOLUTION_X = 9
RESOLUTION_Y = 10
SPACING = 11
AVERAGE_WIDTH = 12
CHARSET_REGISTRY = 13
CHARSET_ENCODING = 14
CHARS_MAX = 65535
class Font(Base):
def __init__(self):
Base.__init__(self)
self.xlfd = []
self.chars = []
self.default_code = -1
@property
def bold(self):
return b'bold' in self.xlfd[XLFD.WEIGHT_NAME].lower()
@property
def italic(self):
return self.xlfd[XLFD.SLANT] in [b'I', b'O']
@property
def proportional(self):
return self.xlfd[XLFD.SPACING] == b'P'
def _read(self, input):
# HEADER
line = input.read_line()
if self.props.parse(line, 'STARTFONT') != b'2.1':
raise Exception('STARTFONT 2.1 expected')
self.xlfd = self.props.read(input, 'FONT', lambda name, value: value.split(b'-', 15))
if len(self.xlfd) != 15 or self.xlfd[0] != b'':
raise Exception('non-XLFD font names are not supported')
self.props.read(input, 'SIZE')
self.bbx = self.props.read(input, 'FONTBOUNDINGBOX', BBX.parse)
line = input.read_lines(skip_comments)
if line and line.startswith(b'STARTPROPERTIES'):
num_props = self.props.parse(line, 'STARTPROPERTIES', fnutil.parse_dec)
for _ in range(0, num_props):
line = input.read_lines(skip_comments)
if line is None:
raise Exception('property expected')
match = re.fullmatch(br'(\w+)\s+([-\d"].*)', line)
if not match:
raise Exception('invalid property format')
name = str(match.group(1), 'ascii')
value = match.group(2)
if self.props.get(name) is not None:
raise Exception('duplicate property')
if name == 'DEFAULT_CHAR':
self.default_code = fnutil.parse_dec(name, value)
self.props[name] = value
if self.props.read(input, 'ENDPROPERTIES') != b'':
raise Exception('ENDPROPERTIES expected')
line = input.read_lines(skip_comments)
# GLYPHS
num_chars = fnutil.parse_dec('CHARS', self.props.parse(line, 'CHARS'), 1, CHARS_MAX)
for _ in range(0, num_chars):
self.chars.append(Char.read(input))
if next((char.code for char in self.chars if char.code == self.default_code), -1) != self.default_code:
raise Exception('invalid DEFAULT_CHAR')
# FINAL
if input.read_lines(skip_comments) != b'ENDFONT':
raise Exception('ENDFONT expected')
if input.read_line() is not None:
raise Exception('garbage after ENDFONT')
return self
@staticmethod
def read(input):
return Font()._read(input) # pylint: disable=protected-access
def write(self, output):
for [name, value] in self.props:
output.write_prop(name, value)
for char in self.chars:
char.write(output)
output.write_line(b'ENDFONT')