main img2tiles.py
  1#!/usr/bin/env python
  2from PIL import Image
  3
  4def gchannel(Val):
  5	return (Val >> 4) & 0xE
  6
  7threshold = 127
  8
  9def get_rgba(im, pixels=None, idx=None, color=None):
 10	A = 255
 11	if color is None:
 12		color = pixels[idx]
 13	if type(color) == int:
 14		R, G, B = im.getpalette()[color*3:color*3+3]
 15	else:
 16		if len(color) == 4:
 17			R, G, B, A = color
 18		elif len(color) == 3:
 19			R, G, B = color
 20		else:
 21			L, A = color
 22			R = G = B = L
 23	return (R, G, B, A)
 24
 25def get_gcolor(im, threshold, pixels=None, idx=None, color=None):
 26	R,G,B,A = get_rgba(im, pixels, idx, color)
 27	if A > threshold:
 28		return (gchannel(R), gchannel(G), gchannel(B))
 29	return 0
 30
 31def get_color_info(im, pixels, rng, threshold, exclude={}):
 32	gencolors = {}
 33	A = 255
 34	pal = None
 35	transparent = False
 36	for idx in rng:
 37		gcolor = get_gcolor(im, threshold, pixels, idx)
 38		if type(gcolor) == tuple:
 39			if not gcolor in exclude:
 40				if gcolor in gencolors:
 41					gencolors[gcolor] += 1
 42				else:
 43					gencolors[gcolor] = 1
 44		else:
 45			transparent = True
 46	glist = [(gencolors[color], color) for color in gencolors]
 47	glist.sort()
 48	glist.reverse()
 49	return glist
 50
 51def get_color_info_both(im, pixels, rng, threshold, exclude={}):
 52	gencolors = {}
 53	A = 255
 54	pal = None
 55	transparent = False
 56	for idx in rng:
 57		gcolor = get_gcolor(im, threshold, pixels, idx)
 58		if type(gcolor) == tuple:
 59			if not gcolor in exclude:
 60				if not gcolor in gencolors:
 61					_,best = best_match(gcolor, (exclude,))
 62					gencolors[gcolor] = (color_dist(gcolor, best), 1)
 63				else:
 64					(dist, count) = gencolors[gcolor]
 65					gencolors[gcolor] = (dist, count+1)
 66		else:
 67			transparent = True
 68	glist = [(gencolors[color][0] * gencolors[color][1], color) for color in gencolors]
 69	glist.sort()
 70	glist.reverse()
 71	
 72	return glist
 73
 74def make_palette(im, trans_thresh, max_global, max_line):
 75	pixels = im.getdata()
 76	(width, height) = im.size
 77	colors = get_color_info(im, pixels, xrange(0, height * width), trans_thresh)
 78	print len(colors), 'distinct 9-bit colors in image'
 79	glob_pal = {}
 80	print 'Static Palette:'
 81	while len(glob_pal) < max_global and len(colors):
 82		idx = len(glob_pal)
 83		(count, color) = colors[0]
 84		print str(idx) + ':', color
 85		glob_pal[color] = idx
 86		colors = get_color_info_both(im, pixels, xrange(0, height * width), trans_thresh, glob_pal)
 87	line_pals = []
 88	if max_global < len(colors):
 89		for line in xrange(0, height):
 90			linestart = line * width
 91			if len(glob_pal):
 92				linecolors = get_color_info_both(im, pixels, xrange(linestart, linestart+width), trans_thresh, glob_pal)
 93			else:
 94				linecolors = get_color_info(im, pixels, xrange(linestart, linestart+width), trans_thresh)
 95			line_pal = {}
 96			while len(line_pal) < max_line and len(linecolors):
 97				(score, color) = linecolors[0]
 98				line_pal[color] = len(line_pal) + max_global
 99				if len(line_pal) < max_line:
100					combo = dict(glob_pal)
101					for color in line_pal:
102						combo[color] = line_pal[color]
103					linecolors = get_color_info_both(im, pixels, xrange(linestart, linestart+width), trans_thresh, combo)
104			#for idx in xrange(0, min(max_line, len(linecolors))):
105			#	(count, color) = linecolors[idx]
106			#	line_pal[color] = idx + max_global
107			line_pals.append(line_pal)
108	return (glob_pal, line_pals, max_global, max_line)
109
110def color_dist(a, b):
111	(ra, ga, ba) = a
112	(rb, gb, bb) = b
113	return (ra-rb)**2 + (ga-gb)**2 + (ba-bb)**2
114
115def best_match(gpixel, pals):
116	bestdist = color_dist((0,0,0), (15, 15, 15))
117	bestpalidx = 0
118	bestcolor = (0,0,0)
119	for i in xrange(0, len(pals)):
120		pal = pals[i]
121		for cur in pal:
122			curdist = color_dist(gpixel, cur)
123			if curdist < bestdist:
124				bestdist = curdist
125				bestpalidx = pal[cur]
126				bestcolor = cur
127	return (bestpalidx, bestcolor)
128
129def trans_image(im, trans_thresh, pal, chunky):
130	(global_pal, line_pals, _, _) = pal
131	pixels = im.getdata()
132	(width, height) = im.size
133	gpixels = []
134	A = 255
135	pal = None
136	x = 0
137	y = 0
138	numchannels = len(im.getbands())
139	offset = 1 if numchannels == 4 or numchannels == 2 else 0
140	for pixel in pixels:
141		if x == width:
142			x = 0
143			y += 1
144			if width % 8 and not chunky:
145				for i in xrange(0, 8-(width%8)):
146					gpixels.append(0)
147		gpixel = get_gcolor(im, trans_thresh, color=pixel)
148		if type(gpixel) == tuple:
149			if gpixel in global_pal:
150				val = global_pal[gpixel]
151			elif gpixel in line_pals[y]:
152				val = line_pals[y][gpixel]
153			else:
154				bestpal,color = best_match(gpixel, (global_pal, line_pals[y]))
155				val = bestpal
156			gpixels.append(offset+val)
157		else:
158			gpixels.append(gpixel)
159		x += 1
160	if width % 8 and not chunky:
161		for i in xrange(0, 8-(width%8)):
162			gpixels.append(0)
163		width += 8-(width%8)
164	if height % 8 and not chunky:
165		for y in xrange(0, 8-(height%8)):
166			for x in xrange(0, width):
167				gpixels.append(0)
168		height += 8-(height%8)
169
170	return (width, height, gpixels)
171
172def appendword(b, word):
173	b.append(word >> 8)
174	b.append(word & 0xff)
175
176def to_tiles(palpix, raw=False, chunky=False, tile_height=8, sprite_order = False):
177	(width, height, pixels) = palpix
178	b = bytearray()
179	if chunky:
180		if not raw:
181			appendword(b, width)
182			appendword(b, height)
183		for pixel in pixels:
184			b.append(pixel)
185	else:
186		cwidth = width/8
187		cheight = height/tile_height
188		words = len(pixels)/4
189		if not raw:
190			appendword(b, words)
191			appendword(b, cwidth)
192			appendword(b, cheight)
193
194		if sprite_order:
195			for cx in xrange(0, cwidth):
196				xstart = cx * 8
197				for cy in xrange(0, cheight):
198					startoff = cy*tile_height*width + xstart
199					for row in xrange(0, tile_height):
200						rowoff = startoff + row*width
201						for bytecol in xrange(0, 4):
202							boff = bytecol * 2 + rowoff
203							#print 'boff:', boff, 'len(pixels)', len(pixels), 'cx', cx, 'cy', cy, 'cwidth', cwidth, 'cheight', cheight
204							#print 'pixels[boff]:', pixels[boff]
205							b.append(pixels[boff] << 4 | pixels[boff+1])
206		else:
207			for cy in xrange(0, cheight):
208				ystart = cy*tile_height*width
209				for cx in xrange(0, cwidth):
210					startoff = (cx*8) + ystart
211					for row in xrange(0, tile_height):
212						rowoff = startoff + row*width
213						for bytecol in xrange(0, 4):
214							boff = bytecol * 2 + rowoff
215							#print 'boff:', boff, 'len(pixels)', len(pixels), 'cx', cx, 'cy', cy, 'cwidth', cwidth, 'cheight', cheight
216							#print 'pixels[boff]:', pixels[boff]
217							b.append(pixels[boff] << 4 | pixels[boff+1])
218	return b
219
220def add_pal_entries(tiles, pal):
221	(global_pal, line_pals, max_global, max_line) = pal
222	tiles.append(max_global)
223	tiles.append(max_line)
224	pal_list = [(0, 0, 0)] * max_global
225	for entry in global_pal:
226		pal_list[global_pal[entry]] = entry
227	for entry in pal_list:
228		(R, G, B) = entry
229		tiles.append(B)
230		tiles.append(G << 4 | R)
231	for line in line_pals:
232		pal_list = [(0, 0, 0)] * max_line
233		for entry in line:
234			pal_list[line[entry]-max_global] = entry
235		for entry in pal_list:
236			(R, G, B) = entry
237			tiles.append(B)
238			tiles.append(G << 4 | R)
239
240
241
242def main(argv):
243
244	posargs = []
245	omit_pal = raw = chunky = False
246	expect_option = None
247	options = {}
248	tile_height = 8
249	sprite_order = False
250	for i in xrange(1, len(argv)):
251		if argv[i].startswith('-'):
252			if argv[i] == '-r':
253				raw = True
254			elif argv[i] == '-p':
255				omit_pal = True
256			elif argv[i] == '-o':
257				sprite_order = True
258			elif argv[i] == '-c':
259				chunky = True
260			elif argv[i] == '-i':
261				tile_height = 16
262			elif argv[i] == '-s' or argv[i] == '--spec':
263				expect_option = 'specfile'
264			else:
265				print 'Unrecognized switch', argv[i]
266				return
267		elif not expect_option is None:
268			options[expect_option] = argv[i]
269			expect_option = None
270		else:
271			posargs.append(argv[i])
272	if len(posargs) < 2 and not ('specfile' in options and len(posargs) >= 1):
273		print "Usage: img2tiles.py [OPTIONS] infile outfile [STATIC_COLORS [DYNAMIC_COLORS]]"
274		return
275	if 'specfile' in options:
276		props = open(options['specfile']).read().strip().split(',')
277		fname,static_colors,dynamic_colors = props[0:3]
278		for prop in props[3:]:
279			if prop == 'chunky':
280				chunky = True
281			elif prop == 'raw':
282				raw = True
283			elif prop == 'nopal':
284				omit_pal = True
285			elif prop == 'interlace':
286				tile_height = 16
287			elif prop == 'sprite':
288				sprite_order = True
289		static_colors = int(static_colors)
290		dynamic_colors = int(dynamic_colors)
291		outfile = posargs[0]
292	else:
293		fname = posargs[0]
294		outfile = posargs[1]
295		static_colors = 8
296		dynamic_colors = 8
297		if len(posargs) > 2:
298			static_colors = int(posargs[2])
299			dynamic_colors = 16-static_colors
300		if len(posargs) > 3:
301			dynamic_colors = int(posargs[3])
302	if dynamic_colors + static_colors > 16:
303		print "No more than 16 combined dynamic and static colors are allowed"
304		return
305	im = Image.open(fname)
306	pal = make_palette(im, threshold, static_colors, dynamic_colors)
307	palpix = trans_image(im, threshold, pal, chunky)
308	tiles = to_tiles(palpix, raw, chunky, tile_height, sprite_order)
309	if not omit_pal:
310		bits = add_pal_entries(tiles, pal)
311	out = open(outfile, 'wb')
312	out.write(tiles)
313
314if __name__ == '__main__':
315	import sys
316	main(sys.argv)