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