Todas las publicaciones

¿Cómo incrusto una fuente japonesa en gpdf?

Pasa los bytes del TTF a gpdf.WithFont al construir el documento. Tres líneas, subset embedding automático y sin CGO.

por gpdf team

La pregunta, en otras palabras

¿Cómo se renderiza texto japonés (o CJK en general) en un PDF creado con gpdf — sin la ceremonia de AddUTF8Font, sin CGO, sin incrustar una fuente de cinco megabytes en cada documento?

Respuesta rápida

Lee los bytes del TTF. Pasa gpdf.WithFont("NotoSansJP", fontBytes) a NewDocument. Si quieres, márcala como predeterminada. Tres líneas de configuración, y gpdf incrusta automáticamente sólo los glifos que realmente usaste — no los 5 MB enteros.

El 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", 12),
    )

    page := doc.AddPage()
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text("こんにちは、世界。", template.FontSize(24), template.Bold())
            c.Text("日本語 PDF、これだけ。")
        })
    })

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

Descarga NotoSansJP-Regular.ttf desde Google Fonts, ponlo junto a main.go y ejecuta go run main.go. Obtienes un PDF de una página con texto japonés.

Qué hacen esas tres líneas en realidad

Dos cosas ocurren bajo el capó, y ninguna necesita tu intervención.

Subset embedding. Noto Sans JP trae unos 17.000 glifos — el peso Regular ocupa unos 5 MB en disco. Si embebieras la fuente completa, un recibo con cuatro líneas de japonés acarrearía cinco megabytes de datos tipográficos. gpdf recorre el texto renderizado, identifica qué IDs de glifo usaste y escribe sólo ese subconjunto en el PDF. Una factura corta suele terminar con 20–40 KB de datos de fuente, no 5 MB.

gofpdf también sabía hacer subset, pero exigía que AddUTF8Font recibiera una ruta de archivo y un flag UTF-8, con la carga ocurriendo mientras el cursor se movía; cambiar de fuente a mitad de documento era incómodo. gpdf registra la fuente una sola vez al construir el documento; a partir de ahí, cada c.Text la referencia por nombre de familia. No hay preparación por llamada.

Sin CGO. Esto importa más de lo que suena. En otros ecosistemas el manejo de fuentes suele pasar por FreeType o HarfBuzz — eso significa una dependencia en C, cachés de build que se invalidan distinto, capas extra en tus imágenes Docker y un cross-compile de macOS a linux/arm64 que deja de ser trivial. gpdf parsea las tablas TrueType en Go puro. go build sigue produciendo un binario estático. Mete en un contenedor distroless el binario Go y el archivo TTF; no necesitas más.

Variantes Bold e Italic

Las familias Noto japonesas entregan un archivo por peso. Para usar negrita, registra el TTF Bold con el sufijo -Bold:

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", 12),
)

Ahora template.Bold() toma la variante -Bold. Misma convención para -Italic y -BoldItalic. Si no registras la variante, la negrita se sintetiza — legible en pantalla, pero no tipográficamente honesto. Para facturas en producción, registra el peso real.

Varios idiomas CJK en el mismo documento

Registrar más de una familia está bien — gpdf las administra independientemente. Cambia por texto con template.FontFamily(...):

jp, _ := os.ReadFile("NotoSansJP-Regular.ttf")
sc, _ := os.ReadFile("NotoSansSC-Regular.ttf")
kr, _ := os.ReadFile("NotoSansKR-Regular.ttf")

doc := gpdf.NewDocument(
    gpdf.WithFont("NotoSansJP", jp),
    gpdf.WithFont("NotoSansSC", sc),
    gpdf.WithFont("NotoSansKR", kr),
    gpdf.WithDefaultFont("NotoSansJP", 12),
)

page.AutoRow(func(r *template.RowBuilder) {
    r.Col(4, func(c *template.ColBuilder) {
        c.Text("日本語")
    })
    r.Col(4, func(c *template.ColBuilder) {
        c.Text("中文", template.FontFamily("NotoSansSC"))
    })
    r.Col(4, func(c *template.ColBuilder) {
        c.Text("한국어", template.FontFamily("NotoSansKR"))
    })
})

La unificación Han hace que japonés y chino simplificado compartan codepoints Unicode, pero sus glifos se dibujan distinto. El mismo codepoint se renderiza con forma distinta según la fuente — elegir fuente no es un tema estético, es un tema de corrección. Si generas facturación electrónica para ambos mercados, necesitas ambas fuentes registradas.

La trampa del tofu

Si escribes japonés pero olvidas WithFont, gpdf hace fallback a las fuentes Base-14 del PDF — ninguna cubre el rango CJK. Los caracteres salen como rectángulos vacíos: lo que en el mundo Unicode llamamos "cajas tofu":

□□□□□、□□。

Si ves eso, la causa siempre es la misma: no registraste una fuente CJK, o estás escribiendo en una familia que no incluye esos glifos. La solución también: agrega WithFont y usa WithDefaultFont, o pasa template.FontFamily en el c.Text.

Lectura relacionada

Prueba gpdf

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

go get github.com/gpdf-dev/gpdf

⭐ Star en GitHub · Leer la documentación