lazarus-ccr/components/captcha/source/captchactrl.pas
2021-10-27 12:09:16 +00:00

674 lines
19 KiB
ObjectPascal

{@@ ----------------------------------------------------------------------------
This unit implements a CAPTCHA component for Lazarus.
AUTHOR: Werner Pamler
LICENSE: LGPL with linking exception (like Lazarus LCL)
See the file COPYING.modifiedLGPL.txt, included in the Lazarus
distribution, for details about the license.
-------------------------------------------------------------------------------}
unit CaptchaCtrl;
{$mode OBJFPC}{$H+}
{$WARN 6058 off : Call to subroutine "$1" marked as inline is not inlined}
interface
uses
Classes, SysUtils, Graphics, Controls;
type
TCaptchaChar = record
Character: String; // Character (must be a string for UTF8)
Angle: Integer; // Rotation angle of character, in degrees
Position: TPoint; // Position of character within buffer bitmap (for TextOut)
FontIndex: Integer; // Index of font to be used
Color: TColor; // Random color of the character
end;
TCaptchaCharArray = array of TCaptchaChar;
TCaptchaLine = record
StartPt: TPoint; // Random start point of the line
EndPt: TPoint; // Random end point of the line
Color: TColor; // Random line color
end;
TCaptchaLineArray = array of TCaptchaLine;
TCaptchaOption = (
coAlphaUpper, coAlphaLower, coNumeric, coCustom,
coRotated, coFont1, coFont2, coLines
);
TCaptchaOptions = set of TCaptchaOption;
TCaptchaCharsOption = coAlphaUpper..coCustom;
TNewCaptchaEvent = (nceNone, nceClick, nceDblClick);
const
DEFAULT_CAPTCHA_OPTIONS = [
coAlphaUpper, coAlphaLower, coNumeric, coCustom,
coRotated, coFont1, coFont2, coLines
];
DEFAULT_CAPTCHA_NUMCHARS = 10;
DEFAULT_CAPTCHA_NUMLINES = 30;
type
TCaptchaLabel = class(TGraphicControl)
private
FBuffer: TBitmap;
FCaptchaChars: TCaptchaCharArray;
FCaptchaLines: TCaptchaLineArray;
FValidChars: array[TCaptchaCharsOption] of string;
FFonts: array[0..1] of TFont;
FInitialized: Boolean;
FMaxAngle: Integer;
FNewCaptchaEvent: TNewCaptchaEvent;
FNumChars: Integer;
FNumLines: Integer;
FOptions: TCaptchaOptions;
function GetCaptchaText: String;
function GetFont(AIndex: Integer): TFont;
function GetValidChars(AIndex: Integer): String;
procedure SetFont(AIndex: Integer; const AValue: TFont);
procedure SetMaxAngle(const AValue: Integer);
procedure SetNumChars(const AValue: Integer);
procedure SetNumLines(const AValue: Integer);
procedure SetOptions(const AValue: TCaptchaOptions);
procedure SetValidChars(AIndex: Integer; const AValue: String);
protected
function AlmostBackgroundColor(AColor: TColor): Boolean;
procedure CreateNewCaptcha(ANumChars, ANumLines: Integer; KeepText,KeepLines: Boolean);
procedure DrawBuffer;
procedure InitAngles;
procedure InitCharPos(KeepVertPos: boolean);
procedure InitFontIndex;
procedure InitLineColors;
procedure InitLines(ACount: Integer; KeepExisting: Boolean);
procedure InitText(ACount: Integer; KeepExisting: Boolean);
procedure InitTextColors;
protected
procedure CalculatePreferredSize(var PreferredWidth, PreferredHeight: integer;
WithThemeSpace: Boolean); override;
procedure Click; override;
procedure DblClick; override;
procedure Paint; override;
procedure Resize; override;
procedure SetColor(AValue: TColor); override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure NewCaptcha;
function Verify(const AText: String): Boolean;
property Text: String read GetCaptchaText;
published
property CustomChars: String index ord(coCustom) read GetValidChars write SetValidChars;
property Font1: TFont index 0 read GetFont write SetFont;
property Font2: TFont index 1 read GetFont write SetFont;
property Options: TCaptchaOptions read FOptions write SetOptions default DEFAULT_CAPTCHA_OPTIONS;
property LowercaseChars: String index ord(coAlphaLower) read GetValidChars write SetValidChars;
property MaxAngle: Integer read FMaxAngle write SetMaxAngle default 60;
property NumericChars: String index ord(coNumeric) read GetValidChars write SetValidChars;
property NewCaptchaEvent: TNewCaptchaEvent read FNewCaptchaEvent write FNewCaptchaEvent default nceNone;
property NumChars: Integer read FNumChars write SetNumChars default DEFAULT_CAPTCHA_NUMCHARS;
property NumLines: Integer read FNumLines write SetNumLines default DEFAULT_CAPTCHA_NUMLINES;
property UppercaseChars: String index ord(coAlphaUpper) read GetValidChars write SetValidChars;
property Align;
property AutoSize default true;
property BorderSpacing;
property Color default clBlack;
property OnClick;
property OnDblClick;
property OnMouseDown;
property OnMouseEnter;
property OnMouseLeave;
property OnMouseUp;
end;
procedure Register;
implementation
{$R captcha_images.res}
uses
LCLIntf, Types, GraphUtil, Math, LazUTF8;
{ Component registration }
procedure Register;
begin
RegisterComponents('Misc', [TCaptchaLabel]);
end;
{ Utility functions }
function RotatePoint(const APoint: TPoint; Angle: Double): TPoint;
var
sinphi, cosphi: Double;
begin
Angle := DegToRad(Angle);
SinCos(angle, sinphi, cosphi);
Result.X := Round( cosphi * APoint.X + sinphi * APoint.Y);
Result.Y := Round(-sinphi * APoint.X + cosphi * APoint.Y);
end;
function RotateRect(const Width, Height: Integer; Angle: Double): TRect;
var
P0, P1, P2, P3: TPoint;
begin
P0 := Point(0, 0);
P1 := RotatePoint(Point(0, Height), Angle);
P2 := RotatePoint(Point(Width, 0), Angle);
P3 := RotatePoint(Point(Width, Height), Angle);
Result.Left := MinValue([P0.X, P1.X, P2.X, P3.X]);
Result.Top := MinValue([P0.Y, P1.Y, P2.Y, P3.Y]);
Result.Right := MaxValue([P0.X, P1.X, P2.X, P3.X]);
Result.Bottom := MaxValue([P0.Y, P1.Y, P2.Y, P3.Y]);
end;
{ TCaptchaLabel }
constructor TCaptchaLabel.Create(AOwner: TComponent);
begin
inherited;
with GetControlClassDefaultSize do
SetInitialBounds(0, 0, 300, 100);
AutoSize := true;
Color := clBlack;
FBuffer := TBitmap.Create;
FBuffer.PixelFormat := pf32bit;
FFonts[0] := TFont.Create;
FFonts[0].Size := 36;
FFonts[1] := TFont.Create;
{$IF DEFINED(MSWindows)}
FFonts[1].Name := 'Courier New';
{$ELSEIF DEFINED(Linux)}
FFonts[1].Name := 'FreeMono';
{$ELSEIF DEFINED(Darwin)}
Fronts[1].Name := 'Courier';
{$IFEND}
FFonts[1].Size := 36;
FOptions := DEFAULT_CAPTCHA_OPTIONS;
FMaxAngle := 60;
FNumChars := DEFAULT_CAPTCHA_NUMCHARS;
FNumLines := DEFAULT_CAPTCHA_NUMLINES;
FValidChars[coAlphaUpper] := 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
FValidChars[coAlphaLower] := 'abcdefghijklmnopqrstuvwxyz';
FValidChars[coNumeric] := '0123456789';
FValidChars[coCustom] := '';
FInitialized := false;
// Do not call Randomize at runtime to facilitate debugging.
if (csDesigning in ComponentState) then
Randomize;
end;
destructor TCaptchaLabel.Destroy;
begin
Finalize(FCaptchaChars);
Finalize(FCaptchaLines);
FreeAndNil(FFonts[0]);
FreeAndNil(FFonts[1]);
FreeAndNil(FBuffer);
inherited;
end;
function TCaptchaLabel.AlmostBackgroundColor(AColor: TColor): Boolean;
const
TOLERANCE = 64;
var
colorH, colorL, colorS: Byte;
bgColorH, bgColorL, bgColorS: Byte;
begin
ColorToHLS(ColorToRGB(AColor), colorH, colorL, colorS);
ColorToHLS(ColorToRGB(Self.Color), bgColorH, bgColorL, bgColorS);
Result := abs(colorL - bgColorL) < TOLERANCE;
end;
procedure TCaptchaLabel.CalculatePreferredSize(
var PreferredWidth, PreferredHeight: integer;
WithThemeSpace: Boolean);
begin
inherited;
CreateNewCaptcha(FNumChars, FNumLines, true, true);
PreferredWidth := FBuffer.Width;
PreferredHeight := 0;
if (coFont1 in FOptions) then
begin
FBuffer.Canvas.Font.Assign(FFonts[0]);
PreferredHeight := FBuffer.Canvas.TextHeight('Tg');
end;
if (coFont2 in FOptions) then
begin
FBuffer.Canvas.Font.Assign(FFonts[1]);
PreferredHeight := Max(PreferredHeight, FBuffer.Canvas.TextHeight('Tg'));
end;
PreferredHeight := 3*PreferredHeight div 2;
end;
procedure TCaptchaLabel.Click;
begin
inherited;
if FNewCaptchaEvent = nceClick then
NewCaptcha;
end;
procedure TCaptchaLabel.CreateNewCaptcha(ANumChars, ANumLines: Integer;
KeepText, KeepLines: Boolean);
begin
if not KeepText then
FCaptchaChars := nil;
FCaptchaLines := nil;
InitText(ANumChars, KeepText);
InitTextColors; // after InitText
InitAngles;
InitCharPos(false);
InitLines(ANumLines, KeepLines); // after InitCharPos
InitLineColors; // after InitLines
DrawBuffer;
end;
procedure TCaptchaLabel.DblClick;
begin
inherited;
if FNewCaptchaEvent = nceDblClick then
NewCaptcha;
end;
procedure TCaptchaLabel.DrawBuffer;
var
i: Integer;
begin
if not Assigned(FBuffer) then
exit;
// Fill the buffer background in the requested color.
FBuffer.Canvas.Brush.Color := Self.Color;
FBuffer.Canvas.Brush.Style := bsSolid;
FBuffer.Canvas.FillRect(0, 0, FBuffer.Width, FBuffer.Height);
// Draw the captcha characters to the buffer bitmap
if (FOptions * [coAlphaUpper, coAlphaLower, coNumeric, coCustom] <> []) and
(FOptions * [coFont1, coFont2] <> []) then
begin
FBuffer.Canvas.Brush.Style := bsClear;
for i := 0 to High(FCaptchaChars) do
with FCaptchaChars[i] do
begin
FBuffer.Canvas.Font.Assign(FFonts[FontIndex]);
FBuffer.Canvas.Font.Color := Color;
if coRotated in FOptions then
FBuffer.Canvas.Font.Orientation := Angle * 10
else
FBuffer.Canvas.Font.Orientation := 0;
FBuffer.Canvas.TextOut(Position.X, Position.Y, Character);
end;
end;
// Draw the captcha lines
if coLines in FOptions then
begin
for i := 0 to High(FCaptchaLines) do
with FCaptchaLines[i] do
begin
FBuffer.Canvas.Pen.Color := Color;
FBuffer.Canvas.Line(StartPt.X, StartPt.Y, EndPt.X, EndPt.Y);
end;
end;
end;
function TCaptchaLabel.GetFont(AIndex: Integer): TFont;
begin
Result := FFonts[AIndex];
end;
function TCaptchaLabel.GetCaptchaText: string;
var
i: Integer;
begin
Result := '';
for i := 0 to High(FCaptchaChars) do
Result := Result + FCaptchaChars[i].Character;
end;
function TCaptchaLabel.GetValidChars(AIndex: Integer): String;
begin
Result := FValidChars[TCaptchaCharsOption(AIndex)];
end;
procedure TCaptchaLabel.InitAngles;
var
i: Integer;
begin
for i := 0 to High(FCaptchaChars) do
FCaptchaChars[i].Angle := Random(FMaxAngle*2) - FMaxAngle;
end;
{ Calculates the character positions and stores them in the ChaptchaChars array
When KeepVertPos is false, the vertical position of the characters is selected
randomly within the height of the control. Otherwise the already stored
vertical positions are used. }
procedure TCaptchaLabel.InitCharPos(KeepVertPos: Boolean);
var
x: Integer;
i: Integer;
R: TRect;
ext: TSize;
w, h: Integer;
fnt: TFont;
maxHeight: Integer;
begin
maxHeight := 0;
x := 0;
for i := 0 to High(FCaptchaChars) do
begin
// Set character font
fnt := FFonts[FCaptchaChars[i].FontIndex];
FBuffer.Canvas.Font.Assign(fnt);
// Get character size
ext := FBuffer.Canvas.TextExtent(FCaptchaChars[i].Character);
// Rotate the character and get the bounds of the enclosing rectangle.
// The rotation occurs around the upper left corner of the character.
if coRotated in FOptions then
R := RotateRect(ext.CX, ext.CY, FCaptchaChars[i].Angle)
else
// unrotated: add some extra space for better legibility
R := Rect(0, 0, ext.CX * 6 div 5, ext.CY);
w := R.Right - R.Left;
h := R.Bottom - R.Top;
// Horizontal drawing coordinate
FCaptchaChars[i].Position.X := x - R.Left;
// Vertical drawing coordinate: randomly inside control
if not KeepVertPos then
begin
if Self.Height > h then
FCaptchaChars[i].Position.Y := Max(0, Random(Height - h) - R.Top)
else
FCaptchaChars[i].Position.Y := 0;
end;
// Find max y coordinate needed to enclose the entire text
maxHeight := Max(maxHeight, FCaptchaChars[i].Position.Y + h);
// Next drawing position
x := x + w;
end;
// Set size of the bitmap buffer so that the entire captcha is enclosed.
FBuffer.SetSize(x, maxHeight);
end;
procedure TCaptchaLabel.InitFontIndex;
var
i: Integer;
begin
if FOptions * [coFont1, coFont2] = [coFont1] then
for i := 0 to High(FCaptchaChars) do
FCaptchaChars[i].FontIndex := 0
else
if FOptions * [coFont1, coFont2] = [coFont2] then
for i := 0 to High(FCaptchaChars) do
FCaptchaChars[i].FontIndex := 1
else
for i := 0 to High(FCaptchaChars) do
FCaptchaChars[i].FontIndex := Random(2);
end;
{ Pick random color for a line.
Make sure that the color is not too close to the background color. }
procedure TCaptchaLabel.InitLineColors;
var
i: Integer;
begin
// Line colors
if (FOptions * [coLines] <> []) then
for i := 0 to High(FCaptchaLines) do
repeat
FCaptchaLines[i].Color := TColor(Random($FFFFFF));
until not AlmostBackgroundColor(FCaptchaLines[i].Color);
end;
procedure TCaptchaLabel.InitLines(ACount: Integer; KeepExisting: Boolean);
var
i, n: Integer;
begin
if coLines in FOptions then
begin
if KeepExisting then
n := Length(FCaptchaLines)
else
n := 0;
SetLength(FCaptchaLines, ACount);
for i := n to High(FCaptchaLines) do
begin
// Select random start and end points
FCaptchaLines[i].StartPt := Point(
Random(FBuffer.Width),
Random(FBuffer.Height)
);
FCaptchaLines[i].EndPt := Point(
Random(FBuffer.Width),
Random(FBuffer.Height)
);
// Select random line color
repeat
FCaptchaLines[i].Color := TColor(Random($FFFFFF));
until not AlmostBackgroundColor(FCaptchaLines[i].Color);
end;
end;
end;
procedure TCaptchaLabel.InitText(ACount: Integer; KeepExisting: Boolean);
var
i, n: Integer;
validChars: String;
co: TCaptchaCharsOption;
begin
if (FOptions * [coAlphaUpper, coAlphaLower, coNumeric, coCustom] <> []) and
(FOptions * [coFont1, coFont2] <> []) then
begin
// Prepare character list for captcha
validChars := '';
for co in TCaptchaCharsOption do
if co in FOptions then
validChars := validChars + FValidChars[co];;
// Remove characters which are hard to distinguish
if FOptions * [coAlphaUpper, coAlphaLower] = [coAlphaUpper, coAlphaLower] then
begin
i := Pos('I', validChars); // Remove upper-case I
if i > 0 then Delete(validChars, i, 1);
i := Pos('l', validChars); // Remove lower-case L
if i > 0 then Delete(validChars, i, 1);
end;
if FOptions * [coAlphaUpper, coNumeric] = [coAlphaUpper, coNumeric] then
begin
i := Pos('O', validChars); // Remove upper-case O
if i > 0 then Delete(validChars, i, 1);
i := Pos('0', validChars); // Remove number zero
if i > 0 then Delete(validChars, i, 1);
end;
if KeepExisting then
n := Length(FCaptchaChars)
else
n := 0;
// Get random captcha characters, but keep previously assigned chars.
SetLength(FCaptchaChars, ACount);
for i := n to High(FCaptchaChars) do
begin
// Pick random character from the validChars. Take care of UTF8.
FCaptchaChars[i].Character := UTF8Copy(validChars, random(UTF8Length(validChars)) + 1, 1);
// Pick one of the fonts
if FOptions * [coFont1, coFont2] = [coFont1] then
FCaptchaChars[i].FontIndex := 1
else
if FOptions * [coFont1, coFont2] = [coFont2] then
FCaptchaChars[i].FontIndex := 2
else
FCaptchaChars[i].FontIndex := Random(2);
if KeepExisting then
begin
// Set random text color
repeat
FCaptchaChars[i].Color := TColor(Random($FFFFFF));
until not AlmostbackgroundColor(FCaptchaChars[i].Color);
// Set random rotation angle
if (coRotated in FOptions) then
FCaptchaChars[i].Angle := Random(FMaxAngle*2) - FMaxAngle;
end;
end;
end else
SetLength(FCaptchaChars, 0);
end;
{ Pick random color for a character.
Make sure that the color is not too close to the background color. }
procedure TCaptchaLabel.InitTextColors;
var
i: Integer;
begin
// Character colors
if (FOptions * [coAlphaUpper, coAlphaLower, coNumeric, coCustom] <> []) then
for i := 0 to High(FCaptchaChars) do
repeat
FCaptchaChars[i].Color := TColor(Random($FFFFFF));
until not AlmostbackgroundColor(FCaptchaChars[i].Color);
end;
procedure TCaptchaLabel.NewCaptcha;
begin
CreateNewCaptcha(FNumChars, FNumLines, false, false);
Invalidate;
end;
procedure TCaptchaLabel.Paint;
begin
Canvas.Draw((Width - FBuffer.Width) div 2, (Height - FBuffer.Height) div 2, FBuffer);
end;
procedure TCaptchaLabel.Resize;
begin
inherited;
if Assigned(FBuffer) and not FInitialized then
begin
CreateNewCaptcha(FNumChars, FNumLines, false, false);
FInitialized := true;
end;
end;
procedure TCaptchaLabel.SetColor(AValue: TColor);
begin
if AValue = Color then
exit;
inherited SetColor(AValue);
InitTextColors;
InitLineColors;
DrawBuffer;
Invalidate;
end;
procedure TCaptchaLabel.SetFont(AIndex: Integer; const AValue: TFont);
begin
if FFonts[AIndex].IsEqual(AValue) then
exit;
FFonts[AIndex].Assign(AValue);
InitFontIndex;
InitCharPos(true);
DrawBuffer;
Invalidate;
end;
procedure TCaptchaLabel.SetMaxAngle(const AValue: Integer);
begin
if AValue = FMaxAngle then
exit;
FMaxAngle := AValue;
InitAngles;
InitCharPos(true);
DrawBuffer;
Invalidate;
end;
procedure TCaptchaLabel.SetNumChars(const AValue: Integer);
begin
if AValue = FNumChars then
exit;
FNumChars := AValue;
InitText(FNumChars, true);
InitAngles;
InitCharPos(false);
InitLines(FNumLines, false);
DrawBuffer;
Invalidate;
end;
procedure TCaptchaLabel.SetNumLines(const AValue: Integer);
begin
if AValue = FNumLines then
exit;
FNumLines := AValue;
InitLines(FNumLines, true);
DrawBuffer;
Invalidate;
end;
procedure TCaptchaLabel.SetOptions(const AValue: TCaptchaOptions);
var
oldOptions: TCaptchaOptions;
begin
if AValue = FOptions then
exit;
oldOptions := FOptions;
FOptions := AValue;
if (oldOptions * [coAlphaUpper, coAlphaLower, coNumeric, coCustom] <>
AValue * [coAlphaUpper, coAlphaLower, coNumeric, coCustom])
then
InitText(FNumChars, false);
if (oldOptions * [coFont1, coFont2] <> AValue * [coFont1, coFont2]) then
begin
InitFontIndex;
InitCharPos(false);
end;
if (oldOptions * [coRotated] <> AValue * [coRotated]) then
begin
InitAngles;
InitCharPos(true);
end;
if oldOptions * [coLines] <> AValue * [coLines] then
InitLines(FNumLines, true);
DrawBuffer;
Invalidate;
end;
procedure TCaptchaLabel.SetValidChars(AIndex: Integer; const AValue: String);
begin
if FValidChars[TCaptchaCharsOption(AIndex)] = AValue then
exit;
FValidChars[TCaptchaCharsOption(AIndex)] := AValue;
NewCaptcha;
end;
function TCaptchaLabel.Verify(const AText: String): Boolean;
begin
Result := (AText = GetCaptchaText);
end;
end.