Mockup: SFont.py

Line 
1 #sfont.py
2 #a rewriting of SFont in python. Uses pygame, but the original used SDL so there's really no change at all except that the code is cleaner and probably slower. On the plus side you can now link SFonts into pygame projects.
3
4 #By Kousu <kousue ut gmuil dut cum> 2006, under a BSD license. SFont.c is under GPL but I don't think it applies because I didn't wind up using any of the code from it since it's such a simple format.
5
6 import pygame
7 #XXX should we init pygame?
8
9 import pygame
10 from pygame.locals import *
11
12 class Font:
13         """
14         A SFont file should be a normal picture file (PNGs work well though anything pygame can load can be used). It should contain, in order, these characters:
15         ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~
16         Leave an extra row of pixels at the top. In this row, between each character, place a string of pink=(255,0,255) pixels to demark the edges of the pixels
17         There is one extra restriction
18         Todo:
19         grab all subsurfaces at font-creation time, not afterwards
20         #XXX should return the length as second in the tuples? len is calc'd twice now, could cut it down)
21         #XXX There's inefficiency: ranges() is run twice for every render()
22         #XXX check this for bad-data bugs. Would it completely assrape if you don't give enough chars? or if accidentally connect two strings of pink together?.
23         Support \n by moving down a line (perhaps split along \n and calculate based on how many lines you get?)
24         
25         Speed up tricks:
26                 +make a write() method that writes directly instead of rendering first
27                 +use numeric to do the wiping of the pixel-alphas
28                 +assume the colour in between chars is the colorkey; then just set_colorkey() of the rendering surface; but this would break any per-pixel alphas present... :(... perhaps check for per-pixel alphas first?
29         Read SFont.c, in particular figure out this part:
30         //#width = mid(self.CharPos[c+1]) - mid(self.CharPos[c])?? #get distance between the centers of this char & the next
31         //#src.x = mid(self.CharPos[x]) #the center of the character
32         //#dst.x = pos - mid(self.CharPos[x]) #pos is kept at the end of the place to write to; have to shift back 1 width.... except appearantly only half the character instead.
33         srcrect.w = dstrect.w =
34             (Font->CharPos[charoffset+2] + Font->CharPos[charoffset+1])/2 -
35             (Font->CharPos[charoffset] + Font->CharPos[charoffset-1])/2;
36         srcrect.x = (Font->CharPos[charoffset]+Font->CharPos[charoffset-1])/2;
37         dstrect.x = x - (float)(Font->CharPos[charoffset]
38                               - Font->CharPos[charoffset-1])/2;
39
40         SDL_BlitSurface(Font->Surface, &srcrect, Surface, &dstrect);
41         """
42         def __init__(self, filename):
43                 """Works like normal pygame.font.Font(): takes a file to load from and a size. Size is a bit broken so far, it just is the vertical size in pixels to scale all the letters to
44                 XXX Doesn't check if pygame is initialized! Will crash if it's not!
45                 Todo: wrap this so it falls back on self.font = pygame.font.SysFont("Times New Roman", 30) if it can't open? or would that be unpythonic?
46                 Todo: allow passing an initial-char arg so that it can cover a different section of chracters (e.g. cover some univocde)
47                 """
48                 self.surface = pygame.image.load(filename)
49                 self.CharPos = [] #holds tuples representing the ranges where to find each character
50                 
51                 """
52                 algo:
53                 find a pink to the left of a non-pink, this marks the start of the letter
54                 find a pink to the right of a non-pink, this marks the end
55                 
56                 Equivilently:
57                 Loop to the right until you find a non-pink, this marks the start of the letter
58                 loop to the right until you find a pink, this marks the end
59                 Save the ranges
60                 """
61                 PINK = self.surface.get_at((0,0)) #the colour to be used to delimit characters
62                 #Tokenize the surface by the pinks at the top
63                 #note: the enumerate() call must be here to support the ickyness below
64                 pixels = enumerate(self.surface.get_at((i, 0)) for i in xrange(self.surface.get_width()))
65                 for i,pixel in pixels:
66                         #discard initial pink pixels...
67                         if pixel == PINK: continue
68                         start = i
69                         #discard non-pinks
70                         while pixel != PINK:
71                                 try: #ick, such awful code
72                                         i,pixel = pixels.next() #NOTE!!! the loop does not work like you think...
73                                 except StopIteration: break
74                         end = i
75                         self.CharPos.append((start,end))
76                         #and do it all over again...
77                         
78         def ranges(self, text): #XXX Kill me
79                 "Get a list of tuples representing the ranges needed to grab the chars out of self.surface for the given text"
80                 for c in text:
81                         c = ord(c)-33 #-33 because no.33 should be the first character in the font
82                         if 0<=c<=len(self.CharPos):
83                                 yield self.CharPos[c]
84                         else: #spaces and non-printables are just taken as the stuff in between the first and second chars. This is how SFont.c does it, don't look at me.
85                                 #yield (self.CharPos[0][1], self.CharPos[1][0])
86                                 yield (0,10) #better way
87         
88         def size(self, text):
89                 """Font.size(text) -> (width, height)
90                 
91                 Give the dimensions needed to draw the string in text
92                 """
93                 width = 0
94                 height = self.surface.get_height()-1 #-1 to account for the row of pinks
95                 for range in self.ranges(text):
96                         width += range[1]-range[0]
97                 return width, height
98        
99         def write(self, surface, pos, text):
100                 """Draw the text onto the surface at position pos=(x,y), just like in SFont.c
101                 
102                 I'm only putting this in here because of SFont... and because it is horribly slow
103                 otherwise due to the bottleneck in render().
104                 But yeah, it doesn't fit the pygame model."""
105                 size = self.size(text)
106                 loc = 0 #location in font file
107                 for c in text:
108                         c = ord(c)-33 #-33 because no.33 should be the first character in the font
109                         if 0<=c<=len(self.CharPos):
110                                 range = self.CharPos[c]
111                                 width = range[1]-range[0]
112                                 char = self.surface.subsurface( (range[0], 1, width, size[1]) )
113                         else: #make a transparent spacer
114                                 width = 10
115                                 char = pygame.Surface((width,size[1]))
116                                 char.set_alpha(0)
117                         surface.blit(char, (pos[0]+loc,pos[1]+0))
118                         loc+=width
119        
120         def render(self, text, antialias, background=(0,0,0,0)):
121                 """Return a surface with the text rendered onto it using this font. Just like pygame.Font.render()
122                 If background is not given it defaults to being completely transparent.
123                 """
124                 drawing = pygame.Surface(self.size(text)) #XXX how to avoid calling size() twice?
125                 drawing = drawing.convert(drawing.get_bitsize(), SRCALPHA)
126                 #zero out the per-pixel alphas because otherwise the default black background shows up
127                 drawing.fill(background)
128                 #draw
129                 self.write(drawing, (0,0), text)
130                 return drawing
131
132
133 if __name__=='__main__':
134         pygame.init()
135         surf = pygame.display.set_mode((300,200))
136         surf.fill((255,0,255))
137         f = Font("24P_Arial_NeonYellow.png")
138        
139         surf.blit(f.render("fyadfyad~fyad!!"), (0,0))
140         pygame.display.flip()
141         while 1:
142                 for e in pygame.event.get():
143                         if e.type==pygame.QUIT:
144                                 import sys
145                                 sys.exit()