ferramenta de text pot importar gifs
ferramenta de text accepta separació entre quadricules de lletres
This commit is contained in:
@@ -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,41 +312,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__":
|
||||
|
||||
Reference in New Issue
Block a user