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
|
|
|
|