¿Qué son WOFF y WOFF2?
WOFF (Web Open Font Format) y WOFF2 son formatos de contenedor de fuentes comprimidos diseñados específicamente para la entrega web. No son nuevos formatos de fuente — envuelven fuentes TrueType (TTF) u OpenType (OTF) existentes en un contenedor comprimido, reduciendo el tamaño del archivo para una carga más rápida de páginas web.
| Especificación | Año | Compresión | Estándar W3C |
|---|---|---|---|
| WOFF 1.0 | 2012 | zlib (deflate) | Recomendación W3C |
| WOFF2 | 2018 | Brotli | Recomendación W3C |
WOFF2 logra aproximadamente un 30% mejor compresión que WOFF 1.0 para los mismos datos de fuente, gracias al superior algoritmo de compresión Brotli y al preprocesamiento específico de fuentes.
Compatibilidad con navegadores:
- WOFF: Todos los navegadores modernos incluyendo IE9+
- WOFF2: Todos los navegadores modernos (Chrome 36+, Firefox 39+, Safari 12+, Edge 14+); sin soporte en IE
Usar Fuentes Web con CSS @font-face
El uso principal de WOFF/WOFF2 es en declaraciones CSS @font-face que incrustan fuentes personalizadas en páginas web:
/* Enfoque moderno: WOFF2 primero, WOFF como alternativa */
@font-face {
font-family: 'MiFuente';
src: url('/fonts/mifuente.woff2') format('woff2'),
url('/fonts/mifuente.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
}
body {
font-family: 'MiFuente', system-ui, -apple-system, sans-serif;
}
La Pila Moderna (Mejores Prácticas 2024)
@font-face {
font-family: 'Inter';
/* Solo WOFF2 — compatible con el 97%+ de navegadores en 2024 */
src: url('/fonts/inter-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: optional; /* evita el cambio de diseño */
}
Convertir TTF/OTF a WOFF/WOFF2
Usando fonttools (Python)
pip install fonttools brotli zopfli
from fontTools.ttLib import TTFont
import os
def convertir_a_woff2(ruta_entrada: str, ruta_salida: str) -> None:
"""Convierte TTF u OTF a WOFF2."""
fuente = TTFont(ruta_entrada)
fuente.flavor = 'woff2'
fuente.save(ruta_salida)
tam_kb = os.path.getsize(ruta_salida) / 1024
print(f"WOFF2 guardado: {ruta_salida} ({tam_kb:.1f} KB)")
def convertir_a_woff(ruta_entrada: str, ruta_salida: str) -> None:
"""Convierte TTF u OTF a WOFF 1.0."""
fuente = TTFont(ruta_entrada)
fuente.flavor = 'woff'
fuente.save(ruta_salida)
# Uso
convertir_a_woff2('Inter-Regular.ttf', 'inter-regular.woff2')
convertir_a_woff('Inter-Regular.ttf', 'inter-regular.woff')
Script de Conversión en Lote
import os, glob
from fontTools.ttLib import TTFont
def convertir_fuentes_en_lote(dir_entrada: str, dir_salida: str) -> None:
"""Convierte todas las fuentes TTF y OTF a WOFF y WOFF2."""
os.makedirs(dir_salida, exist_ok=True)
patrones = [os.path.join(dir_entrada, '*.ttf'),
os.path.join(dir_entrada, '*.otf')]
for patron in patrones:
for ruta_fuente in glob.glob(patron):
nombre = os.path.splitext(os.path.basename(ruta_fuente))[0]
for sabor, ext in [('woff', 'woff'), ('woff2', 'woff2')]:
ruta_salida = os.path.join(dir_salida, f'{nombre}.{ext}')
fuente = TTFont(ruta_fuente)
fuente.flavor = sabor
fuente.save(ruta_salida)
tam_kb = os.path.getsize(ruta_salida) / 1024
print(f" {nombre}.{ext}: {tam_kb:.1f} KB")
convertir_fuentes_en_lote('./fuentes/raw', './fuentes/web')
Subsetting de Fuentes: El Mayor Ahorro de Tamaño
La mayoría de las fuentes contienen miles de glifos para muchos idiomas. Para un sitio web que solo sirve contenido en español, el 90%+ de esos glifos son ancho de banda desperdiciado. El subsetting elimina los glifos e indicaciones no utilizados, reduciendo a menudo el tamaño del archivo entre un 70 y un 90%.
from fontTools import subset as ft_subset
def crear_subset(ruta_entrada: str, ruta_salida: str, unicodes: str) -> None:
"""Crea un WOFF2 subconjunto con solo los caracteres necesarios."""
args = [
ruta_entrada,
f'--output-file={ruta_salida}',
'--flavor=woff2',
f'--unicodes={unicodes}',
]
ft_subset.main(args)
# Subsetting solo para latín (sitios en español/inglés/francés/alemán)
crear_subset(
'SourceSansPro-Regular.ttf',
'source-sans-pro-latin.woff2',
unicodes='U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+20AC,U+FEFF',
)
Usando la CLI directamente
# Subsetting de TTF a caracteres latinos y conversión a WOFF2
pyftsubset Inter-Regular.ttf \
--output-file=inter-latin.woff2 \
--flavor=woff2 \
--unicodes="U+0000-00FF,U+0131,U+0152-0153,U+20AC,U+FEFF"
Comparativa de Tamaño de Archivo WOFF
Para una fuente serif típica (Latin + Cirílico + Griego completo):
| Formato | Tamaño completo | Tamaño subset Latin |
|---|---|---|
| TTF | 800 KB | 120 KB |
| OTF | 700 KB | 110 KB |
| WOFF | 500 KB | 75 KB |
| WOFF2 | 350 KB | 52 KB |
| WOFF2 (subconjunto) | — | 22–40 KB |
Google Fonts sirve subconjuntos WOFF2 dinámicamente basándose en el descriptor CSS unicode-range.
CORS y Servicio de Fuentes
Las fuentes web están sujetas a la Política del Mismo Origen para solicitudes de origen cruzado. Si albergas fuentes en una CDN o dominio diferente, el servidor debe enviar una cabecera CORS:
Access-Control-Allow-Origin: *
Sin esta cabecera, los navegadores rechazarán cargar fuentes de otros orígenes (Firefox aplica esto estrictamente). Para fuentes auto-alojadas (ejemplo de nginx):
location ~* \.(woff|woff2|ttf|otf)$ {
add_header Access-Control-Allow-Origin "*";
expires 365d;
add_header Cache-Control "public, immutable";
}
Leer Metadatos de WOFF en Python
from fontTools.ttLib import TTFont
def inspeccionar_fuente(ruta: str) -> dict:
"""Extrae metadatos de un archivo WOFF, WOFF2, TTF u OTF."""
fuente = TTFont(ruta)
tabla_nombre = fuente['name']
def obtener_nombre(nameID):
registro = tabla_nombre.getName(nameID, 3, 1, 0x0409)
return registro.toUnicode() if registro else None
return {
'familia': obtener_nombre(1),
'subfamilia': obtener_nombre(2),
'nombre_completo': obtener_nombre(4),
'version': obtener_nombre(5),
'copyright': obtener_nombre(0),
'sabor': fuente.flavor,
'num_glifos': len(fuente.getGlyphOrder()),
'upem': fuente['head'].unitsPerEm,
}
print(inspeccionar_fuente('inter-regular.woff2'))
Conversiones relacionadas
Conversiones frecuentes del catálogo: