Skip to content

Commit

Permalink
feat: Add ability to control color of each character
Browse files Browse the repository at this point in the history
  • Loading branch information
mheidarian committed Nov 14, 2024
1 parent 153cfd9 commit 7ca5498
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 3 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ testproject/.coverage
testproject/coverage.xml
testproject/htmlcov/
.tox
.idea
7 changes: 7 additions & 0 deletions captcha/conf/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
CAPTCHA_LETTER_ROTATION = getattr(settings, "CAPTCHA_LETTER_ROTATION", (-35, 35))
CAPTCHA_BACKGROUND_COLOR = getattr(settings, "CAPTCHA_BACKGROUND_COLOR", "#ffffff")
CAPTCHA_FOREGROUND_COLOR = getattr(settings, "CAPTCHA_FOREGROUND_COLOR", "#001100")
CAPTCHA_LETTER_COLOR_FUNCT = getattr(settings, "CAPTCHA_LETTER_COLOR_FUNCT", None)
CAPTCHA_CHALLENGE_FUNCT = getattr(
settings, "CAPTCHA_CHALLENGE_FUNCT", "captcha.helpers.random_char_challenge"
)
Expand Down Expand Up @@ -78,3 +79,9 @@ def filter_functions():
if CAPTCHA_FILTER_FUNCTIONS:
return map(_callable_from_string, CAPTCHA_FILTER_FUNCTIONS)
return []


def get_letter_color(index, char):
if CAPTCHA_LETTER_COLOR_FUNCT:
return _callable_from_string(CAPTCHA_LETTER_COLOR_FUNCT)(index, char)
return CAPTCHA_FOREGROUND_COLOR
38 changes: 38 additions & 0 deletions captcha/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
import re
from io import BytesIO
from unittest.mock import patch, call

from PIL import Image
from testfixtures import LogCapture
Expand Down Expand Up @@ -210,6 +211,33 @@ def test_repeated_challenge(self):
except Exception:
self.fail()

@patch("captcha.tests.tests.random_color_challenge")
def test_custom_letters_color(self, color_challenge_func_mock):
color_challenge_func_mock.return_value = "#ffffff"
_current_captcha_challenge_func = settings.CAPTCHA_CHALLENGE_FUNCT

settings.CAPTCHA_LETTER_COLOR_FUNCT = "captcha.tests.tests.random_color_challenge"
settings.CAPTCHA_CHALLENGE_FUNCT = "captcha.tests.tests.random_char_challenge"

challenge, response = settings.get_challenge()()
_store, _ = CaptchaStore.objects.get_or_create(
challenge=challenge, response=response
)

_response = self.client.get(reverse(
"captcha-image",
kwargs=dict(key=_store.hashkey)
))
assert _response.status_code == 200

_calls = []
for index, char in enumerate(challenge):
_calls.append(call(index, char))
color_challenge_func_mock.assert_has_calls(_calls, any_order=True)

settings.CAPTCHA_LETTER_COLOR_FUNCT = None
settings.CAPTCHA_CHALLENGE_FUNCT = _current_captcha_challenge_func

def test_repeated_challenge_form_submit(self):
__current_challange_function = settings.CAPTCHA_CHALLENGE_FUNCT
for urlname in ("captcha-test", "captcha-test-model-form"):
Expand Down Expand Up @@ -549,3 +577,13 @@ def test_empty_pool_fallback(self):

def trivial_challenge():
return "trivial", "trivial"


def random_color_challenge(index, char):
return "#ffffff"


def random_char_challenge():
chars = "abcdefghijklmnopqrstuvwxyz"
ret = chars[:settings.CAPTCHA_LENGTH]
return ret.upper(), ret
4 changes: 2 additions & 2 deletions captcha/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ def captcha_image(request, key, scale=1):
charlist[-1] += char
else:
charlist.append(char)
for char in charlist:
fgimage = Image.new("RGB", size, settings.CAPTCHA_FOREGROUND_COLOR)
for index, char in enumerate(charlist):
fgimage = Image.new("RGB", size, settings.get_letter_color(index, char))
charimage = Image.new("L", getsize(font, " %s " % char), "#000000")
chardraw = ImageDraw.Draw(charimage)
chardraw.text((0, 0), " %s " % char, font=font, fill="#ffffff")
Expand Down
10 changes: 10 additions & 0 deletions docs/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ Foreground-color of the captcha.

Defaults to ``'#001100'``

CAPTCHA_LETTER_COLOR_FUNCT
------------------------
A string representing a Python callable (i.e., a function) to determine the color of the letters in the CAPTCHA.

Defaults to ``'None'`` (uses CAPTCHA_FOREGROUND_COLOR) for all letters.

This function is called for each letter of the CAPTCHA string.
It takes two arguments: the first is the index of the current letter, and the second is the entire CAPTCHA string.

CAPTCHA_CHALLENGE_FUNCT
------------------------

Expand Down Expand Up @@ -298,3 +307,4 @@ This sample generator that returns six random digits::
for i in range(6):
ret += str(random.randint(0,9))
return ret, ret

16 changes: 16 additions & 0 deletions testproject/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import random


def random_letter_color_challenge(idx, char):
# Generate colorful but balanced RGB values
red = random.randint(64, 200)
green = random.randint(64, 200)
blue = random.randint(64, 200)

# Ensure at least one channel is higher to make it colorful
channels = [red, green, blue]
random.shuffle(channels)
channels[0] = random.randint(150, 255)

# Format the color as a hex string
return f"#{channels[0]:02X}{channels[1]:02X}{channels[2]:02X}"
1 change: 1 addition & 0 deletions testproject/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,5 @@
CAPTCHA_FLITE_PATH = os.environ.get("CAPTCHA_FLITE_PATH", None)
CAPTCHA_SOX_PATH = os.environ.get("CAPTCHA_SOX_PATH", None)
CAPTCHA_BACKGROUND_COLOR = "transparent"
CAPTCHA_LETTER_COLOR_FUNCT = "testproject.helpers.random_letter_color_challenge"
# CAPTCHA_BACKGROUND_COLOR = '#ffffffff'
5 changes: 4 additions & 1 deletion testproject/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@
from .views import home


urlpatterns = [url(r"^$", home), url(r"^captcha/", include("captcha.urls"))]
urlpatterns = [
url(r"^$", home),
url(r"^captcha/", include("captcha.urls")),
]

0 comments on commit 7ca5498

Please sign in to comment.