全部文章

如何在 gpdf 中嵌入带透明度的 PNG?

把 PNG 字节直接传给 c.Image。gpdf 会把 alpha 通道解码成 PDF 的 SMask 对象,透明背景能正确渲染出来。

换个说法的问题

我有一个 logo 或印章,存成了背景透明的 PNG —— 就是 Photoshop、Figma 通常导出的那种 RGBA PNG。把它嵌进 gpdf 生成的 PDF 时,透明区域会保持透明吗?还是 logo 周围会出现一个白色矩形?

一句话回答

把 PNG 字节传给 c.Image 就够了,不需要其他设置。gpdf 会解码 alpha 通道,并和图像一起写出一个 PDF SMask (软掩码) 对象。透明像素会被正确渲染成透明。

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

整个食谱就是这个。不用先把 alpha 平铺到白色背景上,不用把 RGBA 转成 RGB,也不用传什么"启用透明度"的选项。 PNG 还是 PNG,一路保留到 PDF。

一段可以直接跑的完整代码

要让透明度真的可见,PNG 下面得有东西能透出来。在正文上盖水印是最经典的场景 —— page.Absolute 把 logo 钉在固定坐标,正常流式内容在它下方铺满页面。

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("季度报告 — 2026 年 Q1", template.FontSize(20), template.Bold())
            c.Text("第一季度营收同比增长 38%,主要来自企业客户续约和金融服务行业 3 个新客户的签约。基础设施支出趋于平稳,营业利润率扩张到 24%。")
            c.Text("季末员工总数 142 人,比上季度末 128 人净增 14 人,其中工程团队招聘 9 人。")
        })
    })

    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)
    }
}

draft-stamp.png 想象成一张红色加粗的 "DRAFT" 字样、背景完全透明的 RGBA PNG。盖在正文上方时,每个透明像素都会让下面的段落透出来。把 draft-stamp.png 换成任何 logo、印章或签名图都行 —— 同一条代码路径,同一套 SMask 处理。

gpdf 对 PNG 实际做了什么

有意思的部分在 writer 这一侧。PDF 没有"RGBA 图像"这种单一对象,而是一个 RGB 图像加一个可选的灰度 SMask (软掩码) 图像:mask 上每个像素的取值就是主图像对应像素的 alpha 值,渲染时由 PDF 阅读器合成。

把 PNG 交给 gpdf 后,渲染器 (document/render/pdftarget.go) 对像素网格走一遍:

  • 24 bit RGB 进入主图像流,用 FlateDecode 压缩
  • 8 bit alpha 进入单独的 SMask 流,同样用 FlateDecode 压缩
  • 图像字典里加上 /SMask <ref> 指向 alpha 流

如果所有 alpha 采样最终都是 0xFF (完全不透明),gpdf 会丢掉 alpha 缓冲并跳过 SMask 的写入。所以一张 JPEG 风格的不透明 PNG 不会带来任何额外开销,只有 alpha 真正在工作时才会付出成本。

整条路径都是 pure Go —— 标准库的 image/png 负责解码,compress/flate 负责压缩。没有 CGO,也不依赖 libpng。从 macOS 交叉编译到 linux/arm64 (比如 Lambda) 仍然产出一个静态二进制。

JPEG 陷阱

如果你那张"透明" logo 被某个工具导出成了 JPEG,透明度在 gpdf 看到这个文件之前就已经丢了。JPEG 不能携带 alpha 通道,导出工具会用某个背景色 (通常是白色) 把 alpha 平铺掉。

c.Image(jpegBytes) 仍然能工作,但嵌入的图像在原本透明的位置会留下不透明的白色矩形 (有时是黑色、粉色)。修复要往上游走 —— 重新导出成 PNG。gpdf 没有任何选项能把 JPEG 里丢失的透明度恢复回来

"PNG-8"的调色板透明度是另外一回事,gpdf 用的是 Go 标准库 image/png,调色板 PNG 它能正确处理。问题出在资源管线中途意外走了一遍 JPEG —— 数据丢了就丢了。

缩放和水印

实用扩展主要两个。

缩放 logo: 传 template.FitWidth(document.Mm(40))template.FitHeight(document.Mm(20))。PNG 按全分辨率解码,渲染时用 PDF 的坐标变换缩放 —— alpha 不会被重新采样。边缘依然清晰。

对角"DRAFT"水印: 把水印做成 alpha 较弱 (25–40% 左右) 的 PNG,再用上面例子里的 page.Absolute 放上去。因为 alpha 是逐像素的,水印内部的不透明度可以变化 —— 渐变淡出、logo 实线周围半透明填充等等。PDF 阅读器会和下面的文字正确合成。

如果你需要像素级精确的 30% 不透明度叠加,那是图像编辑器一侧的 alpha 烘焙决策。gpdf 只会忠实复现它接收到的 alpha 值,Builder API 中没有提供按图像设置整体不透明度的选项。

文件大小心算

带 alpha 的 PNG → RGB 流 + 灰度 SMask 流,意味着比不带 alpha 的版本大约多 33%。100 KB 的不透明 PNG 嵌进去会变成约 133 KB。一张 logo 完全感觉不到差别。一份 50 页的报告每页都加水印也感觉不到 —— SMask 只注册一次,每页都引用它,不会重复。

如果一张图突然占用好几 MB,问题在原始 PNG 而不在 gpdf 的编码。先让它过一遍 pngquantoxipng 再嵌入,alpha 通道在两种工具下都不会丢。

相关菜谱

试试 gpdf

gpdf 是一个 Go 的 PDF 生成库。MIT 协议、零依赖、PNG 和 TrueType 都用 pure Go 处理。

go get github.com/gpdf-dev/gpdf

⭐ Star on GitHub · Read the docs