Генерируем Identicon в Go

Разработка /
Разработка: Генерируем Identicon в Go
В этой статье мы разработаем простой генератор идентиконов на языке Go. Исходный код вы можете взять здесь.

Но что такое идентикон? Думаю, все видели стандартную аватарку при регистрации на Github. Вот пример:
Разработка: Генерируем Identicon в Go
Вы видите визуальное представление значения хэш-функции. То есть с помощью специального алгоритма (md5 или sha256 и т.п.) захэшировано какое-то слово, при этом вывод этого алгоритма используется для генерации картинки. Поэтому идентикон будет всегда один и тот же при одинаковых входных данных.

Но хватит болтать, пора программировать!

Начнём с описания структуры идентикона:

type Identicon struct {
  name       string
  hash       [16]byte
  color      [3]byte
}

Структура имеет три поля:
  • name: Переданное имя для генерации идентикона.
  • hash: 16 байтовый массив, содержащий хэшированное значение имени, мы используем [16]byte из-за того, что наши хэширующие функции возвращают байтовый массив с 16 значениями.
  • color: 3 байтовый массив, содержащий цвета Red,Green,Blue

Итак, мы описали базовый контейнер для хранения значений. Остаётся слушать пользовательский ввод и хэшировать введённые значения.
Мы используем метод Sum из пакета «crypto/md5», этот метод возвращает [16]byte контрольную сумму от переданного параметра:

func hashInput(input []byte) Identicon {
    // генерируем контрольную сумму из ввода
    checkSum := md5.Sum(input)
    // возвращаем identicon
    return Identicon{
        name: string(input),
        hash: checkSum,
    }
}

Мы можем использовать метод так:

// конвертируем строку в массив байтов
data := []byte("bart")
// вызываем метод hashinput для этого массива байтов  
hashInput(data)
// на выходе: [245 65 70 163 252 130 171 23 229 38 86 149 178 63 100 107]

Теперь необходимо задать цвет идентикона, ведь мы хотим, чтобы он был всегда одинаковым при одном и том же вводе. Трюк в том, что мы возьмём первые 3 байта контрольной суммы и, соответственно, цвет всегда будет одинаковым. Первый будет отвечать за Красный (Red), второй за Зелёный (Green), а третий за Синий (Blue). Так у нас получится валидное значение RGB и мы сможем его использовать вместе с библиотекой color в go.

func pickColor(identicon Identicon) Identicon {
    // сначала создадим массив байтов, размером 3 элемента
    rgb := [3]byte{}
    // затем скопируем первые 3 элемента хэша в массив rgb
    copy(rgb[:], identicon.hash[:3])
    // затем присвоим его полю color 
    identicon.color = rgb
    // вернём изменённый identicon
    return identicon
}

Сейчас в нашем идентиконе есть имя, цвет и хэш. И нюанс в том, что его левая сторона зеркально копирует правую. Нам нужно создать сетку размером 5х5 и если мы возьмём наш массив байтов [245 65 70 163 252 130 171 23 229 38 86 149 178 63 100 107] для примера, то после создания сетки получим

[ 245, 65, 70, 65, 245,
  163, 252, 130, 252, 163,
  171, 23, 229, 23, 171,
  38, 86, 149, 86, 38,
  178, 63, 100, 63, 178 ]

Как видите, левая сторона массива зеркально отражает правую. Можно было бы использовать матрицу, но для нашего примера достаточно и простого массива. Добавим ещё одно поле в структуру идентикона, в котором будем хранить значение сетки.

type Identicon struct {
	name       string
	hash       [16]byte
	color      [3]byte
	grid       []byte // новое поле для сетки
}

func buildGrid(identicon Identicon) Identicon {
    // Создадим пустую сетку
    grid := []byte{}
    // Обходим в цикле весь хэш идентикона с шагом 3,
    // соответственно мы исключим ситуацию обращения за границами массива 
    // и получим 5 чанков по 3 элемента
    for i := 0; i < len(identicon.hash) && i+3 <= len(identicon.hash)-1; i += 3 { 
        // создадим пустой chunk
        chunk := make([]byte, 5)
        // Скопируем элементы из старого массива в новый
        copy(chunk, identicon.hash[i:i+3])
        chunk[3] = chunk[1] // зеркалируем второй элемент
        chunk[4] = chunk[0] // зеркалируем первый элемент
        grid = append(grid, chunk...) // добавляем chunk в сетку
    }
    identicon.grid = grid // заполняем поле grid в идентиконе
    return identicon // возвращаем изменённый identicon
}

Итак, наша сетка для рисования реальной картинки почти готова, но вначале определим какие её ячейки следует закрасить цветом.
Посмотрим ещё раз на получившуюся сетку — будет хорошим вариантом закрасить только нечётные ячейки. Для этого создадим ещё одну структуру, в которой будут значения и индекс элементов нашей сетки.

type GridPoint struct {
	value byte
	index int
}

Добавим её в структуру идентикона для удобства.

type Identicon struct {
	name       string
	hash       [16]byte
	color      [3]byte
	grid       []byte
        gridPoints []GridPoint // Отфильтрованные ячейки сетки
}

В GridPoints будут храниться значения, которые мы закрасим рассчитанным ранее цветом. Напишем метод фильтрации сетки:

func filterOddSquares(identicon Identicon) Identicon {
    grid := []GridPoint{} // создадим пустую сетку, будем заполнять её в цикле
    for i, code := range identicon.grid { // идём в цикле по нашей сетке 
	if code%2 == 0 { // проверяем - нечётное ли число
            // создадим новую Gridpoint, куда положим значение и индекс элементов
	    point := GridPoint{
		value: code,
		index: i,
	    }
                // добавим элемент к новой сетке
	    grid = append(grid, point)
	}
    }
    // присвоим значение 
    identicon.gridPoints = grid
    return identicon // возвращаем изменённый идентикон
}

Теперь в нашем идентиконе заполнены значения, которые нужно закрасить цветом, однако мы не можем нарисовать картинку только по этим значениям — мы должны преобразовать их в пиксельную карту. Так как мы хотим нарисовать прямоугольник, необходимо рассчитать верхнюю левую и нижнюю правую координаты для каждой нечётной точки, а затем по этим значениям нарисуем прямоугольник.

Создадим структуру для хранения размеров.

type Point struct {
    x, y int
}

type DrawingPoint struct {
    topLeft     Point
    bottomRight Point
}

Но как нам связать ячейки сетки с пикселями на пиксельной карте? Вначале определимся с размером изображения — оно будет размером 250 на 250 пикселей. Так как сетка у нас 5х5, то 1 ячейка сетки будет размером 50 на 50 пикселей, ибо 250/5 = 50.
Теперь рассчитаем границы по таким формулам:

  • горизонтальная: (x % 5) * 50
  • вертикальная: (x / 50) * 50
где x = индекс ячейки в сетке

Теперь посмотрим как с этими формулами мы можем рассчитать размеры нашего прямоугольника. Сперва изменим структуру идентикона и добавим поле pixelMap, в нём будут размеры для рисования в png.

type Identicon struct {
    name       string
    hash       [16]byte
    color      [3]byte
    grid       []byte
    gridPoints []GridPoint
    pixelMap   []DrawingPoint // пиксельная карта для рисования
}

И сохраним карту в структуре идентикона:

func buildPixelMap(identicon Identicon) Identicon {
    drawingPoints := []DrawingPoint{} 

    pixelFunc := func(p GridPoint) DrawingPoint {
        horizontal := (p.index % 5) * 50 
        vertical := (p.index / 5) * 50 
        topLeft := Point{horizontal, vertical} 
        bottomRight := Point{horizontal + 50, vertical + 50} 

        return DrawingPoint{ 
	    topLeft,
	    bottomRight,
        }
    }

    for _, gridPoint := range identicon.gridPoints {
        drawingPoints = append(drawingPoints, pixelFunc(gridPoint))
    }
    identicon.pixelMap = drawingPoints
    return identicon
}

Для рисования прямоугольника нужно взять 4 размера из структуры DrawingPoint и передать их функции рисования. Для отрисовки прямоугольников мы будем использовать внешнюю библиотеку draw2dRepo — рисовать прямоугольник с ней намного легче. Мы передадим нашей функции рисования изображение, цвет и размеры, а она создаст готовый прямоугольник для нас.

func rect(img *image.RGBA, col color.Color, x1, y1, x2, y2 float64) {
    gc := draw2dimg.NewGraphicContext(img) // подготовим новый контекст изображения
    gc.SetFillColor(col) // зададим цвет
    gc.MoveTo(x1, y1) // перенесёмся в левый верхний край картинки 
    // нарисуем габаритные линии
    gc.LineTo(x1, y1) 
    gc.LineTo(x1, y2)
    gc.MoveTo(x2, y1) // передвинемся вправый край изображения
    // нарисуем габаритные линии
    gc.LineTo(x2, y1)
    gc.LineTo(x2, y2)
    // сделаем ширину линии в ноль
    gc.SetLineWidth(0)
    // Заполним ячейку
    gc.FillStroke()
}

Итак, рисовать разукрашенные прямоугольники мы умеем. Осталось пройтись по всей карте пикселей и на каждом шаге вызывать эту функцию:

func drawRectangle(identicon Identicon) error {
    // создадим пустую картинку размером 250x250 
    var img = image.NewRGBA(image.Rect(0, 0, 250, 250))
    // получим цвет из соответствующего поля идентикона
    col := color.RGBA{identicon.color[0], identicon.color[1], identicon.color[2], 255}

    // обойдём в цикле pixelmap вызывая функцию rect с передачей ей картинки, цвета и размеров
    for _, pixel := range identicon.pixelMap {
	rect(
            img, 
            col, 
            float64(pixel.topLeft.x), 
            float64(pixel.topLeft.y), 
            float64(pixel.bottomRight.x), 
            float64(pixel.bottomRight.y)
        )
    }
    // Сохраним изображение на диск
    return draw2dimg.SaveToPngFile(identicon.name+".png", img)
}

Готово! Генератор идентикона сделан, осталось одна вещь. Вы видели, что большинство наших функций изменяют идентикон и возвращают идентикон. Это хорошая работа для конвейерной функции. Сделаем её набросок:

type Apply func(Identicon) Identicon

И определим функцию, принимающую x количество типов Apply и применяющих их к идентикону:

func pipe(identicon Identicon, funcs ...Apply) Identicon {
    for _, applyer := range funcs {
	identicon = applyer(identicon)
    }
    return identicon
}

Теперь пропишем весь функционал в методе main() и создадим флаг определения ввода имени в функцию:

func main() {
    var (
	name = flag.String("name", "", "Set the name where you want to generate an Identicon for")
    )
    flag.Parse()

    if *name == "" {
	flag.Usage()
	os.Exit(0)
    }

    data := []byte(*name)
    identicon := hashInput(data)

    // Передадим идентикон, вызывая методы для его обработки
    identicon = pipe(identicon, pickColor, buildGrid, filterOddSquares, buildPixelMap)

    // Передадим идентикон в функцию drawRectangle 
    if err := drawRectangle(identicon); err != nil {
	log.Fatalln(err)
    }
}

Выполнив этот код с именем «bart» мы получим:
Разработка: Генерируем Identicon в Go
Поздравляем, вы создали генератор идентиконов!

Источник: «Tutorial: Identicon generator in Go» by Bart Fokker
0 комментариев
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.