如何在 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 的编码。先让它过一遍 pngquant 或 oxipng 再嵌入,alpha 通道在两种工具下都不会丢。
相关菜谱
- 如何在 gpdf 中嵌入日文字体? —— 同样是"直接传字节"的模式,只不过对象是 TrueType
- 用 Go 50 行以内生成发票 PDF —— 透明公司 logo 在真实文档里通常落在哪
- 为什么 gpdf 比其他 Go PDF 库快 10–30 倍 —— pure Go 的解码路径在微秒维度耗费了什么、又节省了什么
试试 gpdf
gpdf 是一个 Go 的 PDF 生成库。MIT 协议、零依赖、PNG 和 TrueType 都用 pure Go 处理。
go get github.com/gpdf-dev/gpdf