¿Cómo mezclo dos fuentes en el mismo párrafo en gpdf?
Para mezclar fuentes en un párrafo en gpdf, usa c.RichText y pon template.FontFamily en cada span. c.Text solo aplica una fuente a toda la cadena.
La pregunta, dicho de otro modo
Tengo un párrafo — una frase, una etiqueta, una celda de tabla — y quiero parte de él en una fuente y parte en otra. Un fragmento de código en monoespaciada dentro de una línea en Helvetica. Un nombre en japonés en Noto Sans JP junto a un número de pedido ASCII. ¿Cómo cambio de fuente a mitad de párrafo sin partir el texto en bloques separados?
La respuesta rápida
c.Text es la herramienta equivocada aquí. Aplica un solo document.Style — una sola familia de fuente incluida — a toda la cadena. La que quieres es c.RichText, donde cada span lleva su propio estilo:
c.RichText(func(rt *template.RichTextBuilder) {
rt.Span("Run ")
rt.Span("gofmt ./...", template.FontFamily("Courier"))
rt.Span(" before you commit.")
})
Tres spans, dos fuentes, un párrafo. El motor de maquetación corta línea cruzando los límites de los spans igual que un procesador de textos, así que el fragmento monoespaciado fluye en línea con el Helvetica de alrededor.
Courier funciona sin llamar a WithFont porque es una de las fuentes Standard 14 de PDF — todos los lectores ya la tienen, igual que Helvetica y Times-Roman. Si tu segunda fuente es un archivo TrueType que tú aportas (una fuente de marca, una fuente CJK), la registras una vez y la referencias por nombre. Más abajo.
Código que funciona (Helvetica + Courier, sin archivos de fuente)
package main
import (
"log"
"os"
"github.com/gpdf-dev/gpdf"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/pdf"
"github.com/gpdf-dev/gpdf/template"
)
func main() {
doc := gpdf.NewDocument(
gpdf.WithPageSize(gpdf.A4),
gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
)
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.RichText(func(rt *template.RichTextBuilder) {
rt.Span("Run ")
rt.Span("gofmt ./...", template.FontFamily("Courier"))
rt.Span(" before every commit. ")
rt.Span("It is not optional", template.Bold(), template.Italic())
rt.Span(".")
})
c.RichText(func(rt *template.RichTextBuilder) {
rt.Span("The field is ")
rt.Span("created_at", template.FontFamily("Courier"), template.TextColor(pdf.RGBHex(0xB00020)))
rt.Span(" — not ")
rt.Span("createdAt", template.FontFamily("Courier"))
rt.Span(".")
})
})
})
data, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("mixed-fonts.pdf", data, 0o644); err != nil {
log.Fatal(err)
}
}
El cuerpo se queda en Helvetica (la fuente por defecto), los identificadores en línea cambian a Courier, y un span superpone bold + italic sobre la fuente por defecto. Sin WithFont, sin datos de fuente incrustados — el PDF referencia Helvetica, Helvetica-BoldOblique y Courier como entradas Type 1 no incrustadas que todo lector ya tiene.
Qué hace RichText con los spans
Cada rt.Span se convierte en un document.RichTextFragment con su propia copia del estilo. Un span que llamas sin opciones hereda el estilo de bloque — que en RichText es el de la columna, es decir, la fuente y el tamaño por defecto del documento. Un span que llamas con template.FontFamily("Courier") solo sobrescribe ese campo y conserva todo lo demás.
Al maquetar, gpdf parte cada fragmento en runs de palabra, mide cada run con las métricas de la fuente de ese run — por eso una palabra en Courier y otra en Helvetica en la misma línea salen con el ancho correcto — y luego empaqueta los runs en líneas de forma voraz. Todos los runs de una línea comparten una línea base, así que un span de 24 pt junto a uno de 12 pt se alinean abajo y la altura de línea crece para que quepa el alto.
Hay una distinción que despista: el segundo argumento de c.RichText es estilo de nivel párrafo, y las opciones por span son de nivel fragmento:
| Opción | Dónde va |
|---|---|
FontFamily / FontSize / Bold / Italic / TextColor / Underline / Strikethrough | por span — pásalo a cada rt.Span |
AlignLeft / AlignCenter / AlignRight / AlignJustify, altura de línea, TextIndent | nivel párrafo — pásalo como segundo argumento de c.RichText |
Poner AlignRight() en un rt.Span individual no hace nada. La alineación es una propiedad de la línea, no del fragmento.
El caso de verdad: una fuente latina junto a una CJK
Monoespaciada dentro de una frase es la versión fácil. Con la que de verdad se pelea la gente es con mezclar una fuente occidental y una CJK en una línea — una etiqueta en inglés y un valor en japonés, un código de producto y un 商品名. Dos cosas que saber.
Primera, gpdf no elige fuente por sistema de escritura. Si la familia de un span es Helvetica y el texto es 日本語, salen cuadritos de tofu (□) — Helvetica no tiene glifos CJK, y gpdf no va a echar mano en silencio de otra fuente registrada para cubrirlos. Pon tú mismo la familia CJK en el span CJK:
ttf, _ := os.ReadFile("NotoSansJP-Regular.ttf")
doc := gpdf.NewDocument(
gpdf.WithFont("NotoSansJP", ttf),
)
// ...
c.RichText(func(rt *template.RichTextBuilder) {
rt.Span("Customer: ") // por defecto → Helvetica
rt.Span("山田 太郎", template.FontFamily("NotoSansJP")) // CJK → Noto Sans JP
rt.Span(" (ID 10293)") // de vuelta a Helvetica
})
Segunda — y esto vale la pena decirlo claro — la mayoría de las fuentes CJK japonesas ya traen glifos latinos decentes. Noto Sans JP, IPAex, Source Han Sans: todas dibujan ID 10293 perfectamente bien. Así que antes de meterte en una mezcla span a span, pregúntate si de verdad quieres dos fuentes o solo llegaste ahí por costumbre. Si todo el documento es japonés-con-algo-de-ASCII, lo más simple es gpdf.WithDefaultFont("NotoSansJP", 11) y no mezclar nada. Recurre a RichText + FontFamily cuando de verdad quieras un aspecto distinto — una cara latina geométrica y limpia para los números, una CJK humanista para el texto — no solo para que la escritura se renderice.
Cuándo c.Text sigue valiendo
Si toda la cadena es una sola fuente, sigue usando c.Text — es más ligero y se lee mejor. c.Text("発行日: 2026-05-11", template.FontFamily("NotoSansJP")) es una fuente para toda la línea, y c.Text lo resuelve. RichText se gana su sitio solo cuando el estilo cambia dentro de la cadena. No envuelvas una línea de un solo estilo en un callback de RichText solo porque puedas.
Recetas relacionadas
- ¿Cómo uso negrita y cursiva juntas en gpdf? — el mismo mecanismo de spans de
RichText, aplicado al peso y la inclinación en vez de a la familia - ¿Cómo añado una fuente TrueType personalizada a gpdf? — registrar la segunda fuente que quieres mezclar
- ¿Cómo incrusto una fuente japonesa en gpdf? — el recorrido de
WithFontpara el lado CJK de una línea con fuentes mezcladas
Prueba gpdf
gpdf es una biblioteca de Go para generar PDF. Licencia MIT, cero dependencias externas, soporte nativo de CJK.
go get github.com/gpdf-dev/gpdf