Adding elements to $wgSpecialPages must be done before SpecialPage::initList()
[toast/cookiecaptcha.git] / captcha.py
1 #!/usr/bin/python
2 #
3 # Script to generate distorted text images for a captcha system.
4 #
5 # Copyright (C) 2005 Neil Harris
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License along
18 # with this program; if not, write to the Free Software Foundation, Inc.,
19 # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
20 # http://www.gnu.org/copyleft/gpl.html
21 #
22 # Further tweaks by Brion Vibber <brion@pobox.com>:
23 # 2006-01-26: Add command-line options for the various parameters
24
25 import random
26 import Image
27 import ImageFont
28 import ImageDraw
29 import ImageEnhance
30 import ImageOps
31 import math, string, md5
32 import getopt
33 import os
34 import sys
35
36 # Does X-axis wobbly copy, sandwiched between two rotates
37 def wobbly_copy(src, wob, col, scale, ang):
38         x, y = src.size
39         f = random.uniform(4*scale, 5*scale)
40         p = random.uniform(0, math.pi*2)
41         rr = ang+random.uniform(-30, 30) # vary, but not too much
42         int_d = Image.new('RGB', src.size, 0) # a black rectangle
43         rot = src.rotate(rr, Image.BILINEAR)
44         # Do a cheap bounding-box op here to try to limit work below
45         bbx = rot.getbbox()
46         if bbx == None:
47                 print "whoops"
48                 return src
49         else:
50                 l, t, r, b= bbx
51         # and only do lines with content on
52         for i in range(t, b+1):
53                 # Drop a scan line in
54                 xoff = int(math.sin(p+(i*f/y))*wob)
55                 xoff += int(random.uniform(-wob*0.5, wob*0.5))
56                 int_d.paste(rot.crop((0, i, x, i+1)), (xoff, i))
57         # try to stop blurring from building up
58         int_d = int_d.rotate(-rr, Image.BILINEAR)
59         enh = ImageEnhance.Sharpness(int_d)
60         return enh.enhance(2)
61
62
63 def gen_captcha(text, fontname, fontsize, file_name):
64         """Generate a captcha image"""
65         # white text on a black background
66         bgcolor = 0x0
67         fgcolor = 0xffffff
68         # create a font object 
69         font = ImageFont.truetype(fontname,fontsize)
70         # determine dimensions of the text
71         dim = font.getsize(text)
72         # create a new image significantly larger that the text
73         edge = max(dim[0], dim[1]) + 2*min(dim[0], dim[1])
74         im = Image.new('RGB', (edge, edge), bgcolor)
75         d = ImageDraw.Draw(im)
76         x, y = im.size
77         # add the text to the image
78         d.text((x/2-dim[0]/2, y/2-dim[1]/2), text, font=font, fill=fgcolor)
79         k = 3
80         wob = 0.20*dim[1]/k
81         rot = 45
82         # Apply lots of small stirring operations, rather than a few large ones
83         # in order to get some uniformity of treatment, whilst
84         # maintaining randomness
85         for i in range(k):
86                 im = wobbly_copy(im, wob, bgcolor, i*2+3, rot+0)
87                 im = wobbly_copy(im, wob, bgcolor, i*2+1, rot+45)
88                 im = wobbly_copy(im, wob, bgcolor, i*2+2, rot+90)
89                 rot += 30
90         
91         # now get the bounding box of the nonzero parts of the image
92         bbox = im.getbbox()
93         bord = min(dim[0], dim[1])/4 # a bit of a border
94         im = im.crop((bbox[0]-bord, bbox[1]-bord, bbox[2]+bord, bbox[3]+bord))
95         # and turn into black on white
96         im = ImageOps.invert(im)
97                 
98         # save the image, in format determined from filename
99         im.save(file_name)
100
101 if __name__ == '__main__':
102         """This grabs random words from the dictionary 'words' (one
103         word per line) and generates a captcha image for each one,
104         with a keyed salted hash of the correct answer in the filename.
105         
106         To check a reply, hash it in the same way with the same salt and
107         secret key, then compare with the hash value given.
108         """
109         font = "VeraBd.ttf"
110         wordlist = "awordlist.txt"
111         key = "CHANGE_THIS_SECRET!"
112         output = "."
113         count = 20
114         fill = 0
115         verbose = False
116         
117         opts, args = getopt.getopt(sys.argv[1:], "", ["font=", "wordlist=", "key=", "output=", "count=", "fill=", "verbose"])
118         for o, a in opts:
119                 if o == "--font":
120                         font = a
121                 if o == "--wordlist":
122                         wordlist = a
123                 if o == "--key":
124                         key = a
125                 if o == "--output":
126                         output = a
127                 if o == "--count":
128                         count = int(a)
129                 if o == "--fill":
130                         fill = int(a)
131                 if o == "--verbose":
132                         verbose = True
133         
134         if fill:
135                 # Option processing order is not guaranteed, so count the output
136                 # files after...
137                 count = max(0, fill - len(os.listdir(output)))
138         
139         words = [string.lower(x.strip()) for x in open(wordlist).readlines()]
140         words = [x for x in words
141                 if len(x) <= 5 and len(x) >= 4 and x[0] != "f"
142                 and x[0] != x[1] and x[-1] != x[-2]
143                 and (not "'" in x)]
144         for i in range(count):
145                 word1 = words[random.randint(0,len(words)-1)]
146                 word2 = words[random.randint(0,len(words)-1)]
147                 word = word1+word2
148                 salt = "%08x" % random.randrange(2**32)
149                 # 64 bits of hash is plenty for this purpose
150                 hash = md5.new(key+salt+word+key+salt).hexdigest()[:16]
151                 filename = "image_%s_%s.png" % (salt, hash)
152                 if verbose:
153                         print filename
154                 gen_captcha(word, font, 40, os.path.join(output, filename))