ferramenta de text pot importar gifs

ferramenta de text accepta separació entre quadricules de lletres
This commit is contained in:
2026-03-22 19:06:01 +01:00
parent 0c116665bc
commit 94684e8758
6 changed files with 328 additions and 181 deletions

View File

@@ -1,135 +1,134 @@
# Font: aseprite # Font: aseprite — generado desde aseprite_font.gif
# Formato: codepoint_decimal ancho_visual # Generado con tools/font_gen/font_gen.py
# Los gliphos se listan en orden de aparición en el bitmap (izquierda→derecha, arriba→abajo)
box_width 8 box_width 10
box_height 8 box_height 7
columns 15 columns 16
cell_spacing 1
row_spacing 4
# ASCII 32-126 # codepoint_decimal ancho_visual
32 3 32 3 # U+0020
33 1 33 1 # !
34 3 34 3 # "
35 3 35 5 # #
36 4 36 4 # $
37 5 37 5 # %
38 5 38 5 # &
39 2 39 2 # '
40 2 40 2 # (
41 2 41 2 # )
42 5 42 5 # *
43 5 43 5 # +
44 3 44 2 # ,
45 3 45 3 # -
46 1 46 1 # .
47 4 47 3 # /
48 4 48 4 # 0
49 2 49 2 # 1
50 4 50 4 # 2
51 4 51 4 # 3
52 4 52 4 # 4
53 4 53 4 # 5
54 4 54 4 # 6
55 4 55 4 # 7
56 4 56 4 # 8
57 4 57 4 # 9
58 1 58 1 # :
59 1 59 2 # ;
60 3 60 3 # <
61 4 61 4 # =
62 4 62 3 # >
63 4 63 4 # ?
64 7 64 8 # @
65 4 65 4 # A
66 4 66 4 # B
67 4 67 4 # C
68 4 68 4 # D
69 4 69 4 # E
70 4 70 4 # F
71 4 71 4 # G
72 4 72 4 # H
73 2 73 1 # I
74 2 74 2 # J
75 4 75 4 # K
76 4 76 4 # L
77 5 77 5 # M
78 4 78 4 # N
79 5 79 5 # O
80 4 80 4 # P
81 5 81 5 # Q
82 4 82 4 # R
83 4 83 4 # S
84 5 84 5 # T
85 4 85 4 # U
86 5 86 5 # V
87 7 87 7 # W
88 5 88 5 # X
89 5 89 5 # Y
90 4 90 4 # Z
91 2 91 2 # [
92 3 92 3 # \
93 2 93 2 # ]
94 5 94 5 # ^
95 5 95 5 # _
96 3 96 3 # `
97 4 97 4 # a
98 4 98 4 # b
99 4 99 4 # c
100 4 100 4 # d
101 4 101 4 # e
102 2 102 2 # f
103 4 103 4 # g
104 4 104 4 # h
105 1 105 1 # i
106 2 106 2 # j
107 4 107 4 # k
108 1 108 1 # l
109 7 109 7 # m
110 4 110 4 # n
111 4 111 4 # o
112 4 112 4 # p
113 4 113 4 # q
114 3 114 3 # r
115 3 115 3 # s
116 2 116 2 # t
117 4 117 4 # u
118 4 118 4 # v
119 5 119 5 # w
120 5 120 5 # x
121 4 121 4 # y
122 4 122 4 # z
123 3 123 3 # {
124 3 124 1 # |
125 3 125 3 # }
126 5 126 4 # ~
192 5 # À
# Extensiones para ES/CA/VA (descomentar tras añadirlos al bitmap) 193 5 # Á
# 192 4 # À 200 5 # È
# 193 4 # Á 201 5 # É
# 200 4 # È 205 5 # Í
# 201 4 # É 207 5 # Ï
# 205 2 # Í 210 5 # Ò
# 207 2 # Ï 211 5 # Ó
# 210 5 # Ò 218 5 # Ú
# 211 5 # Ó 220 5 # Ü
# 218 4 # Ú 209 5 # Ñ
# 220 4 # Ü 199 5 # Ç
# 209 4 # Ñ 224 5 # à
# 199 4 # Ç 225 5 # á
# 224 4 # à 232 5 # è
# 225 4 # á 233 5 # é
# 232 4 # è 237 5 # í
# 233 4 # é 239 5 # ï
# 237 1 # í 242 5 # ò
# 239 2 # ï 243 5 # ó
# 242 4 # ò 250 5 # ú
# 243 4 # ó 252 5 # ü
# 250 4 # ú 241 5 # ñ
# 252 4 # ü 231 5 # ç
# 241 4 # ñ 161 5 # ¡
# 231 4 # ç 191 5 # ¿
# 161 1 # ¡ 171 5 # «
# 191 4 # ¿ 187 5 # »
# 171 5 # « 183 5 # ·
# 187 5 # »
# 183 1 # · (punt volat)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 640 B

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -59,9 +59,9 @@ ending2:
credits: credits:
instructions: "INSTRUCCIONS:" instructions: "INSTRUCCIONS:"
l0: "AJUDA EL JAILDOC A RECUPERAR" l0: "AJUDA A JAILDOC A RECUPERAR"
l1: "ELS SEUS PROJECTES I ARRIBAR A" l1: "ELS SEUS PROJECTES I ARRIBAR"
l2: "LA JAIL PER ACABAR-LOS" l2: "A LA JAIL PER ACABAR-LOS"
keys: "TECLES:" keys: "TECLES:"
keys_move: "CURSORS PER A MOURE I SALTAR" keys_move: "CURSORS PER A MOURE I SALTAR"
f8: "F8 ACTIVAR/DESACTIVAR MÚSICA" f8: "F8 ACTIVAR/DESACTIVAR MÚSICA"
@@ -119,7 +119,7 @@ ui:
scoreboard: scoreboard:
items: "TRESORS PILLATS " items: "TRESORS PILLATS "
time: " TEMPS " time: " HORA "
rooms: "SALES" rooms: "SALES"
game: game:

View File

@@ -86,6 +86,10 @@ auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr<File> {
ls >> tf->box_height; ls >> tf->box_height;
} else if (key == "columns") { } else if (key == "columns") {
ls >> tf->columns; ls >> tf->columns;
} else if (key == "cell_spacing") {
ls >> tf->cell_spacing;
} else if (key == "row_spacing") {
ls >> tf->row_spacing;
} else { } else {
// Línea de glifo: codepoint_decimal ancho_visual // Línea de glifo: codepoint_decimal ancho_visual
uint32_t codepoint = 0; uint32_t codepoint = 0;
@@ -97,8 +101,9 @@ auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr<File> {
continue; // línea mal formateada, ignorar continue; // línea mal formateada, ignorar
} }
Offset off{}; Offset off{};
off.x = (glyph_index % tf->columns) * tf->box_width; const int row_sp = tf->row_spacing > 0 ? tf->row_spacing : tf->cell_spacing;
off.y = (glyph_index / tf->columns) * tf->box_height; off.x = (glyph_index % tf->columns) * (tf->box_width + tf->cell_spacing) + tf->cell_spacing;
off.y = (glyph_index / tf->columns) * (tf->box_height + row_sp) + tf->cell_spacing;
off.w = width; off.w = width;
tf->offset[codepoint] = off; tf->offset[codepoint] = off;
++glyph_index; ++glyph_index;

View File

@@ -18,9 +18,11 @@ class Text {
}; };
struct File { struct File {
int box_width{0}; // Anchura de la caja de cada caracter en el png int box_width{0}; // Anchura de la caja de cada caracter en el png
int box_height{0}; // Altura de la caja de cada caracter en el png int box_height{0}; // Altura de la caja de cada caracter en el png
int columns{16}; // Número de columnas en el bitmap int columns{16}; // Número de columnas en el bitmap
int cell_spacing{0}; // Píxeles de separación entre columnas (y borde izquierdo/superior)
int row_spacing{0}; // Píxeles de separación entre filas (si difiere de cell_spacing)
std::unordered_map<uint32_t, Offset> offset; // Posición y ancho de cada glifo (clave: codepoint Unicode) std::unordered_map<uint32_t, Offset> offset; // Posición y ancho de cada glifo (clave: codepoint Unicode)
}; };

View File

@@ -1,24 +1,30 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Generador de fuentes bitmap para JailDoctor's Dilemma. """Generador de fuentes bitmap para JailDoctor's Dilemma.
Convierte un archivo .ttf en un GIF indexado + fichero .fnt compatibles Convierte un archivo .ttf (o un GIF existente) en un GIF indexado + fichero .fnt
con el sistema de texto del juego. compatibles con el sistema de texto del juego.
Dependencias: pip install Pillow Dependencias: pip install Pillow
Uso: Uso (desde TTF):
python3 font_gen.py --ttf myfont.ttf --size 8 --output myfont python3 font_gen.py --ttf myfont.ttf --size 8 --output myfont
python3 font_gen.py --ttf myfont.ttf --size 8 --output myfont --dir data/font --box-width 8 python3 font_gen.py --ttf myfont.ttf --size 8 --output myfont --dir data/font --box-width 8
Uso (desde GIF existente con cuadrícula):
python3 font_gen.py --gif myfont.gif --output myfont --columns 16 --cell-spacing 1
Notas: Notas:
- Para fuentes bitmap (pixel fonts) en TTF, usa el tamaño exacto del bitmap strike. - Para fuentes bitmap (pixel fonts) en TTF, usa el tamaño exacto del bitmap strike.
- Los glifos se almacenan como índice de paleta 1 (blanco) sobre fondo transparente (índice 0). - Los glifos se almacenan como índice de paleta 1 (blanco) sobre fondo transparente (índice 0).
- Esto es compatible con SurfaceSprite::render(1, color) del motor del juego. - Esto es compatible con SurfaceSprite::render(1, color) del motor del juego.
- Los caracteres no incluidos en la fuente aparecerán como celdas vacías en el GIF. - Los caracteres no incluidos en la fuente aparecerán como celdas vacías en el GIF.
- El modo --gif acepta un GIF cuya cuadrícula siga el orden de ALL_CHARS.
Celdas vacías (todos los píxeles = índice 0) se marcan como no soportadas.
""" """
import argparse import argparse
import os import os
import shutil
import sys import sys
from math import ceil from math import ceil
@@ -35,6 +41,124 @@ _EXTENDED_CHARS = list("ÀÁÈÉÍÏÒÓÚÜÑÇàáèéíïòóúüñç¡¿«»
ALL_CHARS = _ASCII_CHARS + _EXTENDED_CHARS # 124 total ALL_CHARS = _ASCII_CHARS + _EXTENDED_CHARS # 124 total
def _write_fnt(
output_fnt: str,
output_name: str,
source_name: str,
box_width: int,
box_height: int,
columns: int,
cell_spacing: int,
row_spacing: int,
chars: list[str],
char_widths: dict[str, int],
) -> None:
"""Escribe el fichero .fnt."""
with open(output_fnt, "w", encoding="utf-8") as f:
f.write(f"# Font: {output_name} — generado desde {source_name}\n")
f.write(f"# Generado con tools/font_gen/font_gen.py\n\n")
f.write(f"box_width {box_width}\n")
f.write(f"box_height {box_height}\n")
f.write(f"columns {columns}\n")
if cell_spacing:
f.write(f"cell_spacing {cell_spacing}\n")
if row_spacing and row_spacing != cell_spacing:
f.write(f"row_spacing {row_spacing}\n")
f.write("\n# codepoint_decimal ancho_visual\n")
for ch in chars:
cp = ord(ch)
w = char_widths[ch]
name = ch if ch.isprintable() and not ch.isspace() else f"U+{cp:04X}"
f.write(f"{cp} {w} # {name}\n")
def render_gif_font(
gif_path: str,
output_name: str,
output_dir: str,
columns: int,
box_width: int,
box_height: int,
cell_spacing: int,
row_spacing: int,
) -> None:
"""Genera el .fnt a partir de un GIF de fuente existente con cuadrícula.
Fórmula de posición de cada celda (col, row):
x0 = col * (box_width + cell_spacing) + cell_spacing
y0 = row * (box_height + row_spacing) + cell_spacing
"""
if not os.path.isfile(gif_path):
print(f"Error: No se encuentra el archivo GIF: {gif_path}", file=sys.stderr)
sys.exit(1)
os.makedirs(output_dir, exist_ok=True)
output_gif = os.path.join(output_dir, f"{output_name}.gif")
output_fnt = os.path.join(output_dir, f"{output_name}.fnt")
img = Image.open(gif_path)
if img.mode != "P":
img = img.convert("P")
img_w, img_h = img.size
pixels = img.load()
num_rows = ceil(len(ALL_CHARS) / columns)
stride_x = box_width + cell_spacing
stride_y = box_height + row_spacing
print(f"GIF fuente: {os.path.basename(gif_path)} ({img_w}×{img_h} px)")
print(f"Caja : {box_width}×{box_height} px | {columns} cols, {num_rows} filas | spacing x={cell_spacing} y={row_spacing}")
char_widths: dict[str, int] = {}
supported_chars: list[str] = []
skipped: list[str] = []
for i, ch in enumerate(ALL_CHARS):
col = i % columns
row = i // columns
x0 = col * stride_x + cell_spacing
y0 = row * stride_y + cell_spacing
# Comprobar límites (el GIF puede tener más o menos filas que ALL_CHARS)
if y0 + box_height > img_h or x0 + box_width > img_w:
skipped.append(ch)
continue
# Celda vacía (todos los píxeles = índice 0)
if all(pixels[x0 + px, y0 + py] == 0 for py in range(box_height) for px in range(box_width)):
if ch.isspace():
# El espacio no tiene píxeles visibles: asignar ancho por defecto
char_widths[ch] = max(1, box_width // 2)
supported_chars.append(ch)
else:
skipped.append(ch)
continue
# Medir ancho visual: última columna con algún píxel no-fondo
pixel_width = 0
for px in range(box_width - 1, -1, -1):
if any(pixels[x0 + px, y0 + py] != 0 for py in range(box_height)):
pixel_width = px + 1
break
char_widths[ch] = max(1, pixel_width)
supported_chars.append(ch)
if skipped:
names = "".join(c if c.isprintable() and not c.isspace() else f"[U+{ord(c):04X}]" for c in skipped)
print(f"Omitidos : {len(skipped)} chars vacíos/fuera de rango: {names}")
print(f"Soportados: {len(supported_chars)} caracteres")
# Copiar el GIF al directorio de salida
shutil.copy2(gif_path, output_gif)
print(f"GIF : {output_gif}")
_write_fnt(output_fnt, output_name, os.path.basename(gif_path),
box_width, box_height, columns, cell_spacing, row_spacing, supported_chars, char_widths)
print(f"FNT : {output_fnt}")
def render_font( def render_font(
ttf_path: str, ttf_path: str,
size: int, size: int,
@@ -176,21 +300,8 @@ def render_font(
pass # si magick no está disponible se queda como GIF87a de 256 colores pass # si magick no está disponible se queda como GIF87a de 256 colores
print(f"GIF : {output_gif}") print(f"GIF : {output_gif}")
# --- Generar fichero .fnt --- _write_fnt(output_fnt, output_name, f"{os.path.basename(ttf_path)} size {size}",
ttf_name = os.path.basename(ttf_path) box_width, box_height, columns, 0, 0, chars_to_render, char_widths)
with open(output_fnt, "w", encoding="utf-8") as f:
f.write(f"# Font: {output_name} — generado desde {ttf_name} size {size}\n")
f.write(f"# Generado con tools/font_gen/font_gen.py\n\n")
f.write(f"box_width {box_width}\n")
f.write(f"box_height {box_height}\n")
f.write(f"columns {columns}\n\n")
f.write("# codepoint_decimal ancho_visual\n")
for ch in chars_to_render:
cp = ord(ch)
w = char_widths[ch]
name = ch if ch.isprintable() and not ch.isspace() else f"U+{cp:04X}"
f.write(f"{cp} {w} # {name}\n")
print(f"FNT : {output_fnt}") print(f"FNT : {output_fnt}")
@@ -201,41 +312,71 @@ def main() -> None:
default_dir = os.path.join(project_dir, "data", "font") default_dir = os.path.join(project_dir, "data", "font")
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Genera fuentes bitmap (.gif + .fnt) desde un archivo .ttf.", description="Genera fuentes bitmap (.gif + .fnt) desde un .ttf o un GIF existente.",
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=""" epilog="""
Ejemplos: Ejemplos (desde TTF):
python3 font_gen.py --ttf myfont.ttf --size 8 --output myfont python3 font_gen.py --ttf myfont.ttf --size 8 --output myfont
python3 font_gen.py --ttf myfont.ttf --size 8 --output myfont --box-width 8 python3 font_gen.py --ttf myfont.ttf --size 8 --output myfont --box-width 8
python3 font_gen.py --ttf myfont.ttf --size 16 --output myfont --dir data/font python3 font_gen.py --ttf myfont.ttf --size 16 --output myfont --dir data/font
Ejemplos (desde GIF con cuadrícula):
python3 font_gen.py --gif myfont.gif --output myfont --columns 16 --box-width 10 --box-height 7 --cell-spacing 1
python3 font_gen.py --gif myfont.gif --output myfont --columns 16 --box-width 10 --box-height 7 --cell-spacing 1 --row-spacing 4
Notas: Notas:
- Para pixel fonts (.ttf bitmap), usa el tamaño exacto del bitmap strike. - Para pixel fonts (.ttf bitmap), usa el tamaño exacto del bitmap strike.
- El GIF resultante usa índice 1 = glifo, índice 0 = transparente. - El GIF resultante usa índice 1 = glifo, índice 0 = transparente.
- Se generan 124 caracteres: ASCII 32-126 + extensiones ES/CA/VA. - Se procesan 124 caracteres: ASCII 32-126 + extensiones ES/CA/VA.
- Usa --box-width para forzar una anchura de celda fija (útil para fuentes - En modo --gif, --box-width y --box-height son obligatorios.
cuadradas donde la celda coincide con box_height). - Las celdas completamente vacías (índice 0) se marcan como no soportadas.
- cell_spacing = spacing horizontal entre columnas (y borde izquierdo/superior).
- row_spacing = spacing vertical entre filas (si difiere de cell_spacing).
""", """,
) )
parser.add_argument("--ttf", required=True, help="Ruta al archivo .ttf")
parser.add_argument("--size", required=True, type=int, help="Tamaño en píxeles") # Fuente: TTF o GIF (mutuamente excluyentes)
parser.add_argument("--output", required=True, help="Nombre base de salida (sin extensión)") source_group = parser.add_mutually_exclusive_group(required=True)
parser.add_argument("--dir", default=default_dir, help="Directorio de salida (default: data/font/)") source_group.add_argument("--ttf", help="Ruta al archivo .ttf")
parser.add_argument("--columns", default=15, type=int, help="Columnas en el bitmap (default: 15)") source_group.add_argument("--gif", help="Ruta a un GIF de fuente existente con cuadrícula")
parser.add_argument("--box-width", default=None, type=int, help="Anchura fija de celda en px (default: auto)")
parser.add_argument("--box-height", default=None, type=int, help="Altura fija de celda en px (default: ascent + abs(descent))") parser.add_argument("--size", type=int, help="Tamaño en píxeles (solo con --ttf)")
parser.add_argument("--output", required=True, help="Nombre base de salida (sin extensión)")
parser.add_argument("--dir", default=default_dir, help="Directorio de salida (default: data/font/)")
parser.add_argument("--columns", default=15, type=int, help="Columnas en el bitmap (default: 15)")
parser.add_argument("--box-width", default=None, type=int, help="Anchura fija de celda en px (default: auto)")
parser.add_argument("--box-height", default=None, type=int, help="Altura fija de celda en px (default: auto)")
parser.add_argument("--cell-spacing", default=0, type=int, help="Píxeles de separación entre columnas, y borde (default: 0)")
parser.add_argument("--row-spacing", default=None, type=int, help="Píxeles de separación entre filas (default: igual a --cell-spacing)")
args = parser.parse_args() args = parser.parse_args()
render_font( if args.gif:
ttf_path = args.ttf, if args.box_width is None or args.box_height is None:
size = args.size, parser.error("--box-width y --box-height son obligatorios con --gif")
output_name = args.output, row_sp = args.row_spacing if args.row_spacing is not None else args.cell_spacing
output_dir = args.dir, render_gif_font(
columns = args.columns, gif_path = args.gif,
box_width_override = args.box_width, output_name = args.output,
box_height_override = args.box_height, output_dir = args.dir,
) columns = args.columns,
box_width = args.box_width,
box_height = args.box_height,
cell_spacing = args.cell_spacing,
row_spacing = row_sp,
)
else:
if args.size is None:
parser.error("--size es obligatorio cuando se usa --ttf")
render_font(
ttf_path = args.ttf,
size = args.size,
output_name = args.output,
output_dir = args.dir,
columns = args.columns,
box_width_override = args.box_width,
box_height_override = args.box_height,
)
if __name__ == "__main__": if __name__ == "__main__":