31 from optparse
import OptionParser
38 from PIL
import ImageFont
39 from PIL
import ImageDraw
40 from PIL
import ImageEnhance
41 from PIL
import ImageOps
43 sys.exit(
"This script requires the Python Imaging Library - http://www.pythonware.com/products/pil/")
45 nonalpha = re.compile(
'[^a-z]')
50 f = random.uniform(4*scale, 5*scale)
51 p = random.uniform(0, math.pi*2)
52 rr = ang+random.uniform(-30, 30)
53 int_d = Image.new(
'RGB', src.size, 0)
54 rot = src.rotate(rr, Image.BILINEAR)
62 for i
in range(t, b+1):
64 xoff = int(math.sin(p+(i*f/y))*wob)
65 xoff += int(random.uniform(-wob*0.5, wob*0.5))
66 int_d.paste(rot.crop((0, i, x, i+1)), (xoff, i))
68 int_d = int_d.rotate(-rr, Image.BILINEAR)
69 enh = ImageEnhance.Sharpness(int_d)
74 """Generate a captcha image"""
79 font = ImageFont.truetype(fontname,fontsize)
81 dim = font.getsize(text)
83 edge = max(dim[0], dim[1]) + 2*min(dim[0], dim[1])
84 im = Image.new(
'RGB', (edge, edge), bgcolor)
85 d = ImageDraw.Draw(im)
88 d.text((x/2-dim[0]/2, y/2-dim[1]/2), text, font=font, fill=fgcolor)
103 bord = min(dim[0], dim[1])/4
104 im = im.crop((bbox[0]-bord, bbox[1]-bord, bbox[2]+bord, bbox[3]+bord))
106 im = ImageOps.invert(im)
112 """Generate a subdirectory path out of the first _levels_
113 characters of _hash_, and ensure the directories exist
116 for i
in range(0, levels):
119 subdir = os.path.join(subdir, char)
122 fulldir = os.path.join(basedir, subdir)
123 if not os.path.exists(fulldir):
127 def try_pick_word(words, blacklist, verbose, nwords, min_length, max_length):
128 if words
is not None:
129 word = words[random.randint(0,len(words)-1)]
131 word2 = words[random.randint(0,len(words)-1)]
136 max_length = max_length
if max_length > 0
else 10
137 for i
in range(0, random.randint(min_length, max_length)):
138 word = word + chr(97 + random.randint(0,25))
141 print(
"word is %s" % word)
143 if len(word) < min_length:
145 print(
"skipping word pair '%s' because it has fewer than %d characters" % (word, min_length))
148 if max_length > 0
and len(word) > max_length:
150 print(
"skipping word pair '%s' because it has more than %d characters" % (word, max_length))
153 if nonalpha.search(word):
155 print(
"skipping word pair '%s' because it contains non-alphabetic characters" % word)
158 for naughty
in blacklist:
161 print(
"skipping word pair '%s' because it contains blacklisted word '%s'" % (word, naughty))
165 def pick_word(words, blacklist, verbose, nwords, min_length, max_length):
166 for x
in range(1000):
167 word =
try_pick_word(words, blacklist, verbose, nwords, min_length, max_length)
170 sys.exit(
"Unable to find valid word combinations")
174 words = [x.strip().lower()
for x
in f.readlines()]
178 if __name__ ==
'__main__':
179 """This grabs random words from the dictionary 'words' (one
180 word per line) and generates a captcha image for each one,
181 with a keyed salted hash of the correct answer in the filename.
183 To check a reply, hash it in the same way with the same salt and
184 secret key, then compare with the hash value given.
186 script_dir = os.path.dirname(os.path.realpath(__file__))
187 parser = OptionParser()
188 parser.add_option(
"--wordlist", help=
"A list of words (required)", metavar=
"WORDS.txt")
189 parser.add_option(
"--random", help=
"Use random charcters instead of a wordlist", action=
"store_true")
190 parser.add_option(
"--key", help=
"The passphrase set as $wgCaptchaSecret (required)", metavar=
"KEY")
191 parser.add_option(
"--output", help=
"The directory to put the images in - $wgCaptchaDirectory (required)", metavar=
"DIR")
192 parser.add_option(
"--font", help=
"The font to use (required)", metavar=
"FONT.ttf")
193 parser.add_option(
"--font-size", help=
"The font size (default 40)", metavar=
"N", type=
'int', default=40)
194 parser.add_option(
"--count", help=
"The maximum number of images to make (default 20)", metavar=
"N", type=
'int', default=20)
195 parser.add_option(
"--blacklist", help=
"A blacklist of words that should not be used", metavar=
"FILE", default=os.path.join(script_dir,
"blacklist"))
196 parser.add_option(
"--fill", help=
"Fill the output directory to contain N files, overrides count, cannot be used with --dirs", metavar=
"N", type=
'int')
197 parser.add_option(
"--dirs", help=
"Put the images into subdirectories N levels deep - $wgCaptchaDirectoryLevels", metavar=
"N", type=
'int')
198 parser.add_option(
"--verbose",
"-v", help=
"Show debugging information", action=
'store_true')
199 parser.add_option(
"--number-words", help=
"Number of words from the wordlist which make a captcha challenge (default 2)", type=
'int', default=2)
200 parser.add_option(
"--min-length", help=
"Minimum length for a captcha challenge", type=
'int', default=1)
201 parser.add_option(
"--max-length", help=
"Maximum length for a captcha challenge", type=
'int', default=-1)
203 opts, args = parser.parse_args()
206 wordlist = opts.wordlist
210 sys.exit(
"Need to specify a wordlist")
214 sys.exit(
"Need to specify a key")
218 sys.exit(
"Need to specify an output directory")
219 if opts.font
and os.path.exists(opts.font):
222 sys.exit(
"Need to specify the location of a font")
228 verbose = opts.verbose
229 fontsize = opts.font_size
232 count = max(0, fill - len(os.listdir(output)))
237 words = [x
for x
in words
238 if len(x)
in (4,5)
and x[0] !=
"f"
239 and x[0] != x[1]
and x[-1] != x[-2]]
241 for i
in range(count):
242 word =
pick_word(words, blacklist, verbose, opts.number_words, opts.min_length, opts.max_length)
243 salt =
"%08x" % random.randrange(2**32)
245 md5hash = hashlib.md5((key+salt+word+key+salt).encode(
'utf-8')).hexdigest()[:16]
246 filename =
"image_%s_%s.png" % (salt, md5hash)
249 filename = os.path.join(subdir, filename)
252 gen_captcha(word, font, fontsize, os.path.join(output, filename))