dkforest

A forum and chat platform (onion)
git clone https://git.dasho.dev/n0tr1v/dkforest.git
Log | Files | Refs | LICENSE

captcha.go (17526B)


      1 package captcha
      2 
      3 import (
      4 	"bytes"
      5 	"encoding/base64"
      6 	"errors"
      7 	"fmt"
      8 	"image"
      9 	"image/color"
     10 	"image/draw"
     11 	"image/gif"
     12 	"math"
     13 	"os"
     14 	"sort"
     15 	"strings"
     16 )
     17 
     18 const (
     19 	b64Prefix = "R0lGODlhCAAOAIAAAAQCBPz-_CwAAAAACAAOAAAC"
     20 	alphabet  = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
     21 	alphabet1 = "abdcefgh1ijkImnpoqrstyQuvwxzABCDEGJKMNHLORPFSTlUVWXYZ023456789"
     22 )
     23 
     24 var (
     25 	onColor  = color.RGBA{R: 252, G: 254, B: 252, A: 255}
     26 	redColor = color.RGBA{R: 204, G: 2, B: 4, A: 255}
     27 	offColor = color.RGBA{R: 4, G: 2, B: 4, A: 255}
     28 )
     29 
     30 var b64Map = map[string]string{
     31 	"D_AxdTP8lWthViTno4yvAgA7":             "z",
     32 	"EPAxdTXcDYyvhbokkC8znwoAOw==":         "c",
     33 	"EfAxFG3ZU2AAntrky1Hf-cECADs=":         "l",
     34 	"EfAxdTP8gUMvLCoVnfVW9pcCADs=":         "s",
     35 	"EfAxdTU8jjNSxnfvqqBi9pkCADs=":         "e",
     36 	"EvAxdTECnDNxPplue3hr9kGjAAA7":         "r",
     37 	"E_ARIlK523poxXlqyvRKCcEQKAAAOw==":     "i",
     38 	"E_ARInL53EMrKQmvoS3bGbtKrAoAOw==":     "1",
     39 	"E_ARYiO103IyQkQvtFTiZ51QPAoAOw==":     "J",
     40 	"E_AtAlV5HYiyIWrvm9FN6j1MFAsAOw==":     "X",
     41 	"E_AtAlW5nzuN1rgmzDLpjsBQNAoAOw==":     "H",
     42 	"E_AtAnNZGYDPoWnruTo39aNQRAoAOw==":     "L",
     43 	"E_AtdAMUGVySzYdcw1fi7MFQLAAAOw==":     "P",
     44 	"E_AtdAOznYrwSGQrfQ7gzDROHAsAOw==":     "Z",
     45 	"E_AtdBUzHGhonvrclZDyHsFQLAAAOw==":     "7",
     46 	"E_AtdBkzHFCRTQmdRfrwF8FQLAAAOw==":     "F",
     47 	"E_AtdPUawUPSBRgtpCnPXq9QvAoAOw==":     "T",
     48 	"E_AvVI07UILIvRkrvbK6laNQRAoAOw==":     "3",
     49 	"E_AvdFHR3IstPDBTlZrSxa1QBAoAOw==":     "I",
     50 	"E_AxL0IZWWxrwkrdNQ_wHpVQRAoAOw==":     "t",
     51 	"E_AxdTEC3IPOUHqirLFx7DdGXAoAOw==":     "n",
     52 	"E_AxdTEgHHyRSgvpvDrj7phQKQAAOw==":     "m",
     53 	"E_AxdTGi3TsO0CqtvXrL6ZlwKQAAOw==":     "v",
     54 	"E_AxdTGi3TtRGgodBHnz2JlwKQAAOw==":     "u",
     55 	"E_AxdTGi3TvxOUBvjTlTbDlGXAoAOw==":     "w",
     56 	"E_AxdTGinzNyugeTvYBKWplwKQAAOw==":     "x",
     57 	"E_AxdTU8jjNSxocetKBWfpmQKQAAOw==":     "o",
     58 	"E_AxdTXcDYyh0XflzA_cTplQKQAAOw==":     "a",
     59 	"E_DxIhK24WLp0Ykqvorl5ycpgwoAOw==":     "j",
     60 	"FPAtAlV5HYiyIWqvnWenpT9MHJECADs=":     "Y",
     61 	"FPAtAlW5nzttOQoszJRL_pBQHI0CADs=":     "W",
     62 	"FPAtAlW5nzttTWhluhrITJFQHJECADs=":     "U",
     63 	"FPAtdAMUU4NvMkkRPjpCZyVPHI0CADs=":     "E",
     64 	"FPAvdEFxAzJONhgrfXhra6FKHJECADs=":     "S",
     65 	"FPAxNE7NXWMIREntmzdtvRZKHMUCADs=":     "2",
     66 	"FfARInL3nmLROAYdsjtjKr8vGUmjAAA7":     "A",
     67 	"FfARInL3nmLROAYdsnpeurlEHEmgAAA7":     "0",
     68 	"FfARIxK2n3uTyVXNxXbTHC-pGcmjAAA7":     "d",
     69 	"FfARYuPWnmoSThQrszRht6tEHEmjAAA7":     "f",
     70 	"FfAtAlW5H3By0VTnpHrXwy9EHEmxAAA7":     "V",
     71 	"FfAtAnNZGYDK0SZdg4dmvDUPjMiyAAA7":     "h",
     72 	"FfAtdDNOAXysxSWl1TPbDK8OGcmyAAA7":     "B",
     73 	"FfAvAk0ZHItzRahWwshZ2rLrGcmjAAA7":     "k",
     74 	"FfAxNE7NXVtpyvgqvgAy7r8NGcmyAAA7":     "O",
     75 	"FfAxNE7NXVuJxQlmpHhb9i5EHEmjAAA7":     "9",
     76 	"FfAxVI170YvApXBWpRDf6rJrGsmjAAA7":     "G",
     77 	"FfAxVI17EYDRpRDlpfrc6pYKG0mgAAA7":     "C",
     78 	"FfAxdTEC3IPOUHqirLHlPSfp6hilAAA7":     "p",
     79 	"FfAxdTGi3TtRGgodBHnz2CvrobilAAA7":     "y",
     80 	"FfAxdTPcwTsPGlhjsHQverVJ-8KlAAA7":     "g",
     81 	"FfAxdTUc3oNOGhrQzVhSG7GkOkakAAA7":     "q",
     82 	"FfDxInJZHQTL0MnsvS6iLJsnGcmkAAA7":     "4",
     83 	"FvAtAlXPHYhwpcbuW1HWWbtQQ8jSNAoAOw==": "K",
     84 	"FvAtAnNZGYDK0SZdg4dmbLEQjMjyKAAAOw==": "b",
     85 	"FvAtArWvHYAqOQrlwnlzWqMKGcnSKAAAOw==": "N",
     86 	"FvAtArWzYQRqOgprpQdz57cHGcnSKAAAOw==": "M",
     87 	"FvAtdAMUGVySzXqeaxq3nKYOGcnSKAAAOw==": "R",
     88 	"FvAtdAMUU4NvRfmgk4jxhjEtzMjSKAAAOw==": "5",
     89 	"FvAtdDNOAXxsymYjva7ynBcPGckSKAAAOw==": "D",
     90 	"FvAxNE7NXQMxsfUiZO3yuallQ8jSNAoAOw==": "6",
     91 	"FvAxNE7NXVtpyvgmrIvFywEGGckSKAAAOw==": "Q",
     92 	"FvAxNE7NXWMxgsUehjhXOzXuQsjSRAoAOw==": "8",
     93 }
     94 
     95 // SolveBase64 solve a base64 encoded gif
     96 func SolveBase64(b64Str string) (string, error) {
     97 	b64Str = strings.TrimPrefix(b64Str, "data:image/gif;base64,")
     98 	b64, err := base64.StdEncoding.DecodeString(b64Str)
     99 	if err != nil {
    100 		return "", err
    101 	}
    102 	captchaImg, err := gif.Decode(bytes.NewReader(b64))
    103 	if err != nil {
    104 		return "", err
    105 	}
    106 	if captchaImg.Bounds().Max.X > 60 {
    107 		return SolveDifficulty3(captchaImg)
    108 	}
    109 	return SolveDifficulty2(captchaImg)
    110 }
    111 
    112 // Solve a captcha difficulty 1
    113 // Slice the captcha into 5, and directly compare the slices with our base64 hashmap
    114 func Solve(img image.Image) (answer string, err error) {
    115 	gifImg, ok := img.(*image.Paletted)
    116 	if !ok {
    117 		return "", errors.New("invalid gif image")
    118 	}
    119 	letterSize := image.Point{X: 8, Y: 14}
    120 	cornerPt := image.Point{X: 5, Y: 7}
    121 	for i := 0; i < 5; i++ {
    122 		rect := image.Rectangle{Min: cornerPt, Max: cornerPt.Add(letterSize)}
    123 		letterImg := gifImg.SubImage(rect)
    124 		buf := bytes.Buffer{}
    125 		_ = gif.Encode(&buf, letterImg, nil)
    126 		letterB64 := base64.URLEncoding.EncodeToString(buf.Bytes())
    127 		character, err := getCharByB64(letterB64)
    128 		if err != nil {
    129 			return "", fmt.Errorf("failed to get character %d", i+1)
    130 		}
    131 		answer += character
    132 		cornerPt.X += letterSize.X + 1
    133 	}
    134 	return
    135 }
    136 
    137 // SolveDifficulty2 solve all captcha up to difficulty 2
    138 // For difficulty 2, we slice the captcha into 5 slices, and compare
    139 // which of our base64 images has the best match for the slice.
    140 func SolveDifficulty2(img image.Image) (answer string, err error) {
    141 	gifImg, ok := img.(*image.Paletted)
    142 	if !ok {
    143 		return "", errors.New("invalid gif image")
    144 	}
    145 	letterSize := image.Point{X: 8, Y: 14}
    146 	cornerPt := image.Point{X: 5, Y: 7}
    147 	for i := 0; i < 5; i++ {
    148 		rect := image.Rectangle{Min: cornerPt, Max: cornerPt.Add(letterSize)}
    149 		letterImg := gifImg.SubImage(rect).(*image.Paletted)
    150 		character := ""
    151 	alphabetLoop:
    152 		for _, c := range alphabet1 {
    153 			goodLetterImg, _ := getLetterImg(string(c))
    154 			for y := 0; y < 14; y++ {
    155 				for x := 0; x < 8; x++ {
    156 					if goodLetterImg.At(x, y) == onColor && letterImg.At(cornerPt.X+x, cornerPt.Y+y) != onColor {
    157 						continue alphabetLoop
    158 					}
    159 				}
    160 			}
    161 			character = string(c)
    162 			break
    163 		}
    164 		// This should never happen
    165 		if character == "" {
    166 			return "", errors.New("failed to solve captcha")
    167 		}
    168 		answer += character
    169 		cornerPt.X += letterSize.X + 1
    170 	}
    171 	return
    172 }
    173 
    174 // Count pixels that are On (either white or red)
    175 func countPxOn(img *image.Paletted) (countOn int) {
    176 	for y := 0; y < 14; y++ {
    177 		for x := 0; x < 8; x++ {
    178 			c := img.At(img.Bounds().Min.X+x, img.Bounds().Min.Y+y)
    179 			if c == onColor || c == redColor {
    180 				countOn += 1
    181 			}
    182 		}
    183 	}
    184 	return
    185 }
    186 
    187 // Count pixels that are red
    188 func countRedPx(img *image.Paletted, offset image.Point) (countOn int) {
    189 	for y := 0; y < img.Rect.Bounds().Dy(); y++ {
    190 		for x := 0; x < img.Rect.Bounds().Dx(); x++ {
    191 			c := img.At(offset.X+x, offset.Y+y)
    192 			if c == redColor {
    193 				countOn += 1
    194 			}
    195 		}
    196 	}
    197 	return
    198 }
    199 
    200 type Letter struct {
    201 	Char      string
    202 	Rect      image.Rectangle
    203 	angles    []float64
    204 	neighbors []Letter
    205 }
    206 
    207 func (l Letter) String() string {
    208 	return fmt.Sprintf(`["%s",X:%d,Y:%d]`, l.Char, l.Center().X, l.Center().Y)
    209 }
    210 
    211 func (l Letter) Center() image.Point {
    212 	return image.Point{X: l.Rect.Min.X + 4, Y: l.Rect.Min.Y + 6}
    213 }
    214 
    215 func (l Letter) Key() string {
    216 	return fmt.Sprintf("%s_%d_%d", l.Char, l.Rect.Min.X, l.Rect.Min.Y)
    217 }
    218 
    219 func hasRedInCenterArea(letterImg *image.Paletted) bool {
    220 	for y := 5; y <= 7; y++ {
    221 		for x := 3; x <= 5; x++ {
    222 			letterImgColor := letterImg.At(letterImg.Bounds().Min.X+x, letterImg.Bounds().Min.Y+y)
    223 			if letterImgColor == redColor {
    224 				return true
    225 			}
    226 		}
    227 	}
    228 	return false
    229 }
    230 
    231 // give an image and a valid letter image, return either or not the letter is in that image.
    232 func imgContainsLetter(img, letterImg1 *image.Paletted) bool {
    233 	for y := 0; y < letterImg1.Bounds().Size().Y; y++ {
    234 		for x := 0; x < letterImg1.Bounds().Size().X; x++ {
    235 			goodLetterColor := letterImg1.At(x, y)
    236 			letterImgColor := img.At(img.Bounds().Min.X+x, img.Bounds().Min.Y+y)
    237 			// If we find an Off pixel where it's supposed to be On, skip that letter
    238 			if (goodLetterColor == onColor || goodLetterColor == redColor) &&
    239 				(letterImgColor != onColor && letterImgColor != redColor) {
    240 				return false
    241 			}
    242 		}
    243 	}
    244 	return true
    245 }
    246 
    247 func getContourRedPixels(img *image.Paletted) (out []image.Point) {
    248 	topLeftPt := img.Bounds().Min
    249 	bottomRightPt := img.Bounds().Max
    250 	for i := 0; i < img.Bounds().Dx(); i++ {
    251 		pxColor := img.At(topLeftPt.X+i, topLeftPt.Y)
    252 		if pxColor == redColor {
    253 			out = append(out, image.Point{X: topLeftPt.X + i, Y: topLeftPt.Y})
    254 		}
    255 		pxColor = img.At(topLeftPt.X+i, bottomRightPt.Y-1)
    256 		if pxColor == redColor {
    257 			out = append(out, image.Point{X: topLeftPt.X + i, Y: bottomRightPt.Y - 1})
    258 		}
    259 	}
    260 	for i := 1; i < img.Bounds().Dy()-1; i++ {
    261 		pxColor := img.At(topLeftPt.X, topLeftPt.Y+i)
    262 		if pxColor == redColor {
    263 			out = append(out, image.Point{X: topLeftPt.X, Y: topLeftPt.Y + i})
    264 		}
    265 		if img.Bounds().Dx() < img.Bounds().Dy() {
    266 			if img.Bounds().Max.X == 150 {
    267 				continue
    268 			}
    269 		}
    270 		pxColor = img.At(bottomRightPt.X-1, topLeftPt.Y+i)
    271 		if pxColor == redColor {
    272 			out = append(out, image.Point{X: bottomRightPt.X - 1, Y: topLeftPt.Y + i})
    273 		}
    274 	}
    275 	return
    276 }
    277 
    278 func getAngle(p1, p2 image.Point) float64 {
    279 	return math.Atan2(float64(p1.Y-p2.Y), float64(p1.X-p2.X))
    280 }
    281 
    282 func getLetterInDirection(letter *Letter, angle float64, lettersMap LettersCache) (out *Letter) {
    283 	minAngle := math.MaxFloat64
    284 	// Visit every other letters
    285 	for _, otherLetter := range lettersMap.toSlice() {
    286 		if otherLetter.Key() == letter.Key() {
    287 			continue
    288 		}
    289 		// Find the angle between the two letters
    290 		t := getAngle(otherLetter.Center(), letter.Center())
    291 		if t < 0 {
    292 			t += 2 * math.Pi
    293 		}
    294 		if angle < 0 {
    295 			angle += 2 * math.Pi
    296 		}
    297 		angleDiff := math.Abs(angle - t)
    298 		if angleDiff < minAngle {
    299 			// Keep track of the letter with the smaller angle difference
    300 			minAngle = angleDiff
    301 			out = otherLetter
    302 		}
    303 	}
    304 	return
    305 }
    306 
    307 type LettersCache map[string]*Letter
    308 
    309 func (c LettersCache) toSlice() (out []*Letter) {
    310 	for _, v := range c {
    311 		out = append(out, v)
    312 	}
    313 	sort.Slice(out, func(i, j int) bool { return out[i].String() < out[j].String() })
    314 	return
    315 }
    316 
    317 // To find orientation of ordered triplet (p, q, r).
    318 // The function returns following values
    319 // 0 --> p, q and r are collinear
    320 // 1 --> Clockwise
    321 // 2 --> Counterclockwise
    322 func orientation(p, q, r image.Point) int {
    323 	// See http://www.geeksforgeeks.org/orientation-3-ordered-points/
    324 	// for details of below formula.
    325 	collinear := 0
    326 	clockwise := 1
    327 	counterclockwise := 2
    328 
    329 	val := (q.Y-p.Y)*(r.X-q.X) - (q.X-p.X)*(r.Y-q.Y)
    330 	fmt.Println("TEST", val)
    331 	if val == 0 { // collinear
    332 		return collinear
    333 	}
    334 	// clock or counterclockwise
    335 	if val > 0 {
    336 		return clockwise
    337 	}
    338 	return counterclockwise
    339 }
    340 
    341 // SolveDifficulty3 solve captcha for difficulty 3
    342 // For each pixel, verify if a match is found. If we do have a match,
    343 // verify that we have some "red" in it.
    344 // TODO: figure out how to get the right order
    345 //
    346 // Red circle is 17x17 (initial point)
    347 func SolveDifficulty3(img image.Image) (answer string, err error) {
    348 	gifImg, ok := img.(*image.Paletted)
    349 	if !ok {
    350 		return "", errors.New("invalid gif image")
    351 	}
    352 
    353 	imageWidth := 150
    354 	imageHeight := 200
    355 	letterSize := image.Point{X: 8, Y: 14}
    356 	minPxForLetter := 21
    357 	minStartingPtRedPx := 50
    358 
    359 	var starting *Letter             // Hold the starting letter
    360 	lettersMap := make(LettersCache) // Build a hashmap to quickly access letters
    361 
    362 	// Step1: Find all letters with red on the center
    363 	for y := 0; y < imageHeight; y++ {
    364 		for x := 0; x < imageWidth; x++ {
    365 			topLeftPt := image.Point{X: x, Y: y}
    366 			rect := image.Rectangle{Min: topLeftPt, Max: topLeftPt.Add(letterSize)}
    367 			letterImg := gifImg.SubImage(rect).(*image.Paletted)
    368 
    369 			// We know that minimum amount of pixels on to form a letter is 21
    370 			// We can skip squares that do not have this prerequisite
    371 			if countPxOn(letterImg) < minPxForLetter {
    372 				continue
    373 			}
    374 			// Check middle pixels for red, if no red pixels, we can ignore that square
    375 			if !hasRedInCenterArea(letterImg) {
    376 				continue
    377 			}
    378 
    379 			for _, c := range alphabet1 {
    380 				goodLetterImg, _ := getLetterImg(string(c))
    381 				if !imgContainsLetter(letterImg, goodLetterImg) {
    382 					continue
    383 				}
    384 
    385 				// "w" fits in "W". So if we find "W" 1 px bellow, discard "w"
    386 				if c == 'w' {
    387 					capitalWImg, _ := getLetterImg("W")
    388 					newPt := topLeftPt.Add(image.Point{X: 0, Y: 1})
    389 					rect := image.Rectangle{Min: newPt, Max: newPt.Add(letterSize)}
    390 					onePxDownImg := gifImg.SubImage(rect).(*image.Paletted)
    391 					if imgContainsLetter(onePxDownImg, capitalWImg) {
    392 						continue
    393 					}
    394 				} else if c == 'k' {
    395 					capitalKImg, _ := getLetterImg("K")
    396 					newPt := topLeftPt.Add(image.Point{X: 1, Y: 1})
    397 					rect := image.Rectangle{Min: newPt, Max: newPt.Add(letterSize)}
    398 					onePxUpImg := gifImg.SubImage(rect).(*image.Paletted)
    399 					if imgContainsLetter(onePxUpImg, capitalKImg) {
    400 						continue
    401 					}
    402 				}
    403 
    404 				letter := &Letter{Char: string(c), Rect: rect}
    405 				lettersMap[letter.Key()] = letter // Keep letters in hashmap for easy access
    406 				break
    407 			}
    408 		}
    409 	}
    410 
    411 	if len(lettersMap) != 5 {
    412 		return "", fmt.Errorf("did not find exactly 5 letters (%d)", len(lettersMap))
    413 	}
    414 
    415 	// Step2: Find the starting letter
    416 	for _, letter := range lettersMap.toSlice() {
    417 		p1 := letter.Rect.Min.Add(image.Point{X: -5, Y: -3})
    418 		p2 := letter.Rect.Max.Add(image.Point{X: 6, Y: 2})
    419 		rect := image.Rectangle{Min: p1, Max: p2}
    420 		square := gifImg.SubImage(rect).(*image.Paletted)
    421 
    422 		// Find starting point
    423 		redCount := countRedPx(square, p1)
    424 		if redCount > minStartingPtRedPx {
    425 			starting = letter
    426 			break
    427 		}
    428 	}
    429 
    430 	if starting == nil {
    431 		return "", errors.New("could not find starting letter")
    432 	}
    433 
    434 	code := ""
    435 	letter := starting
    436 	visited := make(map[string]bool)
    437 	for i := 0; i < 5; i++ {
    438 		if _, found := visited[letter.Key()]; found {
    439 			return "", errors.New("already visited node")
    440 		}
    441 		code += letter.Char
    442 		visited[letter.Key()] = true
    443 		if i == 4 {
    444 			break
    445 		}
    446 
    447 		p1 := letter.Rect.Min.Add(image.Point{X: -5, Y: -3})
    448 		p2 := letter.Rect.Max.Add(image.Point{X: 6, Y: 2})
    449 		rect := image.Rectangle{Min: p1, Max: p2}
    450 
    451 		retry := 0
    452 	Loop:
    453 		for {
    454 			retry++
    455 			square := gifImg.SubImage(rect).(*image.Paletted)
    456 			//SaveGif("char_"+letter.Key()+".gif", square)
    457 			redPxPts := getContourRedPixels(square)
    458 
    459 			if i == 0 {
    460 				if len(redPxPts) == 0 {
    461 					if retry < 10 {
    462 						rect.Min = rect.Min.Add(image.Point{X: -1, Y: -1})
    463 						rect.Max = rect.Max.Add(image.Point{X: 1, Y: 1})
    464 						continue
    465 					}
    466 					return "", fmt.Errorf("root %s has no line detected", letter)
    467 				}
    468 				if len(redPxPts) > 1 {
    469 					return "", fmt.Errorf("root %s has more than one line detected", letter)
    470 				}
    471 				redPt := redPxPts[0]
    472 				angle := getAngle(redPt, letter.Center())
    473 				neighbor := getLetterInDirection(letter, angle, lettersMap)
    474 				letter = neighbor
    475 				break
    476 			}
    477 
    478 			if len(redPxPts) == 0 {
    479 				if retry < 10 {
    480 					rect.Min = rect.Min.Add(image.Point{X: -1, Y: -1})
    481 					rect.Max = rect.Max.Add(image.Point{X: 1, Y: 1})
    482 					continue
    483 				}
    484 				return "", fmt.Errorf("letter #%d %s has no line detected", i+1, letter)
    485 			}
    486 			if len(redPxPts) == 1 {
    487 				if retry < 10 {
    488 					rect.Min = rect.Min.Add(image.Point{X: -1, Y: -1})
    489 					rect.Max = rect.Max.Add(image.Point{X: 1, Y: 1})
    490 					continue
    491 				}
    492 				return "", fmt.Errorf("letter #%d %s has only 1 line detected", i+1, letter)
    493 			}
    494 			if len(redPxPts) > 2 {
    495 				return "", fmt.Errorf("letter #%d %s has more than 2 lines detected", i+1, letter)
    496 			}
    497 			fstRedPt := redPxPts[0]
    498 			angle := getAngle(fstRedPt, letter.Center())
    499 			neighbor := getLetterInDirection(letter, angle, lettersMap)
    500 			if _, found := visited[neighbor.Key()]; found {
    501 				fstRedPt := redPxPts[1]
    502 				angle := getAngle(fstRedPt, letter.Center())
    503 				neighbor = getLetterInDirection(letter, angle, lettersMap)
    504 				if _, found := visited[neighbor.Key()]; found {
    505 					if i == 3 {
    506 						for _, l := range lettersMap.toSlice() {
    507 							if _, found := visited[l.Key()]; !found {
    508 								letter = l
    509 								break Loop
    510 							}
    511 						}
    512 					}
    513 					return "", fmt.Errorf("letter #%d %s all neighbors already visited", i+1, letter)
    514 				}
    515 			}
    516 			letter = neighbor
    517 			break
    518 		}
    519 	}
    520 
    521 	answer = code
    522 	return
    523 }
    524 
    525 // Given a base64 string, return the letter that match the gif
    526 func getCharByB64(b64 string) (string, error) {
    527 	b64 = strings.TrimPrefix(b64, b64Prefix)
    528 	if v, found := b64Map[b64]; found {
    529 		return v, nil
    530 	}
    531 	return "", errors.New("character not found")
    532 }
    533 
    534 // Given a letter (eg: "a") return the gif image for that letter
    535 func getLetterImg(letter string) (*image.Paletted, error) {
    536 	for k, v := range b64Map {
    537 		if v == letter {
    538 			goodLetter, _ := base64.URLEncoding.DecodeString(b64Prefix + k)
    539 			img, _ := gif.Decode(bytes.NewReader(goodLetter))
    540 			if palettedImg, ok := img.(*image.Paletted); ok {
    541 				return palettedImg, nil
    542 			}
    543 			break
    544 		}
    545 	}
    546 	return nil, errors.New("letter not found")
    547 }
    548 
    549 // SaveGif save an image on disk
    550 func SaveGif(filename string, img image.Image) {
    551 	f, _ := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0644)
    552 	_ = gif.Encode(f, img, nil)
    553 	_ = f.Close()
    554 }
    555 
    556 // GetAlphabetImg generate an image of all the characters
    557 func GetAlphabetImg() {
    558 	rect := image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: 5 + (62 * 9) + 5, Y: 8 + 14 + 8}}
    559 	newImg := image.NewPaletted(rect, color.Palette{onColor, offColor})
    560 	draw.Draw(newImg, newImg.Bounds(), &image.Uniform{C: color.Black}, image.Point{}, draw.Src)
    561 	letterSize := image.Point{X: 8, Y: 14}
    562 	cornerPt := image.Point{X: 5, Y: 8}
    563 	for _, c := range alphabet {
    564 		b64 := ""
    565 		for k, v := range b64Map {
    566 			if v == string(c) {
    567 				b64 = k
    568 				break
    569 			}
    570 		}
    571 		letterB64, _ := base64.URLEncoding.DecodeString(b64Prefix + b64)
    572 		letterImg, _ := gif.Decode(bytes.NewReader(letterB64))
    573 		r := image.Rectangle{Min: cornerPt, Max: cornerPt.Add(letterSize)}
    574 		draw.Draw(newImg, r, letterImg, image.Point{X: 0, Y: 0}, draw.Src)
    575 		cornerPt.X += letterSize.X + 1
    576 	}
    577 	SaveGif("alphabet.gif", newImg)
    578 }