captcha.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # refer to `https://bitbucket.org/akorn/wheezy.captcha`
  4. import random
  5. import string
  6. import os.path
  7. from io import BytesIO
  8. from PIL import Image
  9. from PIL import ImageFilter
  10. from PIL.ImageDraw import Draw
  11. from PIL.ImageFont import truetype
  12. class Bezier:
  13. def __init__(self):
  14. self.tsequence = tuple([t / 20.0 for t in range(21)])
  15. self.beziers = {}
  16. def pascal_row(self, n):
  17. """ Returns n-th row of Pascal's triangle
  18. """
  19. result = [1]
  20. x, numerator = 1, n
  21. for denominator in range(1, n // 2 + 1):
  22. x *= numerator
  23. x /= denominator
  24. result.append(x)
  25. numerator -= 1
  26. if n & 1 == 0:
  27. result.extend(reversed(result[:-1]))
  28. else:
  29. result.extend(reversed(result))
  30. return result
  31. def make_bezier(self, n):
  32. """ Bezier curves:
  33. http://en.wikipedia.org/wiki/B%C3%A9zier_curve#Generalization
  34. """
  35. try:
  36. return self.beziers[n]
  37. except KeyError:
  38. combinations = self.pascal_row(n - 1)
  39. result = []
  40. for t in self.tsequence:
  41. tpowers = (t ** i for i in range(n))
  42. upowers = ((1 - t) ** i for i in range(n - 1, -1, -1))
  43. coefs = [c * a * b for c, a, b in zip(combinations,
  44. tpowers, upowers)]
  45. result.append(coefs)
  46. self.beziers[n] = result
  47. return result
  48. class Captcha(object):
  49. def __init__(self):
  50. self._bezier = Bezier()
  51. self._dir = os.path.dirname(__file__)
  52. # self._captcha_path = os.path.join(self._dir, '..', 'static', 'captcha')
  53. @staticmethod
  54. def instance():
  55. if not hasattr(Captcha, "_instance"):
  56. Captcha._instance = Captcha()
  57. return Captcha._instance
  58. def initialize(self, width=200, height=75, color=None, text=None, fonts=None):
  59. # self.image = Image.new('RGB', (width, height), (255, 255, 255))
  60. self._text = text if text else random.sample(string.ascii_uppercase + string.ascii_uppercase + '3456789', 4)
  61. self.fonts = fonts if fonts else \
  62. [os.path.join(self._dir, 'fonts', font) for font in ['Arial.ttf', 'Georgia.ttf', 'actionj.ttf']]
  63. self.width = width
  64. self.height = height
  65. self._color = color if color else self.random_color(0, 200, random.randint(220, 255))
  66. @staticmethod
  67. def random_color(start, end, opacity=None):
  68. red = random.randint(start, end)
  69. green = random.randint(start, end)
  70. blue = random.randint(start, end)
  71. if opacity is None:
  72. return red, green, blue
  73. return red, green, blue, opacity
  74. # draw image
  75. def background(self, image):
  76. Draw(image).rectangle([(0, 0), image.size], fill=self.random_color(238, 255))
  77. return image
  78. @staticmethod
  79. def smooth(image):
  80. return image.filter(ImageFilter.SMOOTH)
  81. def curve(self, image, width=4, number=6, color=None):
  82. dx, height = image.size
  83. dx /= number
  84. path = [(dx * i, random.randint(0, height))
  85. for i in range(1, number)]
  86. bcoefs = self._bezier.make_bezier(number - 1)
  87. points = []
  88. for coefs in bcoefs:
  89. points.append(tuple(sum([coef * p for coef, p in zip(coefs, ps)])
  90. for ps in zip(*path)))
  91. Draw(image).line(points, fill=color if color else self._color, width=width)
  92. return image
  93. def noise(self, image, number=50, level=2, color=None):
  94. width, height = image.size
  95. dx = width / 10
  96. width -= dx
  97. dy = height / 10
  98. height -= dy
  99. draw = Draw(image)
  100. for i in range(number):
  101. x = int(random.uniform(dx, width))
  102. y = int(random.uniform(dy, height))
  103. draw.line(((x, y), (x + level, y)), fill=color if color else self._color, width=level)
  104. return image
  105. def text(self, image, fonts, font_sizes=None, drawings=None, squeeze_factor=0.75, color=None):
  106. color = color if color else self._color
  107. fonts = tuple([truetype(name, size)
  108. for name in fonts
  109. for size in font_sizes or (65, 70, 75)])
  110. draw = Draw(image)
  111. char_images = []
  112. for c in self._text:
  113. font = random.choice(fonts)
  114. c_width, c_height = draw.textsize(c, font=font)
  115. char_image = Image.new('RGB', (c_width, c_height), (0, 0, 0))
  116. char_draw = Draw(char_image)
  117. char_draw.text((0, 0), c, font=font, fill=color)
  118. char_image = char_image.crop(char_image.getbbox())
  119. for drawing in drawings:
  120. d = getattr(self, drawing)
  121. char_image = d(char_image)
  122. char_images.append(char_image)
  123. width, height = image.size
  124. offset = int((width - sum(int(i.size[0] * squeeze_factor)
  125. for i in char_images[:-1]) -
  126. char_images[-1].size[0]) / 2)
  127. for char_image in char_images:
  128. c_width, c_height = char_image.size
  129. mask = char_image.convert('L').point(lambda i: i * 1.97)
  130. image.paste(char_image,
  131. (offset, int((height - c_height) / 2)),
  132. mask)
  133. offset += int(c_width * squeeze_factor)
  134. return image
  135. # draw text
  136. @staticmethod
  137. def warp(image, dx_factor=0.27, dy_factor=0.21):
  138. width, height = image.size
  139. dx = width * dx_factor
  140. dy = height * dy_factor
  141. x1 = int(random.uniform(-dx, dx))
  142. y1 = int(random.uniform(-dy, dy))
  143. x2 = int(random.uniform(-dx, dx))
  144. y2 = int(random.uniform(-dy, dy))
  145. image2 = Image.new('RGB',
  146. (width + abs(x1) + abs(x2),
  147. height + abs(y1) + abs(y2)))
  148. image2.paste(image, (abs(x1), abs(y1)))
  149. width2, height2 = image2.size
  150. return image2.transform(
  151. (width, height), Image.QUAD,
  152. (x1, y1,
  153. -x1, height2 - y2,
  154. width2 + x2, height2 + y2,
  155. width2 - x2, -y1))
  156. @staticmethod
  157. def offset(image, dx_factor=0.1, dy_factor=0.2):
  158. width, height = image.size
  159. dx = int(random.random() * width * dx_factor)
  160. dy = int(random.random() * height * dy_factor)
  161. image2 = Image.new('RGB', (width + dx, height + dy))
  162. image2.paste(image, (dx, dy))
  163. return image2
  164. @staticmethod
  165. def rotate(image, angle=25):
  166. return image.rotate(
  167. random.uniform(-angle, angle), Image.BILINEAR, expand=1)
  168. def captcha(self, path=None, fmt='JPEG'):
  169. """Create a captcha.
  170. Args:
  171. path: save path, default None.
  172. fmt: image format, PNG / JPEG.
  173. Returns:
  174. A tuple, (name, text, StringIO.value).
  175. For example:
  176. ('fXZJN4AFxHGoU5mIlcsdOypa', 'JGW9', '\x89PNG\r\n\x1a\n\x00\x00\x00\r...')
  177. """
  178. image = Image.new('RGB', (self.width, self.height), (255, 255, 255))
  179. image = self.background(image)
  180. image = self.text(image, self.fonts, drawings=['warp', 'rotate', 'offset'])
  181. image = self.curve(image)
  182. image = self.noise(image)
  183. image = self.smooth(image)
  184. name = "".join(random.sample(string.ascii_lowercase + string.ascii_uppercase + '3456789', 24))
  185. text = "".join(self._text)
  186. out = BytesIO()
  187. image.save(out, format=fmt)
  188. if path:
  189. image.save(os.path.join(path, name), fmt)
  190. return name, text, out.getvalue()
  191. def generate_captcha(self):
  192. self.initialize()
  193. return self.captcha("")
  194. captcha = Captcha.instance()
  195. if __name__ == '__main__':
  196. print(captcha.generate_captcha())