diff --git a/.gitignore b/.gitignore index a71a3f5..41acc93 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ testproject/.coverage testproject/coverage.xml testproject/htmlcov/ .tox +.idea \ No newline at end of file diff --git a/captcha/conf/settings.py b/captcha/conf/settings.py index 7cbbfeb..405f3d1 100644 --- a/captcha/conf/settings.py +++ b/captcha/conf/settings.py @@ -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" ) @@ -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 diff --git a/captcha/tests/tests.py b/captcha/tests/tests.py index 241453a..37e0299 100644 --- a/captcha/tests/tests.py +++ b/captcha/tests/tests.py @@ -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 @@ -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"): @@ -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 diff --git a/captcha/views.py b/captcha/views.py index d2d7b80..9bc8529 100644 --- a/captcha/views.py +++ b/captcha/views.py @@ -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") diff --git a/docs/advanced.rst b/docs/advanced.rst index 7230275..1f35912 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -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 ------------------------ @@ -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 + diff --git a/testproject/helpers.py b/testproject/helpers.py new file mode 100644 index 0000000..f5a9a56 --- /dev/null +++ b/testproject/helpers.py @@ -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}" diff --git a/testproject/settings.py b/testproject/settings.py index a743f9a..2a1cf31 100644 --- a/testproject/settings.py +++ b/testproject/settings.py @@ -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' diff --git a/testproject/urls.py b/testproject/urls.py index 9e36b9f..8c6bb37 100644 --- a/testproject/urls.py +++ b/testproject/urls.py @@ -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")), +]