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