How do I scale an image proportionally to fit a column?
gpdf already does it. c.Image(bytes) fills the column width and preserves aspect ratio. Use FitWidth or FitHeight for explicit bounds, WithFitMode for the non-default behaviors.
The question, in other words
I have a logo, a chart, or a screenshot — say a 1200×800 PNG — and I want it inside one of my gpdf columns. I do not want to do the aspect-ratio math by hand. I do not want it stretched into an oval. I do not want it overflowing into the next column. Just shrink it to fit, keep it proportional, done.
TL;DR
c.Image(imgBytes)
That is the whole recipe in the most common case. c.Image defaults to FitContain, which scales the image down to the column width while keeping the original aspect ratio. If the image is already smaller than the column, gpdf draws it at its natural size.
Need a smaller bound than the full column? Add template.FitWidth or template.FitHeight:
c.Image(imgBytes, template.FitWidth(document.Mm(40)))
c.Image(imgBytes, template.FitHeight(document.Mm(20)))
Both options preserve the source aspect ratio. You only specify one dimension; gpdf computes the other.
A complete example
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) {
// Narrow column for the logo, bounded to 30mm wide.
r.Col(3, func(c *template.ColBuilder) {
c.Image(logo, template.FitWidth(document.Mm(30)))
})
// Wide column for the chart, default fit fills the available width.
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)
}
}
Two things are happening here. The 3-column logo cell uses FitWidth(30mm) because we want the logo small and consistent regardless of how much room the column has. The 9-column chart cell takes a bare c.Image(chart) because we want the chart to use everything the column will give it. Both stay proportional. Neither needs the source pixel dimensions to be known in code.
What "proportional" actually means in gpdf
Four fit modes exist; one of them is the default and covers maybe 90% of real use:
| Mode | What it does | When to use it |
|---|---|---|
FitContain (default) | Scales down to fit inside the box, preserves aspect, may leave empty space | Logos, charts, screenshots — almost everything |
FitCover | Scales up or down to cover the entire box, preserves aspect, clips overflow | Hero banners, profile photo crops |
FitStretch | Scales to exactly fill the box, distorts aspect | Almost never — usually a bug if you reach for this |
FitOriginal | Renders at the source pixel dimensions converted at 72 DPI | Diagrams that were authored at print resolution and must not be resampled |
FitWidth and FitHeight both pin one dimension and use FitContain for the other. They are the ergonomic shortcut for "I care about width" or "I care about height" — you almost never need to call WithFitMode directly.
The trap people fall into
The mistake we see most often is supplying both a width and a height that don't match the source aspect ratio, then complaining the image looks squished. That happens when you do something like this:
// Don't do this unless you really mean it.
c.Image(img,
template.FitWidth(document.Mm(40)),
template.FitHeight(document.Mm(40)),
)
If your PNG is 1200×800, forcing it into a 40×40 box means one of two things has to give: the aspect ratio (FitStretch behavior) or part of the image (FitCover behavior). The default fit mode is FitContain, so gpdf will keep the aspect and leave one dimension under-filled — the image will be 40mm wide and ~26mm tall, sitting in a 40mm tall slot with empty space below.
The fix is to pick one dimension and trust the math. If you really do need a square crop of a non-square image, you want FitCover, not two competing dimensions:
c.Image(img,
template.FitWidth(document.Mm(40)),
template.FitHeight(document.Mm(40)),
template.WithFitMode(document.FitCover),
)
Pixel size doesn't lie
gpdf reads the intrinsic pixel dimensions out of the PNG or JPEG header before any scaling decision. So a 4000×3000 photo dropped into a 60mm column is not "scaled at the source" — gpdf embeds the full image bytes, and the PDF reader does the resampling at render time. The output PDF will be the same file size as if you had embedded the photo at any other display dimension.
If file size matters more than maximum print quality, downscale the source image with something like image/draw before handing it to gpdf. The library will not silently throw away pixels for you. That choice belongs to you.
What about the layout overflow case?
If a column ends up too narrow at render time — because the page broke unexpectedly, or a table cell shrank to fit content — the default FitContain will gladly scale your logo down to a postage stamp. If that bothers you, set a floor:
c.Image(logo,
template.FitWidth(document.Mm(30)),
template.MinDisplayWidth(document.Mm(20)),
)
MinDisplayWidth tells the layout engine: if you would have to shrink this image below 20mm to make it fit, push it to the next page instead. The image stays legible or it doesn't get drawn — never the worst-of-both middle ground.
Related recipes
- How do I embed a PNG with transparency in gpdf? — same
c.Imageentry point, but with the alpha-channel details - How does the 12-column grid work in gpdf? — what "the column width" actually resolves to
- How do I set column widths in a table? — when the box around your image is a table cell, not a row column
Try gpdf
gpdf is a Go library for generating PDFs. MIT, zero external dependencies, pure-Go image and font handling.
go get github.com/gpdf-dev/gpdf