github.com/keysonZZZ/kmg@v0.0.0-20151121023212-05317bfd7d39/kmgImage/Captcha.go (about)

     1  package kmgImage
     2  
     3  import (
     4  	crand "crypto/rand"
     5  	"image"
     6  	"image/color"
     7  	"image/png"
     8  	"io"
     9  	"math/rand"
    10  	"time"
    11  )
    12  
    13  const (
    14  	stdWidth  = 100
    15  	stdHeight = 40
    16  	maxSkew   = 2
    17  )
    18  const (
    19  	fontWidth  = 5
    20  	fontHeight = 8
    21  	blackChar  = 1
    22  )
    23  
    24  var font = [][]byte{
    25  	{ // 0
    26  		0, 1, 1, 1, 0,
    27  		1, 0, 0, 0, 1,
    28  		1, 0, 0, 0, 1,
    29  		1, 0, 0, 0, 1,
    30  		1, 0, 0, 0, 1,
    31  		1, 0, 0, 0, 1,
    32  		1, 0, 0, 0, 1,
    33  		0, 1, 1, 1, 0,
    34  	},
    35  	{ // 1
    36  		0, 0, 1, 0, 0,
    37  		0, 1, 1, 0, 0,
    38  		1, 0, 1, 0, 0,
    39  		0, 0, 1, 0, 0,
    40  		0, 0, 1, 0, 0,
    41  		0, 0, 1, 0, 0,
    42  		0, 0, 1, 0, 0,
    43  		1, 1, 1, 1, 1,
    44  	},
    45  	{ // 2
    46  		0, 1, 1, 1, 0,
    47  		1, 0, 0, 0, 1,
    48  		0, 0, 0, 0, 1,
    49  		0, 0, 0, 1, 1,
    50  		0, 1, 1, 0, 0,
    51  		1, 0, 0, 0, 0,
    52  		1, 0, 0, 0, 0,
    53  		1, 1, 1, 1, 1,
    54  	},
    55  	{ // 3
    56  		1, 1, 1, 1, 0,
    57  		0, 0, 0, 0, 1,
    58  		0, 0, 0, 1, 0,
    59  		0, 1, 1, 1, 0,
    60  		0, 0, 0, 1, 0,
    61  		0, 0, 0, 0, 1,
    62  		0, 0, 0, 0, 1,
    63  		1, 1, 1, 1, 0,
    64  	},
    65  	{ // 4
    66  		1, 0, 0, 1, 0,
    67  		1, 0, 0, 1, 0,
    68  		1, 0, 0, 1, 0,
    69  		1, 0, 0, 1, 0,
    70  		1, 1, 1, 1, 1,
    71  		0, 0, 0, 1, 0,
    72  		0, 0, 0, 1, 0,
    73  		0, 0, 0, 1, 0,
    74  	},
    75  	{ // 5
    76  		1, 1, 1, 1, 1,
    77  		1, 0, 0, 0, 0,
    78  		1, 0, 0, 0, 0,
    79  		1, 1, 1, 1, 0,
    80  		0, 0, 0, 0, 1,
    81  		0, 0, 0, 0, 1,
    82  		0, 0, 0, 0, 1,
    83  		1, 1, 1, 1, 0,
    84  	},
    85  	{ // 6
    86  		0, 0, 1, 1, 1,
    87  		0, 1, 0, 0, 0,
    88  		1, 0, 0, 0, 0,
    89  		1, 1, 1, 1, 0,
    90  		1, 0, 0, 0, 1,
    91  		1, 0, 0, 0, 1,
    92  		1, 0, 0, 0, 1,
    93  		0, 1, 1, 1, 0,
    94  	},
    95  	{ // 7
    96  		1, 1, 1, 1, 1,
    97  		0, 0, 0, 0, 1,
    98  		0, 0, 0, 0, 1,
    99  		0, 0, 0, 1, 0,
   100  		0, 0, 1, 0, 0,
   101  		0, 1, 0, 0, 0,
   102  		0, 1, 0, 0, 0,
   103  		0, 1, 0, 0, 0,
   104  	},
   105  	{ // 8
   106  		0, 1, 1, 1, 0,
   107  		1, 0, 0, 0, 1,
   108  		1, 0, 0, 0, 1,
   109  		0, 1, 1, 1, 0,
   110  		1, 0, 0, 0, 1,
   111  		1, 0, 0, 0, 1,
   112  		1, 0, 0, 0, 1,
   113  		0, 1, 1, 1, 0,
   114  	},
   115  	{ // 9
   116  		0, 1, 1, 1, 0,
   117  		1, 0, 0, 0, 1,
   118  		1, 0, 0, 0, 1,
   119  		1, 1, 0, 0, 1,
   120  		0, 1, 1, 1, 1,
   121  		0, 0, 0, 0, 1,
   122  		0, 0, 0, 0, 1,
   123  		1, 1, 1, 1, 0,
   124  	},
   125  }
   126  
   127  type Image struct {
   128  	*image.NRGBA
   129  	color   *color.NRGBA
   130  	width   int //a digit width
   131  	height  int //a digit height
   132  	dotsize int
   133  }
   134  
   135  func init() {
   136  	rand.Seed(int64(time.Second))
   137  }
   138  func NewCaptchaImage(digits [4]int, width, height int) *Image {
   139  	img := new(Image)
   140  	r := image.Rect(img.width, img.height, stdWidth, stdHeight)
   141  	img.NRGBA = image.NewNRGBA(r)
   142  	img.color = &color.NRGBA{
   143  		uint8(rand.Intn(129)),
   144  		uint8(rand.Intn(129)),
   145  		uint8(rand.Intn(129)),
   146  		0xFF,
   147  	}
   148  	// Draw background (10 random circles of random brightness)
   149  	img.calculateSizes(width, height, len(digits))
   150  	img.fillWithCircles(10, img.dotsize)
   151  	maxx := width - (img.width+img.dotsize)*len(digits) - img.dotsize
   152  	maxy := height - img.height - img.dotsize*2
   153  	x := rnd(img.dotsize*2, maxx)
   154  	y := rnd(img.dotsize*2, maxy)
   155  	// Draw digits.
   156  	for _, n := range digits {
   157  		img.drawDigit(font[n], x, y)
   158  		x += img.width + img.dotsize
   159  	}
   160  	// Draw strike-through line.
   161  	img.strikeThrough()
   162  	return img
   163  }
   164  func (img *Image) WriteTo(w io.Writer) (int64, error) {
   165  	return 0, png.Encode(w, img)
   166  }
   167  func (img *Image) calculateSizes(width, height, ncount int) {
   168  	// Goal: fit all digits inside the image.
   169  	var border int
   170  	if width > height {
   171  		border = height / 5
   172  	} else {
   173  		border = width / 5
   174  	}
   175  	// Convert everything to floats for calculations.
   176  	w := float64(width - border*2)  //268
   177  	h := float64(height - border*2) //48
   178  	// fw takes into account 1-dot spacing between digits.
   179  	fw := float64(fontWidth) + 1 //6
   180  	fh := float64(fontHeight)    //8
   181  	nc := float64(ncount)        //7
   182  	// Calculate the width of a single digit taking into account only the
   183  	// width of the image.
   184  	nw := w / nc //38
   185  	// Calculate the height of a digit from this width.
   186  	nh := nw * fh / fw //51
   187  	// Digit too high?
   188  	if nh > h {
   189  		// Fit digits based on height.
   190  		nh = h //nh = 44
   191  		nw = fw / fh * nh
   192  	}
   193  	// Calculate dot size.
   194  	img.dotsize = int(nh / fh)
   195  	// Save everything, making the actual width smaller by 1 dot to account
   196  	// for spacing between digits.
   197  	img.width = int(nw)
   198  	img.height = int(nh) - img.dotsize
   199  }
   200  func (img *Image) fillWithCircles(n, maxradius int) {
   201  	color := img.color
   202  	maxx := img.Bounds().Max.X
   203  	maxy := img.Bounds().Max.Y
   204  	for i := 0; i < n; i++ {
   205  		setRandomBrightness(color, 255)
   206  		r := rnd(1, maxradius)
   207  		img.drawCircle(color, rnd(r, maxx-r), rnd(r, maxy-r), r)
   208  	}
   209  }
   210  func (img *Image) drawHorizLine(color color.Color, fromX, toX, y int) {
   211  	for x := fromX; x <= toX; x++ {
   212  		img.Set(x, y, color)
   213  	}
   214  }
   215  func (img *Image) drawCircle(color color.Color, x, y, radius int) {
   216  	f := 1 - radius
   217  	dfx := 1
   218  	dfy := -2 * radius
   219  	xx := 0
   220  	yy := radius
   221  	img.Set(x, y+radius, color)
   222  	img.Set(x, y-radius, color)
   223  	img.drawHorizLine(color, x-radius, x+radius, y)
   224  	for xx < yy {
   225  		if f >= 0 {
   226  			yy--
   227  			dfy += 2
   228  			f += dfy
   229  		}
   230  		xx++
   231  		dfx += 2
   232  		f += dfx
   233  		img.drawHorizLine(color, x-xx, x+xx, y+yy)
   234  		img.drawHorizLine(color, x-xx, x+xx, y-yy)
   235  		img.drawHorizLine(color, x-yy, x+yy, y+xx)
   236  		img.drawHorizLine(color, x-yy, x+yy, y-xx)
   237  	}
   238  }
   239  func (img *Image) strikeThrough() {
   240  	r := 0
   241  	maxx := img.Bounds().Max.X
   242  	maxy := img.Bounds().Max.Y
   243  	y := rnd(maxy/3, maxy-maxy/3)
   244  	for x := 0; x < maxx; x += r {
   245  		r = rnd(1, img.dotsize/3)
   246  		y += rnd(-img.dotsize/2, img.dotsize/2)
   247  		if y <= 0 || y >= maxy {
   248  			y = rnd(maxy/3, maxy-maxy/3)
   249  		}
   250  		img.drawCircle(img.color, x, y, r)
   251  	}
   252  }
   253  func (img *Image) drawDigit(digit []byte, x, y int) {
   254  	skf := rand.Float64() * float64(rnd(-maxSkew, maxSkew))
   255  	xs := float64(x)
   256  	minr := img.dotsize / 2               // minumum radius
   257  	maxr := img.dotsize/2 + img.dotsize/4 // maximum radius
   258  	y += rnd(-minr, minr)
   259  	for yy := 0; yy < fontHeight; yy++ {
   260  		for xx := 0; xx < fontWidth; xx++ {
   261  			if digit[yy*fontWidth+xx] != blackChar {
   262  				continue
   263  			}
   264  			// Introduce random variations.
   265  			or := rnd(minr, maxr)
   266  			ox := x + (xx * img.dotsize) + rnd(0, or/2)
   267  			oy := y + (yy * img.dotsize) + rnd(0, or/2)
   268  			img.drawCircle(img.color, ox, oy, or)
   269  		}
   270  		xs += skf
   271  		x = int(xs)
   272  	}
   273  }
   274  func setRandomBrightness(c *color.NRGBA, max uint8) {
   275  	minc := min3(c.R, c.G, c.B)
   276  	maxc := max3(c.R, c.G, c.B)
   277  	if maxc > max {
   278  		return
   279  	}
   280  	n := rand.Intn(int(max-maxc)) - int(minc)
   281  	c.R = uint8(int(c.R) + n)
   282  	c.G = uint8(int(c.G) + n)
   283  	c.B = uint8(int(c.B) + n)
   284  }
   285  func min3(x, y, z uint8) (o uint8) {
   286  	o = x
   287  	if y < o {
   288  		o = y
   289  	}
   290  	if z < o {
   291  		o = z
   292  	}
   293  	return
   294  }
   295  func max3(x, y, z uint8) (o uint8) {
   296  	o = x
   297  	if y > o {
   298  		o = y
   299  	}
   300  	if z > o {
   301  		o = z
   302  	}
   303  	return
   304  }
   305  
   306  // rnd returns a random number in range [from, to].
   307  func rnd(from, to int) int {
   308  	//println(to+1-from)
   309  	return rand.Intn(to+1-from) + from
   310  }
   311  
   312  const (
   313  	// Standard length of uniuri string to achive ~95 bits of entropy.
   314  	StdLen = 16
   315  	// Length of uniurl string to achive ~119 bits of entropy, closest
   316  	// to what can be losslessly converted to UUIDv4 (122 bits).
   317  	UUIDLen = 20
   318  )
   319  
   320  // Standard characters allowed in uniuri string.
   321  var StdChars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
   322  
   323  // New returns a new random string of the standard length, consisting of
   324  // standard characters.
   325  func New() string {
   326  	return NewLenChars(StdLen, StdChars)
   327  }
   328  
   329  // NewLen returns a new random string of the provided length, consisting of
   330  // standard characters.
   331  func NewLen(length int) string {
   332  	return NewLenChars(length, StdChars)
   333  }
   334  
   335  // NewLenChars returns a new random string of the provided length, consisting
   336  // of the provided byte slice of allowed characters (maximum 256).
   337  func NewLenChars(length int, chars []byte) string {
   338  	b := make([]byte, length)
   339  	r := make([]byte, length+(length/4)) // storage for random bytes.
   340  	clen := byte(len(chars))
   341  	maxrb := byte(256 - (256 % len(chars)))
   342  	i := 0
   343  	for {
   344  		if _, err := io.ReadFull(crand.Reader, r); err != nil {
   345  			panic("error reading from random source: " + err.Error())
   346  		}
   347  		for _, c := range r {
   348  			if c >= maxrb {
   349  				// Skip this number to avoid modulo bias.
   350  				continue
   351  			}
   352  			b[i] = chars[c%clen]
   353  			i++
   354  			if i == length {
   355  				return string(b)
   356  			}
   357  		}
   358  	}
   359  	panic("unreachable")
   360  }