5 from colorsys import rgb_to_hls, hls_to_rgb
6 from xonstat.util import strip_colors, qfont_decode, _all_colors
8 # similar to html_colors() from util.py
9 _contrast_threshold = 0.5
11 # standard colorset (^0 ... ^9)
12 _dec_colors = [ (0.5,0.5,0.5),
25 # function to write compressed PNG (using zlib)
26 def write_png(filename, buf, width, height):
27 width_byte_4 = width * 4
28 # fix color ordering (BGRA -> RGBA)
29 for byte in xrange(width*height):
31 buf[pos:pos+4] = buf[pos+2] + buf[pos+1] + buf[pos+0] + buf[pos+3]
32 raw_data = b"".join(b'\x00' + buf[span:span + width_byte_4] for span in range(0, (height-1) * width * 4 + 1, width_byte_4))
33 def png_pack(png_tag, data):
34 chunk_head = png_tag + data
35 return struct.pack("!I", len(data)) + chunk_head + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head))
38 png_pack(b'IHDR', struct.pack("!2I5B", width, height, 8, 6, 0, 0, 0)),
39 png_pack(b'IDAT', zlib.compress(raw_data, 9)),
40 png_pack(b'IEND', b'')])
41 f = open(filename, "wb")
50 # skin parameters, can be overriden by init
59 def __init__(self, name, **params):
63 'bg': None, # None - plain; otherwise use given texture
64 'bgcolor': None, # transparent bg when bgcolor==None
65 'overlay': None, # add overlay graphic on top of bg
72 'gametype_fontsize':10,
73 'gametype_pos': (101,33),
76 'gametype_color': (0.9, 0.9, 0.9),
77 'gametype_text': "%s",
79 'gametype_upper': True,
81 'nostats_fontsize': 12,
82 'nostats_pos': (101,59),
83 'nostats_color': (0.8, 0.2, 0.1),
85 'nostats_text': "no stats yet!",
89 'elo_color': (1.0, 1.0, 0.5),
90 'elo_text': "Elo %.0f",
94 'rank_color': (0.8, 0.8, 1.0),
95 'rank_text': "Rank %d of %d",
97 'wintext_fontsize': 10,
98 'wintext_pos': (508,3),
99 'wintext_color': (0.8, 0.8, 0.8),
100 'wintext_text': "Win Percentage",
103 'winp_pos': (508,19),
104 'winp_colortop': (0.2, 1.0, 1.0),
105 'winp_colormid': (0.4, 0.8, 0.4),
106 'winp_colorbot': (1.0, 1.0, 0.2),
109 'wins_pos': (508,34),
110 'wins_color': (0.6, 0.8, 0.8),
113 'loss_pos': (508,44),
114 'loss_color': (0.8, 0.8, 0.6),
116 'kdtext_fontsize': 10,
117 'kdtext_pos': (390,3),
119 'kdtext_color': (0.8, 0.8, 0.8),
120 'kdtext_bg': (0.8, 0.8, 0.8, 0.1),
121 'kdtext_text': "Kill Ratio",
125 'kdr_colortop': (0.2, 1.0, 0.2),
126 'kdr_colormid': (0.8, 0.8, 0.4),
127 'kdr_colorbot': (1.0, 0.2, 0.2),
130 'kills_pos': (392,34),
131 'kills_color': (0.6, 0.8, 0.6),
133 'deaths_fontsize': 8,
134 'deaths_pos': (392,44),
135 'deaths_color': (0.8, 0.6, 0.6),
137 'ptime_fontsize': 10,
138 'ptime_pos': (451,60),
139 'ptime_color': (0.1, 0.1, 0.1),
140 'ptime_text': "Playing Time: %s",
144 for k,v in params.items():
145 if self.params.has_key(k):
151 def __getattr__(self, key):
152 if self.params.has_key(key):
153 return self.params[key]
156 def show_text(self, txt, pos, align=0, angle=None, offset=(0,0)):
159 xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
161 ctx.move_to(pos[0]+offset[0]-xoff, pos[1]+offset[1]-yoff)
163 ctx.move_to(pos[0]+offset[0]-xoff-tw, pos[1]+offset[1]-yoff)
165 ctx.move_to(pos[0]+offset[0]-xoff-tw/2, pos[1]+offset[1]-yoff)
168 ctx.rotate(math.radians(angle))
172 def set_font(self, fontsize, color, bold=False, italic=False):
175 slant = C.FONT_SLANT_ITALIC if italic else C.FONT_SLANT_NORMAL
176 weight = C.FONT_WEIGHT_BOLD if bold else C.FONT_WEIGHT_NORMAL
178 ctx.select_font_face(font, slant, weight)
179 ctx.set_font_size(fontsize)
181 ctx.set_source_rgb(color[0], color[0], color[0])
182 elif len(color) == 3:
183 ctx.set_source_rgb(color[0], color[1], color[2])
184 elif len(color) == 4:
185 ctx.set_source_rgba(color[0], color[1], color[2], color[3])
187 ctx.set_source_rgb(1, 1, 1)
189 def render_image(self, data, output_filename):
190 """Render an image for the given player id."""
194 player = data['player']
196 ranks = data['ranks']
197 games_played = data['games_played']['overall']
198 overall_stats = data['overall_stats']['overall']
200 wins, losses, win_pct = games_played.wins, games_played.losses, games_played.win_pct
201 games = games_played.games
202 kills, deaths, kd_ratio = overall_stats.total_kills, overall_stats.total_deaths, overall_stats.k_d_ratio
203 alivetime = overall_stats.total_playing_time
205 # make sorted list of gametypes
207 for gt in data['games_played'].keys():
211 game_types.append(gt) # only uses gametypes with elo values (needed later on)
213 ## make sure gametypes list if sorted correctly (number of games, descending)
214 ##game_types = sorted(game_types, key=lambda x: data['games_played'][x].games, reverse=True)
215 # make sure gametypes list if sorted correctly (total playing time per game type, descending)
216 game_types = sorted(game_types, key=lambda x: data['overall_stats'][x].total_playing_time, reverse=True)
221 surf = C.ImageSurface(C.FORMAT_ARGB32, self.width, self.height)
222 ctx = C.Context(surf)
224 ctx.set_antialias(C.ANTIALIAS_GRAY)
226 # set font hinting options
228 fo.set_antialias(C.ANTIALIAS_GRAY)
229 fo.set_hint_style(C.HINT_STYLE_FULL)
230 fo.set_hint_metrics(C.HINT_METRICS_ON)
231 ctx.set_font_options(fo)
235 if self.bgcolor != None:
236 # plain fillcolor, full transparency possible with (1,1,1,0)
238 ctx.set_operator(C.OPERATOR_SOURCE)
239 ctx.rectangle(0, 0, self.width, self.height)
240 ctx.set_source_rgba(self.bgcolor[0], self.bgcolor[1], self.bgcolor[2], self.bgcolor[3])
246 bg = C.ImageSurface.create_from_png("img/%s.png" % self.bg)
250 bg_w, bg_h = bg.get_width(), bg.get_height()
252 while bg_xoff < self.width:
254 while bg_yoff < self.height:
255 ctx.set_source_surface(bg, bg_xoff, bg_yoff)
256 #ctx.mask_surface(bg)
261 #print "Error: Can't load background texture: %s" % self.bg
264 # draw overlay graphic
265 if self.overlay != None:
267 overlay = C.ImageSurface.create_from_png("img/%s.png" % self.overlay)
268 ctx.set_source_surface(overlay, 0, 0)
269 #ctx.mask_surface(overlay)
272 #print "Error: Can't load overlay texture: %s" % self.overlay
276 ## draw player's nickname with fancy colors
278 # deocde nick, strip all weird-looking characters
279 qstr = qfont_decode(qstr=player.nick, glyph_translation=True).\
284 # # replace weird characters that make problems - TODO
287 #qstr = ''.join(chars)
288 stripped_nick = strip_colors(qstr.replace(' ', '_'))
290 # fontsize is reduced if width gets too large
291 ctx.select_font_face(self.font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
293 while shrinknick < 0.6 * self.nick_fontsize:
294 ctx.set_font_size(self.nick_fontsize - shrinknick)
295 xoff, yoff, tw, th = ctx.text_extents(stripped_nick)[:4]
296 if tw > self.nick_maxwidth:
301 # determine width of single whitespace for later use
302 xoff, yoff, tw, th = ctx.text_extents("_ _")[:4]
304 xoff, yoff, tw, th = ctx.text_extents("__")[:4]
307 # this hilarious code should determine the spacing between characters
312 # split nick into colored segments
314 _all_colors = re.compile(r'(\^\d|\^x[\dA-Fa-f]{3})')
315 parts = _all_colors.split(qstr)
316 while len(parts) > 0:
319 if _all_colors.match(txt):
320 tag = txt[1:] # strip leading '^'
327 if not txt or len(txt) == 0:
328 # only colorcode and no real text, skip this
332 if tag.startswith('x'):
333 r = int(tag[1] * 2, 16) / 255.0
334 g = int(tag[2] * 2, 16) / 255.0
335 b = int(tag[3] * 2, 16) / 255.0
336 hue, light, satur = rgb_to_hls(r, g, b)
337 if light < _contrast_threshold:
338 light = _contrast_threshold
339 r, g, b = hls_to_rgb(hue, light, satur)
341 r,g,b = _dec_colors[int(tag[0])]
343 r,g,b = _dec_colors[7]
345 xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
346 ctx.set_source_rgb(r, g, b)
347 ctx.move_to(self.nick_pos[0] + xoffset - xoff, self.nick_pos[1])
348 ctx.show_text(txt.encode("utf-8"))
350 tw += (len(txt)-len(txt.strip())) * space_w # account for lost whitespaces
351 xoffset += int(tw + sep_w)
353 ## print elos and ranks
355 xoffset, yoffset = 0, 0
357 for gt in game_types[:self.num_gametypes]:
358 if not elos.has_key(gt):
362 # re-align segments if less than max. gametypes are shown
364 if count < self.num_gametypes:
365 diff = self.num_gametypes - count
367 xoffset += (diff-1) * self.gametype_width
368 yoffset += (diff-1) * self.gametype_height
370 xoffset += 0.5 * diff * self.gametype_width
371 yoffset += 0.5 * diff * self.gametype_height
373 # show a number gametypes the player has participated in
374 for gt in game_types[:self.num_gametypes]:
375 if not elos.has_key(gt): # should not happen
378 offset = (xoffset, yoffset)
379 if self.gametype_pos:
380 if self.gametype_upper:
381 txt = self.gametype_text % gt.upper()
383 txt = self.gametype_text % gt.lower()
384 self.set_font(self.gametype_fontsize, self.gametype_color, bold=True)
385 self.show_text(txt, self.gametype_pos, self.gametype_align, offset=offset)
388 txt = self.elo_text % round(elos[gt], 0)
389 self.set_font(self.elo_fontsize, self.elo_color)
390 self.show_text(txt, self.elo_pos, self.elo_align, offset=offset)
392 if ranks.has_key(gt):
393 txt = self.rank_text % ranks[gt]
395 txt = "(preliminary)"
396 self.set_font(self.rank_fontsize, self.rank_color)
397 self.show_text(txt, self.rank_pos, self.rank_align, offset=offset)
399 xoffset += self.gametype_width
400 yoffset += self.gametype_height
403 xoffset += (self.num_gametypes-2) * self.gametype_width
404 yoffset += (self.num_gametypes-2) * self.gametype_height
405 offset = (xoffset, yoffset)
407 txt = self.nostats_text
408 self.set_font(self.nostats_fontsize, self.nostats_color, bold=True)
409 self.show_text(txt, self.nostats_pos, self.nostats_align, angle=self.nostats_angle, offset=offset)
412 # print win percentage
415 txt = self.wintext_text
416 self.set_font(self.wintext_fontsize, self.wintext_color)
417 self.show_text(txt, self.wintext_pos, self.wintext_align)
421 txt = "%.2f%%" % round(win_pct, 2)
427 nr = 2*(win_pct/100-0.5)
428 r = nr*self.winp_colortop[0] + (1-nr)*self.winp_colormid[0]
429 g = nr*self.winp_colortop[1] + (1-nr)*self.winp_colormid[1]
430 b = nr*self.winp_colortop[2] + (1-nr)*self.winp_colormid[2]
433 r = nr*self.winp_colormid[0] + (1-nr)*self.winp_colorbot[0]
434 g = nr*self.winp_colormid[1] + (1-nr)*self.winp_colorbot[1]
435 b = nr*self.winp_colormid[2] + (1-nr)*self.winp_colorbot[2]
436 self.set_font(self.winp_fontsize, (r,g,b), bold=True)
437 self.show_text(txt, self.winp_pos, self.winp_align)
440 txt = "%d win" % wins
443 self.set_font(self.wins_fontsize, self.wins_color)
444 self.show_text(txt, self.wins_pos, self.wins_align)
447 txt = "%d loss" % losses
450 self.set_font(self.loss_fontsize, self.loss_color)
451 self.show_text(txt, self.loss_pos, self.loss_align)
454 # print kill/death ratio
457 txt = self.kdtext_text
458 self.set_font(self.kdtext_fontsize, self.kdtext_color)
459 self.show_text(txt, self.kdtext_pos, self.kdtext_align)
463 txt = "%.3f" % round(kd_ratio, 3)
472 r = nr*self.kdr_colortop[0] + (1-nr)*self.kdr_colormid[0]
473 g = nr*self.kdr_colortop[1] + (1-nr)*self.kdr_colormid[1]
474 b = nr*self.kdr_colortop[2] + (1-nr)*self.kdr_colormid[2]
477 r = nr*self.kdr_colormid[0] + (1-nr)*self.kdr_colorbot[0]
478 g = nr*self.kdr_colormid[1] + (1-nr)*self.kdr_colorbot[1]
479 b = nr*self.kdr_colormid[2] + (1-nr)*self.kdr_colorbot[2]
480 self.set_font(self.kdr_fontsize, (r,g,b), bold=True)
481 self.show_text(txt, self.kdr_pos, self.kdr_align)
484 txt = "%d kill" % kills
487 self.set_font(self.kills_fontsize, self.kills_color)
488 self.show_text(txt, self.kills_pos, self.kills_align)
492 if deaths is not None:
493 txt = "%d death" % deaths
496 self.set_font(self.deaths_fontsize, self.deaths_color)
497 self.show_text(txt, self.deaths_pos, self.deaths_align)
503 txt = self.ptime_text % str(alivetime)
504 self.set_font(self.ptime_fontsize, self.ptime_color)
505 self.show_text(txt, self.ptime_pos, self.ptime_align)
509 #surf.write_to_png(output_filename)
511 imgdata = surf.get_data()
512 write_png(output_filename, imgdata, self.width, self.height)