|
1
|
|
|
#!/usr/bin/python |
|
2
|
|
|
""" |
|
3
|
|
|
This script converts bdf files into png Unicode tilesets for use with |
|
4
|
|
|
programs such as libtcod or python-tdl. |
|
5
|
|
|
|
|
6
|
|
|
Requires scipy, numpy, and PIL. Run from the command line. |
|
7
|
|
|
""" |
|
8
|
|
|
|
|
9
|
|
|
from __future__ import division |
|
10
|
|
|
|
|
11
|
|
|
import sys |
|
12
|
|
|
import os |
|
13
|
|
|
|
|
14
|
|
|
import re |
|
15
|
|
|
import math |
|
16
|
|
|
import itertools |
|
17
|
|
|
import glob |
|
18
|
|
|
import argparse |
|
19
|
|
|
import multiprocessing |
|
20
|
|
|
|
|
21
|
|
|
import scipy.ndimage |
|
22
|
|
|
import scipy.misc |
|
23
|
|
|
try: |
|
24
|
|
|
scipy.misc.imsave |
|
25
|
|
|
except AttributeError: |
|
26
|
|
|
raise SystemExit('Must have python PIL installed') |
|
27
|
|
|
import numpy |
|
28
|
|
|
|
|
29
|
|
|
class Glyph: |
|
30
|
|
|
|
|
31
|
|
|
def __init__(self, data, bbox): |
|
32
|
|
|
"Make a new glyph with the data between STARTCHAR and ENDCHAR" |
|
33
|
|
|
if verbose: |
|
34
|
|
|
print(data) |
|
35
|
|
|
# get character index |
|
36
|
|
|
self.encoding = int(re.search('ENCODING ([0-9-]+)', data).groups()[0]) |
|
37
|
|
|
if self.encoding < 0: |
|
38
|
|
|
# I ran into a -1 encoding once, not sure what to do with it |
|
39
|
|
|
self.encoding += 65536 # just put it at the end I guess |
|
40
|
|
|
|
|
41
|
|
|
# get local bbox |
|
42
|
|
|
match = re.search('\nBBX ([0-9-]+) ([0-9-]+) ([0-9-]+) ([0-9-]+)', data) |
|
43
|
|
|
if match: |
|
44
|
|
|
gbbox = [int(i) for i in match.groups()] |
|
45
|
|
|
else: |
|
46
|
|
|
gbbox = bbox |
|
47
|
|
|
self.font_bbox = bbox |
|
48
|
|
|
self.bbox = gbbox |
|
49
|
|
|
self.width, self.height = self.bbox[:2] |
|
50
|
|
|
|
|
51
|
|
|
# get bitmap |
|
52
|
|
|
match = re.search('\nBITMAP *\n([0-9A-F\n]*)', data, re.IGNORECASE) |
|
53
|
|
|
self.bitmap = numpy.empty([self.height, self.width], bool) |
|
54
|
|
|
if self.height == self.width == 0: |
|
55
|
|
|
return |
|
56
|
|
|
for y,hexcode in enumerate(match.groups()[0].split('\n')): |
|
57
|
|
|
for x, bit in self.parseBits(hexcode, self.width): |
|
58
|
|
|
self.bitmap[y,x] = bit |
|
59
|
|
|
|
|
60
|
|
|
self.sizeAdjust() |
|
61
|
|
|
|
|
62
|
|
|
def sizeAdjust(self): |
|
63
|
|
|
"""If the glyph is bigger than the font (because the user set it smaller) |
|
64
|
|
|
this should be able to shorten the size""" |
|
65
|
|
|
font_width, font_height = self.font_bbox[:2] |
|
66
|
|
|
self.width = min(self.width, font_width) |
|
67
|
|
|
self.height = min(self.height, font_height) |
|
68
|
|
|
self.bbox[:2] = self.width, self.height |
|
69
|
|
|
|
|
70
|
|
|
self.crop() |
|
71
|
|
|
|
|
72
|
|
|
def crop(self): |
|
73
|
|
|
self.bitmap = self.bitmap[-self.height:, :self.width] |
|
74
|
|
|
|
|
75
|
|
|
def zoom(self): |
|
76
|
|
|
h, w = self.bitmap.shape |
|
77
|
|
|
zoom = [self.height / h, self.width / w] |
|
78
|
|
|
self.bitmap = scipy.ndimage.zoom(self.bitmap, zoom, output=float) |
|
79
|
|
|
|
|
80
|
|
|
def blit(self, image, x, y): |
|
81
|
|
|
"""blit to the image array""" |
|
82
|
|
|
# adjust the position with the local bbox |
|
83
|
|
|
x += self.font_bbox[2] - self.bbox[2] |
|
84
|
|
|
y += self.font_bbox[3] - self.bbox[3] |
|
85
|
|
|
x += self.font_bbox[0] - self.bbox[0] |
|
86
|
|
|
y += self.font_bbox[1] - self.bbox[1] |
|
87
|
|
|
image[y:y+self.height, x:x+self.width] = self.bitmap * 255 |
|
88
|
|
|
|
|
89
|
|
|
def parseBits(self, hexcode, width): |
|
90
|
|
|
"""enumerate over bits in a line of data""" |
|
91
|
|
|
bitarray = [] |
|
92
|
|
|
for byte in hexcode[::-1]: |
|
93
|
|
|
bits = int(byte, 16) |
|
94
|
|
|
for x in range(4): |
|
95
|
|
|
bitarray.append(bool((2 ** x) & bits)) |
|
96
|
|
|
bitarray = bitarray[::-1] |
|
97
|
|
|
return enumerate(bitarray[:width]) |
|
98
|
|
|
|
|
99
|
|
|
def glyphThreadInit(verbose_): |
|
100
|
|
|
# pass verbose to threads |
|
101
|
|
|
global verbose |
|
102
|
|
|
verbose = verbose_ |
|
103
|
|
|
|
|
104
|
|
|
def glyphThread(args): |
|
105
|
|
|
# split args to Glyph |
|
106
|
|
|
return Glyph(*args) |
|
107
|
|
|
|
|
108
|
|
|
def convert(filename): |
|
109
|
|
|
print('Converting %s...' % filename) |
|
110
|
|
|
bdf = open(filename, 'r').read() |
|
111
|
|
|
|
|
112
|
|
|
# name the output file |
|
113
|
|
|
outfile = os.path.basename(filename) |
|
114
|
|
|
if '.' in outfile: |
|
115
|
|
|
outfile = outfile.rsplit('.', 1)[0] + '.png' |
|
116
|
|
|
|
|
117
|
|
|
# print out comments |
|
118
|
|
|
for comment in re.findall('\nCOMMENT (.*)', bdf): |
|
119
|
|
|
print(comment) |
|
120
|
|
|
# and copyright |
|
121
|
|
|
match = re.search('\n(COPYRIGHT ".*")', bdf) |
|
122
|
|
|
if match: |
|
123
|
|
|
print(match.groups()[0]) |
|
124
|
|
|
|
|
125
|
|
|
# get bounding box |
|
126
|
|
|
match = re.search('\nFONTBOUNDINGBOX ([0-9-]+) ([0-9-]+) ([0-9-]+) ([0-9-]+)', bdf) |
|
127
|
|
|
bbox = [int(i) for i in match.groups()] |
|
128
|
|
|
if args.font_size: |
|
129
|
|
|
bbox = args.font_size + bbox[2:] |
|
130
|
|
|
fontWidth, fontHeight, fontOffsetX, fontOffsetY = bbox |
|
131
|
|
|
print('Font size: %ix%i' % (fontWidth, fontHeight)) |
|
132
|
|
|
print('Font offset: %i,%i' % (fontOffsetX, fontOffsetY)) |
|
133
|
|
|
|
|
134
|
|
|
# generate glyphs |
|
135
|
|
|
pool = multiprocessing.Pool(args.threads, glyphThreadInit, (verbose,)) |
|
136
|
|
|
glyphData = re.findall('\nSTARTCHAR [^\n]*\n(.*?)\nENDCHAR', bdf, re.DOTALL) |
|
137
|
|
|
glyphTotal = len(glyphData) |
|
138
|
|
|
print('Found %i glyphs' % glyphTotal) |
|
139
|
|
|
sys.stdout.write('please wait...') |
|
140
|
|
|
glyphs = pool.map(glyphThread, zip(glyphData, [bbox] * glyphTotal)) |
|
141
|
|
|
|
|
142
|
|
|
print 'done!' |
|
143
|
|
|
|
|
144
|
|
|
# start rendering to an array |
|
145
|
|
|
imgColumns = args.columns |
|
146
|
|
|
imgRows = 65536 // imgColumns |
|
147
|
|
|
print('Generating a %ix%i tileset' % (imgColumns, imgRows)) |
|
148
|
|
|
imgWidth = imgColumns * fontWidth |
|
149
|
|
|
imgHeight = imgRows * fontHeight |
|
150
|
|
|
image = numpy.zeros([imgHeight, imgWidth], 'u1') |
|
151
|
|
|
for glyph in glyphs: |
|
152
|
|
|
y, x = divmod(glyph.encoding, imgColumns) |
|
153
|
|
|
x, y = x * fontWidth, y * fontHeight |
|
154
|
|
|
glyph.blit(image, x, y) |
|
155
|
|
|
|
|
156
|
|
|
# save as png |
|
157
|
|
|
|
|
158
|
|
|
#rgba = numpy.empty([imgHeight, imgWidth, 4]) |
|
159
|
|
|
#rgba[...,...,0] = image |
|
160
|
|
|
#rgba[...,...,1] = image |
|
161
|
|
|
#rgba[...,...,2] = image |
|
162
|
|
|
#rgba[...,...,:3] = 255 |
|
163
|
|
|
#rgba[...,...,3] = image |
|
164
|
|
|
#scipy.misc.imsave(outfile, rgba) |
|
165
|
|
|
|
|
166
|
|
|
scipy.misc.imsave(outfile, image) |
|
167
|
|
|
print('Saved as %s' % outfile) |
|
168
|
|
|
|
|
169
|
|
|
parser = argparse.ArgumentParser(description='Convert *.bdf fonts to *.png tilesets') |
|
170
|
|
|
parser.add_argument('-v', action='store_true', help='Print debug infromation.') |
|
171
|
|
|
parser.add_argument('-c', '--columns', nargs='?', type=int, default=64, help='Number of characters per row.') |
|
172
|
|
|
parser.add_argument('-t', '--threads', nargs='?', type=int, default=None, help='Number of threads to run. Auto-detects by default.') |
|
173
|
|
|
parser.add_argument('-s', '--font-size', nargs=2, metavar=('width', 'height'), type=int, default=None, help='Scale to this font size.') |
|
174
|
|
|
parser.add_argument('file', nargs='+', help='*.bdf files to convert') |
|
175
|
|
|
|
|
176
|
|
|
verbose = False |
|
177
|
|
|
|
|
178
|
|
|
if __name__ == '__main__': |
|
179
|
|
|
args = parser.parse_args() |
|
180
|
|
|
print(args) |
|
181
|
|
|
verbose = args.v |
|
182
|
|
|
for globs in (glob.iglob(arg) for arg in args.file): |
|
183
|
|
|
for filename in globs: |
|
184
|
|
|
convert(filename) |
|
185
|
|
|
|