-
Notifications
You must be signed in to change notification settings - Fork 8
/
makedef.py
373 lines (350 loc) · 13.2 KB
/
makedef.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
#!/usr/bin/env python
#
# Copyright (C) 2014 Johannes Schauer <[email protected]>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import os
import struct
import json
from collections import defaultdict
from PIL import Image
import numpy as np
ushrtmax = (1<<16)-1
def encode0(im):
data = ''.join([chr(i) for i in list(im.getdata())])
size = len(data)
return data,size
# greedy RLE
# for each pixel, test which encoding manages to encode most data, then apply
# that encoding and look at the next pixel after the encoded chunk
def encode1(im):
pixels = im.load()
w,h = im.size
data = []
# these function return a tuple of the compressed string and the amount of
# pixels compressed
def rle_comp(x,y):
# find all pixels after the first one with the same color
color = pixels[x,y]
if color == 0xff:
# this color can't be run length encoded
return raw_comp(x,y)
else:
count = 1
for x in range(x+1,w):
if pixels[x,y] == color and count < 255:
count += 1
else:
break
return (struct.pack("<BB", color, count-1), count)
def raw_comp(x,y):
# read pixels until finding finding two consecutive ones with the same color
data = [pixels[x,y]]
for x in range(x+1,w):
color = pixels[x,y]
if color != data[-1] and len(data) < 255:
data.append(color)
else:
break
return (struct.pack("<BB%dB"%len(data), 0xff, len(data)-1, *data), len(data))
for y in range(h):
r = ''
x = 0
while x < w:
rlec, rlel = rle_comp(x, y)
rawc, rawl = raw_comp(x, y)
# the message that managed to encode more is chosen
if rlel > rawl:
r += rlec
x += rlel
else:
r += rawc
x += rawl
data.append(r)
# 4*height bytes for lineoffsets
size = 4*h+sum([len(d) for d in data])
return data,size
def encode23chunk(s,e,pixels,y):
r = ''
if pixels[s,y] < 8:
colors = pixels[s,y]
count = 1
else:
colors = [pixels[s,y]]
count = 0
for x in range(s+1,e):
color = pixels[x,y]
if count > 0:
# rle was started
if color == colors and count < 32:
# same color again, increase count
count+=1
else:
# either new color or maximum length reached, so write current one
r+=struct.pack("<B", (colors<<5) | (count-1))
if color < 7:
# new rle color
colors = color
count = 1
else:
# new non rle color
colors = [color]
count = 0
else:
# non rle was started
if color < 7 or len(colors) > 31:
# new rle color, or maximum length reached so write current non rle
r+=struct.pack("<B", (7<<5) | (len(colors)-1))
r+=struct.pack("<%dB"%len(colors), *colors)
if color < 7:
colors = color
count = 1
else:
colors = [color]
count = 0
else:
# new non rle color, so append it to current
colors.append(color)
# write last color
if count > 0:
# write rle
r+=struct.pack("<B", (colors<<5) | (count-1))
else:
# write non rle
r+=struct.pack("<B", (7<<5) | (len(colors)-1))
r+=struct.pack("<%dB"%len(colors), *colors)
return r
# this is like encode3 but a line is not split into 32 pixel chuncks
# the reason for this might just be that format 2 images are always 32 pixel wide
def encode2(im):
pixels = im.load()
w,h = im.size
data = []
for y in range(h):
data.append(encode23chunk(0,w,pixels,y))
# 2*height bytes for lineoffsets plus two unknown bytes
size = 2*h+2+sum(len(d) for d in data)
return data,size
# this is like encode2 but limited to only encoding blocks of 32 pixels at a time
def encode3(im):
pixels = im.load()
w,h = im.size
data = []
for y in range(h):
res = []
# encode each row in 32 pixel blocks
for i in range(w/32):
res.append(encode23chunk(i*32, (i+1)*32, pixels, y))
data.append(res)
# width/16 bytes per line as offset header
size = (w/16)*h+sum(sum([len(e) for e in d]) for d in data)
return data,size
fmtencoders = [encode0,encode1,encode2,encode3]
def makedef(infile, outdir):
infiles = defaultdict(list)
sig = None
with open(infile) as f:
in_json = json.load(f)
t = in_json["type"]
fmt = in_json["format"]
p = os.path.basename(infile)
p = os.path.splitext(p)[0].lower()
d = os.path.dirname(infile)
outname = os.path.join(outdir,p)+".def"
print "writing to %s"%outname
# sanity checks and fill infiles dict
for seq in in_json["sequences"]:
bid = seq["group"]
for f in seq["frames"]:
im = Image.open(os.path.join(d,f))
fw,fh = im.size
if fmt == 2 and (fw != 32 or fh != 32):
print "format 2 must have width and height 32"
return False
lm,tm,rm,bm = im.getbbox() or (0,0,0,0)
# format 3 has to have width and lm divisible by 32
if fmt == 3 and lm%32 != 0:
# shrink lm to the previous multiple of 32
lm = (lm/32)*32
w,h = rm-lm,bm-tm
if fmt == 3 and w%32 != 0:
# grow rm to the next multiple of 32
w = (((w-1)>>5)+1)<<5
rm = lm+w
im = im.crop((lm,tm,rm,bm))
if im.mode == 'P':
cursig =(fw,fh,im.getpalette())
elif im.mode == 'RGBA':
cursig =(fw,fh,None)
else:
print "input images must be rgba or palette based"
return False
if not sig:
sig = cursig
else:
if sig != cursig:
print "sigs must match - got:"
print sig
print cursig
return False
infiles[bid].append((lm,tm,im))
if len(infiles) == 0:
print "no input files detected"
return False
fw,fh,pal = cursig
numframes = sum(len(l) for l in infiles.values())
# input images were RGB, find a good common palette
if not pal:
# create a concatenation of all images to create a good common palette
concatim = Image.new("RGB",(fw,fh*numframes))
num = 0
for _,l in infiles.items():
for _,_,im in l:
concatim.paste(im, (0,fh*num))
num+=1
# convert that concatenation to a palette image to obtain a good common palette
concatim = concatim.convert("P", dither=None, colors=248, palette=Image.ADAPTIVE)
# concatenate the 248 colors to the 8 special ones
pal = [0x00, 0xff, 0xff, # full transparency
0xff, 0x96, 0xff, # shadow border
0xff, 0x64, 0xff, # ???
0xff, 0x32, 0xff, # ???
0xff, 0x00, 0xff, # shadow body
0xff, 0xff, 0x00, # selection highlight
0xb4, 0x00, 0xff, # shadow body below selection
0x00, 0xff, 0x00, # shadow border below selection
] + concatim.getpalette()[:744]
# convert RGBA images to P images with the common palette
for bid,l in infiles.items():
newl = []
for lm,tm,im in l:
w,h = im.size
if w == 0 or h == 0:
imp = None
else:
# must convert to RGB first for quantize() to work
imrgb = im.convert("RGB")
imp = imrgb.quantize(palette=concatim)
# now shift the colors by 8
pix = np.array(imp)
pix += 8
imp = Image.fromarray(pix)
# now replace full transparency in the original RGBA image with index 0
pixrgba = np.array(im)
alpha = pixrgba[:,:,3]
pix[alpha == 0] = 0
# now replace any half-transpareny with shadow body (index 4)
pix[(alpha > 0) & (alpha < 0xff)] = 4
# TODO: calculate shadow border
# now put the palette with the special colors
imp.putpalette(pal)
newl.append((lm,tm,imp))
infiles[bid] = newl
# encode all images according to the required format
for bid,l in infiles.items():
newl = []
for lm,tm,im in l:
if im:
w,h = im.size
data,size = fmtencoders[fmt](im)
else:
w,h = 0,0
data,size = '',0
newl.append((w,h,lm,tm,data,size))
infiles[bid] = newl
outf = open(outname, "w+")
# write the header
# full width and height are not used and not the same for all frames
# in some defs, so just putting the last known value
outf.write(struct.pack("<IIII", t,fw,fh,len(infiles)))
# write the palette
outf.write(struct.pack("768B", *pal))
# the bid table requires 16 bytes for each bid and 13+4 bytes for each entry
bidtablesize = 16*len(infiles)+sum(len(l)*(13+4) for l in infiles.values())
# the position after the bid table is the header plus palette plus bid table size
curoffset = 16+768+bidtablesize
for bid,l in infiles.items():
# write bid and number of frames
# the last two values have unknown meaning
outf.write(struct.pack("<IIII",bid,len(l),0,0))
# write filenames
for i,_ in enumerate(l):
fn = "%02d_%03d.pcx"%(bid,i)
outf.write(struct.pack("13s", fn))
# write data offsets
for w,h,_,_,data,size in l:
outf.write(struct.pack("<I",curoffset))
# every image occupies size depending on its format plus 32 byte header
curoffset += 32+size
for bid,l in infiles.items():
for w,h,lm,tm,data,size in l:
# size
# format
# full width and full height
# width and height
# left and top margin
if fmt == 0:
outf.write(struct.pack("<IIIIIIii",size,fmt,fw,fh,w,h,lm,tm))
outf.write(data)
elif fmt == 1:
outf.write(struct.pack("<IIIIIIii",size,fmt,fw,fh,w,h,lm,tm))
lineoffs = []
acc = 4*h
for d in data:
lineoffs.append(acc)
acc += len(d)
outf.write(struct.pack("<"+"I"*h, *lineoffs))
for i in data:
outf.write(i)
elif fmt == 2:
outf.write(struct.pack("<IIIIIIii",size,fmt,fw,fh,w,h,lm,tm))
lineoffs = []
acc = 0
for d in data:
offs = acc+2*h+2
if offs > ushrtmax:
print "exceeding max ushort value: %d"%offs
return False
lineoffs.append(offs)
acc += len(d)
outf.write(struct.pack("<%dH"%h, *lineoffs))
outf.write(struct.pack("<BB", 0, 0)) # unknown meaning
for i in data:
outf.write(i)
elif fmt == 3:
outf.write(struct.pack("<IIIIIIii",size,fmt,fw,fh,w,h,lm,tm))
# store the offsets for all 32 pixel blocks
acc = 0
lineoffs = []
for d in data:
for e in d:
offs = acc+(w/16)*h
if offs > ushrtmax:
print "exceeding max ushort value: %d"%offs
return False
lineoffs.append(offs)
acc += len(e)
outf.write(struct.pack("<"+"H"*(w/32)*h, *lineoffs))
for d in data: # line
for e in d: # 32 pixel block
outf.write(e)
return True
if __name__ == '__main__':
import sys
if len(sys.argv) != 3:
print "usage: %s infile.json outdir"%sys.argv[0]
exit(1)
ret = makedef(sys.argv[1], sys.argv[2])
exit(0 if ret else 1)