@@ -0,0 +1,15 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
Captcha
|
||||
~~~~~~~
|
||||
|
||||
A captcha library that generates audio and image CAPTCHAs.
|
||||
|
||||
:copyright: (c) 2014 by Hsiaoming Yang.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
__version__ = '0.7.1'
|
||||
__author__ = 'Hsiaoming Yang <me@lepture.com>'
|
||||
__homepage__ = 'https://github.com/lepture/captcha'
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,279 @@
|
||||
# coding: utf-8
|
||||
"""
|
||||
captcha.audio
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Generate Audio CAPTCHAs, with built-in digits CAPTCHA.
|
||||
|
||||
This module is totally inspired by https://github.com/dchest/captcha
|
||||
"""
|
||||
|
||||
import typing as t
|
||||
import os
|
||||
import copy
|
||||
import wave
|
||||
import struct
|
||||
import secrets
|
||||
import operator
|
||||
from functools import reduce
|
||||
|
||||
|
||||
__all__ = ['AudioCaptcha']
|
||||
|
||||
WAVE_SAMPLE_RATE = 8000 # HZ
|
||||
WAVE_HEADER = bytearray(
|
||||
b'RIFF\x00\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x01\x00'
|
||||
b'@\x1f\x00\x00@\x1f\x00\x00\x01\x00\x08\x00data'
|
||||
)
|
||||
WAVE_HEADER_LENGTH = len(WAVE_HEADER) - 4
|
||||
DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data')
|
||||
|
||||
|
||||
def _read_wave_file(filepath: str) -> bytearray:
|
||||
w = wave.open(filepath)
|
||||
data = w.readframes(-1)
|
||||
w.close()
|
||||
return bytearray(data)
|
||||
|
||||
|
||||
def change_speed(body: bytearray, speed: float = 1) -> bytearray:
|
||||
"""Change the voice speed of the wave body."""
|
||||
if speed == 1:
|
||||
return body
|
||||
|
||||
length = int(len(body) * speed)
|
||||
rv = bytearray(length)
|
||||
|
||||
step: float = 0
|
||||
for v in body:
|
||||
i = int(step)
|
||||
while i < int(step + speed) and i < length:
|
||||
rv[i] = v
|
||||
i += 1
|
||||
step += speed
|
||||
return rv
|
||||
|
||||
|
||||
def patch_wave_header(body: bytearray) -> bytearray:
|
||||
"""Patch header to the given wave body.
|
||||
|
||||
:param body: the wave content body, it should be bytearray.
|
||||
"""
|
||||
length = len(body)
|
||||
|
||||
padded = length + length % 2
|
||||
total = WAVE_HEADER_LENGTH + padded
|
||||
|
||||
header = copy.copy(WAVE_HEADER)
|
||||
# fill the total length position
|
||||
header[4:8] = bytearray(struct.pack('<I', total))
|
||||
header += bytearray(struct.pack('<I', length))
|
||||
|
||||
data = header + body
|
||||
|
||||
# the total length is even
|
||||
if length != padded:
|
||||
data = data + bytearray([0])
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def create_noise(length: int, level: int = 4) -> bytearray:
|
||||
"""Create white noise for background"""
|
||||
noise = bytearray(length)
|
||||
adjust = 128 - int(level / 2)
|
||||
i = 0
|
||||
while i < length:
|
||||
v = secrets.randbelow(257)
|
||||
noise[i] = v % level + adjust
|
||||
i += 1
|
||||
return noise
|
||||
|
||||
|
||||
def create_silence(length: int) -> bytearray:
|
||||
"""Create a piece of silence."""
|
||||
data = bytearray(length)
|
||||
i = 0
|
||||
while i < length:
|
||||
data[i] = 128
|
||||
i += 1
|
||||
return data
|
||||
|
||||
|
||||
def change_sound(body: bytearray, level: float = 1) -> bytearray:
|
||||
if level == 1:
|
||||
return body
|
||||
|
||||
body = copy.copy(body)
|
||||
for i, v in enumerate(body):
|
||||
if v > 128:
|
||||
v = int((v - 128) * level + 128)
|
||||
v = max(v, 128)
|
||||
v = min(v, 255)
|
||||
elif v < 128:
|
||||
v = int(128 - (128 - v) * level)
|
||||
v = min(v, 128)
|
||||
v = max(v, 0)
|
||||
body[i] = v
|
||||
return body
|
||||
|
||||
|
||||
def mix_wave(src: bytearray, dst: bytearray) -> bytearray:
|
||||
"""Mix two wave body into one."""
|
||||
if len(src) > len(dst):
|
||||
# output should be longer
|
||||
dst, src = src, dst
|
||||
|
||||
for i, sv in enumerate(src):
|
||||
dv = dst[i]
|
||||
if sv < 128 and dv < 128:
|
||||
dst[i] = int(sv * dv / 128)
|
||||
else:
|
||||
dst[i] = int(2 * (sv + dv) - sv * dv / 128 - 256)
|
||||
return dst
|
||||
|
||||
|
||||
BEEP = _read_wave_file(os.path.join(DATA_DIR, 'beep.wav'))
|
||||
END_BEEP = change_speed(BEEP, 1.4)
|
||||
SILENCE = create_silence(int(WAVE_SAMPLE_RATE / 5))
|
||||
|
||||
|
||||
class AudioCaptcha:
|
||||
"""Create an audio CAPTCHA.
|
||||
|
||||
Create an instance of AudioCaptcha is pretty simple::
|
||||
|
||||
captcha = AudioCaptcha()
|
||||
captcha.write('1234', 'out.wav')
|
||||
|
||||
This module has a built-in digits CAPTCHA, but it is suggested that you
|
||||
create your own voice data library. A voice data library is a directory
|
||||
that contains lots of single charater named directories, for example::
|
||||
|
||||
voices/
|
||||
0/
|
||||
1/
|
||||
2/
|
||||
|
||||
The single charater named directories contain the wave files which pronunce
|
||||
the directory name. A charater directory can has many wave files, this
|
||||
AudioCaptcha will randomly choose one of them.
|
||||
|
||||
You should always use your own voice library::
|
||||
|
||||
captcha = AudioCaptcha(voicedir='/path/to/voices')
|
||||
"""
|
||||
def __init__(self, voicedir: t.Optional[str] = None):
|
||||
if voicedir is None:
|
||||
voicedir = DATA_DIR
|
||||
|
||||
self._voicedir = voicedir
|
||||
self._cache: t.Dict[str, t.List[bytearray]] = {}
|
||||
self._choices: t.List[str] = []
|
||||
|
||||
@property
|
||||
def choices(self) -> t.List[str]:
|
||||
"""Available choices for characters to be generated."""
|
||||
if self._choices:
|
||||
return self._choices
|
||||
for n in os.listdir(self._voicedir):
|
||||
if len(n) == 1 and os.path.isdir(os.path.join(self._voicedir, n)):
|
||||
self._choices.append(n)
|
||||
return self._choices
|
||||
|
||||
def random(self, length: int = 6) -> t.List[str]:
|
||||
"""Generate a random string with the given length.
|
||||
|
||||
:param length: the return string length.
|
||||
"""
|
||||
return [secrets.choice(self.choices) for _ in range(length)]
|
||||
|
||||
def load(self) -> None:
|
||||
"""Load voice data into memory."""
|
||||
for name in self.choices:
|
||||
self._load_data(name)
|
||||
|
||||
def _load_data(self, name: str) -> None:
|
||||
dirname = os.path.join(self._voicedir, name)
|
||||
data: t.List[bytearray] = []
|
||||
for f in os.listdir(dirname):
|
||||
filepath = os.path.join(dirname, f)
|
||||
if f.endswith('.wav') and os.path.isfile(filepath):
|
||||
data.append(_read_wave_file(filepath))
|
||||
self._cache[name] = data
|
||||
|
||||
def _twist_pick(self, key: str) -> bytearray:
|
||||
voice = secrets.choice(self._cache[key])
|
||||
|
||||
# random change speed
|
||||
speed = (secrets.randbelow(31) + 90) / 100.0
|
||||
voice = change_speed(voice, speed)
|
||||
|
||||
# random change sound
|
||||
level = (secrets.randbelow(41) + 80) / 100.0
|
||||
voice = change_sound(voice, level)
|
||||
return voice
|
||||
|
||||
def _noise_pick(self) -> bytearray:
|
||||
key = secrets.choice(self.choices)
|
||||
voice = secrets.choice(self._cache[key])
|
||||
voice = copy.copy(voice)
|
||||
voice.reverse()
|
||||
|
||||
speed = (secrets.randbelow(9) + 8) / 10.0
|
||||
voice = change_speed(voice, speed)
|
||||
|
||||
level = (secrets.randbelow(5) + 2) / 10.0
|
||||
voice = change_sound(voice, level)
|
||||
return voice
|
||||
|
||||
def create_background_noise(self, length: int, chars: str) -> bytearray:
|
||||
noise = create_noise(length, 4)
|
||||
pos = 0
|
||||
while pos < length:
|
||||
sound = self._noise_pick()
|
||||
end = pos + len(sound) + 1
|
||||
noise[pos:end] = mix_wave(sound, noise[pos:end])
|
||||
pos = end + secrets.randbelow(int(WAVE_SAMPLE_RATE / 10) + 1)
|
||||
return noise
|
||||
|
||||
def create_wave_body(self, chars: str) -> bytearray:
|
||||
voices: t.List[bytearray] = []
|
||||
inters: t.List[int] = []
|
||||
for c in chars:
|
||||
voices.append(self._twist_pick(c))
|
||||
i = secrets.randbelow(WAVE_SAMPLE_RATE * 3 - WAVE_SAMPLE_RATE + 1) + WAVE_SAMPLE_RATE
|
||||
inters.append(i)
|
||||
|
||||
durations = map(lambda a: len(a), voices)
|
||||
length = max(durations) * len(chars) + reduce(operator.add, inters)
|
||||
bg = self.create_background_noise(length, chars)
|
||||
|
||||
# begin
|
||||
pos: int = inters[0]
|
||||
for i, v in enumerate(voices):
|
||||
end = pos + len(v) + 1
|
||||
bg[pos:end] = mix_wave(v, bg[pos:end])
|
||||
pos = end + inters[i]
|
||||
|
||||
return BEEP + SILENCE + BEEP + SILENCE + BEEP + bg + END_BEEP
|
||||
|
||||
def generate(self, chars: str) -> bytearray:
|
||||
"""Generate audio CAPTCHA data. The return data is a bytearray.
|
||||
|
||||
:param chars: text to be generated.
|
||||
"""
|
||||
if not self._cache:
|
||||
self.load()
|
||||
body = self.create_wave_body(chars)
|
||||
return patch_wave_header(body)
|
||||
|
||||
def write(self, chars: str, output: str) -> None:
|
||||
"""Generate and write audio CAPTCHA data to the output.
|
||||
|
||||
:param chars: text to be generated.
|
||||
:param output: output destionation.
|
||||
"""
|
||||
data = self.generate(chars)
|
||||
with open(output, 'wb') as f:
|
||||
f.write(data)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,249 @@
|
||||
# coding: utf-8
|
||||
"""
|
||||
captcha.image
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Generate Image CAPTCHAs, just the normal image CAPTCHAs you are using.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import secrets
|
||||
import typing as t
|
||||
from PIL.Image import new as createImage, Image, Transform, Resampling
|
||||
from PIL.ImageDraw import Draw, ImageDraw
|
||||
from PIL.ImageFilter import SMOOTH
|
||||
from PIL.ImageFont import FreeTypeFont, truetype
|
||||
from io import BytesIO
|
||||
|
||||
__all__ = ['ImageCaptcha']
|
||||
|
||||
|
||||
ColorTuple = t.Union[t.Tuple[int, int, int], t.Tuple[int, int, int, int]]
|
||||
|
||||
DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data')
|
||||
DEFAULT_FONTS = [os.path.join(DATA_DIR, 'DroidSansMono.ttf')]
|
||||
|
||||
|
||||
class ImageCaptcha:
|
||||
"""Create an image CAPTCHA.
|
||||
|
||||
Many of the codes are borrowed from wheezy.captcha, with a modification
|
||||
for memory and developer friendly.
|
||||
|
||||
ImageCaptcha has one built-in font, DroidSansMono, which is licensed under
|
||||
Apache License 2. You should always use your own fonts::
|
||||
|
||||
captcha = ImageCaptcha(fonts=['/path/to/A.ttf', '/path/to/B.ttf'])
|
||||
|
||||
You can put as many fonts as you like. But be aware of your memory, all of
|
||||
the fonts are loaded into your memory, so keep them a lot, but not too
|
||||
many.
|
||||
|
||||
:param width: The width of the CAPTCHA image.
|
||||
:param height: The height of the CAPTCHA image.
|
||||
:param fonts: Fonts to be used to generate CAPTCHA images.
|
||||
:param font_sizes: Random choose a font size from this parameters.
|
||||
"""
|
||||
lookup_table: list[int] = [int(i * 1.97) for i in range(256)]
|
||||
character_offset_dx: tuple[int, int] = (0, 4)
|
||||
character_offset_dy: tuple[int, int] = (0, 6)
|
||||
character_rotate: tuple[int, int] = (-30, 30)
|
||||
character_warp_dx: tuple[float, float] = (0.1, 0.3)
|
||||
character_warp_dy: tuple[float, float] = (0.2, 0.3)
|
||||
word_space_probability: float = 0.5
|
||||
word_offset_dx: float = 0.25
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
width: int = 160,
|
||||
height: int = 60,
|
||||
fonts: list[str] | None = None,
|
||||
font_sizes: tuple[int, ...] | None = None):
|
||||
self._width = width
|
||||
self._height = height
|
||||
self._fonts = fonts or DEFAULT_FONTS
|
||||
self._font_sizes = font_sizes or (42, 50, 56)
|
||||
self._truefonts: list[FreeTypeFont] = []
|
||||
|
||||
@property
|
||||
def truefonts(self) -> list[FreeTypeFont]:
|
||||
if self._truefonts:
|
||||
return self._truefonts
|
||||
self._truefonts = [
|
||||
truetype(n, s)
|
||||
for n in self._fonts
|
||||
for s in self._font_sizes
|
||||
]
|
||||
return self._truefonts
|
||||
|
||||
@staticmethod
|
||||
def create_noise_curve(image: Image, color: ColorTuple) -> Image:
|
||||
w, h = image.size
|
||||
x1 = secrets.randbelow(int(w / 5) + 1)
|
||||
x2 = secrets.randbelow(w - int(w / 5) + 1) + int(w / 5)
|
||||
y1 = secrets.randbelow(h - 2 * int(h / 5) + 1) + int(h / 5)
|
||||
y2 = secrets.randbelow(h - y1 - int(h / 5) + 1) + y1
|
||||
points = [x1, y1, x2, y2]
|
||||
end = secrets.randbelow(41) + 160
|
||||
start = secrets.randbelow(21)
|
||||
Draw(image).arc(points, start, end, fill=color)
|
||||
return image
|
||||
|
||||
@staticmethod
|
||||
def create_noise_dots(
|
||||
image: Image,
|
||||
color: ColorTuple,
|
||||
width: int = 3,
|
||||
number: int = 30) -> Image:
|
||||
draw = Draw(image)
|
||||
w, h = image.size
|
||||
while number:
|
||||
x1 = secrets.randbelow(w + 1)
|
||||
y1 = secrets.randbelow(h + 1)
|
||||
draw.line(((x1, y1), (x1 - 1, y1 - 1)), fill=color, width=width)
|
||||
number -= 1
|
||||
return image
|
||||
|
||||
def _draw_character(
|
||||
self,
|
||||
c: str,
|
||||
draw: ImageDraw,
|
||||
color: ColorTuple) -> Image:
|
||||
font = secrets.choice(self.truefonts)
|
||||
_, _, w, h = draw.multiline_textbbox((1, 1), c, font=font)
|
||||
|
||||
dx1 = secrets.randbelow(self.character_offset_dx[1] - self.character_offset_dx[0] + 1) + self.character_offset_dx[0]
|
||||
dy1 = secrets.randbelow(self.character_offset_dy[1] - self.character_offset_dy[0] + 1) + self.character_offset_dy[0]
|
||||
im = createImage('RGBA', (int(w) + dx1, int(h) + dy1))
|
||||
Draw(im).text((dx1, dy1), c, font=font, fill=color)
|
||||
|
||||
# rotate
|
||||
im = im.crop(im.getbbox())
|
||||
im = im.rotate(
|
||||
self.character_rotate[0] + (secrets.randbits(32) / (2**32)) * (self.character_rotate[1] - self.character_rotate[0]),
|
||||
Resampling.BILINEAR,
|
||||
expand=True,
|
||||
)
|
||||
|
||||
# warp
|
||||
dx2 = w * (secrets.randbits(32) / (2**32)) * (self.character_warp_dx[1] - self.character_warp_dx[0]) + self.character_warp_dx[0]
|
||||
dy2 = h * (secrets.randbits(32) / (2**32)) * (self.character_warp_dy[1] - self.character_warp_dy[0]) + self.character_warp_dy[0]
|
||||
x1 = int(secrets.randbits(32) / (2**32) * (dx2 - (-dx2)) + (-dx2))
|
||||
y1 = int(secrets.randbits(32) / (2**32) * (dy2 - (-dy2)) + (-dy2))
|
||||
x2 = int(secrets.randbits(32) / (2**32) * (dx2 - (-dx2)) + (-dx2))
|
||||
y2 = int(secrets.randbits(32) / (2**32) * (dy2 - (-dy2)) + (-dy2))
|
||||
w2 = w + abs(x1) + abs(x2)
|
||||
h2 = h + abs(y1) + abs(y2)
|
||||
data = (
|
||||
x1, y1,
|
||||
-x1, h2 - y2,
|
||||
w2 + x2, h2 + y2,
|
||||
w2 - x2, -y1,
|
||||
)
|
||||
im = im.resize((w2, h2))
|
||||
im = im.transform((int(w), int(h)), Transform.QUAD, data)
|
||||
return im
|
||||
|
||||
def create_captcha_image(
|
||||
self,
|
||||
chars: str,
|
||||
color: ColorTuple,
|
||||
background: ColorTuple) -> Image:
|
||||
"""Create the CAPTCHA image itself.
|
||||
|
||||
:param chars: text to be generated.
|
||||
:param color: color of the text.
|
||||
:param background: color of the background.
|
||||
|
||||
The color should be a tuple of 3 numbers, such as (0, 255, 255).
|
||||
"""
|
||||
image = createImage('RGB', (self._width, self._height), background)
|
||||
draw = Draw(image)
|
||||
|
||||
images: list[Image] = []
|
||||
for c in chars:
|
||||
if secrets.randbits(32) / (2**32) > self.word_space_probability:
|
||||
images.append(self._draw_character(" ", draw, color))
|
||||
images.append(self._draw_character(c, draw, color))
|
||||
|
||||
text_width = sum([im.size[0] for im in images])
|
||||
|
||||
width = max(text_width, self._width)
|
||||
image = image.resize((width, self._height))
|
||||
|
||||
average = int(text_width / len(chars))
|
||||
rand = int(self.word_offset_dx * average)
|
||||
offset = int(average * 0.1)
|
||||
|
||||
for im in images:
|
||||
w, h = im.size
|
||||
mask = im.convert('L').point(self.lookup_table)
|
||||
image.paste(im, (offset, int((self._height - h) / 2)), mask)
|
||||
offset = offset + w + (-secrets.randbelow(rand + 1))
|
||||
|
||||
if width > self._width:
|
||||
image = image.resize((self._width, self._height))
|
||||
|
||||
return image
|
||||
|
||||
def generate_image(self, chars: str,
|
||||
bg_color: ColorTuple | None = None,
|
||||
fg_color: ColorTuple | None = None) -> Image:
|
||||
"""Generate the image of the given characters.
|
||||
|
||||
:param chars: text to be generated.
|
||||
:param bg_color: background color of the image in rgb format (r, g, b).
|
||||
:param fg_color: foreground color of the text in rgba format (r,g,b,a).
|
||||
"""
|
||||
background = bg_color if bg_color else random_color(238, 255)
|
||||
random_fg_color = random_color(10, 200, secrets.randbelow(36) + 220)
|
||||
color: ColorTuple = fg_color if fg_color else random_fg_color
|
||||
|
||||
im = self.create_captcha_image(chars, color, background)
|
||||
self.create_noise_dots(im, color)
|
||||
self.create_noise_curve(im, color)
|
||||
im = im.filter(SMOOTH)
|
||||
return im
|
||||
|
||||
def generate(self, chars: str, format: str = 'png',
|
||||
bg_color: ColorTuple | None = None,
|
||||
fg_color: ColorTuple | None = None) -> BytesIO:
|
||||
"""Generate an Image Captcha of the given characters.
|
||||
|
||||
:param chars: text to be generated.
|
||||
:param format: image file format
|
||||
:param bg_color: background color of the image in rgb format (r, g, b).
|
||||
:param fg_color: foreground color of the text in rgba format (r,g,b,a).
|
||||
"""
|
||||
im = self.generate_image(chars, bg_color=bg_color, fg_color=fg_color)
|
||||
out = BytesIO()
|
||||
im.save(out, format=format)
|
||||
out.seek(0)
|
||||
return out
|
||||
|
||||
def write(self, chars: str, output: str, format: str = 'png',
|
||||
bg_color: ColorTuple | None = None,
|
||||
fg_color: ColorTuple | None = None) -> None:
|
||||
"""Generate and write an image CAPTCHA data to the output.
|
||||
|
||||
:param chars: text to be generated.
|
||||
:param output: output destination.
|
||||
:param format: image file format
|
||||
:param bg_color: background color of the image in rgb format (r, g, b).
|
||||
:param fg_color: foreground color of the text in rgba format (r,g,b,a).
|
||||
"""
|
||||
im = self.generate_image(chars, bg_color=bg_color, fg_color=fg_color)
|
||||
im.save(output, format=format)
|
||||
|
||||
|
||||
def random_color(
|
||||
start: int,
|
||||
end: int,
|
||||
opacity: int | None = None) -> ColorTuple:
|
||||
red = secrets.randbelow(end - start + 1) + start
|
||||
green = secrets.randbelow(end - start + 1) + start
|
||||
blue = secrets.randbelow(end - start + 1) + start
|
||||
if opacity is None:
|
||||
return red, green, blue
|
||||
return red, green, blue, opacity
|
||||
Reference in New Issue
Block a user