/projects/WebControls/Captcha/CaptchaImage.vb

http://pigeoncms.googlecode.com/ · Visual Basic · 496 lines · 345 code · 58 blank · 93 comment · 0 complexity · 69d210872ed21aec9a217a25e7736dac MD5 · raw file

  1. Imports System
  2. Imports System.Drawing
  3. Imports System.Drawing.Drawing2D
  4. Imports System.Drawing.Imaging
  5. ''' <summary>
  6. ''' CAPTCHA image generation class
  7. ''' </summary>
  8. ''' <remarks>
  9. ''' Adapted from the excellent code at
  10. ''' http://www.codeproject.com/aspnet/CaptchaImage.asp
  11. '''
  12. ''' Jeff Atwood
  13. ''' http://www.codinghorror.com/
  14. ''' </remarks>
  15. Public Class CaptchaImage
  16. Private _height As Integer
  17. Private _width As Integer
  18. Private _rand As Random
  19. Private _generatedAt As DateTime
  20. Private _randomText As String
  21. Private _randomTextLength As Integer
  22. Private _randomTextChars As String
  23. Private _fontFamilyName As String
  24. Private _fontWarp As FontWarpFactor
  25. Private _backgroundNoise As BackgroundNoiseLevel
  26. Private _lineNoise As LineNoiseLevel
  27. Private _guid As String
  28. Private _fontWhitelist As String
  29. #Region " Public Enums"
  30. ''' <summary>
  31. ''' Amount of random font warping to apply to rendered text
  32. ''' </summary>
  33. Public Enum FontWarpFactor
  34. None
  35. Low
  36. Medium
  37. High
  38. Extreme
  39. End Enum
  40. ''' <summary>
  41. ''' Amount of background noise to add to rendered image
  42. ''' </summary>
  43. Public Enum BackgroundNoiseLevel
  44. None
  45. Low
  46. Medium
  47. High
  48. Extreme
  49. End Enum
  50. ''' <summary>
  51. ''' Amount of curved line noise to add to rendered image
  52. ''' </summary>
  53. Public Enum LineNoiseLevel
  54. None
  55. Low
  56. Medium
  57. High
  58. Extreme
  59. End Enum
  60. #End Region
  61. #Region " Public Properties"
  62. ''' <summary>
  63. ''' Returns a GUID that uniquely identifies this Captcha
  64. ''' </summary>
  65. Public ReadOnly Property UniqueId() As String
  66. Get
  67. Return _guid
  68. End Get
  69. End Property
  70. ''' <summary>
  71. ''' Returns the date and time this image was last rendered
  72. ''' </summary>
  73. Public ReadOnly Property RenderedAt() As DateTime
  74. Get
  75. Return _generatedAt
  76. End Get
  77. End Property
  78. ''' <summary>
  79. ''' Font family to use when drawing the Captcha text. If no font is provided, a random font will be chosen from the font whitelist for each character.
  80. ''' </summary>
  81. Public Property Font() As String
  82. Get
  83. Return _fontFamilyName
  84. End Get
  85. Set(ByVal Value As String)
  86. Try
  87. Dim font1 As Font = New Font(Value, 12.0!)
  88. _fontFamilyName = Value
  89. font1.Dispose()
  90. Catch ex As Exception
  91. _fontFamilyName = Drawing.FontFamily.GenericSerif.Name
  92. End Try
  93. End Set
  94. End Property
  95. ''' <summary>
  96. ''' Amount of random warping to apply to the Captcha text.
  97. ''' </summary>
  98. Public Property FontWarp() As FontWarpFactor
  99. Get
  100. Return _fontWarp
  101. End Get
  102. Set(ByVal Value As FontWarpFactor)
  103. _fontWarp = Value
  104. End Set
  105. End Property
  106. ''' <summary>
  107. ''' Amount of background noise to apply to the Captcha image.
  108. ''' </summary>
  109. Public Property BackgroundNoise() As BackgroundNoiseLevel
  110. Get
  111. Return _backgroundNoise
  112. End Get
  113. Set(ByVal Value As BackgroundNoiseLevel)
  114. _backgroundNoise = Value
  115. End Set
  116. End Property
  117. Public Property LineNoise() As LineNoiseLevel
  118. Get
  119. Return _lineNoise
  120. End Get
  121. Set(ByVal value As LineNoiseLevel)
  122. _lineNoise = value
  123. End Set
  124. End Property
  125. ''' <summary>
  126. ''' A string of valid characters to use in the Captcha text.
  127. ''' A random character will be selected from this string for each character.
  128. ''' </summary>
  129. Public Property TextChars() As String
  130. Get
  131. Return _randomTextChars
  132. End Get
  133. Set(ByVal Value As String)
  134. _randomTextChars = Value
  135. _randomText = GenerateRandomText()
  136. End Set
  137. End Property
  138. ''' <summary>
  139. ''' Number of characters to use in the Captcha text.
  140. ''' </summary>
  141. Public Property TextLength() As Integer
  142. Get
  143. Return _randomTextLength
  144. End Get
  145. Set(ByVal Value As Integer)
  146. _randomTextLength = Value
  147. _randomText = GenerateRandomText()
  148. End Set
  149. End Property
  150. ''' <summary>
  151. ''' Returns the randomly generated Captcha text.
  152. ''' </summary>
  153. Public ReadOnly Property [Text]() As String
  154. Get
  155. Return _randomText
  156. End Get
  157. End Property
  158. ''' <summary>
  159. ''' Width of Captcha image to generate, in pixels
  160. ''' </summary>
  161. Public Property Width() As Integer
  162. Get
  163. Return _width
  164. End Get
  165. Set(ByVal Value As Integer)
  166. If (Value <= 60) Then
  167. Throw New ArgumentOutOfRangeException("width", Value, "width must be greater than 60.")
  168. End If
  169. _width = Value
  170. End Set
  171. End Property
  172. ''' <summary>
  173. ''' Height of Captcha image to generate, in pixels
  174. ''' </summary>
  175. Public Property Height() As Integer
  176. Get
  177. Return _height
  178. End Get
  179. Set(ByVal Value As Integer)
  180. If Value <= 30 Then
  181. Throw New ArgumentOutOfRangeException("height", Value, "height must be greater than 30.")
  182. End If
  183. _height = Value
  184. End Set
  185. End Property
  186. ''' <summary>
  187. ''' A semicolon-delimited list of valid fonts to use when no font is provided.
  188. ''' </summary>
  189. Public Property FontWhitelist() As String
  190. Get
  191. Return _fontWhitelist
  192. End Get
  193. Set(ByVal value As String)
  194. _fontWhitelist = value
  195. End Set
  196. End Property
  197. #End Region
  198. Public Sub New()
  199. _rand = New Random
  200. _fontWarp = FontWarpFactor.Low
  201. _backgroundNoise = BackgroundNoiseLevel.Low
  202. _lineNoise = LineNoiseLevel.None
  203. _width = 180
  204. _height = 50
  205. _randomTextLength = 5
  206. _randomTextChars = "ACDEFGHJKLNPQRTUVXYZ2346789"
  207. _fontFamilyName = ""
  208. ' -- a list of known good fonts in on both Windows XP and Windows Server 2003
  209. _fontWhitelist = _
  210. "arial;arial black;comic sans ms;courier new;estrangelo edessa;franklin gothic medium;" & _
  211. "georgia;lucida console;lucida sans unicode;mangal;microsoft sans serif;palatino linotype;" & _
  212. "sylfaen;tahoma;times new roman;trebuchet ms;verdana"
  213. _randomText = GenerateRandomText()
  214. _generatedAt = DateTime.Now
  215. _guid = Guid.NewGuid.ToString()
  216. End Sub
  217. ''' <summary>
  218. ''' Forces a new Captcha image to be generated using current property value settings.
  219. ''' </summary>
  220. Public Function RenderImage() As Bitmap
  221. Return GenerateImagePrivate()
  222. End Function
  223. ''' <summary>
  224. ''' Returns a random font family from the font whitelist
  225. ''' </summary>
  226. Private Function RandomFontFamily() As String
  227. Static ff() As String
  228. '-- small optimization so we don't have to split for each char
  229. If ff Is Nothing Then
  230. ff = _fontWhitelist.Split(";"c)
  231. End If
  232. Return ff(_rand.Next(0, ff.Length))
  233. End Function
  234. ''' <summary>
  235. ''' generate random text for the CAPTCHA
  236. ''' </summary>
  237. Private Function GenerateRandomText() As String
  238. Dim sb As New System.Text.StringBuilder(_randomTextLength)
  239. Dim maxLength As Integer = _randomTextChars.Length
  240. For n As Integer = 0 To _randomTextLength - 1
  241. sb.Append(_randomTextChars.Substring(_rand.Next(maxLength), 1))
  242. Next
  243. Return sb.ToString
  244. End Function
  245. ''' <summary>
  246. ''' Returns a random point within the specified x and y ranges
  247. ''' </summary>
  248. Private Function RandomPoint(ByVal xmin As Integer, ByVal xmax As Integer, ByRef ymin As Integer, ByRef ymax As Integer) As PointF
  249. Return New PointF(_rand.Next(xmin, xmax), _rand.Next(ymin, ymax))
  250. End Function
  251. ''' <summary>
  252. ''' Returns a random point within the specified rectangle
  253. ''' </summary>
  254. Private Function RandomPoint(ByVal rect As Rectangle) As PointF
  255. Return RandomPoint(rect.Left, rect.Width, rect.Top, rect.Bottom)
  256. End Function
  257. ''' <summary>
  258. ''' Returns a GraphicsPath containing the specified string and font
  259. ''' </summary>
  260. Private Function TextPath(ByVal s As String, ByVal f As Font, ByVal r As Rectangle) As GraphicsPath
  261. Dim sf As StringFormat = New StringFormat
  262. sf.Alignment = StringAlignment.Near
  263. sf.LineAlignment = StringAlignment.Near
  264. Dim gp As GraphicsPath = New GraphicsPath
  265. gp.AddString(s, f.FontFamily, CType(f.Style, Integer), f.Size, r, sf)
  266. Return gp
  267. End Function
  268. ''' <summary>
  269. ''' Returns the CAPTCHA font in an appropriate size
  270. ''' </summary>
  271. Private Function GetFont() As Font
  272. Dim fsize As Single
  273. Dim fname As String = _fontFamilyName
  274. If fname = "" Then
  275. fname = RandomFontFamily()
  276. End If
  277. Select Case Me.FontWarp
  278. Case FontWarpFactor.None
  279. fsize = Convert.ToInt32(_height * 0.7)
  280. Case FontWarpFactor.Low
  281. fsize = Convert.ToInt32(_height * 0.8)
  282. Case FontWarpFactor.Medium
  283. fsize = Convert.ToInt32(_height * 0.85)
  284. Case FontWarpFactor.High
  285. fsize = Convert.ToInt32(_height * 0.9)
  286. Case FontWarpFactor.Extreme
  287. fsize = Convert.ToInt32(_height * 0.95)
  288. End Select
  289. Return New Font(fname, fsize, FontStyle.Bold)
  290. End Function
  291. ''' <summary>
  292. ''' Renders the CAPTCHA image
  293. ''' </summary>
  294. Private Function GenerateImagePrivate() As Bitmap
  295. Dim fnt As Font = Nothing
  296. Dim rect As Rectangle
  297. Dim br As Brush
  298. Dim bmp As Bitmap = New Bitmap(_width, _height, PixelFormat.Format32bppArgb)
  299. Dim gr As Graphics = Graphics.FromImage(bmp)
  300. gr.SmoothingMode = SmoothingMode.AntiAlias
  301. '-- fill an empty white rectangle
  302. rect = New Rectangle(0, 0, _width, _height)
  303. br = New SolidBrush(Color.White)
  304. gr.FillRectangle(br, rect)
  305. Dim charOffset As Integer = 0
  306. Dim charWidth As Double = _width / _randomTextLength
  307. Dim rectChar As Rectangle
  308. For Each c As Char In _randomText
  309. '-- establish font and draw area
  310. fnt = GetFont()
  311. rectChar = New Rectangle(Convert.ToInt32(charOffset * charWidth), 0, Convert.ToInt32(charWidth), _height)
  312. '-- warp the character
  313. Dim gp As GraphicsPath = TextPath(c, fnt, rectChar)
  314. WarpText(gp, rectChar)
  315. '-- draw the character
  316. br = New SolidBrush(Color.Black)
  317. gr.FillPath(br, gp)
  318. charOffset += 1
  319. Next
  320. AddNoise(gr, rect)
  321. AddLine(gr, rect)
  322. '-- clean up unmanaged resources
  323. fnt.Dispose()
  324. br.Dispose()
  325. gr.Dispose()
  326. Return bmp
  327. End Function
  328. ''' <summary>
  329. ''' Warp the provided text GraphicsPath by a variable amount
  330. ''' </summary>
  331. Private Sub WarpText(ByVal textPath As GraphicsPath, ByVal rect As Rectangle)
  332. Dim WarpDivisor As Single
  333. Dim RangeModifier As Single
  334. Select Case _fontWarp
  335. Case FontWarpFactor.None
  336. Return
  337. Case FontWarpFactor.Low
  338. WarpDivisor = 6
  339. RangeModifier = 1
  340. Case FontWarpFactor.Medium
  341. WarpDivisor = 5
  342. RangeModifier = 1.3
  343. Case FontWarpFactor.High
  344. WarpDivisor = 4.5
  345. RangeModifier = 1.4
  346. Case FontWarpFactor.Extreme
  347. WarpDivisor = 4
  348. RangeModifier = 1.5
  349. End Select
  350. Dim rectF As RectangleF
  351. rectF = New RectangleF(Convert.ToSingle(rect.Left), 0, Convert.ToSingle(rect.Width), rect.Height)
  352. Dim hrange As Integer = Convert.ToInt32(rect.Height / WarpDivisor)
  353. Dim wrange As Integer = Convert.ToInt32(rect.Width / WarpDivisor)
  354. Dim left As Integer = rect.Left - Convert.ToInt32(wrange * RangeModifier)
  355. Dim top As Integer = rect.Top - Convert.ToInt32(hrange * RangeModifier)
  356. Dim width As Integer = rect.Left + rect.Width + Convert.ToInt32(wrange * RangeModifier)
  357. Dim height As Integer = rect.Top + rect.Height + Convert.ToInt32(hrange * RangeModifier)
  358. If left < 0 Then left = 0
  359. If top < 0 Then top = 0
  360. If width > Me.Width Then width = Me.Width
  361. If height > Me.Height Then height = Me.Height
  362. Dim leftTop As PointF = RandomPoint(left, left + wrange, top, top + hrange)
  363. Dim rightTop As PointF = RandomPoint(width - wrange, width, top, top + hrange)
  364. Dim leftBottom As PointF = RandomPoint(left, left + wrange, height - hrange, height)
  365. Dim rightBottom As PointF = RandomPoint(width - wrange, width, height - hrange, height)
  366. Dim points As PointF() = New PointF() {leftTop, rightTop, leftBottom, rightBottom}
  367. Dim m As New Matrix
  368. m.Translate(0, 0)
  369. textPath.Warp(points, rectF, m, WarpMode.Perspective, 0)
  370. End Sub
  371. ''' <summary>
  372. ''' Add a variable level of graphic noise to the image
  373. ''' </summary>
  374. Private Sub AddNoise(ByVal graphics1 As Graphics, ByVal rect As Rectangle)
  375. Dim density As Integer
  376. Dim size As Integer
  377. Select Case _backgroundNoise
  378. Case BackgroundNoiseLevel.None
  379. Return
  380. Case BackgroundNoiseLevel.Low
  381. density = 30
  382. size = 40
  383. Case BackgroundNoiseLevel.Medium
  384. density = 18
  385. size = 40
  386. Case BackgroundNoiseLevel.High
  387. density = 16
  388. size = 39
  389. Case BackgroundNoiseLevel.Extreme
  390. density = 12
  391. size = 38
  392. End Select
  393. Dim br As New SolidBrush(Color.Black)
  394. Dim max As Integer = Convert.ToInt32(Math.Max(rect.Width, rect.Height) / size)
  395. For i As Integer = 0 To Convert.ToInt32((rect.Width * rect.Height) / density)
  396. graphics1.FillEllipse(br, _rand.Next(rect.Width), _rand.Next(rect.Height), _
  397. _rand.Next(max), _rand.Next(max))
  398. Next
  399. br.Dispose()
  400. End Sub
  401. ''' <summary>
  402. ''' Add variable level of curved lines to the image
  403. ''' </summary>
  404. Private Sub AddLine(ByVal graphics1 As Graphics, ByVal rect As Rectangle)
  405. Dim length As Integer
  406. Dim width As Single
  407. Dim linecount As Integer
  408. Select Case _lineNoise
  409. Case LineNoiseLevel.None
  410. Return
  411. Case LineNoiseLevel.Low
  412. length = 4
  413. width = Convert.ToSingle(_height / 31.25) ' 1.6
  414. linecount = 1
  415. Case LineNoiseLevel.Medium
  416. length = 5
  417. width = Convert.ToSingle(_height / 27.7777) ' 1.8
  418. linecount = 1
  419. Case LineNoiseLevel.High
  420. length = 3
  421. width = Convert.ToSingle(_height / 25) ' 2.0
  422. linecount = 2
  423. Case LineNoiseLevel.Extreme
  424. length = 3
  425. width = Convert.ToSingle(_height / 22.7272) ' 2.2
  426. linecount = 3
  427. End Select
  428. Dim pf(length) As PointF
  429. Dim p As New Pen(Color.Black, width)
  430. For l As Integer = 1 To linecount
  431. For i As Integer = 0 To length
  432. pf(i) = RandomPoint(rect)
  433. Next
  434. graphics1.DrawCurve(p, pf, 1.75)
  435. Next
  436. p.Dispose()
  437. End Sub
  438. End Class