123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223 |
- #!/usr/bin/env python
- # -*- coding: utf-8 -*-
- # refer to `https://bitbucket.org/akorn/wheezy.captcha`
- import random
- import string
- import os.path
- from io import BytesIO
- from PIL import Image
- from PIL import ImageFilter
- from PIL.ImageDraw import Draw
- from PIL.ImageFont import truetype
- class Bezier:
- def __init__(self):
- self.tsequence = tuple([t / 20.0 for t in range(21)])
- self.beziers = {}
- def pascal_row(self, n):
- """ Returns n-th row of Pascal's triangle
- """
- result = [1]
- x, numerator = 1, n
- for denominator in range(1, n // 2 + 1):
- x *= numerator
- x /= denominator
- result.append(x)
- numerator -= 1
- if n & 1 == 0:
- result.extend(reversed(result[:-1]))
- else:
- result.extend(reversed(result))
- return result
- def make_bezier(self, n):
- """ Bezier curves:
- http://en.wikipedia.org/wiki/B%C3%A9zier_curve#Generalization
- """
- try:
- return self.beziers[n]
- except KeyError:
- combinations = self.pascal_row(n - 1)
- result = []
- for t in self.tsequence:
- tpowers = (t ** i for i in range(n))
- upowers = ((1 - t) ** i for i in range(n - 1, -1, -1))
- coefs = [c * a * b for c, a, b in zip(combinations,
- tpowers, upowers)]
- result.append(coefs)
- self.beziers[n] = result
- return result
- class Captcha(object):
- def __init__(self):
- self._bezier = Bezier()
- self._dir = os.path.dirname(__file__)
- # self._captcha_path = os.path.join(self._dir, '..', 'static', 'captcha')
- @staticmethod
- def instance():
- if not hasattr(Captcha, "_instance"):
- Captcha._instance = Captcha()
- return Captcha._instance
- def initialize(self, width=200, height=75, color=None, text=None, fonts=None):
- # self.image = Image.new('RGB', (width, height), (255, 255, 255))
- self._text = text if text else random.sample(string.ascii_uppercase + string.ascii_uppercase + '3456789', 4)
- self.fonts = fonts if fonts else \
- [os.path.join(self._dir, 'fonts', font) for font in ['Arial.ttf', 'Georgia.ttf', 'actionj.ttf']]
- self.width = width
- self.height = height
- self._color = color if color else self.random_color(0, 200, random.randint(220, 255))
- @staticmethod
- def random_color(start, end, opacity=None):
- red = random.randint(start, end)
- green = random.randint(start, end)
- blue = random.randint(start, end)
- if opacity is None:
- return red, green, blue
- return red, green, blue, opacity
- # draw image
- def background(self, image):
- Draw(image).rectangle([(0, 0), image.size], fill=self.random_color(238, 255))
- return image
- @staticmethod
- def smooth(image):
- return image.filter(ImageFilter.SMOOTH)
- def curve(self, image, width=4, number=6, color=None):
- dx, height = image.size
- dx /= number
- path = [(dx * i, random.randint(0, height))
- for i in range(1, number)]
- bcoefs = self._bezier.make_bezier(number - 1)
- points = []
- for coefs in bcoefs:
- points.append(tuple(sum([coef * p for coef, p in zip(coefs, ps)])
- for ps in zip(*path)))
- Draw(image).line(points, fill=color if color else self._color, width=width)
- return image
- def noise(self, image, number=50, level=2, color=None):
- width, height = image.size
- dx = width / 10
- width -= dx
- dy = height / 10
- height -= dy
- draw = Draw(image)
- for i in range(number):
- x = int(random.uniform(dx, width))
- y = int(random.uniform(dy, height))
- draw.line(((x, y), (x + level, y)), fill=color if color else self._color, width=level)
- return image
- def text(self, image, fonts, font_sizes=None, drawings=None, squeeze_factor=0.75, color=None):
- color = color if color else self._color
- fonts = tuple([truetype(name, size)
- for name in fonts
- for size in font_sizes or (65, 70, 75)])
- draw = Draw(image)
- char_images = []
- for c in self._text:
- font = random.choice(fonts)
- c_width, c_height = draw.textsize(c, font=font)
- char_image = Image.new('RGB', (c_width, c_height), (0, 0, 0))
- char_draw = Draw(char_image)
- char_draw.text((0, 0), c, font=font, fill=color)
- char_image = char_image.crop(char_image.getbbox())
- for drawing in drawings:
- d = getattr(self, drawing)
- char_image = d(char_image)
- char_images.append(char_image)
- width, height = image.size
- offset = int((width - sum(int(i.size[0] * squeeze_factor)
- for i in char_images[:-1]) -
- char_images[-1].size[0]) / 2)
- for char_image in char_images:
- c_width, c_height = char_image.size
- mask = char_image.convert('L').point(lambda i: i * 1.97)
- image.paste(char_image,
- (offset, int((height - c_height) / 2)),
- mask)
- offset += int(c_width * squeeze_factor)
- return image
- # draw text
- @staticmethod
- def warp(image, dx_factor=0.27, dy_factor=0.21):
- width, height = image.size
- dx = width * dx_factor
- dy = height * dy_factor
- x1 = int(random.uniform(-dx, dx))
- y1 = int(random.uniform(-dy, dy))
- x2 = int(random.uniform(-dx, dx))
- y2 = int(random.uniform(-dy, dy))
- image2 = Image.new('RGB',
- (width + abs(x1) + abs(x2),
- height + abs(y1) + abs(y2)))
- image2.paste(image, (abs(x1), abs(y1)))
- width2, height2 = image2.size
- return image2.transform(
- (width, height), Image.QUAD,
- (x1, y1,
- -x1, height2 - y2,
- width2 + x2, height2 + y2,
- width2 - x2, -y1))
- @staticmethod
- def offset(image, dx_factor=0.1, dy_factor=0.2):
- width, height = image.size
- dx = int(random.random() * width * dx_factor)
- dy = int(random.random() * height * dy_factor)
- image2 = Image.new('RGB', (width + dx, height + dy))
- image2.paste(image, (dx, dy))
- return image2
- @staticmethod
- def rotate(image, angle=25):
- return image.rotate(
- random.uniform(-angle, angle), Image.BILINEAR, expand=1)
- def captcha(self, path=None, fmt='JPEG'):
- """Create a captcha.
- Args:
- path: save path, default None.
- fmt: image format, PNG / JPEG.
- Returns:
- A tuple, (name, text, StringIO.value).
- For example:
- ('fXZJN4AFxHGoU5mIlcsdOypa', 'JGW9', '\x89PNG\r\n\x1a\n\x00\x00\x00\r...')
- """
- image = Image.new('RGB', (self.width, self.height), (255, 255, 255))
- image = self.background(image)
- image = self.text(image, self.fonts, drawings=['warp', 'rotate', 'offset'])
- image = self.curve(image)
- image = self.noise(image)
- image = self.smooth(image)
- name = "".join(random.sample(string.ascii_lowercase + string.ascii_uppercase + '3456789', 24))
- text = "".join(self._text)
- out = BytesIO()
- image.save(out, format=fmt)
- if path:
- image.save(os.path.join(path, name), fmt)
- return name, text, out.getvalue()
- def generate_captcha(self):
- self.initialize()
- return self.captcha("")
- captcha = Captcha.instance()
- if __name__ == '__main__':
- print(captcha.generate_captcha())
|