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 "math/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// encodeToPNG encodes an image to PNG and returns
85// the result as a byte slice.
86func (m *Image) encodedPNG() []byte {
87 var buf bytes.Buffer
88 if err := png.Encode(&buf, m.Paletted); err != nil {
89 panic(err.Error())
90 }
91 return buf.Bytes()
92}
93
94// WriteTo writes captcha image in PNG format into the given writer.
95func (m *Image) WriteTo(w io.Writer) (int64, error) {
96 n, err := w.Write(m.encodedPNG())
97 return int64(n), err
98}
99
100func (m *Image) calculateSizes(width, height, ncount int) {
101 // Goal: fit all digits inside the image.
102 var border int
103 if width > height {
104 border = height / 4
105 } else {
106 border = width / 4
107 }
108 // Convert everything to floats for calculations.
109 w := float64(width - border*2)
110 h := float64(height - border*2)
111 // fw takes into account 1-dot spacing between digits.
112 fw := float64(fontWidth + 1)
113 fh := float64(fontHeight)
114 nc := float64(ncount)
115 // Calculate the width of a single digit taking into account only the
116 // width of the image.
117 nw := w / nc
118 // Calculate the height of a digit from this width.
119 nh := nw * fh / fw
120 // Digit too high?
121 if nh > h {
122 // Fit digits based on height.
123 nh = h
124 nw = fw / fh * nh
125 }
126 // Calculate dot size.
127 m.dotSize = int(nh / fh)
128 // Save everything, making the actual width smaller by 1 dot to account
129 // for spacing between digits.
130 m.numWidth = int(nw) - m.dotSize
131 m.numHeight = int(nh)
132}
133
134func (m *Image) drawHorizLine(fromX, toX, y int, colorIdx uint8) {
135 for x := fromX; x <= toX; x++ {
136 m.SetColorIndex(x, y, colorIdx)
137 }
138}
139
140func (m *Image) drawCircle(x, y, radius int, colorIdx uint8) {
141 f := 1 - radius
142 dfx := 1
143 dfy := -2 * radius
144 xo := 0
145 yo := radius
146
147 m.SetColorIndex(x, y+radius, colorIdx)
148 m.SetColorIndex(x, y-radius, colorIdx)
149 m.drawHorizLine(x-radius, x+radius, y, colorIdx)
150
151 for xo < yo {
152 if f >= 0 {
153 yo--
154 dfy += 2
155 f += dfy
156 }
157 xo++
158 dfx += 2
159 f += dfx
160 m.drawHorizLine(x-xo, x+xo, y+yo, colorIdx)
161 m.drawHorizLine(x-xo, x+xo, y-yo, colorIdx)
162 m.drawHorizLine(x-yo, x+yo, y+xo, colorIdx)
163 m.drawHorizLine(x-yo, x+yo, y-xo, colorIdx)
164 }
165}
166
167func (m *Image) fillWithCircles(n, maxradius int) {
168 maxx := m.Bounds().Max.X
169 maxy := m.Bounds().Max.Y
170 for i := 0; i < n; i++ {
171 colorIdx := uint8(rnd(1, circleCount-1))
172 r := rnd(1, maxradius)
173 m.drawCircle(rnd(r, maxx-r), rnd(r, maxy-r), r, colorIdx)
174 }
175}
176
177func (m *Image) strikeThrough() {
178 maxx := m.Bounds().Max.X
179 maxy := m.Bounds().Max.Y
180 y := rnd(maxy/3, maxy-maxy/3)
181 amplitude := rndf(5, 20)
182 period := rndf(80, 180)
183 dx := 2.0 * math.Pi / period
184 for x := 0; x < maxx; x++ {
185 xo := amplitude * math.Cos(float64(y)*dx)
186 yo := amplitude * math.Sin(float64(x)*dx)
187 for yn := 0; yn < m.dotSize; yn++ {
188 r := rnd(0, m.dotSize)
189 m.drawCircle(x+int(xo), y+int(yo)+(yn*m.dotSize), r/2, 1)
190 }
191 }
192}
193
194func (m *Image) drawDigit(digit []byte, x, y int) {
195 skf := rndf(-maxSkew, maxSkew)
196 xs := float64(x)
197 r := m.dotSize / 2
198 y += rnd(-r, r)
199 for yo := 0; yo < fontHeight; yo++ {
200 for xo := 0; xo < fontWidth; xo++ {
201 if digit[yo*fontWidth+xo] != blackChar {
202 continue
203 }
204 m.drawCircle(x+xo*m.dotSize, y+yo*m.dotSize, r, 1)
205 }
206 xs += skf
207 x = int(xs)
208 }
209}
210
211func (m *Image) distort(amplude float64, period float64) {
212 w := m.Bounds().Max.X
213 h := m.Bounds().Max.Y
214
215 oldm := m.Paletted
216 newm := image.NewPaletted(image.Rect(0, 0, w, h), oldm.Palette)
217
218 dx := 2.0 * math.Pi / period
219 for x := 0; x < w; x++ {
220 for y := 0; y < h; y++ {
221 xo := amplude * math.Sin(float64(y)*dx)
222 yo := amplude * math.Cos(float64(x)*dx)
223 newm.SetColorIndex(x, y, oldm.ColorIndexAt(x+int(xo), y+int(yo)))
224 }
225 }
226 m.Paletted = newm
227}
228
229func randomBrightness(c color.RGBA, max uint8) color.RGBA {
230 minc := min3(c.R, c.G, c.B)
231 maxc := max3(c.R, c.G, c.B)
232 if maxc > max {
233 return c
234 }
235 n := rand.Intn(int(max-maxc)) - int(minc)
236 return color.RGBA{
237 uint8(int(c.R) + n),
238 uint8(int(c.G) + n),
239 uint8(int(c.B) + n),
240 uint8(c.A),
241 }
242}
243
244func min3(x, y, z uint8) (m uint8) {
245 m = x
246 if y < m {
247 m = y
248 }
249 if z < m {
250 m = z
251 }
252 return
253}
254
255func max3(x, y, z uint8) (m uint8) {
256 m = x
257 if y > m {
258 m = y
259 }
260 if z > m {
261 m = z
262 }
263 return
264}