Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add ability to control color of each character #235

Merged
merged 1 commit into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")),
]
Loading