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

в 8:31, , рубрики: django, PIL, python, метки: , ,

Капча с помощью 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 картинки и сравнить с ключом, введенным пользователем, как угодно душе.

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

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

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js