-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathtilemapper.py
451 lines (323 loc) · 13.7 KB
/
tilemapper.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
#!/usr/bin/python
from PIL import Image, ImageOps
import hashlib
import ctypes
import argparse
import math
from pathlib import Path
VERSION = '0.9.3'
DESCRIPTION = 'CIQ tilemapper {0} (c) 2017-2019 Franco Trimboli'.format(VERSION)
# globals
TILE_SIZE = 24
SCREEN_SIZE = 240
MAX_CHARS = 576
tileTable = []
hashTable = {}
charTable = []
charsToIgnore = [141]
charCurrent = 0
charOffset = 32
fontTileX = 0
fontTileY = 0
# command line defaults
debug = False
rotate = True
destinationResolution = 240
destinationFilename = 'destfont'
sourceFilename = 'souce.png'
def main():
global tileTable
global TILE_SIZE
global destinationResolution
global destinationFilename
global debug
args = parseArgs()
if args.debug:
debug = True
# header
print(DESCRIPTION)
destinationResolution = abs(args.resolution)
destinationFilename = args.output
# Define the mode we are in, and target resolution
print('Tile mode is {0}'.format(args.mode))
print('Target resolution {0}x{0}'.format(destinationResolution))
# number of chars we will use for our fontmap, 24x24 in this case
totalCharsForTilemap = 24
TILE_SIZE = args.tile_size
# create a font canvas to store the processed tiles
fontCanvas = newCanvas(TILE_SIZE*totalCharsForTilemap,TILE_SIZE*totalCharsForTilemap)
# process input files
filesToProcess = args.input
if len(filesToProcess) > 1:
print('Batch process {0} files'.format(len(filesToProcess)))
# let's iterate through all the source files one by one
for sourceFilename in filesToProcess:
print('Input file to process "{0}"'.format(sourceFilename))
# attempt to load the source image, and process it
canvas = loadPNG(sourceFilename)
if canvas:
# print the details of the source image
detailsCanvas(canvas)
sanitiseCanvas(canvas)
# we pre-process the canvas to prep it for processing
canvas = preprocessCanvas(canvas)
if args.mode == 'rotate':
# angle function
angleStart,angleStop,angleSteps = args.angle_begin, args.angle_end, args.angle_step
processAngle(canvas,fontCanvas,angleStart,angleStop,angleSteps)
else:
# single frame function
canvas = scaleCanvas(canvas,destinationResolution)
processFrames(canvas,fontCanvas)
print("\nDone")
# have we processed any tiles, then save the destination font files
if len(tileTable) > 0:
filename = destinationFilename
try:
# save datafile, includes the JSON data for the tiles
dataFile = generateDataFile()
f = open(destinationFilename+'.json', 'w')
f.write(str(dataFile))
f.close()
# save font description file
fontFile = generateFontFile(destinationFilename)
f = open(destinationFilename+'.fnt', 'w')
f.write(str(fontFile))
f.close()
# save final tilemap image
fontCanvas.save(filename+'.png', "PNG")
print('Output saved as "{0}.fnt", "{0}.json", "{0}.png"'.format(filename))
except Exception:
print('ERROR: Cannot save files')
def parseArgs():
global args
global DESCRIPTION
parser = argparse.ArgumentParser(description=DESCRIPTION)
parser.add_argument('-i','--input', nargs='+', required=True, help='input png file(s)')
parser.add_argument('-o','--output', type=str, required=True, help='output file (png,fnt,json)')
parser.add_argument("-m",'--mode', choices=['static', 'rotate'], default='static', type=str, help='\'static\' will process one or more static frames. \'rotate\' will rotate a frame')
parser.add_argument("-r",'--resolution', default=240, type=int, help='resolution of target device')
parser.add_argument("-d",'--debug', type=bool, nargs='?', const=True, default=False, help='turn debug mode on')
parser.add_argument("-u",'--angle-begin', type=int, help='begin angle (rotate mode)')
parser.add_argument("-v",'--angle-end', type=int, help='end angle (rotate mode)')
parser.add_argument("-s",'--angle-step', type=int, help='step between angles (rotate mode)')
parser.add_argument("-t",'--tile-size', type=int, default=24, help='tile size (experimental)')
args = parser.parse_args()
if (args.mode == 'rotate' and
not (isinstance(args.angle_begin,int) and
isinstance(args.angle_end,int) and
isinstance(args.angle_step,int)
)
):
parser.error('The --mode (-m) argument in \'rotate\' requires the --angle-begin, --angle-end, and --angle-step arguments')
return args
# process a single frame into tiles
def processFrames(canvas,fontCanvas):
global TILE_SIZE
numberOfFrames = 1
currentFrame = 0
tileArray = processTiles(canvas,TILE_SIZE,fontCanvas)
tileTable.append(tileArray)
currentFrame += 1;
progress = '{:2.1%}'.format(currentFrame / numberOfFrames)
print("Progress: {0}, frames: {1}, tiles: {2}".format(progress,len(tileTable),len(hashTable)), end="\r")
if debug:
print(tileTable)
return
# rotate and process a multile frames into tiles
def processAngle(canvas,fontCanvas, angleStart, angleStop, angleSteps):
global TILE_SIZE
global destinationResolution
numberOfFrames = len(range(angleStart, angleStop+angleSteps, angleSteps))
print('{0} frame(s) to process '.format(numberOfFrames))
currentFrame = 0
for angle in range(angleStart, angleStop+angleSteps, angleSteps):
currentCanvas = canvas.rotate(angle, Image.BICUBIC)
currentCanvas = scaleCanvas(currentCanvas,destinationResolution)
tileArray = processTiles(currentCanvas,TILE_SIZE,fontCanvas)
tileTable.append(tileArray)
currentFrame += 1;
progress = '{:2.1%}'.format(currentFrame / numberOfFrames)
print("Progress: {0}, frames: {1}, tiles: {2}".format(progress,len(tileTable),len(hashTable)), end="\r")
if debug:
print(tileTable)
return
# display the details of the current image
def detailsCanvas(canvas):
print('Format is {0}, resolution {1}x{2}, mode {3}'.format(canvas.format, canvas.size[0], canvas.size[1], canvas.mode))
return
# load a png, store into a canvas
def loadPNG(filename):
try:
img = Image.open(filename)
if debug:
print('[loadPNG] loaded "{0}"'.format(filename))
return img
except:
print('ERROR: Couldn\'t load "{0}" error...'.format(filename))
return False
# crop image to smallest dimension to make it square
def preprocessCanvas(canvas):
canvas = invertCanvas(canvas)
return canvas
# TODO: check that the image is square, at least
def sanitiseCanvas(canvas):
return
# scale image to destination
def scaleCanvas(canvas,newsize):
if (canvas.size[0] == newsize):
return canvas
else:
return canvas.resize((newsize,newsize), Image.LANCZOS)
# invert the canvas as per the font spec
def invertCanvas(canvas):
if canvas.mode == 'RGBA':
r,g,b,a = canvas.split()
rgb_image = Image.merge('RGB', (r,g,b))
inverted_image = ImageOps.invert(rgb_image)
else:
inverted_image = ImageOps.invert(canvas)
if debug:
print(canvas.format, canvas.size, canvas.mode)
return inverted_image
# send a valid tile to the font canvas
def pushToFontTiles(canvas,data,x,y):
canvas.paste(data,(x,y))
return
# check the tile for data, compare or store the hash
def checkTileForData(canvas):
try:
w = canvas.size[0]
h = canvas.size[1]
data = []
hasData = 0;
for u in range(w):
for v in range(h):
pixels = canvas.getpixel((u,v))
data.append(pixels)
hasData = hasData + sum(pixels[:3])
# we hash the tile to check for duplicates
if hasData:
data_md5 = hashlib.md5(str(data).encode()).hexdigest()
return data_md5
else:
return False
except:
return False
# fetch the next tile from the source canvas
def fetchTile(canvas,x,y):
try:
extents = (x*TILE_SIZE, y*TILE_SIZE, (x*TILE_SIZE)+TILE_SIZE, (y*TILE_SIZE)+TILE_SIZE)
tile = canvas.crop(extents)
return tile
except:
print('Problem fetching a tile...')
return False
# TODO: check if tile already exists
def checkHashArray():
return
# generate the .fnt file
def generateFontFile(filename):
global hashTable
global TILE_SIZE
hashList = []
for item in hashTable.values():
hashList.append(item)
sortedHash = sorted(hashList, key=lambda k: k['char'])
lines = 'info face={0} size={1} bold=0 italic=0 charset=ascii unicode=0 stretchH=100 smooth=1 aa=1 padding=0,0,0,0 spacing=0,0 outline=0\ncommon lineHeight={2} base={3} scaleW=256 scaleH=256 pages=1 packed=0\npage id=0 file="{4}.png"\nchars count={5}\n'.format(Path(filename).name,TILE_SIZE,TILE_SIZE,TILE_SIZE,Path(filename).name,len(sortedHash))
for item in sortedHash:
lines = lines + 'char id={0} x={1} y={2} width={3} height={4} xoffset=0 yoffset=0 xadvance={5} page=0 chnl=15\n'.format(str(item['char']),str(item['xc']*TILE_SIZE),str(item['yc']*TILE_SIZE),str(TILE_SIZE),str(TILE_SIZE),str(TILE_SIZE))
return lines
# generate the .json file
def generateDataFile():
global tileTable
global TILE_SIZE
groupData = []
for group in tileTable:
groupList = []
for tile in group:
packed = packData(tile['x'],tile['y'],tile['char'])
groupList.append(packed)
groupData.append(groupList)
if debug:
print(groupData)
return groupData
# data in the json file is an array of signed 32-bit integers,
# and each integer is bit-packed as follows;
# 0b00000000000000000000111111111111 = font char, 12-bits
# 0b00000000001111111111000000000000 = x pos, 10-bits
# 0b11111111110000000000000000000000 = y pos, 10-bits
def packData(x,y,char):
global TILE_SIZE
dx = x*TILE_SIZE
dy = y*TILE_SIZE
ymask = 0xFFC00000
xmask = 0x003FF000
charmask = 0x00000FFF
u = (char&charmask)|((dx<<12)&xmask)|((dy<<22)&ymask)
return ctypes.c_int32(u).value
# create a new canvas, default colour is grey
def newCanvas(w,h):
canvas = Image.new('RGB', (w, h), color = (128,128,128))
return canvas
# process the current canvas, given the current tileSize, and send to destination fontCanvas
def processTiles(canvas,tileSize,fontCanvas):
global charCurrent
global hashTable
global fontTileX
global fontTileY
global TILE_SIZE
global MAX_CHARS
canvasWidth = canvas.size[0]
canvasHeight = canvas.size[1]
uTiles = math.ceil(canvasWidth / tileSize)
vTiles = math.ceil(canvasHeight / tileSize)
tileArray = []
try:
# let's iterate across the canvas in the x, then y dimension
for y in range(vTiles):
for x in range(uTiles):
# fetch a tile, and generate its hash
currentTile = fetchTile(canvas,x,y)
hashOfTile = checkTileForData(currentTile)
# do we have a valid hash? great, then we have data
if hashOfTile:
if debug:
print('[processTiles] got tile data at x:{0} y:{1} with hash:{2}'.format(x,y,hashOfTile))
thisChar = 0
# does this hash exist in the global hash table? then we have a duplicate
if hashOfTile in hashTable:
if debug:
print('[processTiles] collision with existing hash:{0}'.format(hashOfTile))
thisChar = hashTable[hashOfTile]['char']
thisTile = {'x':x, 'y':y, 'hash':hashOfTile, 'char':thisChar, 'xc':fontTileX, 'yc':fontTileY}
tileArray.append(thisTile)
# if not, looks like this is a new tile, so store it
else:
if debug:
print('[processTiles] appended hash:{0}'.format(hashOfTile))
# increment tile y pos, and start again
if (((fontTileX*TILE_SIZE)) >= fontCanvas.size[0]):
fontTileX = 0
fontTileY += 1
thisChar = charOffset+charCurrent
thisTile = {'x':x, 'y':y, 'hash':hashOfTile, 'char':thisChar, 'xc':fontTileX, 'yc':fontTileY}
tileArray.append(thisTile)
hashTable[hashOfTile] = { 'xc':fontTileX, 'yc':fontTileY, 'char':thisChar}
# let's push this tile to the destination fontCanvas
pushToFontTiles(fontCanvas,currentTile,fontTileX*TILE_SIZE,fontTileY*TILE_SIZE)
# increment the font char, and tile
charCurrent += 1
fontTileX += 1
# is this font char in the array of chars to ignore? then skip
while charCurrent in charsToIgnore:
charCurrent += 1
# have we exceeded the maximum char size? if so, we quit
if charCurrent > (MAX_CHARS):
print('ERROR: number of tiles have exceeded maximum size ({0}). Try breaking your files up, or use a larger tile size.'.format(MAX_CHARS))
quit()
return tileArray
except Exception as e:
print('ERROR: {0}'.format(e))
return False
main()