Todas las publicaciones

¿Cómo uso negrita y cursiva juntas en gpdf?

Basta con pasar template.Bold() y template.Italic() al mismo span. Pero con fuentes TrueType hay que registrar las cuatro variantes: si falta BoldItalic, la búsqueda cae silenciosamente a la familia base.

La pregunta, dicha de otra forma

Quiero que una palabra —o una línea entera— aparezca al mismo tiempo en negrita y en cursiva dentro del PDF. ¿Cómo pongo ambos estilos a la vez y por qué a veces el resultado no se ve ni negrita ni cursiva?

La respuesta corta

Pasa ambas opciones en la misma llamada a c.Text:

c.Text("WARNING", template.Bold(), template.Italic())

gpdf construye el ID de variante Family-BoldItalic y lo busca entre las fuentes registradas. Para las familias Adobe Standard 14 (Helvetica, Courier, Times) esto funciona tal cual: gpdf aliasea internamente -BoldItalic al nombre canónico -BoldOblique y usa las métricas AFM incorporadas. Con una fuente TrueType que registres tú, hay que registrar las cuatro variantes o la búsqueda cae en silencio a la familia base.

Ese segundo punto es donde viven la mayoría de los bugs.

Código funcional (Helvetica, sin registrar fuentes)

package main

import (
    "log"
    "os"

    "github.com/gpdf-dev/gpdf"
    "github.com/gpdf-dev/gpdf/document"
    "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.Text("Regular Helvetica.")
            c.Text("Bold only.", template.Bold())
            c.Text("Italic only.", template.Italic())
            c.Text("Bold and italic.", template.Bold(), template.Italic())
        })
    })

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

Cuatro líneas, cuatro estilos visibles. Ni una sola llamada a WithFont. El PDF resultante referencia Helvetica, Helvetica-Bold, Helvetica-Oblique y Helvetica-BoldOblique como entradas Type 1 no embebidas, y cualquier visor de PDF ya las tiene.

Qué hace gpdf en realidad

El resolver compone el ID de variante a partir de los flags de estilo:

Bold()Italic()ID de variante que se busca
nonoHelvetica
noHelvetica-Bold
noHelvetica-Italic → alias a Helvetica-Oblique
Helvetica-BoldItalic → alias a Helvetica-BoldOblique

El paso del alias es lo único que hace especial a Helvetica. buildFontVariantID siempre emite los sufijos genéricos -Italic / -BoldItalic sin importar la familia; luego el hook de init de Standard 14 apunta Helvetica-Italic a Helvetica-Oblique y Helvetica-BoldItalic a Helvetica-BoldOblique, de modo que las métricas coinciden con lo que dibuja el visor. Courier recibe el mismo tratamiento. Times no necesita alias porque su nombre canónico ya es Times-Italic / Times-BoldItalic.

La trampa: las fuentes TrueType necesitan las cuatro registros

Aquí es donde los documentos CJK se rompen en silencio. Si registras Noto Sans JP pero olvidas una variante, el hueco que falta no pasa por Bold o Italic como consuelo: cae directo a la familia base.

// Parece bien. No lo está.
doc := gpdf.NewDocument(
    gpdf.WithFont("NotoSansJP", regular),
    gpdf.WithFont("NotoSansJP-Bold", bold),
    gpdf.WithDefaultFont("NotoSansJP", 12),
)

// Esto se dibuja en NotoSansJP normal: ni negrita ni cursiva.
c.Text("強調したい", template.Bold(), template.Italic())

La razón está en el resolver. Primero busca NotoSansJP-BoldItalic, falla y cae a exactamente una cosa: la familia base NotoSansJP. No hay paso intermedio que pruebe con la versión en negrita como consuelo. Pediste bold-italic, recibiste regular.

El arreglo es registrar cada variante que vayas a usar:

package main

import (
    "log"
    "os"

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

func main() {
    regular := mustRead("NotoSansJP-Regular.ttf")
    bold := mustRead("NotoSansJP-Bold.ttf")
    italic := mustRead("NotoSansJP-Italic.ttf")
    boldItalic := mustRead("NotoSansJP-BoldItalic.ttf")

    doc := gpdf.NewDocument(
        gpdf.WithPageSize(gpdf.A4),
        gpdf.WithFont("NotoSansJP", regular),
        gpdf.WithFont("NotoSansJP-Bold", bold),
        gpdf.WithFont("NotoSansJP-Italic", italic),
        gpdf.WithFont("NotoSansJP-BoldItalic", boldItalic),
        gpdf.WithDefaultFont("NotoSansJP", 12),
    )

    page := doc.AddPage()
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text("Texto normal")
            c.Text("énfasis", template.Bold(), template.Italic())
        })
    })

    data, _ := doc.Generate()
    os.WriteFile("jp-emphasis.pdf", data, 0o644)
}

func mustRead(path string) []byte {
    b, err := os.ReadFile(path)
    if err != nil { log.Fatal(err) }
    return b
}

De paso: la distribución oficial de Noto Sans JP no trae realmente un corte cursivo (slanted) —la tipografía japonesa apenas usa cursiva—, así que en la práctica la mayoría de los documentos japoneses registran solo regular y bold y no llaman a template.Italic() sobre spans japoneses. Está bien. La regla es: si nunca llamas a Italic() sobre una familia, no necesitas su variante cursiva. La trampa solo aparece cuando llamas a Italic() y no has registrado el archivo.

Mezclar negrita y cursiva en el mismo párrafo

c.Text aplica un único estilo a toda la cadena. Para énfasis en medio de la frase usa c.RichText:

c.RichText(func(rt *template.RichTextBuilder) {
    rt.Span("The ")
    rt.Span("quick brown fox", template.Bold(), template.Italic())
    rt.Span(" jumps over the lazy dog.")
})

Cada rt.Span tiene sus propios flags de estilo y el motor de layout hace los saltos de línea entre spans como lo haría un procesador de texto. Poner Bold() + Italic() en un único Span va al mismo lookup de variante -BoldItalic que c.Text: es el mismo camino en el código.

Un detalle que conviene nombrar: Bold() e Italic() son conmutativas. template.Italic(), template.Bold() y template.Bold(), template.Italic() producen salidas idénticas. Establecen dos campos distintos (FontWeight y FontStyle) sobre el mismo document.Style, así que el orden no importa.

Recetas relacionadas

Prueba gpdf

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

go get github.com/gpdf-dev/gpdf

⭐ Star on GitHub · Leer la documentación