diff --git a/pyproject.toml b/pyproject.toml index 7819b99..8a5ff60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,6 +64,7 @@ exclude_lines = [ ] [tool.mypy] +strict = true python_version = "3.8" files = ["src/captcha"] show_error_codes = true diff --git a/src/captcha/audio.py b/src/captcha/audio.py index 858b2ca..9a608ec 100644 --- a/src/captcha/audio.py +++ b/src/captcha/audio.py @@ -188,12 +188,12 @@ class AudioCaptcha: """ return random.sample(self.choices, length) - def load(self): + def load(self) -> None: """Load voice data into memory.""" for name in self.choices: self._load_data(name) - def _load_data(self, name: str): + def _load_data(self, name: str) -> None: dirname = os.path.join(self._voicedir, name) data: t.List[bytearray] = [] for f in os.listdir(dirname): @@ -227,7 +227,7 @@ class AudioCaptcha: voice = change_sound(voice, level) return voice - def create_background_noise(self, length: int, chars: str): + def create_background_noise(self, length: int, chars: str) -> bytearray: noise = create_noise(length, 4) pos = 0 while pos < length: @@ -268,7 +268,7 @@ class AudioCaptcha: body = self.create_wave_body(chars) return patch_wave_header(body) - def write(self, chars: str, output: str): + def write(self, chars: str, output: str) -> None: """Generate and write audio CAPTCHA data to the output. :param chars: text to be generated. @@ -276,4 +276,4 @@ class AudioCaptcha: """ data = self.generate(chars) with open(output, 'wb') as f: - return f.write(data) + f.write(data) diff --git a/src/captcha/image.py b/src/captcha/image.py index 27b5549..c492bbd 100644 --- a/src/captcha/image.py +++ b/src/captcha/image.py @@ -10,7 +10,7 @@ import os import random import typing as t from PIL.Image import new as createImage, Image, QUAD, BILINEAR -from PIL.ImageDraw import Draw +from PIL.ImageDraw import Draw, ImageDraw from PIL.ImageFilter import SMOOTH from PIL.ImageFont import FreeTypeFont, truetype from io import BytesIO @@ -24,11 +24,6 @@ DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data') DEFAULT_FONTS = [os.path.join(DATA_DIR, 'DroidSansMono.ttf')] -table: t.List[int] = [] -for i in range(256): - table.append(int(i * 1.97)) - - class ImageCaptcha: """Create an image CAPTCHA. @@ -49,6 +44,8 @@ class ImageCaptcha: :param fonts: Fonts to be used to generate CAPTCHA images. :param font_sizes: Random choose a font size from this parameters. """ + lookup_table: t.List[int] = [int(i * 1.97) for i in range(256)] + def __init__( self, width: int = 160, @@ -73,7 +70,7 @@ class ImageCaptcha: return self._truefonts @staticmethod - def create_noise_curve(image: Image, color: ColorTuple): + def create_noise_curve(image: Image, color: ColorTuple) -> Image: w, h = image.size x1 = random.randint(0, int(w / 5)) x2 = random.randint(w - int(w / 5), w) @@ -100,6 +97,38 @@ class ImageCaptcha: number -= 1 return image + def _draw_character(self, c: str, draw: ImageDraw, color: ColorTuple) -> Image: + font = random.choice(self.truefonts) + _, _, w, h = draw.textbbox((1, 1), c, font=font) + + dx1 = random.randint(0, 4) + dy1 = random.randint(0, 6) + im = createImage('RGBA', (w + dx1, h + dy1)) + Draw(im).text((dx1, dy1), c, font=font, fill=color) + + # rotate + im = im.crop(im.getbbox()) + im = im.rotate(random.uniform(-30, 30), BILINEAR, expand=True) + + # warp + dx2 = w * random.uniform(0.1, 0.3) + dy2 = h * random.uniform(0.2, 0.3) + x1 = int(random.uniform(-dx2, dx2)) + y1 = int(random.uniform(-dy2, dy2)) + x2 = int(random.uniform(-dx2, dx2)) + y2 = int(random.uniform(-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((w, h), QUAD, data) + return im + def create_captcha_image( self, chars: str, @@ -116,43 +145,11 @@ class ImageCaptcha: image = createImage('RGB', (self._width, self._height), background) draw = Draw(image) - def _draw_character(c: str): - font = random.choice(self.truefonts) - _, _, w, h = draw.textbbox((1, 1), c, font=font) - - dx1 = random.randint(0, 4) - dy1 = random.randint(0, 6) - im = createImage('RGBA', (w + dx1, h + dy1)) - Draw(im).text((dx1, dy1), c, font=font, fill=color) - - # rotate - im = im.crop(im.getbbox()) - im = im.rotate(random.uniform(-30, 30), BILINEAR, expand=True) - - # warp - dx2 = w * random.uniform(0.1, 0.3) - dy2 = h * random.uniform(0.2, 0.3) - x1 = int(random.uniform(-dx2, dx2)) - y1 = int(random.uniform(-dy2, dy2)) - x2 = int(random.uniform(-dx2, dx2)) - y2 = int(random.uniform(-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((w, h), QUAD, data) - return im - images: t.List[Image] = [] for c in chars: if random.random() > 0.5: - images.append(_draw_character(" ")) - images.append(_draw_character(c)) + 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]) @@ -165,7 +162,7 @@ class ImageCaptcha: for im in images: w, h = im.size - mask = im.convert('L').point(table) + mask = im.convert('L').point(self.lookup_table) image.paste(im, (offset, int((self._height - h) / 2)), mask) offset = offset + w + random.randint(-rand, 0) diff --git a/src/captcha/py.typed b/src/captcha/py.typed new file mode 100644 index 0000000..e69de29