Post

TypeBubbleX Devlog #08: Criando Gerenciador de imagem

TypeBubbleX Devlog #08: Criando Gerenciador de imagem

Introdução e Contexto

Hoje vou criar um Gerenciador de Imagem. No post anterior, eu tinha mencionado que faria o Gerenciador de Obras, mas enquanto eu estava desenvolvendo essa parte, percebi que precisava de uma infraestrutura sólida para lidar com as imagens primeiro.

O Gerenciador de Imagem que vou criar terá somente funções básicas, como carregar, salvar e verificar. Não terá um sistema de cache por enquanto, pois decidi focar no fluxo principal do app e só otimizar esse ponto quando o aplicativo começar a apresentar gargalos.

Então, vamos começar!

Código Comentado

O Gerenciador de Imagem vai funcionar como um Autoload (Singleton) na Godot, garantindo que qualquer tela do TypeBubbleX consiga ler ou gravar imagens de forma global.

Então, vamos partir para o código.

1
2
3
extends Node

var base_path : String : set = set_base_path

Aqui definimos a inicialização do nosso nó e criamos a variável base_path, que guardará o caminho absoluto do diretório de imagens no computador do usuário. Usamos um setter (set_base_path) personalizado para garantir um comportamento automatizado sempre que esse caminho for modificado pelo aplicativo.

1
2
3
4
5
func set_base_path(_base_path : String) -> void:
	base_path = _base_path
	
	if not DirAccess.dir_exists_absolute(base_path):
		DirAccess.make_dir_absolute(base_path)

É o setter da nossa variável de caminho. Sempre que definimos a pasta raiz de imagens gerenciada pelo app, o código faz uma checagem de segurança: se a pasta ainda não existir fisicamente no sistema operacional, o app se encarrega de criá-la na hora.

1
2
3
4
func _get_file_path(uuid : String) -> String:
	var prefix : String = uuid.substr(0, 2)
	var filename : String = uuid + ".png"
	return base_path.path_join(prefix).path_join(filename)

Esta é uma função utilitária vital para a organização do disco. Para evitar que uma única pasta acumule milhares de arquivos (o que degrada a performance do sistema operacional), pegamos os dois primeiros caracteres do UUID para criar uma subpasta (ex: ab/). Todos os arquivos internos do app são padronizados e salvos com a extensão .png. Futuramente, poderá ser adicionada uma configuração que permita ao usuário escolher a extensão desejada, mas, por enquanto, o padrão será exclusivamente .png.

1
2
3
4
5
6
func has_image(uuid : String) -> bool:
	if uuid.is_empty():
		return false
	
	var path : String = _get_file_path(uuid)
	return FileAccess.file_exists(path)

Uma função de validação. Usamos esse método para checar se o arquivo correspondente àquele UUID realmente existe em disco.

1
2
3
4
5
6
7
8
9
10
11
12
13
func load_from_uuid(uuid : String) -> Texture2D:
	var path : String = _get_file_path(uuid)
	
	if path.is_empty() or not FileAccess.file_exists(path):
		return null
		
	var img : Image = Image.new()
	var error : Error = img.load(path)
	
	if error == OK and not img.is_empty():
		return ImageTexture.create_from_image(img)
		
	return null

Responsável por carregar as imagens internas do aplicativo. Como o próprio TypeBubbleX controla e padroniza esses arquivos como PNGs legítimos na hora do salvamento, o método lê o arquivo diretamente do disco e o converte em uma Texture2D pronta para ser usada nos nós de interface da Godot.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func load_from_path(external_path : String) -> Texture2D:
	if external_path.is_empty() or not FileAccess.file_exists(external_path):
		return null
		
	var img : Image = Image.new()
	var error : Error = img.load(external_path)
	
	if error != OK:
		img = _recover_image_with_wrong_extension(external_path)
	
	if img and not img.is_empty():
		return ImageTexture.create_from_image(img)
		
	return null

Este é o ponto de entrada para arquivos externos (quando o usuário está importando um mangá/manhwa/manhua de fora). Como arquivos da internet frequentemente vêm com problemas no formato, se o carregamento padrão da Godot falhar por incompatibilidade, o código intercepta o erro e aciona nosso sistema de recuperação forçada.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
func _recover_image_with_wrong_extension(path : String) -> Image:
	var file : FileAccess = FileAccess.open(path, FileAccess.READ)
	if not file:
		return null
		
	var file_length : int = file.get_length()
	if file_length < 4:
		file.close()
		return null
		
	var header : PackedByteArray = file.get_buffer(12)
	var header_size : int = header.size()
	
	var is_png : bool = header_size >= 4 and header[0] == 0x89 and header[1] == 0x50 and header[2] == 0x4E and header[3] == 0x47
	var is_jpg : bool = header_size >= 3 and header[0] == 0xFF and header[1] == 0xD8 and header[2] == 0xFF
	var is_webp : bool = header_size >= 12 and \
				   header[0] == 0x52 and header[1] == 0x49 and header[2] == 0x46 and header[3] == 0x46 and \
				   header[8] == 0x57 and header[9] == 0x45 and header[10] == 0x42 and header[11] == 0x50

	if not (is_png or is_jpg or is_webp):
		file.close()
		return null
	
	file.seek(0)
	var buffer : PackedByteArray = file.get_buffer(file_length)
	file.close()
	
	var recovered_img := Image.new()
	var err := OK
	
	if is_png:
		err = recovered_img.load_png_from_buffer(buffer)
	elif is_jpg:
		err = recovered_img.load_jpg_from_buffer(buffer)
	elif is_webp:
		err = recovered_img.load_webp_from_buffer(buffer)
		
	if err == OK:
		return recovered_img
		
	return null

Uma função importante para o nosso gerenciador. Usuários costumam renomear arquivos alterando a extensão manualmente (ex: transformar um .jpg em .png mudando apenas o nome), o que quebra a engine. Esta função abre o arquivo de forma binária e lê os cabeçalhos brutos (Magic Numbers). Identificando a assinatura real da imagem (PNG, JPG ou WebP), nós ignoramos a extensão do arquivo e forçamos a Godot a decodificar o buffer de bytes com o interpretador correto.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func save_image(img : Image, uuid : String) -> bool:
	if uuid.is_empty() or not img or img.is_empty():
		return false
		
	var dest_path : String = _get_file_path(uuid)
	var dir_path : String = dest_path.get_base_dir()
	
	if not DirAccess.dir_exists_absolute(dir_path):
		var err : Error = DirAccess.make_dir_absolute(dir_path)
		if err != OK:
			return false
		
	var error : Error = img.save_png(dest_path)
	return error == OK

Salva a imagem recebida de forma definitiva no ecossistema do app. Ele descobre qual será a subpasta gerada pelo UUID, cria a estrutura de diretórios dinamicamente usando make_dir_absolute (caso a pasta de 2 dígitos ainda não exista) e exporta a imagem convertendo-a obrigatoriamente para um arquivo PNG limpo e padronizado.

1
2
3
4
5
6
7
8
func delete_image(uuid : String) -> bool:
	if not has_image(uuid):
		return false
		
	var path : String = _get_file_path(uuid)
	var error : Error = DirAccess.remove_absolute(path)
	
	return error == OK

O responsável pela limpeza. Quando deletamos uma obra ou uma página do nosso banco de dados, chamamos essa função para apagar o arquivo físico correspondente do armazenamento local, evitando que o workspace acumule arquivos órfãos inúteis (“lixo”) no computador do usuário.

Próximos Passos

Bem, é isso! Essa etapa de infraestrutura foi rápida, mas essencial para dar a flexibilidade que o nosso ecossistema precisa. Agora que o gerenciador de imagem está pronto, no próximo post vamos finalmente entrar de cabeça no desenvolvimento… do Gerenciador de Telas! (O de Obras vai ter que esperar mais um pouquinho).

Muito obrigado por acompanhar até aqui e nos vemos no próximo post!

Esta postagem está licenciada sob CC BY 4.0 pelo autor.