muinaisosoitin/bdf_extract.py
Juhani Krekelä 7bad58c7f4 Create new cursors for the missing CSS cursor names
- vertical-text, ew-resize, and nesw-resize are trivial modifications of
  xterm, double_arrow, and sizing respectively
- context-menu and progress are based on left_ptr
- not-allowed is based on circle
- zoom-in and zoom-out are based on dot
- col-resize and row-resize are inspired by based_arrow_{up,down}
- alias and copy were made with an attempt to emulate the visual style
  of some of the weirder X11 cursors
2025-10-17 01:26:04 +03:00

161 lines
4.8 KiB
Python

#!/usr/bin/env python3
import argparse
import sys
parser = argparse.ArgumentParser()
modegroup = parser.add_mutually_exclusive_group(required = True)
modegroup.add_argument('--bitmap', help='extract the bitmap data', action='store_true')
modegroup.add_argument('--cursor-config', help='create xcursorgen(1) configuration file', action='store_true')
parser.add_argument('-i', metavar='FILE', action='append', required=True, help='.bdf file to search for cursors in')
parser.add_argument('cursor', help='the name of the cursor to extract')
parser.add_argument('out', help='PAM file with graphics data')
args = parser.parse_args()
def read_glyphs(f):
ignored_lines = (
b'STARTFONT',
b'COMMENT',
b'CONTENTVERSION',
b'FONT',
b'SIZE',
b'FONTBOUNDINGBOX',
b'METRICSSET',
b'SWIDTH',
b'DWIDTH',
b'SWIDTH1',
b'DWIDTH1',
b'VVECTOR',
b'CHARS',
b'ENCODING',
b'ENDFONT'
)
glyphs = {}
in_properties = False
in_bitmap = False
glyph_name = b''
bitmap = []
for line in f.readlines():
fields = line.split()
# Ignore the properties, as they're not needed and make parsing
# more annoying
if in_properties and fields[0] != b'ENDPROPERTIES':
continue
# Also ignore the lines that don't have data we care about
elif fields[0] in ignored_lines:
continue
match fields[0]:
case b'STARTPROPERTIES':
assert len(fields) == 2
in_properties = True
case b'ENDPROPERTIES':
assert len(fields) == 1
in_properties = False
case b'STARTCHAR':
assert glyph_name == b''
glyph_name, = fields[1:]
case b'BBX':
glyph_width, glyph_height, x_offset, y_offset = map(int, fields[1:])
case b'BITMAP':
assert glyph_name
assert len(fields) == 1
in_bitmap = True
case b'ENDCHAR':
assert glyph_name
# BDF coördinates have y increase upwards
bitmap.reverse()
glyphs[glyph_name] = (glyph_width, glyph_height, x_offset, y_offset, bitmap)
glyph_name = b''
bitmap = []
in_bitmap = False
case _:
assert in_bitmap
data, = fields
pixels = []
for char in data.decode():
value = int(char, 16)
pixels.append(value >> 3)
pixels.append((value >> 2) & 1)
pixels.append((value >> 1) & 1)
pixels.append(value & 1)
bitmap.append(pixels)
return glyphs
for fontfile in args.i:
with open(fontfile, 'rb') as f:
glyphs = read_glyphs(f)
if args.cursor.encode() in glyphs:
break
else:
print(f'Error: cursor {args.cursor} not found', file=sys.stderr)
sys.exit(1)
cursor_width, cursor_height, cursor_min_x, cursor_min_y, cursor_bitmap = glyphs[args.cursor.encode()]
mask_width, mask_height, mask_min_x, mask_min_y, mask_bitmap = glyphs[args.cursor.encode() + b'_mask']
# These intervals are half-open, so max_* is actually one more than the
# highest value that axis will have
cursor_max_x = cursor_min_x + cursor_width
cursor_max_y = cursor_min_y + cursor_height
mask_max_x = mask_min_x + mask_width
mask_max_y = mask_min_y + mask_height
if args.bitmap:
combined_bitmap = []
for y in range(mask_min_y, mask_max_y):
row = []
for x in range(mask_min_x, mask_max_x):
alpha = mask_bitmap[y - mask_min_y][x - mask_min_x]
value = 0
if cursor_min_x <= x < cursor_max_x and cursor_min_y <= y < cursor_max_y:
value = cursor_bitmap[y - cursor_min_y][x - cursor_min_x]
row.append((value, alpha))
combined_bitmap.append(row)
# PAM has y increase downwards
combined_bitmap.reverse()
with open(args.out, 'wb') as f:
f.write(b'P7\nWIDTH %i\nHEIGHT %i\nDEPTH 2\nMAXVAL 255\nTUPLTYPE GRAYSCALE_ALPHA\nENDHDR\n' % (len(combined_bitmap[0]), len(combined_bitmap)))
for row in combined_bitmap:
for value, alpha in row:
f.write((b'\xff', b'\x00')[value])
f.write((b'\x00', b'\xff')[alpha])
elif args.cursor_config:
# We need to give the hotspot coördinate in a different coördinate
# space. BDF has it always at (0, -1) right under the baseline and
# the bounds of the image can change arbitrarily, while PAM locks
# the top-left corner to (0, 0)
# The x-coördinate of 0 in PAM corresponds to mask_min_x in BDF
# Therefore the x-coördinate of the (0, -1) point in BDF is at
# -mask_min_x in the PAM image
hotspot_x = -mask_min_x
# The y axis of the PAM file is flipped compared to the BDF, so
# the top left corner actually corresponds to the bottom left in the
# source data
# Like how BDF's (0, -1) is -mask_min_x pixels to the right from the
# left edge in the PAM, it is -1 - mask_min_y pixels up from the
# bottom edge
# The bottom row of pixels is located at
# height - 1
# = mask_max_y - mask_min_y - 1
# Subtracting -1 - mask_min_y from that
# mask_max_y - mask_min_y - 1 - (-1 - mask_min_y)
# = mask_max_y - mask-min-y - 1 + 1 + mask_min_y
# = mask_max_y
hotspot_y = mask_max_y
# Going with nominal size 24 because that's the default aiui?
# Maybe should go with 16 instead?
print(f'24 {hotspot_x} {hotspot_y} {args.out}')