全部文章

如何在 gpdf 中按比例缩放图片以适配列宽?

把字节传给 c.Image 即可。gpdf 默认按列宽等比缩放。需要明确尺寸时再用 FitWidth / FitHeight。

换个说法的问题

我有一个 logo、图表或截图 —— 比如 1200×800 的 PNG —— 想放进 gpdf 的某一列里。我不想手算宽高比,不想让它被拉成椭圆,也不想让它溢出到下一列。等比缩小到合适大小,搞定,就这些。

TL;DR

c.Image(imgBytes)

最常见的情况下,这就是全部。c.Image 默认是 FitContain,会按列宽等比缩放,保持原始宽高比。如果图片本身就比列窄,gpdf 就按原尺寸画出来。

需要比整列更小的边界?加 template.FitWidthtemplate.FitHeight

c.Image(imgBytes, template.FitWidth(document.Mm(40)))
c.Image(imgBytes, template.FitHeight(document.Mm(20)))

两个选项都保持原宽高比。只指定一个维度,另一个由 gpdf 计算。

完整示例

package main

import (
    "log"
    "os"

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

func main() {
    logo, err := os.ReadFile("logo.png")
    if err != nil {
        log.Fatal(err)
    }
    chart, err := os.ReadFile("chart.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) {
        // logo 用窄列,固定 30mm 宽
        r.Col(3, func(c *template.ColBuilder) {
            c.Image(logo, template.FitWidth(document.Mm(30)))
        })
        // chart 用宽列,默认填满可用宽度
        r.Col(9, func(c *template.ColBuilder) {
            c.Image(chart)
        })
    })

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

3 列宽的 logo 单元格用 FitWidth(30mm),因为我们想让 logo 不论列宽多少都保持小而一致。9 列宽的 chart 单元格只用 c.Image(chart),让它充分利用列内可用宽度。两个都按比例缩放,代码里都不需要知道源图的像素数。

gpdf 中的"等比"到底指什么

总共 4 种 fit 模式,1 种是默认,覆盖大约 90% 的实际需求:

模式行为适用场景
FitContain (默认)按比例缩小适配 box,保持宽高比,可能在某一维留白logo、图表、截图 —— 几乎全部
FitCover按比例放大或缩小完全覆盖 box,裁掉溢出hero 横幅、头像方形裁剪
FitStretch完全填满 box,会扭曲宽高比几乎不用,用了通常是 bug
FitOriginal按 72 DPI 换算后的源像素尺寸渲染按印刷分辨率制作、不能重采样的图

FitWidthFitHeight 都是固定一维、另一维用 FitContain 算。它们是"我关心宽度"或"我关心高度"的语法糖 —— 几乎不用直接调 WithFitMode

容易踩的坑

最常见的错误是同时给宽和高,且和源宽高比不匹配,然后抱怨图被压扁。比如这种写法:

// 除非你真的想这么做,不要写这样的代码
c.Image(img,
    template.FitWidth(document.Mm(40)),
    template.FitHeight(document.Mm(40)),
)

如果 PNG 是 1200×800,硬塞进 40×40 的 box,要么牺牲宽高比 (FitStretch 行为),要么牺牲一部分图像 (FitCover 行为)。默认 fit 模式是 FitContain,所以 gpdf 会保持比例、让某一维填不满 —— 图片会变成 40mm 宽、约 26mm 高,下方空着 14mm。

修法是只指定一维、信任计算。如果真的需要把非方形图裁成方的,用 FitCover,而不是两个互相矛盾的尺寸:

c.Image(img,
    template.FitWidth(document.Mm(40)),
    template.FitHeight(document.Mm(40)),
    template.WithFitMode(document.FitCover),
)

像素数不会骗人

gpdf 在做缩放决策前会从 PNG/JPEG header 读取原始像素尺寸。所以把 4000×3000 的照片塞进 60mm 列里,并不是"在源端缩小"—— gpdf 把完整字节嵌入 PDF,重采样在 PDF 阅读器渲染时做。无论你把显示尺寸改成多少,输出 PDF 的字节数都一样。

如果文件大小比印刷质量更重要,先用 image/draw 把源图缩小,再交给 gpdf。库不会替你悄悄丢像素 —— 这个选择留给调用方。

应对布局溢出

如果列在渲染时变得比预期窄 —— 比如分页意外断开,或表格单元格收紧适配内容 —— 默认的 FitContain 会乐意把 logo 缩成邮票大小。不想这样,就设个下限:

c.Image(logo,
    template.FitWidth(document.Mm(30)),
    template.MinDisplayWidth(document.Mm(20)),
)

MinDisplayWidth 告诉布局引擎:如果要把图缩到 20mm 以下才能放下,那就别画在这一页,推到下一页。要么图片清晰可读,要么不画 —— 不会有"两边都不讨好"的中间态。

相关菜谱

试试 gpdf

gpdf 是一个 Go 的 PDF 生成库。MIT 协议、零外部依赖、纯 Go 处理图像和字体。

go get github.com/gpdf-dev/gpdf

⭐ 在 GitHub 上 Star · 阅读文档