#! /usr/bin/env python # # automatic word-wrapping markup font interface for pygame # Copyright (C) 2002 Michael Urman # # 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # import pygame class _markup: def __init__(self, val): self.val = val class _markup_bold(_markup): def __repr__(self): return ['</B>','<B>'][self.val] class _markup_italic(_markup): def __repr__(self): return ['</I>','<I>'][self.val] class _markup_underline(_markup): def __repr__(self): return ['</U>','<U>'][self.val] def _markup_split(text): import re blocks = re.split('(</?[biuBIU]>)', text) blocks = [ s for s in blocks if s != '' ] for i in range(len(blocks)): if blocks[i] in ('<b>', '<B>'): blocks[i] = _markup_bold(1) elif blocks[i] in ('</b>', '</B>'): blocks[i] = _markup_bold(0) elif blocks[i] in ('<i>', '<I>'): blocks[i] = _markup_italic(1) elif blocks[i] in ('</i>', '</I>'): blocks[i] = _markup_italic(0) elif blocks[i] in ('<u>', '<U>'): blocks[i] = _markup_underline(1) elif blocks[i] in ('</u>', '</U>'): blocks[i] = _markup_underline(0) else: blocks[i] = re.sub('<', '<', blocks[i]) blocks[i] = re.sub('&', '&', blocks[i]) return blocks class MarkFont: def __init__(self, *args, **kvargs): self._font = pygame.font.Font(*args, **kvargs) self._spacewidth, self._lineheight = self._font.size(' ') self._lineheight += self._font.get_linesize() def __getattr__(self, attr): """__getattr__(self, attr) -> attribute Pass everything but render to the pygame.Font instance""" return getattr(self._font, attr) def render(self, text, antialias, fore_RGBA, back_RGBA=None, width=0, height=0): """MarkFont.render(text, antialias, fore_RGBA, [back_RGBA], [width], [height]) -> Surface Render the given text into a surface no wider than the width of the passed surface, and no taller than the height. Newlines work as you might expect.""" if width < 0 or height < 0: raise ValueError("Arguments width and height must not be negative") font = self._font spacewidth = self._spacewidth lineheight = self._lineheight args = (antialias, fore_RGBA) if back_RGBA is not None: args += (back_RGBA,) lines = text.splitlines(); line_images = [] top = 0 maxwidth = 0 for line in lines: if width: # try to fit the whole line in at once if font.size(line)[0] < width: if len(line): surf = font.render(line, *args) else: surf = font.render(' ', *args) line_images.append(surf) top += lineheight # otherwise try a word at a time else: aline = [] for word in line.split(): aline.append(word) linewidth = font.size(' '.join(aline))[0] # write all but last when the last pushes it over if linewidth > width: surf = font.render(' '.join(aline[:-1]), *args) line_images.append(surf) top += lineheight aline = aline[-1:] # write the final portion (could be all of it) else: surf = font.render(' '.join(aline), *args) line_images.append(surf) top += lineheight # if no requested width, just render the lines else: surf = font.render(line, *args) line_images.append(surf) if line_images[-1].get_width() > maxwidth: maxwidth = line_images[-1].get_width() if len(line_images) == 1: return line_images[0] useheight = top usewidth = maxwidth if width > usewidth: usewidth = width if useheight > height > 0: useheight = height # oddly convert_alpha() gives us a better alpha channel than a SRCALPHA # flag to the surface creation final_render = pygame.surface.Surface((usewidth, useheight)).convert_alpha() # worse, due to the way this all defaults and works, i need to make the # image all clear or i lose the alpha channel. makes sense, but what a # gotcha! by reading from the line_image, it should support a bgcolor. img = line_images[0] color = img.get_at((img.get_width()-1, 0)) final_render.fill(color) #final_render.fill((0,0,0)) #final_render.fill(line_images[0].get_at((0,0)), (1, 1, usewidth-2, useheight-2)) top = 0 for i in line_images: final_render.blit(i, (0,top)) top += lineheight return final_render def render_markup(self, text, antialias, fore_RGBA, back_RGBA=None, width=0, height=0): """WrapFont.render_markup(text, antialias, fore_RGBA, [back_RGBA], [width], [height]) -> Surface Render the given text into a surface no wider than the width of the passed surface, and no taller than the height. Newlines work as you might expect. Furthermore, simple html style tags control the styles available: <b>bold</b> <i>italic</i> <u>underline</u>. They can be nested, and currently proper nesting is not checked. Note: Due to the lack of kerning information available and the high interface at which this works, rendering a single word in more than one manner (<b>foo<i>bar</i></b>) is ugly.""" if width < 0 or height < 0: raise ValueError("Arguments width and height must not be negative") font = self._font spacewidth = self._spacewidth lineheight = self._lineheight try: self._chartable except AttributeError: self._chartable = {} for i in range(128): c1 = chr(i) s1 = font.size(c1)[0] for j in range(128): c2 = chr(j) s2 = font.size(c2)[0] s = c1+c2 self._chartable[s] = font.size(s)[0] - s1 - s2 args = (antialias, fore_RGBA) if back_RGBA is not None: args += (back_RGBA,) lines = text.splitlines(); images = [] left = 0 top = 0-lineheight maxwidth = 0 for line in lines: top += lineheight if left > maxwidth: maxwidth = left left = 0 lastchar = None for block in _markup_split(line): # handle markup if isinstance(block, _markup): if isinstance(block, _markup_bold): font.set_bold(block.val) elif isinstance(block, _markup_italic): font.set_italic(block.val) elif isinstance(block, _markup_underline): font.set_underline(block.val) elif isinstance(block, _markup_partialspace): left += self.val else: if lastchar is not None and len(block): left += self._chartable[lastchar + block[0]] softspace = None # try to draw the line if 0 < width <= left + self.size(block)[0]: # break it up if it's too big for word in block.split(): surf = font.render(word, *args) wordwidth = surf.get_size()[0] if softspace is not None: softspace += self._chartable[ ' '+word[0] ] left += softspace + spacewidth if width <= left + wordwidth: top += lineheight left = 0 images.append((left, top, surf,word)) left += wordwidth softspace = self._chartable[ word[-1]+' ' ] elif len(block): surf = font.render(block, *args) blockwidth = surf.get_size()[0] images.append((left, top, surf,block)) left += blockwidth lastchar = block[-1] if len(images) == 1: return images[0][2] useheight = top+lineheight usewidth = maxwidth if width > usewidth: usewidth = width if useheight > height > 0: useheight = height # oddly convert_alpha() gives us a better alpha channel than a SRCALPHA # flag to the surface creation final_render = pygame.surface.Surface((usewidth, useheight)).convert_alpha() # worse, due to the way this all defaults and works, i need to make the # image all clear or i lose the alpha channel. makes sense, but what a # gotcha! by reading from the line_image, it should support a bgcolor. img = images[0][2] color = img.get_at((img.get_width()-1, 0)) final_render.fill(color) for x,y,i,t in images: # uncomment the following to get red boxes around the pieces #w,h = i.get_size() #surf = pygame.surface.Surface((w,h)).convert_alpha() #surf.fill((255,0,0)) #surf.fill((0,0,0,0), (1,1,w-2,h-2)) #final_render.blit(surf, (x,y)) final_render.blit(i, (x,y)) top += lineheight return final_render if __name__ == '__main__': teststring = """This is a <b>test</b> of <b><i>MarkFont</i></b> What's better than a quick little <i>markup</i> string to get good yet simple font effects in your game? <b>Nothing</b>""" pygame.display.init() pygame.font.init() screen = pygame.display.set_mode((640,480)) back = pygame.surface.Surface((640,480)).convert() font = MarkFont(None, 24) text1 = font.render(teststring, 1, (0,0,0), None, 300) text2 = font.render_markup(teststring, 1, (0,0,0), None, 300) w1, h1 = text1.get_size() x1, y1 = 0,0 dx1, dy1 = 1, 1 w2, h2 = text2.get_size() x2, y2 = h1,w1 dx2, dy2 = -1, 1 for i in range(0, 100, 10): back.fill((155+i, 155+i, 155+i), (i, i, 639-2*i, 479-2*i)) noquit = 1 while noquit: screen.blit(back, (0,0)) screen.blit(text1, (x1,y1)) screen.blit(text2, (x2,y2)) pygame.display.update(screen.get_rect()) if not 0 <= x1 + dx1 < x1 + dx1 + w1 < 640: dx1 = -dx1 if not 0 <= y1 + dy1 < y1 + dy1 + h1 < 480: dy1 = -dy1 x1 += dx1 y1 += dy1 if not 0 <= x2 + dx2 < x2 + dx2 + w2 < 640: dx2 = -dx2 if not 0 <= y2 + dy2 < y2 + dy2 + h2 < 480: dy2 = -dy2 x2 += dx2 y2 += dy2 events = pygame.event.get() for event in events: if event and event.type in (pygame.QUIT, pygame.KEYDOWN): noquit = 0