image: use paletted image. This simplifies code and makes it faster. It also produces smaller PNGs. Before: captcha.BenchmarkNewImage 500 6198230 ns/op captcha.BenchmarkImageWriteTo 100 17517110 ns/op 0.14 MB/s After: captcha.BenchmarkNewImage 500 3869968 ns/op captcha.BenchmarkImageWriteTo 200 7237604 ns/op 0.23 MB/s (Core 2 Duo @ 2.26 GHz, MacBook) Currently the background is white, not transparent, because PNG encoder doesn't support paletted images with alpha channel. Submitted CL: http://codereview.appspot.com/4432078 Change alpha to 0x00 once it's accepted to make captcha transparent.
Dmitry Chestnykh dmitry@codingrobots.com
Thu, 28 Apr 2011 20:09:50 +0200
1 files changed,
56 insertions(+),
36 deletions(-)
jump to
M
image.go
→
image.go
@@ -16,31 +16,49 @@ StdWidth = 240
StdHeight = 80 // Maximum absolute skew factor of a single digit. maxSkew = 0.7 + // Number of background circles. + circleCount = 20 ) type Image struct { - *image.NRGBA - primaryColor image.NRGBAColor - numWidth int - numHeight int - dotSize int + *image.Paletted + numWidth int + numHeight int + dotSize int } func init() { rand.Seed(time.Seconds()) } -// NewImage returns a new captcha image of the given width and height with the -// given digits, where each digit must be in range 0-9. -func NewImage(digits []byte, width, height int) *Image { - img := new(Image) - img.NRGBA = image.NewNRGBA(width, height) - img.primaryColor = image.NRGBAColor{ +func randomPalette() image.PalettedColorModel { + p := make([]image.Color, circleCount+1) + // Transparent color. + // TODO(dchest). Currently it's white, not transparent, because PNG + // encoder doesn't support paletted images with alpha channel. + // Submitted CL: http://codereview.appspot.com/4432078 Change alpha to + // 0x00 once it's accepted. + p[0] = image.RGBAColor{0xFF, 0xFF, 0xFF, 0xFF} + // Primary color. + prim := image.RGBAColor{ uint8(rand.Intn(129)), uint8(rand.Intn(129)), uint8(rand.Intn(129)), 0xFF, } + p[1] = prim + // Circle colors. + for i := 2; i <= circleCount; i++ { + p[i] = randomBrightness(prim, 255) + } + return p +} + +// NewImage returns a new captcha image of the given width and height with the +// given digits, where each digit must be in range 0-9. +func NewImage(digits []byte, width, height int) *Image { + img := new(Image) + img.Paletted = image.NewPaletted(width, height, randomPalette()) img.calculateSizes(width, height, len(digits)) // Randomly position captcha inside the image. maxx := width - (img.numWidth+img.dotSize)*len(digits) - img.dotSize@@ -73,7 +91,7 @@ // doesn't report this.
// WriteTo writes captcha image in PNG format into the given writer. func (img *Image) WriteTo(w io.Writer) (int64, os.Error) { - return 0, png.Encode(w, img) + return 0, png.Encode(w, img.Paletted) } func (img *Image) calculateSizes(width, height, ncount int) {@@ -110,22 +128,22 @@ img.numWidth = int(nw) - img.dotSize
img.numHeight = int(nh) } -func (img *Image) drawHorizLine(color image.Color, fromX, toX, y int) { +func (img *Image) drawHorizLine(fromX, toX, y int, colorIndex uint8) { for x := fromX; x <= toX; x++ { - img.Set(x, y, color) + img.SetColorIndex(x, y, colorIndex) } } -func (img *Image) drawCircle(color image.Color, x, y, radius int) { +func (img *Image) drawCircle(x, y, radius int, colorIndex uint8) { f := 1 - radius dfx := 1 dfy := -2 * radius xx := 0 yy := radius - img.Set(x, y+radius, color) - img.Set(x, y-radius, color) - img.drawHorizLine(color, x-radius, x+radius, y) + img.SetColorIndex(x, y+radius, colorIndex) + img.SetColorIndex(x, y-radius, colorIndex) + img.drawHorizLine(x-radius, x+radius, y, colorIndex) for xx < yy { if f >= 0 {@@ -136,21 +154,20 @@ }
xx++ dfx += 2 f += dfx - img.drawHorizLine(color, x-xx, x+xx, y+yy) - img.drawHorizLine(color, x-xx, x+xx, y-yy) - img.drawHorizLine(color, x-yy, x+yy, y+xx) - img.drawHorizLine(color, x-yy, x+yy, y-xx) + img.drawHorizLine(x-xx, x+xx, y+yy, colorIndex) + img.drawHorizLine(x-xx, x+xx, y-yy, colorIndex) + img.drawHorizLine(x-yy, x+yy, y+xx, colorIndex) + img.drawHorizLine(x-yy, x+yy, y-xx, colorIndex) } } func (img *Image) fillWithCircles(n, maxradius int) { - color := img.primaryColor maxx := img.Bounds().Max.X maxy := img.Bounds().Max.Y for i := 0; i < n; i++ { - setRandomBrightness(&color, 255) + colorIndex := uint8(rnd(1, circleCount-1)) r := rnd(1, maxradius) - img.drawCircle(color, rnd(r, maxx-r), rnd(r, maxy-r), r) + img.drawCircle(rnd(r, maxx-r), rnd(r, maxy-r), r, colorIndex) } }@@ -166,7 +183,7 @@ xo := amplitude * math.Cos(float64(y)*dx)
yo := amplitude * math.Sin(float64(x)*dx) for yn := 0; yn < img.dotSize; yn++ { r := rnd(0, img.dotSize) - img.drawCircle(img.primaryColor, x+int(xo), y+int(yo)+(yn*img.dotSize), r/2) + img.drawCircle(x+int(xo), y+int(yo)+(yn*img.dotSize), r/2, 1) } } }@@ -183,7 +200,7 @@ continue
} ox := x + xx*img.dotSize oy := y + yy*img.dotSize - img.drawCircle(img.primaryColor, ox, oy, r) + img.drawCircle(ox, oy, r, 1) } xs += skf x = int(xs)@@ -194,30 +211,33 @@ func (img *Image) distort(amplude float64, period float64) {
w := img.Bounds().Max.X h := img.Bounds().Max.Y - oldImg := img.NRGBA - newImg := image.NewNRGBA(w, h) + oldImg := img.Paletted + newImg := image.NewPaletted(w, h, oldImg.Palette) dx := 2.0 * math.Pi / period for x := 0; x < w; x++ { for y := 0; y < h; y++ { ox := amplude * math.Sin(float64(y)*dx) oy := amplude * math.Cos(float64(x)*dx) - newImg.Set(x, y, oldImg.At(x+int(ox), y+int(oy))) + newImg.SetColorIndex(x, y, oldImg.ColorIndexAt(x+int(ox), y+int(oy))) } } - img.NRGBA = newImg + img.Paletted = newImg } -func setRandomBrightness(c *image.NRGBAColor, max uint8) { +func randomBrightness(c image.RGBAColor, max uint8) image.RGBAColor { minc := min3(c.R, c.G, c.B) maxc := max3(c.R, c.G, c.B) if maxc > max { - return + return c } n := rand.Intn(int(max-maxc)) - int(minc) - c.R = uint8(int(c.R) + n) - c.G = uint8(int(c.G) + n) - c.B = uint8(int(c.B) + n) + return image.RGBAColor{ + uint8(int(c.R) + n), + uint8(int(c.G) + n), + uint8(int(c.B) + n), + uint8(c.A), + } } func min3(x, y, z uint8) (o uint8) {