550 lines
22 KiB
Python
550 lines
22 KiB
Python
#!/usr/bin/env python3
|
||
"""Generador de fuentes bitmap (.gif + .fnt) desde TTF o GIF existente.
|
||
|
||
Herramienta standalone con dos interfaces:
|
||
|
||
- CLI: python3 font_gen.py --ttf myfont.ttf --size 8 --output myfont
|
||
- GUI: python3 font_gen.py (sin argumentos abre la interfaz Tk)
|
||
|
||
Genera un GIF indexado (índice 0 = fondo, índice 1 = glifo) más un .fnt
|
||
de texto con el ancho visual de cada caracter, listos para motores de
|
||
texto bitmap.
|
||
|
||
Dependencias: pip install Pillow
|
||
Opcional: imagemagick (binario `magick`) para forzar paleta de 2 colores.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import os
|
||
import shutil
|
||
import sys
|
||
from dataclasses import dataclass, field
|
||
from math import ceil
|
||
|
||
try:
|
||
from PIL import Image, ImageDraw, ImageFont
|
||
except ImportError:
|
||
print("Error: Pillow no está instalado. Ejecuta: pip install Pillow", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
# Conjunto de caracteres en el mismo orden que los ficheros .fnt del juego.
|
||
# ASCII 32-126 primero, luego extensiones para castellano, catalán y valenciano.
|
||
_ASCII_CHARS = [chr(i) for i in range(32, 127)] # 95 chars: espacio … ~
|
||
_EXTENDED_CHARS = list("ÀÁÈÉÌÍÒÓÙÚÑÇàáèéìíòóùúñç¡¿«»·©") # 30 chars ES/CA/VA + ©
|
||
ALL_CHARS = _ASCII_CHARS + _EXTENDED_CHARS # 125 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",
|
||
"¡": "!", "¿": "?", "«": "<", "»": ">", "·": ".",
|
||
}
|
||
|
||
|
||
@dataclass
|
||
class FontBitmapResult:
|
||
"""Resultado puro del rasterizado de un TTF (sin tocar disco).
|
||
|
||
Devuelto por build_font_bitmap() y consumido por save_font_files() y
|
||
por la GUI para mostrar el preview.
|
||
"""
|
||
img: "Image.Image" # PIL P-mode, idx 0 = fondo, idx 1 = glifo
|
||
chars: list[str] # caracteres incluidos en orden
|
||
char_widths: dict[str, int] # ancho visual por caracter
|
||
box_width: int
|
||
box_height: int
|
||
columns: int
|
||
fallback_used: dict[str, str] = field(default_factory=dict) # ch → ch dibujado
|
||
truly_skipped: list[str] = field(default_factory=list)
|
||
|
||
|
||
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 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 build_font_bitmap(
|
||
ttf_path: str,
|
||
size: int,
|
||
columns: int = 15,
|
||
box_width_override: int | None = None,
|
||
box_height_override: int | None = None,
|
||
strip_accents: bool = False,
|
||
) -> FontBitmapResult:
|
||
"""Rasteriza un TTF a un bitmap de fuente en memoria. No toca disco.
|
||
|
||
Lanza FileNotFoundError si el TTF no existe; RuntimeError si Pillow
|
||
no puede cargarlo.
|
||
"""
|
||
if not os.path.isfile(ttf_path):
|
||
raise FileNotFoundError(f"No se encuentra el archivo TTF: {ttf_path}")
|
||
|
||
try:
|
||
font = ImageFont.truetype(ttf_path, size)
|
||
except OSError as e:
|
||
raise RuntimeError(f"No se pudo cargar la fuente: {e}") from e
|
||
|
||
# Scratch tentativo para la detección de .notdef. Sus dimensiones solo
|
||
# afectan a la comparación pixel-perfect entre el char candidato y el
|
||
# glifo .notdef; lo único que importa es la consistencia interna entre
|
||
# ambas renders. El scratch definitivo del bitmap se recalcula después.
|
||
ascent, descent = font.getmetrics()
|
||
notdef_scratch_h = ascent + abs(descent)
|
||
cap_tops = [font.getbbox(ch)[1] for ch in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" if font.getbbox(ch)]
|
||
notdef_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, notdef_scratch_h), (0, 0, 0, 0))
|
||
ImageDraw.Draw(_nd_img).text((-_nd_bbox[0], notdef_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:
|
||
if _notdef_bytes is None:
|
||
return False
|
||
bbox = font.getbbox(ch)
|
||
if not bbox:
|
||
return True
|
||
img = Image.new("RGBA", (_tmp_w, notdef_scratch_h), (0, 0, 0, 0))
|
||
ImageDraw.Draw(img).text((-bbox[0], notdef_y_offset), ch, font=font, fill=(255, 255, 255, 255))
|
||
return img.tobytes() == _notdef_bytes
|
||
|
||
# Clasificar chars: soporte nativo, fallback ASCII, o sin soporte.
|
||
chars_to_render: list[str] = []
|
||
char_render_as: dict[str, str] = {}
|
||
truly_skipped: list[str] = []
|
||
|
||
for ch in ALL_CHARS:
|
||
if ch.isspace():
|
||
chars_to_render.append(ch)
|
||
char_render_as[ch] = ch
|
||
elif strip_accents and ch in CHAR_FALLBACKS and font.getlength(CHAR_FALLBACKS[ch]) >= 1.0:
|
||
chars_to_render.append(ch)
|
||
char_render_as[ch] = CHAR_FALLBACKS[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)
|
||
|
||
fallback_used = {ch: r for ch, r in char_render_as.items() if r != ch}
|
||
|
||
# Anchos iniciales basados en advance — solo se usan para dimensionar el canvas;
|
||
# el ancho real del .fnt se mide desde píxeles tras renderizar.
|
||
char_widths: dict[str, int] = {ch: max(1, int(font.getlength(char_render_as[ch])))
|
||
for ch in chars_to_render}
|
||
|
||
box_width = box_width_override if box_width_override is not None else max(char_widths.values())
|
||
|
||
# Scratch definitivo: lo dimensionamos al span vertical real (bbox)
|
||
# de los caracteres que vamos a dibujar, INCLUIDOS los acentos. Si solo
|
||
# nos basásemos en el top de las mayúsculas (cap_tops), À/É/Ç quedarían
|
||
# con el acento por encima de y=0 y se recortarían. y_offset se ajusta
|
||
# para que el píxel más alto de cualquier glifo caiga en y=0.
|
||
draw_chars = {char_render_as[ch] for ch in chars_to_render if not ch.isspace()}
|
||
glyph_bboxes = [b for b in (font.getbbox(c) for c in draw_chars) if b]
|
||
if glyph_bboxes:
|
||
top_y = min(b[1] for b in glyph_bboxes)
|
||
bot_y = max(b[3] for b in glyph_bboxes)
|
||
scratch_h = max(1, bot_y - top_y)
|
||
y_offset = -top_y
|
||
else:
|
||
scratch_h = notdef_scratch_h
|
||
y_offset = 0
|
||
|
||
# --- Pase 1: rasterizar cada glifo a un scratch RGBA ---
|
||
# POSICIONAMIENTO VERTICAL: y_offset alinea el píxel más alto de
|
||
# cualquier glifo (incluido el de un acento) con el borde superior del
|
||
# scratch. El recorte pixel-tight posterior compensa que las mayúsculas
|
||
# sin acento queden ligeramente más bajas en su celda.
|
||
# POSICIONAMIENTO HORIZONTAL: -bbox[0] compensa el bearing izquierdo.
|
||
glyph_bytes: dict[str, bytes] = {}
|
||
for ch in chars_to_render:
|
||
draw_ch = char_render_as[ch]
|
||
bbox = font.getbbox(draw_ch)
|
||
if not bbox:
|
||
continue # sin glifo (ej. espacio): celda vacía, correcto.
|
||
char_img = Image.new("RGBA", (box_width, scratch_h), (0, 0, 0, 0))
|
||
ImageDraw.Draw(char_img).text((-bbox[0], y_offset), draw_ch,
|
||
font=font, fill=(255, 255, 255, 255))
|
||
glyph_bytes[ch] = char_img.tobytes()
|
||
|
||
# --- Alto pixel-tight ---
|
||
# Como con el ancho visual: tomamos solo las filas con algún píxel
|
||
# opaco en cualquier caracter. Recorta leading sobrante por arriba y
|
||
# descenders no usados por abajo.
|
||
if box_height_override is not None:
|
||
box_height = box_height_override
|
||
y_crop = 0
|
||
elif not glyph_bytes:
|
||
box_height = scratch_h
|
||
y_crop = 0
|
||
else:
|
||
min_y, max_y = scratch_h, -1
|
||
for b in glyph_bytes.values():
|
||
for py in range(scratch_h):
|
||
row = py * box_width * 4
|
||
if any(b[row + px * 4 + 3] > 128 for px in range(box_width)):
|
||
if py < min_y: min_y = py
|
||
if py > max_y: max_y = py
|
||
if max_y < 0:
|
||
box_height = scratch_h
|
||
y_crop = 0
|
||
else:
|
||
box_height = max_y - min_y + 1
|
||
y_crop = min_y
|
||
|
||
rows = ceil(len(chars_to_render) / columns)
|
||
img_width = columns * box_width
|
||
img_height = rows * box_height
|
||
|
||
# Buffer de píxeles: 0 = fondo, 1 = glifo
|
||
pixels = bytearray(img_width * img_height)
|
||
|
||
# --- Pase 2: componer cada glifo en el bitmap final, recortado ---
|
||
for i, ch in enumerate(chars_to_render):
|
||
col = i % columns
|
||
row = i // columns
|
||
cell_x = col * box_width
|
||
cell_y = row * box_height
|
||
|
||
b = glyph_bytes.get(ch)
|
||
if b is None:
|
||
continue
|
||
|
||
for py in range(box_height):
|
||
src_y = py + y_crop
|
||
if src_y >= scratch_h:
|
||
break
|
||
for px in range(box_width):
|
||
src = (src_y * box_width + px) * 4
|
||
if b[src + 3] > 128:
|
||
pixels[(cell_y + py) * img_width + (cell_x + px)] = 1
|
||
|
||
# Ancho visual real: última columna con algún píxel opaco en la
|
||
# zona visible. Reemplaza el advance tipográfico de getlength().
|
||
for px in range(box_width - 1, -1, -1):
|
||
if any(b[((y_crop + py) * box_width + px) * 4 + 3] > 128
|
||
for py in range(box_height)
|
||
if y_crop + py < scratch_h):
|
||
char_widths[ch] = px + 1
|
||
break
|
||
|
||
img = Image.frombytes("P", (img_width, img_height), bytes(pixels))
|
||
palette = [0] * 768
|
||
palette[3] = palette[4] = palette[5] = 255 # índice 1 → blanco
|
||
img.putpalette(palette)
|
||
|
||
return FontBitmapResult(
|
||
img=img, chars=chars_to_render, char_widths=char_widths,
|
||
box_width=box_width, box_height=box_height, columns=columns,
|
||
fallback_used=fallback_used, truly_skipped=truly_skipped,
|
||
)
|
||
|
||
|
||
def save_font_files(
|
||
result: FontBitmapResult,
|
||
output_name: str,
|
||
output_dir: str,
|
||
source_desc: str,
|
||
cell_spacing: int = 0,
|
||
row_spacing: int = 0,
|
||
) -> tuple[str, str]:
|
||
"""Escribe el GIF + .fnt de un FontBitmapResult.
|
||
|
||
Devuelve (gif_path, fnt_path).
|
||
"""
|
||
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")
|
||
|
||
# Pillow sin transparency escribe GIF87a. Luego magick reduce la paleta a
|
||
# 2 colores (lzw_min=2) para compatibilidad exacta con parsers GIF estrictos.
|
||
result.img.save(output_gif, optimize=False)
|
||
try:
|
||
import subprocess
|
||
subprocess.run(
|
||
["magick", output_gif, "-colors", "2", f"GIF87:{output_gif}"],
|
||
check=True, capture_output=True,
|
||
)
|
||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||
pass # si magick no está disponible se queda como GIF87a de 256 colores
|
||
|
||
_write_fnt(output_fnt, output_name, source_desc,
|
||
result.box_width, result.box_height, result.columns,
|
||
cell_spacing, row_spacing, result.chars, result.char_widths)
|
||
return output_gif, output_fnt
|
||
|
||
|
||
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
|
||
|
||
if y0 + box_height > img_h or x0 + box_width > img_w:
|
||
skipped.append(ch)
|
||
continue
|
||
|
||
if all(pixels[x0 + px, y0 + py] == 0 for py in range(box_height) for px in range(box_width)):
|
||
if ch.isspace():
|
||
char_widths[ch] = max(1, box_width // 2)
|
||
supported_chars.append(ch)
|
||
else:
|
||
skipped.append(ch)
|
||
continue
|
||
|
||
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")
|
||
|
||
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,
|
||
output_name: str,
|
||
output_dir: str,
|
||
columns: int,
|
||
box_width_override: int | None,
|
||
box_height_override: int | None,
|
||
strip_accents: bool = False,
|
||
) -> None:
|
||
"""Wrapper CLI: rasteriza el TTF y escribe los ficheros, con logging."""
|
||
try:
|
||
result = build_font_bitmap(ttf_path, size, columns,
|
||
box_width_override, box_height_override,
|
||
strip_accents)
|
||
except (FileNotFoundError, RuntimeError) as e:
|
||
print(f"Error: {e}", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
if result.truly_skipped:
|
||
print(f"Omitidos : {len(result.truly_skipped)} chars sin soporte ni fallback: "
|
||
f"{''.join(result.truly_skipped)}")
|
||
if result.fallback_used:
|
||
print(f"Fallback : {len(result.fallback_used)} chars con fallback: " +
|
||
" ".join(f"{ch}→{r}" for ch, r in result.fallback_used.items()))
|
||
|
||
rows = ceil(len(result.chars) / result.columns)
|
||
print(f"Fuente : {os.path.basename(ttf_path)}, size={size}")
|
||
print(f"Caja : {result.box_width}×{result.box_height} px | {result.columns} columnas, {rows} filas")
|
||
print(f"Bitmap : {result.img.width}×{result.img.height} px | {len(result.chars)} caracteres")
|
||
|
||
gif_path, fnt_path = save_font_files(
|
||
result, output_name, output_dir,
|
||
f"{os.path.basename(ttf_path)} size {size}",
|
||
)
|
||
print(f"GIF : {gif_path}")
|
||
print(f"FNT : {fnt_path}")
|
||
|
||
|
||
def _launch_gui() -> None:
|
||
"""Importa y arranca la GUI Tk. Importación perezosa para no exigir
|
||
tkinter en uso CLI puro."""
|
||
try:
|
||
from font_gen_gui import run_gui
|
||
except ImportError as e:
|
||
print(f"Error: no se pudo cargar la GUI ({e}).\n"
|
||
f"Asegúrate de que font_gen_gui.py está junto a font_gen.py "
|
||
f"y que tkinter está disponible.", file=sys.stderr)
|
||
sys.exit(1)
|
||
run_gui()
|
||
|
||
|
||
def main() -> None:
|
||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||
|
||
parser = argparse.ArgumentParser(
|
||
description="Genera fuentes bitmap (.gif + .fnt) desde un .ttf o un GIF existente.",
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
epilog="""
|
||
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 ./out
|
||
|
||
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
|
||
|
||
GUI:
|
||
python3 font_gen.py (sin argumentos abre la interfaz Tk)
|
||
python3 font_gen.py --gui
|
||
|
||
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 procesan 124 caracteres: ASCII 32-126 + extensiones ES/CA/VA.
|
||
- En modo --gif, --box-width y --box-height son obligatorios.
|
||
- cell_spacing = spacing horizontal entre columnas (y borde izquierdo/superior).
|
||
- row_spacing = spacing vertical entre filas (si difiere de cell_spacing).
|
||
""",
|
||
)
|
||
|
||
parser.add_argument("--gui", action="store_true", help="Abre la interfaz gráfica Tk")
|
||
|
||
source_group = parser.add_mutually_exclusive_group()
|
||
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", help="Nombre base de salida (sin extensión)")
|
||
parser.add_argument("--dir", default=script_dir, help="Directorio de salida (default: junto al script)")
|
||
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)")
|
||
parser.add_argument("--no-accents", action="store_true",
|
||
help="Renderiza À/É/ñ/Ç… como A/E/n/C (sin acentos). El codepoint original se conserva en el .fnt.")
|
||
|
||
args = parser.parse_args()
|
||
|
||
if args.gui:
|
||
_launch_gui()
|
||
return
|
||
|
||
if not args.ttf and not args.gif:
|
||
parser.error("se requiere --ttf, --gif, o --gui")
|
||
if not args.output:
|
||
parser.error("--output es obligatorio en modo CLI")
|
||
|
||
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,
|
||
strip_accents = args.no_accents,
|
||
)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
if len(sys.argv) == 1:
|
||
_launch_gui()
|
||
else:
|
||
main()
|