Капча с помощью PIL или практический велосипед

от автора


Приветствую. Возникла ситуация по написанию капчи. Хотел с нуля разобраться в этой теме, так сказать, почувствовать на собственной шкуре, как решаются подобные вопросы. Программисты со стажем, думаю, закидают шапками, но, буду надеется, кому-нить подобный вариант покажется полезен, хотя бы толика из того, что тут опишу.

Итак, имеется Django, необходимо сделать капчу для понятных целей. Полный процесс валидности описывать не буду, переду непосредственно к примеру, как получить на выходе в браузере динамическую картинку. Что из себя будет представлять капча — шестизначное число, каждая цифра будет иметь свой цвет, шрифт, величину, гулять по вертикали и горизонтали, плюс, будут гулять рандомные шумы в виде палочек.

Для начало, подключим нужные нам библиотеки:

from hashlib import md5 #поднадобится для генерации ключа from PIL import Image, ImageDraw, ImageFont #набор инструментов из библиотеки PIL import random #функция рандома from StringIO import StringIO #будем сохранять картинку в оперативку 

Создаем новую функцию:

def capthaGenerate(request): 

Пропишем переменную, где у нас будут лежать шрифты формата .ttf, при генерации капчи, система будет рандомно брать какой-нить один шрифт для написания цифры:

path = "/nginx/project/files/static/c/" 

Создаем новое изображение средствами PIL:

im = Image.new('RGBA', (200, 50), (0, 0, 0, 0)) draw = ImageDraw.Draw(im) 

Определяем переменные, с которыми будем в дальнейшем работать:

number = "" #сюда занесем наше шестизначное значение из капчи, которое потом преобразуем в ключ md5 margin_left = 0 #здесь будем хранить сколько сделать отступов слева цифре в капче margin_top = 0 #аналогично, только отступ сверху colorNUM = ("0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f") #здесь мы перечислили все доступные варианты для RGB-значений в шестнадцатеричном формате 

Далее, начинаем делать цикл в шесть заходов, по одному заходу на каждую цифру капчи. Первым делом, нам надо определит цвет цифре, для этого я прописал следующее:

font_color = "#"+str(random.randint(0,9)) y = 0 while (y < 5): 	rand = random.choice(colorNUM) 	font_color = font_color+rand 	y = y+1 

Вначале определяется, скажем так, яркость цифры, пройдясь в фотошопе по палитре цветов, определил неопытным глазом, что все цвета, начинающиеся с 0 и заканчивающие 9, не имеют ярких цветов, что хорошо, в случае светлых тонов заднего фона, по сему, цифра будет видна конечному пользователю нормально. Тем самым, первым шагом идет присвоение к переменной font_color цифры 0-9. Далее, идет цикл на 5 повторений, для рандомного выбора из словаря colorNUM, тем самым мы получаем, на выходе font_color со значением, допустим "#381dcd".

С цветом определились, поехали дальше. Далее, мы рисуем линию:

#определяем рандомные значения для рисования линии rand_x11 = random.randint(0,100) rand_x12 = random.randint(100,200) rand_y11 = random.randint(0,50) rand_y12 = random.randint(0,50) #рисуем саму линию draw.line((rand_x11, rand_y11, rand_x12, rand_y12), fill="#a9a6a6") 

Дальше нам надо выбрать рандомный шрифт для цифры. Я выбрал 10 .ttf шрифтов, в которых хорошо читаются цифры, но при этом стиль в каждом индивидуальный. Положил все в папку, путь к которой указал в переменой path. Дальше, определил переменную, которая рандомно выбирает цифру в диапазоне 1-10:

font_rand =str(random.randint(1,10)) 

Рандомно выбираем размер шрифта:

fontSize_rand =random.randint(30,40) 

Объявляем саму переменную для подключения шрифта:

font = ImageFont.truetype(path+"fonts/"+font_rand+".ttf", fontSize_rand) 

Дальше приступаем к рисованию цифры:

a=str(random.randint(0,9)) #Генерируем цифру 

Рисуем цифру:

draw.text((margin_left,margin_top), a,fill=str(font_color),font=font) #Перед циклом мы дали переменным margin_left,margin_top нулевые значения, то есть, первая цифра у нас всегда имеет одно положение, однако, так же будет скакать, поскольку шрифты всегда будут разными, как и ее размер 

Рисуем еще одну линию для шума:

rand_x11 = random.randint(0,100) rand_x12 = random.randint(100,200) rand_y11 = random.randint(0,50) rand_y12 = random.randint(0,50) draw.line((rand_x11, rand_y11, rand_x12, rand_y12), fill="#a9a6a6") 

Прибавим значения отступов слева и сверху для следующих цифр:

margin_left = margin_left+random.randint(20,35) #берем предыдущее значение переменной и прибавляем 20-35 пикселей margin_top = random.randint(0,20) 

В конце цикла записываем наше значение капчи в переменную

number = number+a 

Цикл закончился. Мы имеем с одной стороны картинку, с другой- в отдельной переменной ее значение. Далее нам надо вернуть зашифрованное значение и саму картинку.

Объвляем соль:

salt = "$@!SAf*$@FFVXZA_%(1512czvaRV" 

Делам ключик:

key = md5(str(number+salt)).hexdigest() 

Дальше, нам надо вернуть картинку, чтоб браузер мог ее отобразить пользователю. Для этого воспользуемся кодированием в base64.
Объявляем переменную для выгрузки картинки в буфер:

output = StringIO() 

Выгружаем:

im.save(output, format="PNG") 

Получаем значение, кодируем в base64 и чистим регулярным выражением от символов новых строк:

contents = output.getvalue().encode("base64").replace("\n", "") 

Формируем строку в html вид:

img_tag = '<img value="'+key+'" src="data:image/png;base64,{0}">'.format(contents) 

Очищаем буфер:

output.close() 

Заканчиваем функцию, возвращаем капчу:

return img_tag 

Теперь, при вызове функции capthaGenerate, мы будем получать капчу в виде:

<img src="data:image/png;base64,iVBORw0KGgo.......IAAAAASUVORK5CYII=" value="7751c855c78d509b94f3e07e3d4e28f9"> 

Для валидности капчи нам достаточно передать серверу значение, которое ввел пользователь и value картинки, после чего, значение пользователя привести в подобного рода ключ, применив md5+соль и сравнивать на совпадения значений, ну, или раскодировать value картинки и сравнить с ключом, введенным пользователем, как угодно душе.

На выходе получаем вот такую капчу:

Полноценной код выглядит вот так:

from hashlib import md5 from PIL import Image, ImageDraw, ImageFont import random from StringIO import StringIO  def capthaGenerate(request):     path = "/usr/share/nginx/wavebox/files/static/c/"     im = Image.new('RGBA', (200, 50), (0, 0, 0, 0))     draw = ImageDraw.Draw(im)     number = ""     margin_left = 0     margin_top = 0     colorNUM = ("0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f")     i = 0     while (i < 6):         font_color = "#"+str(random.randint(0,9))         y = 0         while (y < 5):             rand = random.choice(colorNUM)             font_color = font_color+rand             y = y+1         rand_x11 = random.randint(0,100)         rand_x12 = random.randint(100,200)         rand_y11 = random.randint(0,50)         rand_y12 = random.randint(0,50)         draw.line((rand_x11, rand_y11, rand_x12, rand_y12), fill="#a9a6a6")         font_rand =str(random.randint(1,10))         fontSize_rand =random.randint(30,40)         font = ImageFont.truetype(path+"fonts/"+font_rand+".ttf", fontSize_rand)         a=str(random.randint(0,9))         draw.text((margin_left,margin_top), a,fill=str(font_color),font=font)         rand_x11 = random.randint(0,100)         rand_x12 = random.randint(100,200)         rand_y11 = random.randint(0,50)         rand_y12 = random.randint(0,50)         draw.line((rand_x11, rand_y11, rand_x12, rand_y12), fill="#a9a6a6")         margin_left = margin_left+random.randint(20,35)         margin_top = random.randint(0,20)         i = i+1         number = number+a     salt = "$@!SAf*$@)ASFfacnq==124-2542SFDQ!@$1512czvaRV"     key = md5(str(number+salt)).hexdigest()     output = StringIO()     im.save(output, format="PNG")     contents = output.getvalue().encode("base64").replace("\n", "")     img_tag = '<img value="'+key+'" src="data:image/png;base64,{0}">'.format(contents)     output.close()     return img_tag 

Данный метод неидеален и можно много чего еще внести, к примеру, сделать рандомный цвет и толщину линий + сделать задний фон и добавить шум в виде шариков, да много чего еще, главное понять принцип работы, а там уже полет фантазии. Благодарю за внимание, надеюсь, кому-нибудь данная статья будет полезна.

ссылка на оригинал статьи http://habrahabr.ru/post/175079/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *