import os import random from io import BytesIO from PIL import Image, ImageDraw, ImageFont, ImageFilter from django.http import HttpResponse, JsonResponse from django.views import View from django.urls import reverse from django.utils import timezone from pingcap_sso.models import CaptchaStore # Distance of the drawn text from the top of the captcha image DISTANCE_FROM_TOP = 4 CAPTCHA_FONT_PATH = os.path.normpath( os.path.join( os.path.dirname(__file__), 'Vera.ttf', ) ) CAPTCHA_PUNCTUATION = """_"',.;:-""" CAPTCHA_LETTER_ROTATION = (-35, 35) CAPTCHA_FOREGROUND_COLOR = "#0b3c01" CAPTCHA_BACKGROUND_COLOR = "#ffffff" def get_letter_color(index, challenge): return CAPTCHA_FOREGROUND_COLOR def getsize(font, text): if hasattr(font, "getbbox"): _top, _left, _right, _bottom = font.getbbox(text) return _right - _left, _bottom - _top elif hasattr(font, "getoffset"): return tuple([x + y for x, y in zip(font.getsize(text), font.getoffset(text))]) else: return font.getsize(text) def makeimg(size): if CAPTCHA_BACKGROUND_COLOR == "transparent": image = Image.new("RGBA", size) else: image = Image.new("RGB", size, CAPTCHA_BACKGROUND_COLOR) return image def post_smooth(image): return image.filter(ImageFilter.SMOOTH) def noise_arcs(draw, image): size = image.size draw.arc([-20, -20, size[0], 20], 0, 295, fill=CAPTCHA_FOREGROUND_COLOR) draw.line( [-20, 20, size[0] + 20, size[1] - 20], fill=CAPTCHA_FOREGROUND_COLOR ) draw.line([-20, 0, size[0] + 20, size[1]], fill=CAPTCHA_FOREGROUND_COLOR) return draw def noise_dots(draw, image): size = image.size for p in range(int(size[0] * size[1] * 0.1)): draw.point( (random.randint(0, size[0]), random.randint(0, size[1])), fill=CAPTCHA_FOREGROUND_COLOR, ) return draw class CaptchaImageView(View): def get(self, request, identifier, *args, **kwargs): try: store = CaptchaStore.objects.get( identifier=identifier, expires_at__gt=timezone.now() ) except CaptchaStore.DoesNotExist: # HTTP 410 Gone status so that crawlers don't index these expired urls. return HttpResponse(status=410) random.seed(identifier) # Do not generate different images for the same key text = store.challenge font = ImageFont.truetype(CAPTCHA_FONT_PATH, 44) size = getsize(font, text) size = (size[0] * 2, int(size[1] * 1.4)) image = makeimg(size) xpos = 2 charlist = [] for char in text: if char in CAPTCHA_PUNCTUATION and len(charlist) >= 1: charlist[-1] += char else: charlist.append(char) for index, char in enumerate(charlist): fgimage = Image.new( "RGB", size, get_letter_color(index, "".join(charlist)) ) charimage = Image.new("L", getsize(font, " %s " % char), "#000000") chardraw = ImageDraw.Draw(charimage) chardraw.text((0, 0), " %s " % char, font=font, fill="#ffffff") if CAPTCHA_LETTER_ROTATION: charimage = charimage.rotate( random.randrange(*CAPTCHA_LETTER_ROTATION), expand=0, resample=Image.BICUBIC, ) charimage = charimage.crop(charimage.getbbox()) maskimage = Image.new("L", size) maskimage.paste( charimage, ( xpos, DISTANCE_FROM_TOP, xpos + charimage.size[0], DISTANCE_FROM_TOP + charimage.size[1], ), ) size = maskimage.size image = Image.composite(fgimage, image, maskimage) xpos = xpos + 2 + charimage.size[0] image = image.crop((0, 0, xpos + 1, size[1])) draw = ImageDraw.Draw(image) for f in (noise_arcs, noise_dots): draw = f(draw, image) for f in (post_smooth,): image = f(image) out = BytesIO() image.save(out, "PNG") out.seek(0) response = HttpResponse(content_type="image/png") response.write(out.read()) response["Content-length"] = out.tell() # At line :50 above we fixed the random seed so that we always generate the # same image, see: https://github.com/mbi/django-simple-captcha/pull/194 # This is a problem though, because knowledge of the seed will let an attacker # predict the next random (globally). We therefore reset the random here. # Reported in https://github.com/mbi/django-simple-captcha/pull/221 random.seed() return response