mirror of
https://github.com/lepture/captcha.git
synced 2025-08-06 18:24:48 +08:00
refactor!: restructure the code, add type hints
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -17,3 +17,4 @@ cover/
|
|||||||
*.c
|
*.c
|
||||||
*.so
|
*.so
|
||||||
venv/
|
venv/
|
||||||
|
.venv/
|
||||||
|
21
.travis.yml
21
.travis.yml
@ -1,21 +0,0 @@
|
|||||||
language: python
|
|
||||||
|
|
||||||
python:
|
|
||||||
- "2.7"
|
|
||||||
- "3.5"
|
|
||||||
- "3.6"
|
|
||||||
- "pypy"
|
|
||||||
|
|
||||||
install:
|
|
||||||
- if [[ $TRAVIS_PYTHON_VERSION != 'pypy' ]]; then pip install -q Pillow wheezy.captcha; fi
|
|
||||||
|
|
||||||
script:
|
|
||||||
- python setup.py -q nosetests
|
|
||||||
|
|
||||||
after_success:
|
|
||||||
- pip install coveralls
|
|
||||||
- coverage run --source=captcha setup.py -q nosetests
|
|
||||||
- coveralls
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
email: false
|
|
@ -1,4 +1,4 @@
|
|||||||
include LICENSE
|
include LICENSE
|
||||||
include README.rst
|
include README.rst
|
||||||
recursive-include captcha/data *.wav
|
recursive-include src/captcha/data *.wav
|
||||||
recursive-include captcha/data *.ttf
|
recursive-include src/captcha/data *.ttf
|
||||||
|
16
appveyor.yml
16
appveyor.yml
@ -1,16 +0,0 @@
|
|||||||
build: false
|
|
||||||
|
|
||||||
environment:
|
|
||||||
matrix:
|
|
||||||
- PYTHON: "C:\\Python27-x64"
|
|
||||||
- PYTHON: "C:\\Python36-x64"
|
|
||||||
|
|
||||||
init:
|
|
||||||
- SET PATH=%PYTHON%;%PATH%
|
|
||||||
- python -c "import sys;sys.stdout.write(sys.version)"
|
|
||||||
|
|
||||||
install:
|
|
||||||
- python -m pip install nose Pillow wheezy.captcha
|
|
||||||
|
|
||||||
test_script:
|
|
||||||
- python -m nose -s
|
|
70
pyproject.toml
Normal file
70
pyproject.toml
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
[project]
|
||||||
|
name = "captcha"
|
||||||
|
description = "A captcha library that generates audio and image CAPTCHAs."
|
||||||
|
authors = [{name = "Hsiaoming Yang", email="me@lepture.com"}]
|
||||||
|
dependencies = [
|
||||||
|
"Pillow",
|
||||||
|
]
|
||||||
|
license = {text = "BSD-3-Clause"}
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
dynamic = ["version"]
|
||||||
|
readme = "README.rst"
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Environment :: Console",
|
||||||
|
"Environment :: Web Environment",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: BSD License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Programming Language :: Python",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
|
"Topic :: Security",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Documentation = "https://captcha.lepture.com/"
|
||||||
|
Source = "https://github.com/lepture/captcha"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
version = {attr = "captcha.__version__"}
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
captcha = ["py.typed"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
pythonpath = ["src", "."]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
filterwarnings = ["error"]
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
branch = true
|
||||||
|
source = ["captcha"]
|
||||||
|
|
||||||
|
[tool.coverage.paths]
|
||||||
|
source = ["src"]
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
exclude_lines = [
|
||||||
|
"pragma: no cover",
|
||||||
|
"raise NotImplementedError",
|
||||||
|
"@(abc\\.)?abstractmethod",
|
||||||
|
"@overload",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
python_version = "3.8"
|
||||||
|
files = ["src/captcha"]
|
||||||
|
show_error_codes = true
|
||||||
|
pretty = true
|
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
Pillow
|
||||||
|
types-Pillow
|
||||||
|
pytest
|
||||||
|
flake8
|
||||||
|
mypy
|
62
setup.py
62
setup.py
@ -1,61 +1,3 @@
|
|||||||
#!/usr/bin/env python
|
from setuptools import setup
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
try:
|
setup()
|
||||||
from setuptools import setup
|
|
||||||
except ImportError:
|
|
||||||
from distutils.core import setup
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import captcha
|
|
||||||
from email.utils import parseaddr
|
|
||||||
|
|
||||||
kwargs = {}
|
|
||||||
if not hasattr(sys, 'pypy_version_info'):
|
|
||||||
kwargs['install_requires'] = ['Pillow']
|
|
||||||
|
|
||||||
author, author_email = parseaddr(captcha.__author__)
|
|
||||||
|
|
||||||
|
|
||||||
def fopen(filename):
|
|
||||||
with open(filename) as f:
|
|
||||||
return f.read()
|
|
||||||
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name='captcha',
|
|
||||||
version=captcha.__version__,
|
|
||||||
author=author,
|
|
||||||
author_email=author_email,
|
|
||||||
url=captcha.__homepage__,
|
|
||||||
packages=['captcha'],
|
|
||||||
description='A captcha library that generates audio and image CAPTCHAs.',
|
|
||||||
long_description=fopen('README.rst'),
|
|
||||||
license='BSD',
|
|
||||||
zip_safe=False,
|
|
||||||
include_package_data=True,
|
|
||||||
tests_require=['nose'],
|
|
||||||
test_suite='nose.collector',
|
|
||||||
classifiers=[
|
|
||||||
'Development Status :: 4 - Beta',
|
|
||||||
'Environment :: Console',
|
|
||||||
'Intended Audience :: Developers',
|
|
||||||
'License :: OSI Approved',
|
|
||||||
'License :: OSI Approved :: BSD License',
|
|
||||||
'Operating System :: MacOS',
|
|
||||||
'Operating System :: POSIX',
|
|
||||||
'Operating System :: POSIX :: Linux',
|
|
||||||
'Programming Language :: Python',
|
|
||||||
'Programming Language :: Python :: 2',
|
|
||||||
'Programming Language :: Python :: 2.7',
|
|
||||||
'Programming Language :: Python :: 3',
|
|
||||||
'Programming Language :: Python :: 3.4',
|
|
||||||
'Programming Language :: Python :: 3.5',
|
|
||||||
'Programming Language :: Python :: 3.6',
|
|
||||||
'Programming Language :: Python :: 3.6',
|
|
||||||
'Programming Language :: Python :: Implementation',
|
|
||||||
'Programming Language :: Python :: Implementation :: CPython',
|
|
||||||
'Programming Language :: Python :: Implementation :: PyPy',
|
|
||||||
],
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
|
@ -10,6 +10,6 @@
|
|||||||
:license: BSD, see LICENSE for more details.
|
:license: BSD, see LICENSE for more details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = '0.4'
|
__version__ = '0.5.0'
|
||||||
__author__ = 'Hsiaoming Yang <me@lepture.com>'
|
__author__ = 'Hsiaoming Yang <me@lepture.com>'
|
||||||
__homepage__ = 'https://github.com/lepture/captcha'
|
__homepage__ = 'https://github.com/lepture/captcha'
|
@ -8,17 +8,14 @@
|
|||||||
This module is totally inspired by https://github.com/dchest/captcha
|
This module is totally inspired by https://github.com/dchest/captcha
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import typing as t
|
||||||
import os
|
import os
|
||||||
import copy
|
import copy
|
||||||
import wave
|
import wave
|
||||||
import struct
|
import struct
|
||||||
import random
|
import random
|
||||||
import operator
|
import operator
|
||||||
|
from functools import reduce
|
||||||
import sys
|
|
||||||
if sys.version_info[0] != 2:
|
|
||||||
import functools
|
|
||||||
reduce = functools.reduce
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['AudioCaptcha']
|
__all__ = ['AudioCaptcha']
|
||||||
@ -32,14 +29,14 @@ WAVE_HEADER_LENGTH = len(WAVE_HEADER) - 4
|
|||||||
DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data')
|
DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data')
|
||||||
|
|
||||||
|
|
||||||
def _read_wave_file(filepath):
|
def _read_wave_file(filepath: str) -> bytearray:
|
||||||
w = wave.open(filepath)
|
w = wave.open(filepath)
|
||||||
data = w.readframes(-1)
|
data = w.readframes(-1)
|
||||||
w.close()
|
w.close()
|
||||||
return bytearray(data)
|
return bytearray(data)
|
||||||
|
|
||||||
|
|
||||||
def change_speed(body, speed=1):
|
def change_speed(body: bytearray, speed: float = 1) -> bytearray:
|
||||||
"""Change the voice speed of the wave body."""
|
"""Change the voice speed of the wave body."""
|
||||||
if speed == 1:
|
if speed == 1:
|
||||||
return body
|
return body
|
||||||
@ -47,7 +44,7 @@ def change_speed(body, speed=1):
|
|||||||
length = int(len(body) * speed)
|
length = int(len(body) * speed)
|
||||||
rv = bytearray(length)
|
rv = bytearray(length)
|
||||||
|
|
||||||
step = 0
|
step: float = 0
|
||||||
for v in body:
|
for v in body:
|
||||||
i = int(step)
|
i = int(step)
|
||||||
while i < int(step + speed) and i < length:
|
while i < int(step + speed) and i < length:
|
||||||
@ -57,7 +54,7 @@ def change_speed(body, speed=1):
|
|||||||
return rv
|
return rv
|
||||||
|
|
||||||
|
|
||||||
def patch_wave_header(body):
|
def patch_wave_header(body: bytearray) -> bytearray:
|
||||||
"""Patch header to the given wave body.
|
"""Patch header to the given wave body.
|
||||||
|
|
||||||
:param body: the wave content body, it should be bytearray.
|
:param body: the wave content body, it should be bytearray.
|
||||||
@ -81,7 +78,7 @@ def patch_wave_header(body):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def create_noise(length, level=4):
|
def create_noise(length: int, level: int = 4) -> bytearray:
|
||||||
"""Create white noise for background"""
|
"""Create white noise for background"""
|
||||||
noise = bytearray(length)
|
noise = bytearray(length)
|
||||||
adjust = 128 - int(level / 2)
|
adjust = 128 - int(level / 2)
|
||||||
@ -93,7 +90,7 @@ def create_noise(length, level=4):
|
|||||||
return noise
|
return noise
|
||||||
|
|
||||||
|
|
||||||
def create_silence(length):
|
def create_silence(length: int) -> bytearray:
|
||||||
"""Create a piece of silence."""
|
"""Create a piece of silence."""
|
||||||
data = bytearray(length)
|
data = bytearray(length)
|
||||||
i = 0
|
i = 0
|
||||||
@ -103,25 +100,25 @@ def create_silence(length):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def change_sound(body, level=1):
|
def change_sound(body: bytearray, level: float = 1) -> bytearray:
|
||||||
if level == 1:
|
if level == 1:
|
||||||
return body
|
return body
|
||||||
|
|
||||||
body = copy.copy(body)
|
body = copy.copy(body)
|
||||||
for i, v in enumerate(body):
|
for i, v in enumerate(body):
|
||||||
if v > 128:
|
if v > 128:
|
||||||
v = (v - 128) * level + 128
|
v = int((v - 128) * level + 128)
|
||||||
v = max(int(v), 128)
|
v = max(v, 128)
|
||||||
v = min(v, 255)
|
v = min(v, 255)
|
||||||
elif v < 128:
|
elif v < 128:
|
||||||
v = 128 - (128 - v) * level
|
v = int(128 - (128 - v) * level)
|
||||||
v = min(int(v), 128)
|
v = min(v, 128)
|
||||||
v = max(v, 0)
|
v = max(v, 0)
|
||||||
body[i] = v
|
body[i] = v
|
||||||
return body
|
return body
|
||||||
|
|
||||||
|
|
||||||
def mix_wave(src, dst):
|
def mix_wave(src: bytearray, dst: bytearray) -> bytearray:
|
||||||
"""Mix two wave body into one."""
|
"""Mix two wave body into one."""
|
||||||
if len(src) > len(dst):
|
if len(src) > len(dst):
|
||||||
# output should be longer
|
# output should be longer
|
||||||
@ -141,7 +138,7 @@ END_BEEP = change_speed(BEEP, 1.4)
|
|||||||
SILENCE = create_silence(int(WAVE_SAMPLE_RATE / 5))
|
SILENCE = create_silence(int(WAVE_SAMPLE_RATE / 5))
|
||||||
|
|
||||||
|
|
||||||
class AudioCaptcha(object):
|
class AudioCaptcha:
|
||||||
"""Create an audio CAPTCHA.
|
"""Create an audio CAPTCHA.
|
||||||
|
|
||||||
Create an instance of AudioCaptcha is pretty simple::
|
Create an instance of AudioCaptcha is pretty simple::
|
||||||
@ -166,16 +163,16 @@ class AudioCaptcha(object):
|
|||||||
|
|
||||||
captcha = AudioCaptcha(voicedir='/path/to/voices')
|
captcha = AudioCaptcha(voicedir='/path/to/voices')
|
||||||
"""
|
"""
|
||||||
def __init__(self, voicedir=None):
|
def __init__(self, voicedir: t.Optional[str] = None):
|
||||||
if voicedir is None:
|
if voicedir is None:
|
||||||
voicedir = DATA_DIR
|
voicedir = DATA_DIR
|
||||||
|
|
||||||
self._voicedir = voicedir
|
self._voicedir = voicedir
|
||||||
self._cache = {}
|
self._cache: t.Dict[str, t.List[bytearray]] = {}
|
||||||
self._choices = []
|
self._choices: t.List[str] = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def choices(self):
|
def choices(self) -> t.List[str]:
|
||||||
"""Available choices for characters to be generated."""
|
"""Available choices for characters to be generated."""
|
||||||
if self._choices:
|
if self._choices:
|
||||||
return self._choices
|
return self._choices
|
||||||
@ -184,7 +181,7 @@ class AudioCaptcha(object):
|
|||||||
self._choices.append(n)
|
self._choices.append(n)
|
||||||
return self._choices
|
return self._choices
|
||||||
|
|
||||||
def random(self, length=6):
|
def random(self, length: int = 6) -> t.List[str]:
|
||||||
"""Generate a random string with the given length.
|
"""Generate a random string with the given length.
|
||||||
|
|
||||||
:param length: the return string length.
|
:param length: the return string length.
|
||||||
@ -196,16 +193,16 @@ class AudioCaptcha(object):
|
|||||||
for name in self.choices:
|
for name in self.choices:
|
||||||
self._load_data(name)
|
self._load_data(name)
|
||||||
|
|
||||||
def _load_data(self, name):
|
def _load_data(self, name: str):
|
||||||
dirname = os.path.join(self._voicedir, name)
|
dirname = os.path.join(self._voicedir, name)
|
||||||
data = []
|
data: t.List[bytearray] = []
|
||||||
for f in os.listdir(dirname):
|
for f in os.listdir(dirname):
|
||||||
filepath = os.path.join(dirname, f)
|
filepath = os.path.join(dirname, f)
|
||||||
if f.endswith('.wav') and os.path.isfile(filepath):
|
if f.endswith('.wav') and os.path.isfile(filepath):
|
||||||
data.append(_read_wave_file(filepath))
|
data.append(_read_wave_file(filepath))
|
||||||
self._cache[name] = data
|
self._cache[name] = data
|
||||||
|
|
||||||
def _twist_pick(self, key):
|
def _twist_pick(self, key: str) -> bytearray:
|
||||||
voice = random.choice(self._cache[key])
|
voice = random.choice(self._cache[key])
|
||||||
|
|
||||||
# random change speed
|
# random change speed
|
||||||
@ -217,7 +214,7 @@ class AudioCaptcha(object):
|
|||||||
voice = change_sound(voice, level)
|
voice = change_sound(voice, level)
|
||||||
return voice
|
return voice
|
||||||
|
|
||||||
def _noise_pick(self):
|
def _noise_pick(self) -> bytearray:
|
||||||
key = random.choice(self.choices)
|
key = random.choice(self.choices)
|
||||||
voice = random.choice(self._cache[key])
|
voice = random.choice(self._cache[key])
|
||||||
voice = copy.copy(voice)
|
voice = copy.copy(voice)
|
||||||
@ -230,7 +227,7 @@ class AudioCaptcha(object):
|
|||||||
voice = change_sound(voice, level)
|
voice = change_sound(voice, level)
|
||||||
return voice
|
return voice
|
||||||
|
|
||||||
def create_background_noise(self, length, chars):
|
def create_background_noise(self, length: int, chars: str):
|
||||||
noise = create_noise(length, 4)
|
noise = create_noise(length, 4)
|
||||||
pos = 0
|
pos = 0
|
||||||
while pos < length:
|
while pos < length:
|
||||||
@ -240,20 +237,20 @@ class AudioCaptcha(object):
|
|||||||
pos = end + random.randint(0, int(WAVE_SAMPLE_RATE / 10))
|
pos = end + random.randint(0, int(WAVE_SAMPLE_RATE / 10))
|
||||||
return noise
|
return noise
|
||||||
|
|
||||||
def create_wave_body(self, chars):
|
def create_wave_body(self, chars: str) -> bytearray:
|
||||||
voices = []
|
voices: t.List[bytearray] = []
|
||||||
inters = []
|
inters: t.List[int] = []
|
||||||
for key in chars:
|
for c in chars:
|
||||||
voices.append(self._twist_pick(key))
|
voices.append(self._twist_pick(c))
|
||||||
v = random.randint(WAVE_SAMPLE_RATE, WAVE_SAMPLE_RATE * 3)
|
i = random.randint(WAVE_SAMPLE_RATE, WAVE_SAMPLE_RATE * 3)
|
||||||
inters.append(v)
|
inters.append(i)
|
||||||
|
|
||||||
durations = map(lambda a: len(a), voices)
|
durations = map(lambda a: len(a), voices)
|
||||||
length = max(durations) * len(chars) + reduce(operator.add, inters)
|
length = max(durations) * len(chars) + reduce(operator.add, inters)
|
||||||
bg = self.create_background_noise(length, chars)
|
bg = self.create_background_noise(length, chars)
|
||||||
|
|
||||||
# begin
|
# begin
|
||||||
pos = inters[0]
|
pos: int = inters[0]
|
||||||
for i, v in enumerate(voices):
|
for i, v in enumerate(voices):
|
||||||
end = pos + len(v) + 1
|
end = pos + len(v) + 1
|
||||||
bg[pos:end] = mix_wave(v, bg[pos:end])
|
bg[pos:end] = mix_wave(v, bg[pos:end])
|
||||||
@ -261,7 +258,7 @@ class AudioCaptcha(object):
|
|||||||
|
|
||||||
return BEEP + SILENCE + BEEP + SILENCE + BEEP + bg + END_BEEP
|
return BEEP + SILENCE + BEEP + SILENCE + BEEP + bg + END_BEEP
|
||||||
|
|
||||||
def generate(self, chars):
|
def generate(self, chars: str) -> bytearray:
|
||||||
"""Generate audio CAPTCHA data. The return data is a bytearray.
|
"""Generate audio CAPTCHA data. The return data is a bytearray.
|
||||||
|
|
||||||
:param chars: text to be generated.
|
:param chars: text to be generated.
|
||||||
@ -271,7 +268,7 @@ class AudioCaptcha(object):
|
|||||||
body = self.create_wave_body(chars)
|
body = self.create_wave_body(chars)
|
||||||
return patch_wave_header(body)
|
return patch_wave_header(body)
|
||||||
|
|
||||||
def write(self, chars, output):
|
def write(self, chars: str, output: str):
|
||||||
"""Generate and write audio CAPTCHA data to the output.
|
"""Generate and write audio CAPTCHA data to the output.
|
||||||
|
|
||||||
:param chars: text to be generated.
|
:param chars: text to be generated.
|
@ -8,94 +8,28 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
from PIL import Image
|
import typing as t
|
||||||
from PIL import ImageFilter
|
from PIL.Image import new as createImage, Image, QUAD, BILINEAR
|
||||||
from PIL.ImageDraw import Draw
|
from PIL.ImageDraw import Draw
|
||||||
from PIL.ImageFont import truetype
|
from PIL.ImageFilter import SMOOTH
|
||||||
try:
|
from PIL.ImageFont import FreeTypeFont, truetype
|
||||||
from cStringIO import StringIO as BytesIO
|
from io import BytesIO
|
||||||
except ImportError:
|
|
||||||
from io import BytesIO
|
__all__ = ['ImageCaptcha']
|
||||||
try:
|
|
||||||
from wheezy.captcha import image as wheezy_captcha
|
|
||||||
except ImportError:
|
ColorTuple = t.Union[t.Tuple[int, int, int], t.Tuple[int, int, int, int]]
|
||||||
wheezy_captcha = None
|
|
||||||
|
|
||||||
DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data')
|
DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data')
|
||||||
DEFAULT_FONTS = [os.path.join(DATA_DIR, 'DroidSansMono.ttf')]
|
DEFAULT_FONTS = [os.path.join(DATA_DIR, 'DroidSansMono.ttf')]
|
||||||
|
|
||||||
if wheezy_captcha:
|
|
||||||
__all__ = ['ImageCaptcha', 'WheezyCaptcha']
|
|
||||||
else:
|
|
||||||
__all__ = ['ImageCaptcha']
|
|
||||||
|
|
||||||
try:
|
table: t.List[int] = []
|
||||||
_QUAD = Image.Transform.QUAD
|
for i in range(256):
|
||||||
except AttributeError:
|
table.append(int(i * 1.97))
|
||||||
_QUAD = Image.QUAD
|
|
||||||
|
|
||||||
try:
|
|
||||||
_BILINEAR = Image.Resampling.BILINEAR
|
|
||||||
except AttributeError:
|
|
||||||
_BILINEAR = Image.BILINEAR
|
|
||||||
|
|
||||||
table = []
|
|
||||||
for i in range( 256 ):
|
|
||||||
table.append( int(i * 1.97) )
|
|
||||||
|
|
||||||
|
|
||||||
class _Captcha(object):
|
class ImageCaptcha:
|
||||||
def generate(self, chars, format='png'):
|
|
||||||
"""Generate an Image Captcha of the given characters.
|
|
||||||
|
|
||||||
:param chars: text to be generated.
|
|
||||||
:param format: image file format
|
|
||||||
"""
|
|
||||||
im = self.generate_image(chars)
|
|
||||||
out = BytesIO()
|
|
||||||
im.save(out, format=format)
|
|
||||||
out.seek(0)
|
|
||||||
return out
|
|
||||||
|
|
||||||
def write(self, chars, output, format='png'):
|
|
||||||
"""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
|
|
||||||
"""
|
|
||||||
im = self.generate_image(chars)
|
|
||||||
return im.save(output, format=format)
|
|
||||||
|
|
||||||
|
|
||||||
class WheezyCaptcha(_Captcha):
|
|
||||||
"""Create an image CAPTCHA with wheezy.captcha."""
|
|
||||||
def __init__(self, width=200, height=75, fonts=None):
|
|
||||||
self._width = width
|
|
||||||
self._height = height
|
|
||||||
self._fonts = fonts or DEFAULT_FONTS
|
|
||||||
|
|
||||||
def generate_image(self, chars):
|
|
||||||
text_drawings = [
|
|
||||||
wheezy_captcha.warp(),
|
|
||||||
wheezy_captcha.rotate(),
|
|
||||||
wheezy_captcha.offset(),
|
|
||||||
]
|
|
||||||
fn = wheezy_captcha.captcha(
|
|
||||||
drawings=[
|
|
||||||
wheezy_captcha.background(),
|
|
||||||
wheezy_captcha.text(fonts=self._fonts, drawings=text_drawings),
|
|
||||||
wheezy_captcha.curve(),
|
|
||||||
wheezy_captcha.noise(),
|
|
||||||
wheezy_captcha.smooth(),
|
|
||||||
],
|
|
||||||
width=self._width,
|
|
||||||
height=self._height,
|
|
||||||
)
|
|
||||||
return fn(chars)
|
|
||||||
|
|
||||||
|
|
||||||
class ImageCaptcha(_Captcha):
|
|
||||||
"""Create an image CAPTCHA.
|
"""Create an image CAPTCHA.
|
||||||
|
|
||||||
Many of the codes are borrowed from wheezy.captcha, with a modification
|
Many of the codes are borrowed from wheezy.captcha, with a modification
|
||||||
@ -115,26 +49,31 @@ class ImageCaptcha(_Captcha):
|
|||||||
:param fonts: Fonts to be used to generate CAPTCHA images.
|
:param fonts: Fonts to be used to generate CAPTCHA images.
|
||||||
:param font_sizes: Random choose a font size from this parameters.
|
:param font_sizes: Random choose a font size from this parameters.
|
||||||
"""
|
"""
|
||||||
def __init__(self, width=160, height=60, fonts=None, font_sizes=None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
width: int = 160,
|
||||||
|
height: int = 60,
|
||||||
|
fonts: t.Optional[t.List[str]] = None,
|
||||||
|
font_sizes: t.Optional[t.Tuple[int]] = None):
|
||||||
self._width = width
|
self._width = width
|
||||||
self._height = height
|
self._height = height
|
||||||
self._fonts = fonts or DEFAULT_FONTS
|
self._fonts = fonts or DEFAULT_FONTS
|
||||||
self._font_sizes = font_sizes or (42, 50, 56)
|
self._font_sizes = font_sizes or (42, 50, 56)
|
||||||
self._truefonts = []
|
self._truefonts: t.List[FreeTypeFont] = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def truefonts(self):
|
def truefonts(self) -> t.List[FreeTypeFont]:
|
||||||
if self._truefonts:
|
if self._truefonts:
|
||||||
return self._truefonts
|
return self._truefonts
|
||||||
self._truefonts = tuple([
|
self._truefonts = [
|
||||||
truetype(n, s)
|
truetype(n, s)
|
||||||
for n in self._fonts
|
for n in self._fonts
|
||||||
for s in self._font_sizes
|
for s in self._font_sizes
|
||||||
])
|
]
|
||||||
return self._truefonts
|
return self._truefonts
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_noise_curve(image, color):
|
def create_noise_curve(image: Image, color: ColorTuple):
|
||||||
w, h = image.size
|
w, h = image.size
|
||||||
x1 = random.randint(0, int(w / 5))
|
x1 = random.randint(0, int(w / 5))
|
||||||
x2 = random.randint(w - int(w / 5), w)
|
x2 = random.randint(w - int(w / 5), w)
|
||||||
@ -147,7 +86,11 @@ class ImageCaptcha(_Captcha):
|
|||||||
return image
|
return image
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_noise_dots(image, color, width=3, number=30):
|
def create_noise_dots(
|
||||||
|
image: Image,
|
||||||
|
color: ColorTuple,
|
||||||
|
width: int = 3,
|
||||||
|
number: int = 30) -> Image:
|
||||||
draw = Draw(image)
|
draw = Draw(image)
|
||||||
w, h = image.size
|
w, h = image.size
|
||||||
while number:
|
while number:
|
||||||
@ -157,7 +100,11 @@ class ImageCaptcha(_Captcha):
|
|||||||
number -= 1
|
number -= 1
|
||||||
return image
|
return image
|
||||||
|
|
||||||
def create_captcha_image(self, chars, color, background):
|
def create_captcha_image(
|
||||||
|
self,
|
||||||
|
chars: str,
|
||||||
|
color: ColorTuple,
|
||||||
|
background: ColorTuple) -> Image:
|
||||||
"""Create the CAPTCHA image itself.
|
"""Create the CAPTCHA image itself.
|
||||||
|
|
||||||
:param chars: text to be generated.
|
:param chars: text to be generated.
|
||||||
@ -166,32 +113,29 @@ class ImageCaptcha(_Captcha):
|
|||||||
|
|
||||||
The color should be a tuple of 3 numbers, such as (0, 255, 255).
|
The color should be a tuple of 3 numbers, such as (0, 255, 255).
|
||||||
"""
|
"""
|
||||||
image = Image.new('RGB', (self._width, self._height), background)
|
image = createImage('RGB', (self._width, self._height), background)
|
||||||
draw = Draw(image)
|
draw = Draw(image)
|
||||||
|
|
||||||
def _draw_character(c):
|
def _draw_character(c: str):
|
||||||
font = random.choice(self.truefonts)
|
font = random.choice(self.truefonts)
|
||||||
try:
|
_, _, w, h = draw.textbbox((1, 1), c, font=font)
|
||||||
_, _, w, h = draw.textbbox((1, 1), c, font=font)
|
|
||||||
except AttributeError:
|
|
||||||
w, h = draw.textsize(c, font=font)
|
|
||||||
|
|
||||||
dx = random.randint(0, 4)
|
dx1 = random.randint(0, 4)
|
||||||
dy = random.randint(0, 6)
|
dy1 = random.randint(0, 6)
|
||||||
im = Image.new('RGBA', (w + dx, h + dy))
|
im = createImage('RGBA', (w + dx1, h + dy1))
|
||||||
Draw(im).text((dx, dy), c, font=font, fill=color)
|
Draw(im).text((dx1, dy1), c, font=font, fill=color)
|
||||||
|
|
||||||
# rotate
|
# rotate
|
||||||
im = im.crop(im.getbbox())
|
im = im.crop(im.getbbox())
|
||||||
im = im.rotate(random.uniform(-30, 30), _BILINEAR, expand=1)
|
im = im.rotate(random.uniform(-30, 30), BILINEAR, expand=True)
|
||||||
|
|
||||||
# warp
|
# warp
|
||||||
dx = w * random.uniform(0.1, 0.3)
|
dx2 = w * random.uniform(0.1, 0.3)
|
||||||
dy = h * random.uniform(0.2, 0.3)
|
dy2 = h * random.uniform(0.2, 0.3)
|
||||||
x1 = int(random.uniform(-dx, dx))
|
x1 = int(random.uniform(-dx2, dx2))
|
||||||
y1 = int(random.uniform(-dy, dy))
|
y1 = int(random.uniform(-dy2, dy2))
|
||||||
x2 = int(random.uniform(-dx, dx))
|
x2 = int(random.uniform(-dx2, dx2))
|
||||||
y2 = int(random.uniform(-dy, dy))
|
y2 = int(random.uniform(-dy2, dy2))
|
||||||
w2 = w + abs(x1) + abs(x2)
|
w2 = w + abs(x1) + abs(x2)
|
||||||
h2 = h + abs(y1) + abs(y2)
|
h2 = h + abs(y1) + abs(y2)
|
||||||
data = (
|
data = (
|
||||||
@ -201,10 +145,10 @@ class ImageCaptcha(_Captcha):
|
|||||||
w2 - x2, -y1,
|
w2 - x2, -y1,
|
||||||
)
|
)
|
||||||
im = im.resize((w2, h2))
|
im = im.resize((w2, h2))
|
||||||
im = im.transform((w, h), _QUAD, data)
|
im = im.transform((w, h), QUAD, data)
|
||||||
return im
|
return im
|
||||||
|
|
||||||
images = []
|
images: t.List[Image] = []
|
||||||
for c in chars:
|
for c in chars:
|
||||||
if random.random() > 0.5:
|
if random.random() > 0.5:
|
||||||
images.append(_draw_character(" "))
|
images.append(_draw_character(" "))
|
||||||
@ -230,7 +174,7 @@ class ImageCaptcha(_Captcha):
|
|||||||
|
|
||||||
return image
|
return image
|
||||||
|
|
||||||
def generate_image(self, chars):
|
def generate_image(self, chars: str) -> Image:
|
||||||
"""Generate the image of the given characters.
|
"""Generate the image of the given characters.
|
||||||
|
|
||||||
:param chars: text to be generated.
|
:param chars: text to be generated.
|
||||||
@ -240,11 +184,36 @@ class ImageCaptcha(_Captcha):
|
|||||||
im = self.create_captcha_image(chars, color, background)
|
im = self.create_captcha_image(chars, color, background)
|
||||||
self.create_noise_dots(im, color)
|
self.create_noise_dots(im, color)
|
||||||
self.create_noise_curve(im, color)
|
self.create_noise_curve(im, color)
|
||||||
im = im.filter(ImageFilter.SMOOTH)
|
im = im.filter(SMOOTH)
|
||||||
return im
|
return im
|
||||||
|
|
||||||
|
def generate(self, chars: str, format: str = 'png') -> BytesIO:
|
||||||
|
"""Generate an Image Captcha of the given characters.
|
||||||
|
|
||||||
def random_color(start, end, opacity=None):
|
:param chars: text to be generated.
|
||||||
|
:param format: image file format
|
||||||
|
"""
|
||||||
|
im = self.generate_image(chars)
|
||||||
|
out = BytesIO()
|
||||||
|
im.save(out, format=format)
|
||||||
|
out.seek(0)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def write(self, chars: str, output: str, format: str = 'png') -> 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
|
||||||
|
"""
|
||||||
|
im = self.generate_image(chars)
|
||||||
|
im.save(output, format=format)
|
||||||
|
|
||||||
|
|
||||||
|
def random_color(
|
||||||
|
start: int,
|
||||||
|
end: int,
|
||||||
|
opacity: t.Optional[int]=None) -> ColorTuple:
|
||||||
red = random.randint(start, end)
|
red = random.randint(start, end)
|
||||||
green = random.randint(start, end)
|
green = random.randint(start, end)
|
||||||
blue = random.randint(start, end)
|
blue = random.randint(start, end)
|
@ -1,15 +1,9 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
import sys
|
from captcha.image import ImageCaptcha
|
||||||
|
|
||||||
if not hasattr(sys, 'pypy_version_info'):
|
|
||||||
from captcha.image import ImageCaptcha, WheezyCaptcha
|
|
||||||
|
|
||||||
def test_image_generate():
|
def test_image_generate():
|
||||||
captcha = ImageCaptcha()
|
captcha = ImageCaptcha()
|
||||||
data = captcha.generate('1234')
|
data = captcha.generate('1234')
|
||||||
assert hasattr(data, 'read')
|
assert hasattr(data, 'read')
|
||||||
|
|
||||||
captcha = WheezyCaptcha()
|
|
||||||
data = captcha.generate('1234')
|
|
||||||
assert hasattr(data, 'read')
|
|
||||||
|
Reference in New Issue
Block a user