Completed
Push — master ( 1e7b7c...87d517 )
by Kyle
01:04
created

Glyph.crop()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
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