All posts

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:

ModeWhat it doesWhen to use it
FitContain (default)Scales down to fit inside the box, preserves aspect, may leave empty spaceLogos, charts, screenshots — almost everything
FitCoverScales up or down to cover the entire box, preserves aspect, clips overflowHero banners, profile photo crops
FitStretchScales to exactly fill the box, distorts aspectAlmost never — usually a bug if you reach for this
FitOriginalRenders at the source pixel dimensions converted at 72 DPIDiagrams 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.

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

⭐ Star on GitHub · Read the docs