Todas las publicaciones

¿Cómo incrusto un PNG con transparencia en gpdf?

Pasa los bytes del PNG a c.Image. gpdf decodifica el canal alfa en un objeto SMask de PDF y los fondos transparentes se renderizan correctamente.

La pregunta, dicho de otra forma

Tengo un logo o un sello guardado como PNG con fondo transparente — logo.png, RGBA, lo típico que exporta Photoshop o Figma. Cuando lo incrusto en un PDF generado por gpdf, ¿la zona transparente se queda transparente y mi color de página se ve a través? ¿O voy a acabar con un cuadrado blanco alrededor del logo?

La respuesta rápida

Pasa los bytes del PNG a c.Image y nada más. gpdf decodifica el canal alfa y escribe un objeto SMask (máscara suave) de PDF junto a la imagen. Los píxeles transparentes se renderizan como transparentes.

logo, _ := os.ReadFile("logo.png")
c.Image(logo, template.FitWidth(document.Mm(40)))

Esa es toda la receta. No aplanas el alfa contra un fondo blanco. No conviertes RGBA a RGB. No pasas ninguna opción para "activar la transparencia". El PNG sigue siendo PNG hasta llegar al PDF.

Un ejemplo completo que puedes ejecutar

Para ver el alfa funcionando de verdad necesitas algo bajo el PNG. Una marca de agua sobre el texto del cuerpo es el caso canónico — page.Absolute clava el logo en coordenadas fijas mientras el contenido en flujo normal llena la página debajo.

package main

import (
    "log"
    "os"

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

func main() {
    stamp, err := os.ReadFile("draft-stamp.png")
    if err != nil {
        log.Fatal(err)
    }

    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("Informe trimestral — Q1 2026", template.FontSize(20), template.Bold())
            c.Text("Los ingresos crecieron un 38% interanual, impulsados por renovaciones empresariales y tres nuevos clientes en servicios financieros. El margen operativo se expandió al 24% al estabilizarse el gasto en infraestructura.")
            c.Text("La plantilla cerró el trimestre en 142 personas, frente a 128 al cierre del Q4. Ingeniería representó 9 de las 14 incorporaciones netas.")
        })
    })

    page.Absolute(document.Mm(60), document.Mm(120), func(c *template.ColBuilder) {
        c.Image(stamp, template.FitWidth(document.Mm(80)))
    })

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

El sello "DRAFT" es un PNG RGBA con letras rojas en negrita y fondo completamente transparente. Cuando aterriza encima del texto del cuerpo, cada píxel transparente revela el párrafo de abajo. Sustituye draft-stamp.png por cualquier logo, sello o imagen de firma — misma ruta de código, mismo tratamiento del SMask.

Lo que gpdf hace de verdad con el PNG

La parte interesante está en el lado del writer. PDF no tiene un objeto único de "imagen RGBA". Tiene un objeto de imagen RGB más una imagen SMask (máscara suave) opcional en escala de grises, donde cada píxel de la máscara es el valor alfa del píxel correspondiente de la imagen principal. El lector de PDF las compone en tiempo de render.

Cuando le pasas un PNG a gpdf, el renderer (document/render/pdftarget.go) recorre la malla de píxeles una vez:

  • 24 bits de RGB van al stream principal de la imagen, comprimidos con FlateDecode.
  • 8 bits de alfa van a un stream SMask separado, también con FlateDecode.
  • El diccionario de la imagen recibe /SMask <ref> apuntando al stream alfa.

Si todas las muestras alfa terminan siendo 0xFF (totalmente opacas), gpdf descarta el buffer de alfa y se salta la escritura del SMask. Así, un PNG opaco al estilo JPEG no te cuesta nada extra en la salida. El coste solo aparece cuando el alfa hace trabajo real.

Toda la ruta es Go puro — image/png de la biblioteca estándar hace la decodificación y compress/flate se encarga de la compresión. Sin CGO, sin dependencia de libpng. Compilar de macOS a linux/arm64 para una Lambda sigue produciendo un binario estático.

La trampa del JPEG

Si tu logo "transparente" salió de una herramienta como JPEG, la transparencia ya no existe antes de que gpdf vea el archivo. JPEG no puede llevar canal alfa. La herramienta que exportó ha aplanado el alfa contra el color de fondo que le pareciera, normalmente blanco.

c.Image(jpegBytes) funciona bien, pero la imagen incrustada tendrá un rectángulo opaco blanco (o negro, o rosa) donde antes había píxeles transparentes. La solución está aguas arriba: re-exporta como PNG. No hay opción en gpdf que recupere la transparencia perdida en el JPEG.

Lo mismo aplica al "PNG-8" con transparencia de paleta. El decoder de gpdf usa el image/png estándar de Go, que maneja correctamente los PNG de paleta, así que ese caso funciona. Pero si tu pipeline de activos pasó por un paso JPEG sin querer, los datos ya están perdidos.

Tamaño y marcas de agua

Dos extensiones prácticas cubren la mayoría de casos reales.

Escalar el logo: pasa template.FitWidth(document.Mm(40)) o template.FitHeight(document.Mm(20)). El PNG se decodifica en su resolución completa y luego se escala en tiempo de render con la transformación de coordenadas de PDF — no hay paso de remuestreo sobre el alfa. Bordes nítidos en cualquier caso.

Marcas de agua diagonales "DRAFT": cuece la marca de agua como un PNG con alfa tenue (en torno al 25–40%) y suéltala en la página con page.Absolute, igual que el ejemplo de arriba con el sello. Como el alfa es por píxel, puedes variar la opacidad dentro de la marca — desvanecimientos en gradiente, rellenos semitransparentes alrededor de líneas sólidas del logo. El lector de PDF la compone correctamente con el texto debajo.

Si necesitas un overlay con un 30% de opacidad pixel-perfect, esa es una decisión de horneado del alfa en tu editor de imagen. gpdf reproduce los valores alfa que recibe; no ofrece un multiplicador de opacidad por imagen en la API del builder.

Sanity check de tamaño de archivo

PNG con alfa → stream RGB + stream SMask en escala de grises significa alrededor de un 33% más de bytes que la misma imagen sin alfa. Una incrustación de PNG opaco de 100 KB pasa a unos 133 KB con el canal alfa adjunto. Para un logo es invisible. Para un informe de 50 páginas con marca de agua en cada página también es invisible — el SMask se registra una vez y se referencia desde cada página, no se duplica.

Si una sola imagen empieza a costarte megabytes, es el PNG fuente, no la codificación de gpdf. Pásalo por pngquant o oxipng antes de incrustar. El canal alfa sobrevive a ambos.

Recetas relacionadas

Prueba gpdf

gpdf es una librería Go para generar PDFs. Licencia MIT, cero dependencias externas, manejo de PNG y TrueType en Go puro.

go get github.com/gpdf-dev/gpdf

⭐ Star on GitHub · Read the docs