Todas las publicaciones

¿Cómo uso Noto Sans JP con gpdf?

Registra el TTF static de NotoSansJP-Regular con gpdf.WithFont. No uses la variable font. gpdf subsetea los 17.000 glifos a menos de 40 KB por PDF.

por gpdf team

La pregunta, reformulada

Quieres renderizar texto japonés en un documento de gpdf y has elegido Noto Sans JP — la sans-serif gratuita de Google bajo SIL OFL que cubre el rango JIS completo. Ya has descargado el zip de Google Fonts. Lo que falta por aclarar son tres cosas: qué archivo elegir, qué pesos registrar y cuál es la única trampa escondida dentro del zip.

TL;DR

Usa el TTF static NotoSansJP-Regular.ttf que está dentro de la carpeta static/ del zip. No uses la variable font de la raíz. Pásalo a gpdf.WithFont("NotoSansJP", bytes) y márcalo como fuente por defecto. gpdf subsetea los ~17.000 glifos y deja solo los que realmente renderizaste — una factura típica acaba con 20–40 KB de datos de fuente en el PDF final.

Ejemplo completo

package main

import (
    "log"
    "os"

    "github.com/gpdf-dev/gpdf"
    "github.com/gpdf-dev/gpdf/document"
    "github.com/gpdf-dev/gpdf/template"
)

func main() {
    font, err := os.ReadFile("NotoSansJP-Regular.ttf")
    if err != nil {
        log.Fatal(err)
    }

    doc := gpdf.NewDocument(
        gpdf.WithPageSize(gpdf.A4),
        gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
        gpdf.WithFont("NotoSansJP", font),
        gpdf.WithDefaultFont("NotoSansJP", 11),
    )

    page := doc.AddPage()
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text("請求書", template.FontSize(28), template.Bold())
            c.Text("Noto Sans JP、これで十分。")
        })
    })

    data, err := doc.Generate()
    if err != nil {
        log.Fatal(err)
    }
    if err := os.WriteFile("invoice.pdf", data, 0o644); err != nil {
        log.Fatal(err)
    }
}

Descarga el zip de Noto Sans JP desde Google Fonts, extrae static/NotoSansJP-Regular.ttf, ponlo junto a main.go y ejecuta go run main.go.

Usa el TTF static, no la variable font

Abre la página de Google Fonts, pulsa Get font → Download all y descomprime. Dentro hay dos cosas que parecen intercambiables y no lo son:

  • NotoSansJP-VariableFont_wght.ttf en la raíz — la variable font, ~7 MB, lleva todos los pesos del 100 al 900 en un único archivo
  • static/ — nueve TTFs separados, de NotoSansJP-Thin.ttf a NotoSansJP-Black.ttf, unos 5 MB cada uno

Usa los de static/.

El parser TrueType de gpdf está deliberadamente acotado. Maneja contornos de glifos, glifos compuestos, cmap y hmtx — las tablas necesarias para renderizar texto con un peso fijo. Pero no procesa fvar, gvar ni HVAR, que son las tablas OpenType que hacen que una variable font sea de verdad variable. Si le pasas VariableFont_wght.ttf ocurre una de dos cosas: o el parser falla de forma limpia, o — peor — usa los glifos de la instancia por defecto e ignora en silencio cualquier eje de peso que creías estar ajustando.

La aritmética del tamaño también juega a favor del static. Una variable font arrastra los contornos de todos los pesos en el mismo archivo — ese es justo el diseño. Si solo usas Regular, estás pagando ocho pesos que no se renderizan nunca. Static Regular pesa 5 MB; la variable, 7 MB. El subsetting recortará ambos, pero el static es una entrada más limpia.

Las cuatro líneas que importan

Todo lo interesante vive en las opciones del constructor:

doc := gpdf.NewDocument(
    gpdf.WithFont("NotoSansJP", font),
    gpdf.WithDefaultFont("NotoSansJP", 11),
)

El nombre de la familia ("NotoSansJP") es arbitrario. gpdf lo usa como clave de búsqueda — no como ruta del sistema de archivos ni como campo leído de los metadatos de la fuente. Llámalo "body", "jp" o "Noto" si en tu código suena más natural. Solo mantén la coherencia con el argumento que pases más adelante a template.FontFamily(...).

WithDefaultFont es la opción que te ahorra escribir template.FontFamily("NotoSansJP") en cada c.Text. Si la omites, gpdf cae en Helvetica — que no cubre ni un solo codepoint CJK — y obtienes cuadraditos vacíos (tofu, □□□) en todo el texto sin familia explícita. Media hora buscando por qué "solo los títulos salen bien".

¿Qué pesos necesitas realmente?

La mayoría de facturas, recibos e informes necesitan dos: Regular y Bold. Registra los dos:

reg,  _ := os.ReadFile("NotoSansJP-Regular.ttf")
bold, _ := os.ReadFile("NotoSansJP-Bold.ttf")

doc := gpdf.NewDocument(
    gpdf.WithFont("NotoSansJP", reg),
    gpdf.WithFont("NotoSansJP-Bold", bold),
    gpdf.WithDefaultFont("NotoSansJP", 11),
)

Al registrar con el sufijo -Bold, template.Bold() lo recoge automáticamente. Misma regla para -Italic y -BoldItalic, aunque ten en cuenta que Noto Sans JP no trae cursiva — las fuentes CJK generalmente no publican variante oblicua porque el diseño de los glifos no tiene una inclinación natural. Si tu layout pide énfasis cursivo en una tirada japonesa, usa color, tamaño o peso en su lugar.

Folletos de marketing a veces piden Medium o SemiBold para citas destacadas. Perfectamente válido. Regístralos con cualquier sufijo y referencia por nombre de familia directamente:

gpdf.WithFont("NotoSansJP-Medium", medium)
// ...
c.Text("見出し", template.FontFamily("NotoSansJP-Medium"))

El atajo de Bold/Italic por sufijo solo se conecta con los nombres literales -Bold / -Italic / -BoldItalic. Cualquier otra cosa se referencia por nombre de familia.

El tamaño después del subsetting

Noto Sans JP Regular pesa ~5 MB en disco. Esa cifra empuja a algunos equipos a montar un CDN aparte para fuentes o a añadir postprocesado para quitar las fuentes del PDF. Con gpdf no hace falta ninguna de las dos cosas.

Esto es lo que acaba en el PDF:

DocumentoGlifos usadosDatos de fuente en el PDF
Recibo de una línea (~15 caract.)~14~11 KB
Factura habitual (~200 caract.)~80~28 KB
Informe de 10 páginas (~8.000 caract.)~900~180 KB
Volcado tipo diccionario (JIS Nivel 1 completo)~6.800~2,1 MB

(gpdf v1.0, subsetting estático activo. Los números varían unos KB según en qué parte de CFF y hmtx caigan los IDs de glifo.)

Para una factura final de 50 KB, más de la mitad son datos de fuente. Aun así es una fracción mínima frente a los 5 MB que incrustarías sin subsetting, y el visor abre el PDF al instante.

Noto Sans JP vs. Noto Sans CJK JP — no las confundas

Hay dos familias Noto que afirman manejar japonés y los nombres las hacen parecer intercambiables. No lo son.

Noto Sans JP es la que quieres. Se distribuye como TTF, apunta a un único idioma y cada peso es un archivo independiente. Es la descarga de Google Fonts.

Noto Sans CJK JP es la superfamilia pan-CJK. Se distribuye como OpenType Collection (.ttc), un solo archivo que contiene japonés, chino simplificado, chino tradicional y coreano unificados en un mismo paquete. Es lo que se entregaba en las primeras releases de Noto y lo que encontrarás en notofonts.github.io/noto-cjk.

gpdf soporta TTF directamente. TTC es un formato contenedor — tendrías que elegir el índice de face correcto antes de entregar los bytes a WithFont, y el cmap de cada face está ajustado para una locale CJK específica, lo que significa que estás tomando decisiones implícitas sobre unificación Han. Es más claro tomarlas explícitamente eligiendo el TTF específico de JP.

¿Empiezas hoy? Usa Noto Sans JP. ¿Ya tienes un NotoSansCJK-Regular.ttc en un proyecto heredado? Extrae el face JP con pyftsubset o fonttools y commitea el TTF resultante como artefacto canónico del repo.

Empaquetar la fuente en el binario

Los generadores de PDF suelen correr en contenedores, y la forma más limpia de distribuir la fuente es compilarla dentro:

package main

import (
    _ "embed"

    "github.com/gpdf-dev/gpdf"
)

//go:embed NotoSansJP-Regular.ttf
var notoJP []byte

func main() {
    doc := gpdf.NewDocument(
        gpdf.WithFont("NotoSansJP", notoJP),
        gpdf.WithDefaultFont("NotoSansJP", 11),
    )
    // ...
}

El binario crece de ~8 MB a ~13 MB. A cambio, la imagen Docker tiene un único artefacto en lugar de dos, COPY --from=builder /app /app basta, y nadie puede publicar un contenedor roto por olvidar el archivo de fuente. Para un batch que genera miles de PDFs al día, este es el valor por defecto correcto.

Lectura relacionada

Prueba gpdf

gpdf es una librería Go para generar PDFs. Licencia MIT, cero dependencias externas, soporte nativo para CJK.

go get github.com/gpdf-dev/gpdf

⭐ Star en GitHub · Leer la documentación