Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![Go Reference](https://pkg.go.dev/badge/github.com/gpdf-dev/gpdf.svg)](https://pkg.go.dev/github.com/gpdf-dev/gpdf)
[![CI](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml/badge.svg)](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml)
![coverage](https://img.shields.io/badge/coverage-86.1%25-green)
![coverage](https://img.shields.io/badge/coverage-85.8%25-green)
[![Go Report Card](https://goreportcard.com/badge/github.com/gpdf-dev/gpdf)](https://goreportcard.com/report/github.com/gpdf-dev/gpdf)
[![Go Version](https://img.shields.io/badge/Go-%3E%3D1.22-blue)](https://go.dev/)
[![Website](https://img.shields.io/badge/Website-gpdf.dev-blue)](https://gpdf.dev/)
Expand Down Expand Up @@ -857,6 +857,7 @@ doc.Render(f)
| Option | Description |
|---|---|
| `template.QRSize(value)` | Set QR code size |
| `template.QRMinSize(value)` | Minimum display size — overflow to next page instead of shrinking below |
| `template.QRErrorCorrection(level)` | Set error correction (L/M/Q/H) |
| `template.QRScale(n)` | Set module scale factor |

Expand Down
1 change: 1 addition & 0 deletions README_es.md
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,7 @@ doc.Render(f)
| Opción | Descripción |
|---|---|
| `template.QRSize(value)` | Tamaño del código QR |
| `template.QRMinSize(value)` | Tamaño mínimo — desborda a la siguiente página en lugar de encogerse por debajo |
| `template.QRErrorCorrection(level)` | Nivel de corrección de errores (L/M/Q/H) |
| `template.QRScale(n)` | Factor de escala del módulo |

Expand Down
1 change: 1 addition & 0 deletions README_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,7 @@ doc.Render(f)
| オプション | 説明 |
|---|---|
| `template.QRSize(value)` | QRコードのサイズを設定 |
| `template.QRMinSize(value)` | 最小表示サイズ — これを下回る場合は次ページへ送る |
| `template.QRErrorCorrection(level)` | 誤り訂正レベルを設定 (L/M/Q/H) |
| `template.QRScale(n)` | モジュールスケール係数を設定 |

Expand Down
1 change: 1 addition & 0 deletions README_ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,7 @@ doc.Render(f)
| 옵션 | 설명 |
|---|---|
| `template.QRSize(value)` | QR 코드 크기 설정 |
| `template.QRMinSize(value)` | 최소 표시 크기 — 이 값 미만이 되면 다음 페이지로 이동 |
| `template.QRErrorCorrection(level)` | 오류 정정 레벨 설정 (L/M/Q/H) |
| `template.QRScale(n)` | 모듈 스케일 팩터 설정 |

Expand Down
1 change: 1 addition & 0 deletions README_pt.md
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,7 @@ merged, _ := gpdf.Merge(
| Opção | Descrição |
|---|---|
| `template.QRSize(value)` | Tamanho do QR code |
| `template.QRMinSize(value)` | Tamanho mínimo — transborda para a próxima página em vez de encolher abaixo |
| `template.QRErrorCorrection(level)` | Nível de correção de erros (L/M/Q/H) |
| `template.QRScale(n)` | Fator de escala do módulo |

Expand Down
1 change: 1 addition & 0 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,7 @@ doc.Render(f)
| 选项 | 说明 |
|---|---|
| `template.QRSize(value)` | 设置二维码大小 |
| `template.QRMinSize(value)` | 最小显示尺寸 — 低于此值时溢出到下一页 |
| `template.QRErrorCorrection(level)` | 设置纠错等级(L/M/Q/H) |
| `template.QRScale(n)` | 设置模块缩放因子 |

Expand Down
55 changes: 55 additions & 0 deletions _examples/builder/34_image_min_size_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package builder_test

import (
"image/color"
"testing"

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

// TestExample_34_ImageMinSize verifies that an image with MinDisplayHeight
// overflows to the next page when the remaining space would force it below
// the configured minimum, instead of being shrunk to fit.
func TestExample_34_ImageMinSize(t *testing.T) {
doc := template.New(
template.WithPageSize(document.A4),
template.WithMargins(document.UniformEdges(document.Mm(20))),
)

page := doc.AddPage()

page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("Image Minimum Display Size", template.FontSize(18), template.Bold())
c.Spacer(document.Mm(5))
})
})

// Fill most of the page so the image's natural height cannot fit.
for i := 0; i < 23; i++ {
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
"Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")
})
})
}

// A tall image (300x500 px → 0.6 aspect ratio). Displayed at 80mm width
// its natural height is ~133mm. The remaining space on page 1 is far
// smaller than the 100mm minimum, so the image must move to page 2.
imgData := testutil.TestImagePNG(t, 300, 500, color.RGBA{R: 100, G: 149, B: 237, A: 255})

page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Image(imgData,
template.FitWidth(document.Mm(80)),
template.MinDisplayHeight(document.Mm(100)),
)
})
})

testutil.GeneratePDFSharedGolden(t, "34_image_min_size.pdf", doc)
}
58 changes: 58 additions & 0 deletions _examples/gotemplate/34_image_min_size_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package gotemplate_test

import (
"encoding/base64"
"image/color"
"testing"

"github.com/gpdf-dev/gpdf/_examples/testutil"
"github.com/gpdf-dev/gpdf/template"
)

// TestTmpl_34_ImageMinSize is the Go-template counterpart of
// TestExample_34_ImageMinSize. It shares the same golden file.
func TestTmpl_34_ImageMinSize(t *testing.T) {
imgData := testutil.TestImagePNG(t, 300, 500, color.RGBA{R: 100, G: 149, B: 237, A: 255})

schema := []byte(`{
"page": {"size": "A4", "margins": "20mm"},
"body": [
{"row": {"cols": [
{"span": 12, "elements": [
{"type": "text", "content": "{{.Title}}", "style": {"size": 18, "bold": true}},
{"type": "spacer", "height": "5mm"}
]}
]}},
{{range .Fillers}}
{"row": {"cols": [
{"span": 12, "elements": [
{"type": "text", "content": "{{.}}"}
]}
]}},
{{end}}
{"row": {"cols": [
{"span": 12, "elements": [
{"type": "image", "image": {"src": "{{.ImgB64}}", "width": "80mm", "minHeight": "100mm"}}
]}
]}}
]
}`)

fillers := make([]string, 23)
for i := range fillers {
fillers[i] = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
"Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
}

data := map[string]any{
"Title": "Image Minimum Display Size",
"Fillers": fillers,
"ImgB64": base64.StdEncoding.EncodeToString(imgData),
}

doc, err := template.FromJSON(schema, data)
if err != nil {
t.Fatalf("FromJSON error: %v", err)
}
testutil.GeneratePDFSharedGolden(t, "34_image_min_size.pdf", doc)
}
53 changes: 53 additions & 0 deletions _examples/json/34_image_min_size_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package json_test

import (
"encoding/base64"
"fmt"
"image/color"
"strings"
"testing"

"github.com/gpdf-dev/gpdf/_examples/testutil"
"github.com/gpdf-dev/gpdf/template"
)

// TestJSON_34_ImageMinSize is the JSON-schema counterpart of
// TestExample_34_ImageMinSize. It shares the same golden file.
func TestJSON_34_ImageMinSize(t *testing.T) {
imgData := testutil.TestImagePNG(t, 300, 500, color.RGBA{R: 100, G: 149, B: 237, A: 255})
imgB64 := base64.StdEncoding.EncodeToString(imgData)

// Build 23 filler rows identical to the Builder/GoTemplate cases.
var filler strings.Builder
for i := 0; i < 23; i++ {
filler.WriteString(`{"row": {"cols": [
{"span": 12, "elements": [
{"type": "text", "content": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."}
]}
]}},`)
}

schema := []byte(fmt.Sprintf(`{
"page": {"size": "A4", "margins": "20mm"},
"body": [
{"row": {"cols": [
{"span": 12, "elements": [
{"type": "text", "content": "Image Minimum Display Size", "style": {"size": 18, "bold": true}},
{"type": "spacer", "height": "5mm"}
]}
]}},
%s
{"row": {"cols": [
{"span": 12, "elements": [
{"type": "image", "image": {"src": "%s", "width": "80mm", "minHeight": "100mm"}}
]}
]}}
]
}`, filler.String(), imgB64))

doc, err := template.FromJSON(schema, nil)
if err != nil {
t.Fatalf("FromJSON error: %v", err)
}
testutil.GeneratePDFSharedGolden(t, "34_image_min_size.pdf", doc)
}
Binary file added _examples/testdata/golden/34_image_min_size.pdf
Binary file not shown.
2 changes: 2 additions & 0 deletions docs/05-elements.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ c.QRCode("HELLO",
"qrcode": {
"data": "https://gpdf.dev",
"size": "30mm",
"minSize": "20mm",
"errorCorrection": "M"
}
}
Expand All @@ -349,6 +350,7 @@ c.QRCode("HELLO",
| Option | Description |
|---|---|
| `QRSize(v)` | Display size (width = height) |
| `QRMinSize(v)` | Minimum display size. When the layout would shrink the QR below this value it overflows to the next page instead of rendering at an unscannable size |
| `QRErrorCorrection(level)` | Error correction: `LevelL`, `LevelM` (default), `LevelQ`, `LevelH` |
| `QRScale(s)` | Pixels per QR module (affects image resolution) |

Expand Down
9 changes: 9 additions & 0 deletions document/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ type Image struct {
DisplayWidth Value
// DisplayHeight is the explicit display height set by template options.
DisplayHeight Value
// MinDisplayWidth prevents the layout engine from shrinking the image
// below this width to fit the remaining space. When the clamped width
// would fall below this value the image is moved to the next page
// instead. UnitAuto (the zero value) preserves the original behavior.
MinDisplayWidth Value
// MinDisplayHeight is the vertical counterpart to MinDisplayWidth.
// When the clamped height would fall below this value the image is
// moved to the next page. UnitAuto preserves the original behavior.
MinDisplayHeight Value
}

// ImageFitMode controls how an image is scaled within its layout bounds.
Expand Down
49 changes: 48 additions & 1 deletion document/layout/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,13 @@ func (bl *BlockLayout) layoutVerticalChild(bc *blockContext, child document.Docu
return bc.overflowResult(placed, cursorY, children[i:]), true
}

// When a child overflows without producing any bounds (e.g. an image
// whose clamped size would violate MinDisplay* constraints), defer the
// entire child to the next page instead of emitting a zero-size placeholder.
if childResult.Overflow != nil && childResult.Bounds.Width == 0 && childResult.Bounds.Height == 0 {
return bc.overflowResult(placed, cursorY, children[i:]), true
}

return childResult, false
}

Expand Down Expand Up @@ -546,7 +553,12 @@ func (bl *BlockLayout) layoutImage(child document.DocumentNode, constraints Cons
}
}

displayW, displayH = clampImageSize(img.FitMode, displayW, displayH, aspectRatio, constraints)
clampedW, clampedH := clampImageSize(img.FitMode, displayW, displayH, aspectRatio, constraints)
if shouldOverflowImageForMinimum(displayW, clampedW, img.MinDisplayWidth, constraints.AvailableWidth) ||
shouldOverflowImageForMinimum(displayH, clampedH, img.MinDisplayHeight, constraints.AvailableHeight) {
return Result{Overflow: img}
}
displayW, displayH = clampedW, clampedH

return Result{
Bounds: document.Rectangle{
Expand Down Expand Up @@ -621,6 +633,41 @@ func clampImageSize(fitMode document.ImageFitMode, w, h, aspectRatio float64, co
return w, h
}

// shouldOverflowImageForMinimum reports whether shrinking an image to fit
// the available space would violate a configured minimum display dimension.
// When true, the caller should move the image to the next page instead of
// rendering it at a shrunken size.
func shouldOverflowImageForMinimum(original, clamped float64, minimum document.Value, available float64) bool {
resolvedMinimum, ok := resolveImageMinimum(minimum, available)
if !ok {
return false
}
if !wasImageShrunk(original, clamped) {
return false
}
// Only overflow when the original size would have honored the minimum.
// This prevents an infinite loop when the minimum itself exceeds the
// full page's available space.
return original >= resolvedMinimum && clamped < resolvedMinimum
}

// resolveImageMinimum converts a configured minimum image dimension into
// points and reports whether a usable minimum was provided.
func resolveImageMinimum(minimum document.Value, available float64) (float64, bool) {
const defaultFontSize = 12.0
if minimum.Unit == document.UnitAuto || minimum.Amount <= 0 {
return 0, false
}
return minimum.Resolve(available, defaultFontSize), true
}

// wasImageShrunk reports whether clamping reduced a dimension by more than
// a small floating-point tolerance.
func wasImageShrunk(original, clamped float64) bool {
const shrinkEpsilon = 1e-6
return clamped < original-shrinkEpsilon
}

// resolveBorderWidths extracts the four border widths as resolved edges.
func resolveBorderWidths(border document.BorderEdges, parentWidth, fontSize float64) document.ResolvedEdges {
var top, right, bottom, left float64
Expand Down
2 changes: 1 addition & 1 deletion internal/buildinfo/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ package buildinfo

// Version is the library version. It is the single source of truth used by
// the public gpdf.Version constant and the default PDF Producer metadata.
const Version = "1.0.5"
const Version = "1.0.6"
4 changes: 4 additions & 0 deletions schema/gpdf.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,10 @@
"$ref": "#/$defs/dimension",
"description": "QR code size (width and height)."
},
"minSize": {
"$ref": "#/$defs/dimension",
"description": "Minimum display size. If the layout would shrink the QR code below this value it overflows to the next page instead."
},
"errorCorrection": {
"type": "string",
"description": "Error correction level. L=~7%, M=~15% (default), Q=~25%, H=~30%.",
Expand Down
Loading
Loading