3 # Script to generate distorted text images for a captcha system.
5 # Copyright (C) 2005 Neil Harris
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.
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.
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
22 # Further tweaks by Brion Vibber <brion@pobox.com>:
23 # 2006-01-26: Add command-line options for the various parameters
24 # 2007-02-19: Add --dirs param for hash subdirectory splits
32 import math, string, md5
37 # Does X-axis wobbly copy, sandwiched between two rotates
38 def wobbly_copy(src, wob, col, scale, ang):
40 f = random.uniform(4*scale, 5*scale)
41 p = random.uniform(0, math.pi*2)
42 rr = ang+random.uniform(-30, 30) # vary, but not too much
43 int_d = Image.new('RGB', src.size, 0) # a black rectangle
44 rot = src.rotate(rr, Image.BILINEAR)
45 # Do a cheap bounding-box op here to try to limit work below
52 # and only do lines with content on
53 for i in range(t, b+1):
55 xoff = int(math.sin(p+(i*f/y))*wob)
56 xoff += int(random.uniform(-wob*0.5, wob*0.5))
57 int_d.paste(rot.crop((0, i, x, i+1)), (xoff, i))
58 # try to stop blurring from building up
59 int_d = int_d.rotate(-rr, Image.BILINEAR)
60 enh = ImageEnhance.Sharpness(int_d)
64 def gen_captcha(text, fontname, fontsize, file_name):
65 """Generate a captcha image"""
66 # white text on a black background
69 # create a font object
70 font = ImageFont.truetype(fontname,fontsize)
71 # determine dimensions of the text
72 dim = font.getsize(text)
73 # create a new image significantly larger that the text
74 edge = max(dim[0], dim[1]) + 2*min(dim[0], dim[1])
75 im = Image.new('RGB', (edge, edge), bgcolor)
76 d = ImageDraw.Draw(im)
78 # add the text to the image
79 d.text((x/2-dim[0]/2, y/2-dim[1]/2), text, font=font, fill=fgcolor)
83 # Apply lots of small stirring operations, rather than a few large ones
84 # in order to get some uniformity of treatment, whilst
85 # maintaining randomness
87 im = wobbly_copy(im, wob, bgcolor, i*2+3, rot+0)
88 im = wobbly_copy(im, wob, bgcolor, i*2+1, rot+45)
89 im = wobbly_copy(im, wob, bgcolor, i*2+2, rot+90)
92 # now get the bounding box of the nonzero parts of the image
94 bord = min(dim[0], dim[1])/4 # a bit of a border
95 im = im.crop((bbox[0]-bord, bbox[1]-bord, bbox[2]+bord, bbox[3]+bord))
96 # and turn into black on white
97 im = ImageOps.invert(im)
99 # save the image, in format determined from filename
102 def gen_subdir(basedir, hash, levels):
103 """Generate a subdirectory path out of the first _levels_
104 characters of _hash_, and ensure the directories exist
107 for i in range(0, levels):
110 subdir = os.path.join(subdir, char)
113 fulldir = os.path.join(basedir, subdir)
114 if not os.path.exists(fulldir):
118 def try_pick_word(words, blacklist, verbose):
119 word1 = words[random.randint(0,len(words)-1)]
120 word2 = words[random.randint(0,len(words)-1)]
122 for naughty in blacklist:
125 print "skipping word pair '%s' because it contains blacklisted word '%s'" % (word, naughty)
129 def pick_word(words, blacklist, verbose):
131 word = try_pick_word(words, blacklist, verbose)
135 def read_wordlist(filename):
136 return [string.lower(x.strip()) for x in open(wordlist).readlines()]
138 if __name__ == '__main__':
139 """This grabs random words from the dictionary 'words' (one
140 word per line) and generates a captcha image for each one,
141 with a keyed salted hash of the correct answer in the filename.
143 To check a reply, hash it in the same way with the same salt and
144 secret key, then compare with the hash value given.
147 wordlist = "awordlist.txt"
149 key = "CHANGE_THIS_SECRET!"
156 opts, args = getopt.getopt(sys.argv[1:], "", ["font=", "wordlist=", "blacklist=", "key=", "output=", "count=", "fill=", "dirs=", "verbose"])
160 if o == "--wordlist":
162 if o == "--blacklist":
178 # Option processing order is not guaranteed, so count the output
180 count = max(0, fill - len(os.listdir(output)))
182 words = read_wordlist(wordlist)
183 words = [x for x in words
184 if len(x) <= 5 and len(x) >= 4 and x[0] != "f"
185 and x[0] != x[1] and x[-1] != x[-2]
189 blacklist = read_wordlist(blacklistfile)
193 for i in range(count):
194 word = pick_word(words, blacklist, verbose)
195 salt = "%08x" % random.randrange(2**32)
196 # 64 bits of hash is plenty for this purpose
197 hash = md5.new(key+salt+word+key+salt).hexdigest()[:16]
198 filename = "image_%s_%s.png" % (salt, hash)
200 subdir = gen_subdir(output, hash, dirs)
201 filename = os.path.join(subdir, filename)
204 gen_captcha(word, font, 40, os.path.join(output, filename))