Приветствую. Возникла ситуация по написанию капчи. Хотел с нуля разобраться в этой теме, так сказать, почувствовать на собственной шкуре, как решаются подобные вопросы. Программисты со стажем, думаю, закидают шапками, но, буду надеется, кому-нить подобный вариант покажется полезен, хотя бы толика из того, что тут опишу.
Итак, имеется 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
Данный метод неидеален и можно много чего еще внести, к примеру, сделать рандомный цвет и толщину линий + сделать задний фон и добавить шум в виде шариков, да много чего еще, главное понять принцип работы, а там уже полет фантазии. Благодарю за внимание, надеюсь, кому-нибудь данная статья будет полезна.
Автор: Ska1n