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

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

View File

@@ -86,6 +86,10 @@ auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr<File> {
ls >> tf->box_height;
} else if (key == "columns") {
ls >> tf->columns;
} else if (key == "cell_spacing") {
ls >> tf->cell_spacing;
} else if (key == "row_spacing") {
ls >> tf->row_spacing;
} else {
// Línea de glifo: codepoint_decimal ancho_visual
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
}
Offset off{};
off.x = (glyph_index % tf->columns) * tf->box_width;
off.y = (glyph_index / tf->columns) * tf->box_height;
const int row_sp = tf->row_spacing > 0 ? tf->row_spacing : tf->cell_spacing;
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;
tf->offset[codepoint] = off;
++glyph_index;

View File

@@ -21,6 +21,8 @@ class Text {
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 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)
};

View File

@@ -1,24 +1,30 @@
#!/usr/bin/env python3
"""Generador de fuentes bitmap para JailDoctor's Dilemma.
Convierte un archivo .ttf en un GIF indexado + fichero .fnt compatibles
con el sistema de texto del juego.
Convierte un archivo .ttf (o un GIF existente) en un GIF indexado + fichero .fnt
compatibles con el sistema de texto del juego.
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 --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:
- 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).
- 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.
- 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 os
import shutil
import sys
from math import ceil
@@ -35,6 +41,124 @@ _EXTENDED_CHARS = list("ÀÁÈÉÍÏÒÓÚÜÑÇàáèéíïòóúüñç¡¿«»
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(
ttf_path: str,
size: int,
@@ -176,21 +300,8 @@ def render_font(
pass # si magick no está disponible se queda como GIF87a de 256 colores
print(f"GIF : {output_gif}")
# --- Generar fichero .fnt ---
ttf_name = os.path.basename(ttf_path)
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")
_write_fnt(output_fnt, output_name, f"{os.path.basename(ttf_path)} size {size}",
box_width, box_height, columns, 0, 0, chars_to_render, char_widths)
print(f"FNT : {output_fnt}")
@@ -201,32 +312,62 @@ def main() -> None:
default_dir = os.path.join(project_dir, "data", "font")
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,
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 --box-width 8
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:
- Para pixel fonts (.ttf bitmap), usa el tamaño exacto del bitmap strike.
- El GIF resultante usa índice 1 = glifo, índice 0 = transparente.
- Se generan 124 caracteres: ASCII 32-126 + extensiones ES/CA/VA.
- Usa --box-width para forzar una anchura de celda fija (útil para fuentes
cuadradas donde la celda coincide con box_height).
- Se procesan 124 caracteres: ASCII 32-126 + extensiones ES/CA/VA.
- En modo --gif, --box-width y --box-height son obligatorios.
- 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)
source_group = parser.add_mutually_exclusive_group(required=True)
source_group.add_argument("--ttf", help="Ruta al archivo .ttf")
source_group.add_argument("--gif", help="Ruta a un GIF de fuente existente con cuadrícula")
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: ascent + abs(descent))")
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()
if args.gif:
if args.box_width is None or args.box_height is None:
parser.error("--box-width y --box-height son obligatorios con --gif")
row_sp = args.row_spacing if args.row_spacing is not None else args.cell_spacing
render_gif_font(
gif_path = args.gif,
output_name = args.output,
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,