Compare commits
3 Commits
0c116665bc
...
911ee7a13e
| Author | SHA1 | Date | |
|---|---|---|---|
| 911ee7a13e | |||
| b876ccbb09 | |||
| 94684e8758 |
@@ -101,32 +101,32 @@ columns 15
|
||||
124 1 # |
|
||||
125 4 # }
|
||||
126 4 # ~
|
||||
192 4 # À
|
||||
193 4 # Á
|
||||
200 4 # È
|
||||
201 4 # É
|
||||
205 4 # Í
|
||||
207 4 # Ï
|
||||
210 4 # Ò
|
||||
211 4 # Ó
|
||||
218 4 # Ú
|
||||
220 4 # Ü
|
||||
209 4 # Ñ
|
||||
199 4 # Ç
|
||||
224 4 # à
|
||||
225 4 # á
|
||||
232 4 # è
|
||||
233 4 # é
|
||||
192 6 # À
|
||||
193 6 # Á
|
||||
200 6 # È
|
||||
201 6 # É
|
||||
205 6 # Í
|
||||
207 6 # Ï
|
||||
210 6 # Ò
|
||||
211 6 # Ó
|
||||
218 6 # Ú
|
||||
220 6 # Ü
|
||||
209 6 # Ñ
|
||||
199 6 # Ç
|
||||
224 5 # à
|
||||
225 5 # á
|
||||
232 5 # è
|
||||
233 5 # é
|
||||
237 4 # í
|
||||
239 4 # ï
|
||||
242 4 # ò
|
||||
243 4 # ó
|
||||
250 4 # ú
|
||||
252 4 # ü
|
||||
241 4 # ñ
|
||||
231 4 # ç
|
||||
161 4 # ¡
|
||||
191 4 # ¿
|
||||
242 5 # ò
|
||||
243 5 # ó
|
||||
250 5 # ú
|
||||
252 5 # ü
|
||||
241 5 # ñ
|
||||
231 5 # ç
|
||||
161 2 # ¡
|
||||
191 6 # ¿
|
||||
171 4 # «
|
||||
187 4 # »
|
||||
183 4 # ·
|
||||
183 2 # ·
|
||||
|
||||
|
Before Width: | Height: | Size: 759 B After Width: | Height: | Size: 837 B |
@@ -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 # ·
|
||||
|
||||
|
Before Width: | Height: | Size: 640 B After Width: | Height: | Size: 3.9 KiB |
@@ -97,3 +97,32 @@ columns 15
|
||||
121 6 # y
|
||||
122 7 # z
|
||||
126 6 # ~
|
||||
192 6 # À
|
||||
193 6 # Á
|
||||
200 7 # È
|
||||
201 7 # É
|
||||
205 6 # Í
|
||||
207 6 # Ï
|
||||
210 7 # Ò
|
||||
211 7 # Ó
|
||||
218 6 # Ú
|
||||
220 6 # Ü
|
||||
209 7 # Ñ
|
||||
199 7 # Ç
|
||||
224 6 # à
|
||||
225 6 # á
|
||||
232 7 # è
|
||||
233 7 # é
|
||||
237 6 # í
|
||||
239 6 # ï
|
||||
242 7 # ò
|
||||
243 7 # ó
|
||||
250 6 # ú
|
||||
252 6 # ü
|
||||
241 7 # ñ
|
||||
231 7 # ç
|
||||
161 2 # ¡
|
||||
191 6 # ¿
|
||||
171 5 # «
|
||||
187 5 # »
|
||||
183 2 # ·
|
||||
|
||||
|
Before Width: | Height: | Size: 784 B After Width: | Height: | Size: 959 B |
@@ -101,32 +101,32 @@ columns 15
|
||||
124 1 # |
|
||||
125 3 # }
|
||||
126 4 # ~
|
||||
192 3 # À
|
||||
193 3 # Á
|
||||
200 3 # È
|
||||
201 3 # É
|
||||
205 3 # Í
|
||||
207 3 # Ï
|
||||
210 3 # Ò
|
||||
211 3 # Ó
|
||||
218 3 # Ú
|
||||
220 3 # Ü
|
||||
209 3 # Ñ
|
||||
199 3 # Ç
|
||||
224 3 # à
|
||||
225 3 # á
|
||||
232 3 # è
|
||||
233 3 # é
|
||||
237 3 # í
|
||||
239 3 # ï
|
||||
242 3 # ò
|
||||
243 3 # ó
|
||||
250 3 # ú
|
||||
252 3 # ü
|
||||
241 3 # ñ
|
||||
192 5 # À
|
||||
193 5 # Á
|
||||
200 4 # È
|
||||
201 4 # É
|
||||
205 1 # Í
|
||||
207 1 # Ï
|
||||
210 5 # Ò
|
||||
211 5 # Ó
|
||||
218 5 # Ú
|
||||
220 5 # Ü
|
||||
209 5 # Ñ
|
||||
199 5 # Ç
|
||||
224 4 # à
|
||||
225 4 # á
|
||||
232 4 # è
|
||||
233 4 # é
|
||||
237 1 # í
|
||||
239 1 # ï
|
||||
242 4 # ò
|
||||
243 4 # ó
|
||||
250 4 # ú
|
||||
252 4 # ü
|
||||
241 4 # ñ
|
||||
231 3 # ç
|
||||
161 3 # ¡
|
||||
191 3 # ¿
|
||||
161 1 # ¡
|
||||
191 4 # ¿
|
||||
171 3 # «
|
||||
187 3 # »
|
||||
183 3 # ·
|
||||
183 1 # ·
|
||||
|
||||
|
Before Width: | Height: | Size: 596 B After Width: | Height: | Size: 648 B |
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -18,9 +18,11 @@ class Text {
|
||||
};
|
||||
|
||||
struct File {
|
||||
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 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)
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -34,6 +40,137 @@ _ASCII_CHARS = [chr(i) for i in range(32, 127)] # 95 chars: espacio
|
||||
_EXTENDED_CHARS = list("ÀÁÈÉÍÏÒÓÚÜÑÇàáèéíïòóúüñç¡¿«»·") # 29 chars ES/CA/VA
|
||||
ALL_CHARS = _ASCII_CHARS + _EXTENDED_CHARS # 124 total
|
||||
|
||||
# Caracteres de fallback para TTFs sin soporte de acentos/especiales.
|
||||
# Si el TTF no tiene el char, se dibuja el fallback en su celda del bitmap.
|
||||
# El .fnt sigue registrando el codepoint original → texto localizado funciona.
|
||||
CHAR_FALLBACKS: dict[str, str] = {
|
||||
"À": "A", "Á": "A", "È": "E", "É": "E",
|
||||
"Í": "I", "Ï": "I", "Ò": "O", "Ó": "O",
|
||||
"Ú": "U", "Ü": "U", "Ñ": "N", "Ç": "C",
|
||||
"à": "a", "á": "a", "è": "e", "é": "e",
|
||||
"í": "i", "ï": "i", "ò": "o", "ó": "o",
|
||||
"ú": "u", "ü": "u", "ñ": "n", "ç": "c",
|
||||
"¡": "!", "¿": "?", "«": "<", "»": ">", "·": ".",
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
@@ -66,32 +203,69 @@ def render_font(
|
||||
ascent, descent = font.getmetrics()
|
||||
box_height = box_height_override if box_height_override is not None else (ascent + abs(descent))
|
||||
|
||||
# Filtrar chars: solo incluir los que el TTF soporta realmente.
|
||||
# Un char se descarta si su advance es 0 (el TTF no lo tiene) y no es un
|
||||
# espacio. Evita que chars sin glifo aparezcan con width=1 en el .fnt,
|
||||
# lo que causaría solapamiento masivo al renderizar texto localizado.
|
||||
chars_to_render = [
|
||||
ch for ch in ALL_CHARS
|
||||
if ch.isspace() or font.getlength(ch) >= 1.0
|
||||
]
|
||||
skipped = [ch for ch in ALL_CHARS if ch not in chars_to_render]
|
||||
if skipped:
|
||||
print(f"Omitidos : {len(skipped)} chars sin soporte en este TTF: {''.join(skipped)}")
|
||||
# Calcular y_offset antes de la clasificación (necesario para detectar .notdef)
|
||||
cap_tops = [font.getbbox(ch)[1] for ch in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" if font.getbbox(ch)]
|
||||
y_offset = -round(sum(cap_tops) / len(cap_tops)) if cap_tops else 0
|
||||
|
||||
# Detectar el glifo .notdef: algunos TTF devuelven un glifo sustituto (con
|
||||
# advance > 0 y bbox válido) para chars que no tienen en su cmap. Esto provoca
|
||||
# falsos positivos en el test getlength >= 1. Se renderiza U+FFFE (garantizado
|
||||
# ausente en cualquier fuente de uso normal) y se guardan sus píxeles como
|
||||
# referencia. Cualquier char con píxeles idénticos se considera no soportado.
|
||||
_tmp_w = box_width_override or 32
|
||||
_nd_bbox = font.getbbox(chr(0xFFFE))
|
||||
if _nd_bbox:
|
||||
_nd_img = Image.new("RGBA", (_tmp_w, box_height), (0, 0, 0, 0))
|
||||
ImageDraw.Draw(_nd_img).text((-_nd_bbox[0], y_offset), chr(0xFFFE),
|
||||
font=font, fill=(255, 255, 255, 255))
|
||||
_notdef_bytes = _nd_img.tobytes()
|
||||
else:
|
||||
_notdef_bytes = None
|
||||
|
||||
def _is_notdef(ch: str) -> bool:
|
||||
"""True si el char renderiza el glifo .notdef en lugar de un glifo real."""
|
||||
if _notdef_bytes is None:
|
||||
return False
|
||||
bbox = font.getbbox(ch)
|
||||
if not bbox:
|
||||
return True
|
||||
img = Image.new("RGBA", (_tmp_w, box_height), (0, 0, 0, 0))
|
||||
ImageDraw.Draw(img).text((-bbox[0], y_offset), ch, font=font, fill=(255, 255, 255, 255))
|
||||
return img.tobytes() == _notdef_bytes
|
||||
|
||||
# Clasificar chars: soportados nativamente, con fallback, o sin soporte.
|
||||
# Un char tiene soporte nativo si su advance >= 1 Y no renderiza .notdef.
|
||||
# Si no, se busca en CHAR_FALLBACKS. Sin soporte y sin fallback, se omite.
|
||||
chars_to_render: list[str] = []
|
||||
char_render_as: dict[str, str] = {} # char → qué char dibujar realmente
|
||||
truly_skipped: list[str] = []
|
||||
|
||||
for ch in ALL_CHARS:
|
||||
if ch.isspace():
|
||||
chars_to_render.append(ch)
|
||||
char_render_as[ch] = ch
|
||||
elif font.getlength(ch) >= 1.0 and not _is_notdef(ch):
|
||||
chars_to_render.append(ch)
|
||||
char_render_as[ch] = ch
|
||||
elif ch in CHAR_FALLBACKS and font.getlength(CHAR_FALLBACKS[ch]) >= 1.0:
|
||||
chars_to_render.append(ch)
|
||||
char_render_as[ch] = CHAR_FALLBACKS[ch]
|
||||
else:
|
||||
truly_skipped.append(ch)
|
||||
|
||||
if truly_skipped:
|
||||
print(f"Omitidos : {len(truly_skipped)} chars sin soporte ni fallback: {''.join(truly_skipped)}")
|
||||
fallback_used = [ch for ch, r in char_render_as.items() if r != ch]
|
||||
if fallback_used:
|
||||
print(f"Fallback : {len(fallback_used)} chars con fallback: " +
|
||||
" ".join(f"{ch}→{char_render_as[ch]}" for ch in fallback_used))
|
||||
|
||||
# Medir advance width tipográfico: solo se usa para calcular box_width del canvas
|
||||
# cuando el usuario no lo especifica. El ancho real del .fnt se mide desde píxeles.
|
||||
char_widths: dict[str, int] = {}
|
||||
for ch in chars_to_render:
|
||||
char_widths[ch] = max(1, int(font.getlength(ch)))
|
||||
|
||||
# Calcular el offset vertical para eliminar el espacio en blanco en la parte
|
||||
# superior de la celda. Muchas fuentes bitmap tienen un em-box más grande que
|
||||
# los píxeles visibles (ascent incluye espacio interno). Usamos las letras
|
||||
# mayúsculas como referencia de "cap height": su bbox[1] indica cuántos
|
||||
# píxeles en blanco hay sobre los caracteres más altos, y restamos ese valor
|
||||
# para que las mayúsculas queden alineadas al borde superior de la celda.
|
||||
cap_tops = [font.getbbox(ch)[1] for ch in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" if font.getbbox(ch)]
|
||||
y_offset = -round(sum(cap_tops) / len(cap_tops)) if cap_tops else 0
|
||||
render_ch = char_render_as[ch]
|
||||
char_widths[ch] = max(1, int(font.getlength(render_ch)))
|
||||
|
||||
# box_width: anchura de cada celda en el bitmap.
|
||||
# Si el usuario la especifica, se usa tal cual. Si no, se calcula como el
|
||||
@@ -120,7 +294,8 @@ def render_font(
|
||||
cell_x = col * box_width
|
||||
cell_y = row * box_height
|
||||
|
||||
bbox = font.getbbox(ch)
|
||||
draw_ch = char_render_as[ch] # char que realmente se dibuja (puede ser fallback)
|
||||
bbox = font.getbbox(draw_ch)
|
||||
if not bbox:
|
||||
# Sin glifos visibles (ej. espacio): celda vacía, correcto.
|
||||
continue
|
||||
@@ -137,7 +312,7 @@ def render_font(
|
||||
# glifo al inicio de la celda, compensando el bearing izquierdo.
|
||||
char_img = Image.new("RGBA", (box_width, box_height), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(char_img)
|
||||
draw.text((-bbox[0], y_offset), ch, font=font, fill=(255, 255, 255, 255))
|
||||
draw.text((-bbox[0], y_offset), draw_ch, font=font, fill=(255, 255, 255, 255))
|
||||
|
||||
# Umbralizar alpha y volcar al buffer de índices
|
||||
char_bytes = char_img.tobytes()
|
||||
@@ -176,21 +351,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,41 +363,71 @@ 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")
|
||||
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))")
|
||||
|
||||
# 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: 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()
|
||||
|
||||
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 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,
|
||||
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__":
|
||||
|
||||