]> de.git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/batch/badges/skin.py
a2bb0ed83336a12ea50deecc4534301e86d8881f
[xonotic/xonstat.git] / xonstat / batch / badges / skin.py
1 import math
2 import re
3 import zlib, struct
4 import cairo as C
5 from colorsys import rgb_to_hls, hls_to_rgb
6 from xonstat.util import strip_colors, qfont_decode, _all_colors
7
8 # similar to html_colors() from util.py
9 _contrast_threshold = 0.5
10
11 _dec_colors = [ (0.5,0.5,0.5),
12                 (1.0,0.0,0.0),
13                 (0.2,1.0,0.0),
14                 (1.0,1.0,0.0),
15                 (0.2,0.4,1.0),
16                 (0.2,1.0,1.0),
17                 (1.0,0.2,102),
18                 (1.0,1.0,1.0),
19                 (0.6,0.6,0.6),
20                 (0.5,0.5,0.5)
21             ]
22
23
24 def writepng(filename, buf, width, height):
25     width_byte_4 = width * 4
26     # fix color ordering (BGR -> RGB)
27     for byte in xrange(width*height):
28         pos = byte * 4
29         buf[pos:pos+4] = buf[pos+2] + buf[pos+1] + buf[pos+0] + buf[pos+3]
30     # merge lines
31     #raw_data = b"".join(b'\x00' + buf[span:span + width_byte_4] for span in xrange((height - 1) * width * 4, -1, - width_byte_4))
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))
36     data = b"".join([
37         b'\x89PNG\r\n\x1a\n',
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")
42     try:
43         f.write(data)
44     finally:
45         f.close()
46
47
48 class Skin:
49
50     # skin parameters, can be overriden by init
51     params = {}
52
53     # skin name
54     name = ""
55
56     def __init__(self, name, **params):
57         # default parameters
58         self.name = name
59         self.params = {
60             'bg':               None,           # None - plain; otherwise use given texture
61             'bgcolor':          None,           # transparent bg when bgcolor==None
62             'overlay':          None,           # add overlay graphic on top of bg
63             'font':             "Xolonium",
64             'width':            560,
65             'height':           70,
66             'nick_fontsize':    20,
67             'nick_pos':         (56,18),
68             'nick_maxwidth':    280,
69             'gametype_fontsize':10,
70             'gametype_pos':     (101,33),
71             'gametype_width':   94,
72             'gametype_color':   (0.9, 0.9, 0.9),
73             'gametype_text':    "[ %s ]",
74             'gametype_center':  True,
75             'num_gametypes':    3,
76             'nostats_fontsize': 12,
77             'nostats_pos':      (101,59),
78             'nostats_color':    (0.8, 0.2, 0.1),
79             'nostats_angle':    -10,
80             'nostats_text':     "no stats yet!",
81             'nostats_center':   True,
82             'elo_pos':          (101,47),
83             'elo_fontsize':     10,
84             'elo_color':        (1.0, 1.0, 0.5),
85             'elo_text':         "Elo %.0f",
86             'elo_center':       True,
87             'rank_fontsize':    8,
88             'rank_pos':         (101,57),
89             'rank_color':       (0.8, 0.8, 1.0),
90             'rank_text':        "Rank %d of %d",
91             'rank_center':      True,
92             'wintext_fontsize': 10,
93             'wintext_pos':      (508,3),
94             'wintext_color':    (0.8, 0.8, 0.8),
95             'wintext_text':     "Win Percentage",
96             'winp_fontsize':    12,
97             'winp_pos':         (508,19),
98             'winp_colortop':    (0.2, 1.0, 1.0),
99             'winp_colormid':    (0.4, 0.8, 0.4),
100             'winp_colorbot':    (1.0, 1.0, 0.2),
101             'wins_fontsize':    8,
102             'wins_pos':         (508,33),
103             'wins_color':       (0.6, 0.8, 0.8),
104             'loss_fontsize':    8,
105             'loss_pos':         (508,43),
106             'loss_color':       (0.8, 0.8, 0.6),
107             'kdtext_fontsize':  10,
108             'kdtext_pos':       (390,3),
109             'kdtext_width':     102,
110             'kdtext_color':     (0.8, 0.8, 0.8),
111             'kdtext_bg':        (0.8, 0.8, 0.8, 0.1),
112             'kdtext_text':      "Kill Ratio",
113             'kdr_fontsize':     12,
114             'kdr_pos':          (392,19),
115             'kdr_colortop':     (0.2, 1.0, 0.2),
116             'kdr_colormid':     (0.8, 0.8, 0.4),
117             'kdr_colorbot':     (1.0, 0.2, 0.2),
118             'kills_fontsize':   8,
119             'kills_pos':        (392,46),
120             'kills_color':      (0.6, 0.8, 0.6),
121             'deaths_fontsize':  8,
122             'deaths_pos':       (392,56),
123             'deaths_color':     (0.8, 0.6, 0.6),
124             'ptime_fontsize':   10,
125             'ptime_pos':        (451,60),
126             'ptime_color':      (0.1, 0.1, 0.1),
127             'ptime_text':       "Playing Time: %s",
128         }
129         
130         for k,v in params.items():
131             if self.params.has_key(k):
132                 self.params[k] = v
133
134     def __str__(self):
135         return self.name
136
137     def __getattr__(self, key):
138         if self.params.has_key(key):
139             return self.params[key]
140         return None
141
142     def render_image(self, data, output_filename):
143         """Render an image for the given player id."""
144
145         # setup variables
146
147         player          = data.player
148         elos            = data.elos
149         ranks           = data.ranks
150         #games           = data.total_stats['games']
151         wins, losses    = data.total_stats['wins'], data.total_stats['losses']
152         games           = wins + losses
153         kills, deaths   = data.total_stats['kills'], data.total_stats['deaths']
154         alivetime       = data.total_stats['alivetime']
155
156
157         font = "Xolonium"
158         if self.font == 1:
159             font = "DejaVu Sans"
160
161
162         # build image
163
164         surf = C.ImageSurface(C.FORMAT_ARGB32, self.width, self.height)
165         ctx = C.Context(surf)
166         ctx.set_antialias(C.ANTIALIAS_GRAY)
167         
168         # draw background
169         if self.bg == None:
170             if self.bgcolor != None:
171                 # plain fillcolor, full transparency possible with (1,1,1,0)
172                 ctx.save()
173                 ctx.set_operator(C.OPERATOR_SOURCE)
174                 ctx.rectangle(0, 0, self.width, self.height)
175                 ctx.set_source_rgba(self.bgcolor[0], self.bgcolor[1], self.bgcolor[2], self.bgcolor[3])
176                 ctx.fill()
177                 ctx.restore()
178         else:
179             try:
180                 # background texture
181                 bg = C.ImageSurface.create_from_png("img/%s.png" % self.bg)
182                 
183                 # tile image
184                 if bg:
185                     bg_w, bg_h = bg.get_width(), bg.get_height()
186                     bg_xoff = 0
187                     while bg_xoff < self.width:
188                         bg_yoff = 0
189                         while bg_yoff < self.height:
190                             ctx.set_source_surface(bg, bg_xoff, bg_yoff)
191                             #ctx.mask_surface(bg)
192                             ctx.paint()
193                             bg_yoff += bg_h
194                         bg_xoff += bg_w
195             except:
196                 #print "Error: Can't load background texture: %s" % self.bg
197                 pass
198
199         # draw overlay graphic
200         if self.overlay != None:
201             try:
202                 overlay = C.ImageSurface.create_from_png("img/%s.png" % self.overlay)
203                 ctx.set_source_surface(overlay, 0, 0)
204                 #ctx.mask_surface(overlay)
205                 ctx.paint()
206             except:
207                 #print "Error: Can't load overlay texture: %s" % self.overlay
208                 pass
209
210
211         ## draw player's nickname with fancy colors
212         
213         # deocde nick, strip all weird-looking characters
214         qstr = qfont_decode(player.nick).replace('^^', '^').replace(u'\x00', '')
215         chars = []
216         for c in qstr:
217             # replace weird characters that make problems - TODO
218             if ord(c) < 128:
219                 chars.append(c)
220         qstr = ''.join(chars)
221         stripped_nick = strip_colors(qstr.replace(' ', '_'))
222         
223         # fontsize is reduced if width gets too large
224         ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
225         shrinknick = 0
226         while shrinknick < 10:
227             ctx.set_font_size(self.nick_fontsize - shrinknick)
228             xoff, yoff, tw, th = ctx.text_extents(stripped_nick)[:4]
229             if tw > self.nick_maxwidth:
230                 shrinknick += 2
231                 continue
232             break
233
234         # determine width of single whitespace for later use
235         xoff, yoff, tw, th = ctx.text_extents("_")[:4]
236         space_w = tw
237
238         # split nick into colored segments
239         xoffset = 0
240         _all_colors = re.compile(r'(\^\d|\^x[\dA-Fa-f]{3})')
241         parts = _all_colors.split(qstr)
242         while len(parts) > 0:
243             tag = None
244             txt = parts[0]
245             if _all_colors.match(txt):
246                 tag = txt[1:]  # strip leading '^'
247                 if len(parts) < 2:
248                     break
249                 txt = parts[1]
250                 del parts[1]
251             del parts[0]
252                 
253             if not txt or len(txt) == 0:
254                 # only colorcode and no real text, skip this
255                 continue
256             
257             if tag:
258                 if tag.startswith('x'):
259                     r = int(tag[1] * 2, 16) / 255.0
260                     g = int(tag[2] * 2, 16) / 255.0
261                     b = int(tag[3] * 2, 16) / 255.0
262                     hue, light, satur = rgb_to_hls(r, g, b)
263                     if light < _contrast_threshold:
264                         light = _contrast_threshold
265                         r, g, b = hls_to_rgb(hue, light, satur)
266                 else:
267                     r,g,b = _dec_colors[int(tag[0])]
268             else:
269                 r,g,b = _dec_colors[7]
270             
271             ctx.set_source_rgb(r, g, b)
272             ctx.move_to(self.nick_pos[0] + xoffset, self.nick_pos[1])
273             ctx.show_text(txt)
274
275             xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
276             tw += (len(txt)-len(txt.strip())) * space_w  # account for lost whitespaces
277             xoffset += tw + 2
278
279         ## print elos and ranks
280         
281         # show up to three gametypes the player has participated in
282         xoffset = 0
283         for gt in data.total_stats['gametypes'][:self.num_gametypes]:
284             if not elos.has_key(gt) or not ranks.has_key(gt):
285                 continue
286
287             if self.gametype_pos:
288                 ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_BOLD)
289                 ctx.set_font_size(self.gametype_fontsize)
290                 ctx.set_source_rgb(self.gametype_color[0],self.gametype_color[1],self.gametype_color[2])
291                 txt = self.gametype_text % gt.upper()
292                 xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
293                 if self.gametype_center:
294                     ctx.move_to(self.gametype_pos[0]+xoffset-xoff-tw/2, self.gametype_pos[1]-yoff)
295                 else:
296                     ctx.move_to(self.gametype_pos[0]+xoffset-xoff-tw, self.gametype_pos[1]-yoff)
297                 ctx.show_text(txt)
298
299             if not elos.has_key(gt) or not ranks.has_key(gt):
300                 pass
301                 if self.nostats_pos:
302                     ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_BOLD)
303                     ctx.set_font_size(self.nostats_fontsize)
304                     ctx.set_source_rgb(self.nostats_color[0],self.nostats_color[1],self.nostats_color[2])
305                     txt = self.nostats_text
306                     xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
307                     if self.nostats_center:
308                         ctx.move_to(self.nostats_pos[0]+xoffset-xoff-tw/2, self.nostats_pos[1]-yoff)
309                     else:
310                         ctx.move_to(self.nostats_pos[0]+xoffset-xoff, self.nostats_pos[1]-yoff)
311                     ctx.save()
312                     ctx.rotate(math.radians(self.nostats_angle))
313                     ctx.show_text(txt)
314                     ctx.restore()
315             else:
316                 if self.elo_pos:
317                     ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
318                     ctx.set_font_size(self.elo_fontsize)
319                     ctx.set_source_rgb(self.elo_color[0], self.elo_color[1], self.elo_color[2])
320                     txt = self.elo_text % round(elos[gt], 0)
321                     xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
322                     if self.elo_center:
323                         ctx.move_to(self.elo_pos[0]+xoffset-xoff-tw/2, self.elo_pos[1]-yoff)
324                     else:
325                         ctx.move_to(self.elo_pos[0]+xoffset-xoff, self.elo_pos[1]-yoff)
326                     ctx.show_text(txt)
327                 if  self.rank_pos:
328                     ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
329                     ctx.set_font_size(self.rank_fontsize)
330                     ctx.set_source_rgb(self.rank_color[0], self.rank_color[1], self.rank_color[2])
331                     txt = self.rank_text % ranks[gt]
332                     xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
333                     if self.rank_center:
334                         ctx.move_to(self.rank_pos[0]+xoffset-xoff-tw/2, self.rank_pos[1]-yoff)
335                     else:
336                         ctx.move_to(self.rank_pos[0]+xoffset-xoff, self.rank_pos[1]-yoff)
337                     ctx.show_text(txt)
338             
339             xoffset += self.gametype_width
340
341
342         # print win percentage
343
344         if self.wintext_pos:
345             ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
346             ctx.set_font_size(self.wintext_fontsize)
347             ctx.set_source_rgb(self.wintext_color[0], self.wintext_color[1], self.wintext_color[2])
348             txt = self.wintext_text
349             xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
350             ctx.move_to(self.wintext_pos[0]-xoff-tw/2, self.wintext_pos[1]-yoff)
351             ctx.show_text(txt)
352
353         txt = "???"
354         try:
355             ratio = float(wins)/games
356             txt = "%.2f%%" % round(ratio * 100, 2)
357         except:
358             ratio = 0
359         
360         if self.winp_pos:
361             ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_BOLD)
362             ctx.set_font_size(self.winp_fontsize)
363             if ratio >= 0.5:
364                 nr = 2*(ratio-0.5)
365                 r = nr*self.winp_colortop[0] + (1-nr)*self.winp_colormid[0]
366                 g = nr*self.winp_colortop[1] + (1-nr)*self.winp_colormid[1]
367                 b = nr*self.winp_colortop[2] + (1-nr)*self.winp_colormid[2]
368             else:
369                 nr = 2*ratio
370                 r = nr*self.winp_colormid[0] + (1-nr)*self.winp_colorbot[0]
371                 g = nr*self.winp_colormid[1] + (1-nr)*self.winp_colorbot[1]
372                 b = nr*self.winp_colormid[2] + (1-nr)*self.winp_colorbot[2]
373             ctx.set_source_rgb(r, g, b)
374             xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
375             ctx.move_to(self.winp_pos[0]-xoff-tw/2, self.winp_pos[1]-yoff)
376             ctx.show_text(txt)
377
378         if self.wins_pos:
379             ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
380             ctx.set_font_size(self.wins_fontsize)
381             ctx.set_source_rgb(self.wins_color[0], self.wins_color[1], self.wins_color[2])
382             txt = "%d win" % wins
383             if wins != 1:
384                 txt += "s"
385             xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
386             ctx.move_to(self.wins_pos[0]-xoff-tw/2, self.wins_pos[1]-yoff)
387             ctx.show_text(txt)
388
389         if self.loss_pos:
390             ctx.set_font_size(self.loss_fontsize)
391             ctx.set_source_rgb(self.loss_color[0], self.loss_color[1], self.loss_color[2])
392             txt = "%d loss" % losses
393             if losses != 1:
394                 txt += "es"
395             xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
396             ctx.move_to(self.loss_pos[0]-xoff-tw/2, self.loss_pos[1]-yoff)
397             ctx.show_text(txt)
398
399
400         # print kill/death ratio
401
402         if self.kdtext_pos:
403             ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
404             ctx.set_font_size(self.kdtext_fontsize)
405             ctx.set_source_rgb(self.kdtext_color[0], self.kdtext_color[1], self.kdtext_color[2])
406             txt = self.kdtext_text
407             xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
408             ctx.move_to(self.kdtext_pos[0]-xoff-tw/2, self.kdtext_pos[1]-yoff)
409             ctx.show_text(txt)
410         
411         txt = "???"
412         try:
413             ratio = float(kills)/deaths
414             txt = "%.3f" % round(ratio, 3)
415         except:
416             ratio = 0
417
418         if self.kdr_pos:
419             ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_BOLD)
420             ctx.set_font_size(self.kdr_fontsize)
421             if ratio >= 1.0:
422                 nr = ratio-1.0
423                 if nr > 1:
424                     nr = 1
425                 r = nr*self.kdr_colortop[0] + (1-nr)*self.kdr_colormid[0]
426                 g = nr*self.kdr_colortop[1] + (1-nr)*self.kdr_colormid[1]
427                 b = nr*self.kdr_colortop[2] + (1-nr)*self.kdr_colormid[2]
428             else:
429                 nr = ratio
430                 r = nr*self.kdr_colormid[0] + (1-nr)*self.kdr_colorbot[0]
431                 g = nr*self.kdr_colormid[1] + (1-nr)*self.kdr_colorbot[1]
432                 b = nr*self.kdr_colormid[2] + (1-nr)*self.kdr_colorbot[2]
433             ctx.set_source_rgb(r, g, b)
434             xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
435             ctx.move_to(self.kdr_pos[0]-xoff-tw/2, self.kdr_pos[1]-yoff)
436             ctx.show_text(txt)
437
438         if self.kills_pos:
439             ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
440             ctx.set_font_size(self.kills_fontsize)
441             ctx.set_source_rgb(self.kills_color[0], self.kills_color[1], self.kills_color[2])
442             txt = "%d kill" % kills
443             if kills != 1:
444                 txt += "s"
445             xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
446             ctx.move_to(self.kills_pos[0]-xoff-tw/2, self.kills_pos[1]+yoff)
447             ctx.show_text(txt)
448
449         if self.deaths_pos:
450             ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
451             ctx.set_font_size(self.deaths_fontsize)
452             ctx.set_source_rgb(self.deaths_color[0], self.deaths_color[1], self.deaths_color[2])
453             if deaths is not None:
454                 txt = "%d death" % deaths
455                 if deaths != 1:
456                     txt += "s"
457             else:
458                 txt = ""
459             xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
460             ctx.move_to(self.deaths_pos[0]-xoff-tw/2, self.deaths_pos[1]+yoff)
461             ctx.show_text(txt)
462
463
464         # print playing time
465
466         if self.ptime_pos:
467             ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
468             ctx.set_font_size(self.ptime_fontsize)
469             ctx.set_source_rgb(self.ptime_color[0], self.ptime_color[1], self.ptime_color[2])
470             txt = self.ptime_text % str(alivetime)
471             xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
472             ctx.move_to(self.ptime_pos[0]-xoff-tw/2, self.ptime_pos[1]-yoff)
473             ctx.show_text(txt)
474
475
476         # save to PNG
477         #surf.write_to_png(output_filename)
478         surf.flush()
479         imgdata = surf.get_data()
480         writepng(output_filename, imgdata, self.width, self.height)
481