Go transformando imagem PNG em código C

Um pequeno utilitário escrito em Go que transforma imagens PNG em C raw

Já tem algum tempo eu estava desenvolvendo um projeto usando a DISCO-F746NG, uma placa bem interessante e com muitos recursos. Um dos recursos mais legais é o LCD-TFT com 4.3” e multi-touch. E a programação da placa é feita usando mbed que é um ambiente bastante bom apesar de eu torcer o nariz para ele.

Apesar de muitas qualidades essa placa não tem recursos muito avançados para tratamento de imagem via hardware, para desenhar você simplesmente escreve na memória de vídeo… no bom e velho estilo dos anos 90 gravando pixel a pixel :D

Até existem algumas funções para desenho de imagens mas não por hardware, todas elas fazem a mesma coisa ou seja ler de um arquivo, decodificar e copiar para a memória de vídeo. Então para não ter que me preocupar com ler arquivos de nenhum outro lugar (que sempre pode falhar) eu preferi incluir as imagens dento do meu próprio executável já no formato da memória de vídeo, assim alem de mais seguro é mais rápido.

Inicialmente eu ia escrever uma nova versão do velho bin2c, um utilitário ancestral que converte arquivos binários em código C. Mas como eu teria que ler um arquivo PNG e não queria nem usar nenhuma lib externa nem procurar eu mesmo como decodificar os cabeçalhos do PNG achei que seria uma ótima oportunidade para brincar com imagens usando Go.

png2c

A ideia é bem simples, ler o arquivo PMG, pegar as dimensões da imagem e gerar um arquivo C com as dimensões e dois vetores, um contendo as cores no formato ARGB e outro contendo os pixels referenciando cada cor do vetor de cores. Esse formato é interessante porque comprime um pouco a imagem e o resultado final é menor, entretanto isso limita a 256 cores por imagem, o que não é exatamente um problema.

space invader

Criei uma versão minúscula do monstro clássico do space invaders invaders.png com apenas 8x11 pixels para demonstrar a saída do programa.

./png2c -file=invader.png

A saída são algumas constantes como largura, altura e o numero de cores e os dois vetores, um com as cores e outro com os pixels.

const uint32_t invader_png_width = 11;
const uint32_t invader_png_height = 8;
const uint32_t invader_png_ncolors = 2;
const uint32_t invader_png_color_index[] = {
	0xFFFFFFFF,
	0xFF000000
};
const uint8_t invader_png_bitmap[] = {
	0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01,
	0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,
	0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01,
	0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00,
	0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00,
	0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,
	0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x01, 0x01, 0x01, 0x00
};

E para gerar um arquivo é só redirecionar a saída padrão.

./png2c -file=invader.png > invaders.h

Para evitar colisão e ficar fácil de referenciar as constantes eu usei o nome das imagens e extensão como sufixo das constantes e vetores, para tratar a string do nome do arquivo eu usei strings.Replace da seguinte forma:

var definePrefix = strings.Replace(fileName, ".", "_", -1)
definePrefix = strings.Replace(definePrefix, "/", "_", -1)

Eu não uso nomes de arquivos com espaços nem nada muito estranho quando se trata de recursos de algum projeto mas talvez seja interessante melhorar essa limpeza para ficar mais genérica e segura.

Um truque importante é antes de começar a ler a imagem pixel a pixel é retornar o ponteiro de leitura para o primeiro byte, isso porque DecodeConfig pode ter deixado o ponteiro em um lugar desconhecido da imagem.

image.DecodeConfig(imgfile)
.
.
.
imgfile.Seek(0, 0)

Então é só uma questão de decodificar a imagem ler cada um dos pixels em um loop até acabar e já aproveito para colocar no formato ARGB. Conforme vou lendo eu coloco as cores em um map que chamei de colorList, se a cor já existir no map eu uso o índice da que já existe, caso contrario eu adiciono ao map.

img, _, err := image.Decode(imgfile)
.
.
.
r, g, b, a := img.At(x, y).RGBA()
argb := fmt.Sprintf("0x%02X%02X%02X%02X",
    uint8(a),
    uint8(r),
    uint8(g),
    uint8(b))

No final eu jogo o resultado na saída padrão, prefiro trabalhar assim que gerar um arquivo diretamente.

E agora o código completo (que também esta no GitHub).

package main

import (
	"flag"
	"fmt"
	"image"
	"image/png"
	"os"
	"strings"
)

func init() {
	image.RegisterFormat("png", "png", png.Decode, png.DecodeConfig)
}

func png2c(fileName string) {
	var colorList = make(map[string]int)
	var colorCount = 0
	var bitmapArray = "\t"

	var definePrefix = strings.Replace(fileName, ".", "_", -1)
	definePrefix = strings.Replace(definePrefix, "/", "_", -1)

	imgfile, err := os.Open(fileName)

	if err != nil {
		fmt.Fprintf(os.Stderr, "%v file not found!\n", fileName)
		os.Exit(1)
	}

	defer imgfile.Close()

	imgCfg, _, err := image.DecodeConfig(imgfile)
	if err != nil {
		fmt.Fprintf(os.Stderr, "%v\n", err)
		os.Exit(1)
	}

	width := imgCfg.Width
	height := imgCfg.Height

	imgfile.Seek(0, 0)

	img, _, err := image.Decode(imgfile)
	if err != nil {
		fmt.Printf("error trying to decode %v. %v!\n", fileName, err)
		os.Exit(1)
	}

	var lineCount = 6
	for x := 0; x < width; x++ {
		for y := 0; y < height; y++ {
			r, g, b, a := img.At(x, y).RGBA()
			argb := fmt.Sprintf("0x%02X%02X%02X%02X",
				uint8(a),
				uint8(r),
				uint8(g),
				uint8(b))

			var patern = "0x%02X, "
			if y == height-1 && x == width-1 {
				patern = "0x%02X"
			}

			if _, ok := colorList[argb]; ok {
				//fmt.Println("element found")
				bitmapArray += fmt.Sprintf(patern, colorList[argb])
			} else {
				colorList[argb] = colorCount
				bitmapArray += fmt.Sprintf(patern, colorCount)
				colorCount++
				//fmt.Println("element not found")
			}

			if lineCount > 70 {
				bitmapArray += "\n\t"
				lineCount = 0
			}

			lineCount += 6
			//fmt.Printf("[%d:%d] %s\n", x, y, argb)
		}
	}

	ncolors := len(colorList)

	fmt.Printf("const uint32_t %s_width = %d;\n", definePrefix, width)
	fmt.Printf("const uint32_t %s_height = %d;\n", definePrefix, height)
	fmt.Printf("const uint32_t %s_ncolors = %d;\n", definePrefix, ncolors)
	fmt.Printf("const uint32_t %s_color_index[] = { \n", definePrefix)

	i := 0
	for k := range colorList {
		i++
		if i == ncolors {
			fmt.Printf("\t%s\n", k)
		} else {
			fmt.Printf("\t%s,\n", k)
		}
	}

	fmt.Printf("};\n")

	fmt.Printf("const uint8_t %s_bitmap[] = {\n", definePrefix)
	fmt.Printf(bitmapArray)
	fmt.Printf("\n};\n")
}

func main() {
	var file string

	flag.StringVar(&file, "file", "", "filename.png")
	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, "usage: png2c -file=filename.png\n")
		flag.PrintDefaults()
		os.Exit(1)
	}

	flag.Parse()

	if file == "" {
		flag.Usage()
	}

	png2c(file)
}

Desenhando no LCD da F746NG

Para o vetor gerado pelo png2c e desenhar no LCD-TFT é basicamente um loop dentro de outro consumindo os vetores e gravando na memória de vídeo.

void drawImg(const int x,
             const int y,
             const int width,
             const int height,
             const int ncolors,
             const uint32_t color_index[],
             const uint8_t bitmap[],
             LCD_DISCO_F746NG &lcd) {

    int index = 0;
    int width_aux = x + width;
    int height_aux = y + height;
    int x_aux = x;
    int y_aux;

    for(;x_aux<width_aux;x_aux++) {
        y_aux = y;
        for(;y_aux<height_aux;y_aux++) {
            lcd.DrawPixel(x_aux, y_aux, color_index[bitmap[index]]);
            index++;
        }
    }
}

Apesar de ser um modo arcaico de trabalhar com vídeo funciona bem para criar interfaces e criar painéis de controle bem arrojados e nos meus testes mesmo fazendo redesenho da tela toda a velocidade foi aceitável. Claro que não serve para jogos, mas isso é mais um problema do LCD-TFT que sempre vai gerar sombras quando tentar criar sprites e mover eles pela tela.

comments powered by Disqus