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}