all repos — captcha @ 31ad576274dd153db41c6e9ba9d78b92a22708a7

Go package captcha implements generation and verification of image and audio CAPTCHAs.

image.go (view raw)

  1// Copyright 2011 Dmitry Chestnykh. All rights reserved.
  2// Use of this source code is governed by a MIT-style
  3// license that can be found in the LICENSE file.
  4
  5package captcha
  6
  7import (
  8	"image"
  9	"image/color"
 10	"image/png"
 11	"io"
 12	"math"
 13	"os"
 14	"rand"
 15)
 16
 17const (
 18	// Standard width and height of a captcha image.
 19	StdWidth  = 240
 20	StdHeight = 80
 21	// Maximum absolute skew factor of a single digit.
 22	maxSkew = 0.7
 23	// Number of background circles.
 24	circleCount = 20
 25)
 26
 27type Image struct {
 28	*image.Paletted
 29	numWidth  int
 30	numHeight int
 31	dotSize   int
 32}
 33
 34func randomPalette() color.Palette {
 35	p := make([]color.Color, circleCount+1)
 36	// Transparent color.
 37	p[0] = color.RGBA{0xFF, 0xFF, 0xFF, 0x00}
 38	// Primary color.
 39	prim := color.RGBA{
 40		uint8(rand.Intn(129)),
 41		uint8(rand.Intn(129)),
 42		uint8(rand.Intn(129)),
 43		0xFF,
 44	}
 45	p[1] = prim
 46	// Circle colors.
 47	for i := 2; i <= circleCount; i++ {
 48		p[i] = randomBrightness(prim, 255)
 49	}
 50	return p
 51}
 52
 53// NewImage returns a new captcha image of the given width and height with the
 54// given digits, where each digit must be in range 0-9.
 55func NewImage(digits []byte, width, height int) *Image {
 56	m := new(Image)
 57	m.Paletted = image.NewPaletted(image.Rect(0, 0, width, height), randomPalette())
 58	m.calculateSizes(width, height, len(digits))
 59	// Randomly position captcha inside the image.
 60	maxx := width - (m.numWidth+m.dotSize)*len(digits) - m.dotSize
 61	maxy := height - m.numHeight - m.dotSize*2
 62	var border int
 63	if width > height {
 64		border = height / 5
 65	} else {
 66		border = width / 5
 67	}
 68	x := rnd(border, maxx-border)
 69	y := rnd(border, maxy-border)
 70	// Draw digits.
 71	for _, n := range digits {
 72		m.drawDigit(font[n], x, y)
 73		x += m.numWidth + m.dotSize
 74	}
 75	// Draw strike-through line.
 76	m.strikeThrough()
 77	// Apply wave distortion.
 78	m.distort(rndf(5, 10), rndf(100, 200))
 79	// Fill image with random circles.
 80	m.fillWithCircles(circleCount, m.dotSize)
 81	return m
 82}
 83
 84// BUG(dchest): While Image conforms to io.WriterTo interface, its WriteTo
 85// method returns 0 instead of the actual bytes written because png.Encode
 86// doesn't report this.
 87
 88// WriteTo writes captcha image in PNG format into the given writer.
 89func (m *Image) WriteTo(w io.Writer) (int64, os.Error) {
 90	return 0, png.Encode(w, m.Paletted)
 91}
 92
 93func (m *Image) calculateSizes(width, height, ncount int) {
 94	// Goal: fit all digits inside the image.
 95	var border int
 96	if width > height {
 97		border = height / 4
 98	} else {
 99		border = width / 4
100	}
101	// Convert everything to floats for calculations.
102	w := float64(width - border*2)
103	h := float64(height - border*2)
104	// fw takes into account 1-dot spacing between digits.
105	fw := float64(fontWidth + 1)
106	fh := float64(fontHeight)
107	nc := float64(ncount)
108	// Calculate the width of a single digit taking into account only the
109	// width of the image.
110	nw := w / nc
111	// Calculate the height of a digit from this width.
112	nh := nw * fh / fw
113	// Digit too high?
114	if nh > h {
115		// Fit digits based on height.
116		nh = h
117		nw = fw / fh * nh
118	}
119	// Calculate dot size.
120	m.dotSize = int(nh / fh)
121	// Save everything, making the actual width smaller by 1 dot to account
122	// for spacing between digits.
123	m.numWidth = int(nw) - m.dotSize
124	m.numHeight = int(nh)
125}
126
127func (m *Image) drawHorizLine(fromX, toX, y int, colorIdx uint8) {
128	for x := fromX; x <= toX; x++ {
129		m.SetColorIndex(x, y, colorIdx)
130	}
131}
132
133func (m *Image) drawCircle(x, y, radius int, colorIdx uint8) {
134	f := 1 - radius
135	dfx := 1
136	dfy := -2 * radius
137	xo := 0
138	yo := radius
139
140	m.SetColorIndex(x, y+radius, colorIdx)
141	m.SetColorIndex(x, y-radius, colorIdx)
142	m.drawHorizLine(x-radius, x+radius, y, colorIdx)
143
144	for xo < yo {
145		if f >= 0 {
146			yo--
147			dfy += 2
148			f += dfy
149		}
150		xo++
151		dfx += 2
152		f += dfx
153		m.drawHorizLine(x-xo, x+xo, y+yo, colorIdx)
154		m.drawHorizLine(x-xo, x+xo, y-yo, colorIdx)
155		m.drawHorizLine(x-yo, x+yo, y+xo, colorIdx)
156		m.drawHorizLine(x-yo, x+yo, y-xo, colorIdx)
157	}
158}
159
160func (m *Image) fillWithCircles(n, maxradius int) {
161	maxx := m.Bounds().Max.X
162	maxy := m.Bounds().Max.Y
163	for i := 0; i < n; i++ {
164		colorIdx := uint8(rnd(1, circleCount-1))
165		r := rnd(1, maxradius)
166		m.drawCircle(rnd(r, maxx-r), rnd(r, maxy-r), r, colorIdx)
167	}
168}
169
170func (m *Image) strikeThrough() {
171	maxx := m.Bounds().Max.X
172	maxy := m.Bounds().Max.Y
173	y := rnd(maxy/3, maxy-maxy/3)
174	amplitude := rndf(5, 20)
175	period := rndf(80, 180)
176	dx := 2.0 * math.Pi / period
177	for x := 0; x < maxx; x++ {
178		xo := amplitude * math.Cos(float64(y)*dx)
179		yo := amplitude * math.Sin(float64(x)*dx)
180		for yn := 0; yn < m.dotSize; yn++ {
181			r := rnd(0, m.dotSize)
182			m.drawCircle(x+int(xo), y+int(yo)+(yn*m.dotSize), r/2, 1)
183		}
184	}
185}
186
187func (m *Image) drawDigit(digit []byte, x, y int) {
188	skf := rndf(-maxSkew, maxSkew)
189	xs := float64(x)
190	r := m.dotSize / 2
191	y += rnd(-r, r)
192	for yo := 0; yo < fontHeight; yo++ {
193		for xo := 0; xo < fontWidth; xo++ {
194			if digit[yo*fontWidth+xo] != blackChar {
195				continue
196			}
197			m.drawCircle(x+xo*m.dotSize, y+yo*m.dotSize, r, 1)
198		}
199		xs += skf
200		x = int(xs)
201	}
202}
203
204func (m *Image) distort(amplude float64, period float64) {
205	w := m.Bounds().Max.X
206	h := m.Bounds().Max.Y
207
208	oldm := m.Paletted
209	newm := image.NewPaletted(image.Rect(0, 0, w, h), oldm.Palette)
210
211	dx := 2.0 * math.Pi / period
212	for x := 0; x < w; x++ {
213		for y := 0; y < h; y++ {
214			xo := amplude * math.Sin(float64(y)*dx)
215			yo := amplude * math.Cos(float64(x)*dx)
216			newm.SetColorIndex(x, y, oldm.ColorIndexAt(x+int(xo), y+int(yo)))
217		}
218	}
219	m.Paletted = newm
220}
221
222func randomBrightness(c color.RGBA, max uint8) color.RGBA {
223	minc := min3(c.R, c.G, c.B)
224	maxc := max3(c.R, c.G, c.B)
225	if maxc > max {
226		return c
227	}
228	n := rand.Intn(int(max-maxc)) - int(minc)
229	return color.RGBA{
230		uint8(int(c.R) + n),
231		uint8(int(c.G) + n),
232		uint8(int(c.B) + n),
233		uint8(c.A),
234	}
235}
236
237func min3(x, y, z uint8) (m uint8) {
238	m = x
239	if y < m {
240		m = y
241	}
242	if z < m {
243		m = z
244	}
245	return
246}
247
248func max3(x, y, z uint8) (m uint8) {
249	m = x
250	if y > m {
251		m = y
252	}
253	if z > m {
254		m = z
255	}
256	return
257}