- 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
161 lines
4.8 KiB
Python
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}')
|