Skip to content
Open
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: 3 additions & 0 deletions docs/v2/features/text.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@ Text can be created as a standalone `Component`, wrapped directly into a `Col`,
| `BreakLineStrategy` | `breakline.Strategy` | `EmptySpaceStrategy` | `EmptySpaceStrategy` breaks on spaces; `DashStrategy` breaks mid-word with a hyphen |
| `VerticalPadding` | `float64` | `0` | Extra spacing between lines (mm) |
| `Hyperlink` | `*string` | `nil` | URL — makes the text a clickable link (rendered in blue) |
| `Rotation` | `float64` | `0` | Rotation angle in degrees. Positive values rotate counter-clockwise, negative values clockwise. The cell auto-expands vertically to contain the rotated bounding box. |
| `RotationPivot` | `rotationpivot.Pivot` | `{Center, Middle}` | Anchor point of the rotation. `Horizontal`: `Start`, `Center`, `End`. `Vertical`: `Top`, `Middle`, `Bottom`. For multi-line text the vertical axis applies to the whole block. |

## Usage notes

- When `Hyperlink` is set, the text color is overridden with blue regardless of `Color`.
- `Top` and `Left`/`Right` are clamped to the cell dimensions if they exceed it.
- `BreakLineStrategy` only applies when the text does not fit on a single line.
- For justified text on the last line, spacing may revert to default space width to avoid stretching a few characters across the full width.
- `Rotation` is purely additive: `Rotation == 0` produces byte-identical PDFs to before the feature existed. When set, `GetHeight` returns the rotated bounding box height (`w·|sinθ| + h·|cosθ|`) so the layout reserves enough vertical space; the rendered text baseline is shifted so the rotated bounding box sits inside the cell for any combination of pivot and angle sign.

## GoDoc
* [constructor : New](https://pkg.go.dev/github.com/johnfercher/maroto/v2/pkg/components/text#New)
Expand Down
4 changes: 4 additions & 0 deletions internal/providers/gofpdf/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ func (g *provider) GetLinesQuantity(text string, textProp *props.Text, colWidth
return g.text.GetLinesQuantity(text, textProp, colWidth)
}

func (g *provider) GetStringWidth(text string, textProp *props.Text) float64 {
return g.text.GetStringWidth(text, textProp)
}

func (g *provider) GetFontHeight(prop *props.Font) float64 {
return g.font.GetHeight(prop.Family, prop.Style, prop.Size)
}
Expand Down
96 changes: 86 additions & 10 deletions internal/providers/gofpdf/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gofpdf

import (
"fmt"
"math"
"strings"
"unicode"
"unicode/utf8"
Expand All @@ -10,6 +11,7 @@ import (
"github.com/johnfercher/maroto/v2/pkg/consts/align"
"github.com/johnfercher/maroto/v2/pkg/consts/breakline"
"github.com/johnfercher/maroto/v2/pkg/consts/fontfamily"
"github.com/johnfercher/maroto/v2/pkg/consts/rotationpivot"
"github.com/johnfercher/maroto/v2/pkg/core"
"github.com/johnfercher/maroto/v2/pkg/core/entity"
"github.com/johnfercher/maroto/v2/pkg/props"
Expand Down Expand Up @@ -71,20 +73,87 @@ func (s *Text) Add(text string, cell *entity.Cell, textProp *props.Text) {
unicodeText := s.textToUnicode(text, textProp)
stringWidth := s.pdf.GetStringWidth(unicodeText)

// If should add one line
// Determine the lines up-front so multi-line rotation can pivot around the
// whole block, not just the first line.
var lines []string
if stringWidth <= width {
s.addLine(textProp, x, width, y, stringWidth, unicodeText)
s.font.SetColor(originalColor)
return
lines = []string{unicodeText}
} else if textProp.BreakLineStrategy == breakline.EmptySpaceStrategy {
lines = s.getLinesBreakingLineFromSpace(strings.Split(unicodeText, " "), width)
} else {
lines = s.getLinesBreakingLineWithDash(unicodeText, width)
}

var lines []string
// Rotation honours both axes of textProp.RotationPivot. The baseline of
// the first line is shifted so the rotated bounding box of the whole
// (multi-line) block sits inside the Text.GetHeight-expanded cell.
if textProp.Rotation != 0 {
marginLeft, marginTop, _, _ := s.pdf.GetMargins()
n := float64(len(lines))
textHeight := n*fontHeight + (n-1)*textProp.VerticalPadding
blockWidth := stringWidth
if blockWidth > width {
blockWidth = width
}

if textProp.BreakLineStrategy == breakline.EmptySpaceStrategy {
words := strings.Split(unicodeText, " ")
lines = s.getLinesBreakingLineFromSpace(words, width)
} else {
lines = s.getLinesBreakingLineWithDash(unicodeText, width)
var alignOffsetX float64
switch textProp.Align {
case align.Center:
alignOffsetX = (width - blockWidth) / 2
case align.Right:
alignOffsetX = width - blockWidth
}
if alignOffsetX < 0 {
alignOffsetX = 0
}

var pivotOffsetX float64
switch textProp.RotationPivot.Horizontal {
case rotationpivot.Start:
pivotOffsetX = 0
case rotationpivot.End:
pivotOffsetX = blockWidth
default: // Center
pivotOffsetX = blockWidth / 2
}
var pivotOffsetY float64
switch textProp.RotationPivot.Vertical {
case rotationpivot.Top:
pivotOffsetY = 0
case rotationpivot.Bottom:
pivotOffsetY = textHeight
default: // Middle
pivotOffsetY = textHeight / 2
}

rad := textProp.Rotation * math.Pi / 180
sin, cos := math.Sin(rad), math.Cos(rad)
// Distance any rotated corner rises above the pivot. Corners relative
// to the pivot are TL=(-px,-py), TR=(W-px,-py), BR=(W-px,H-py), BL=(-px,H-py);
// rotated y is -dx*sin + dy*cos, so -y = dx*sin - dy*cos. Take the max.
px, py := pivotOffsetX, pivotOffsetY
W, H := blockWidth, textHeight
upExtent := math.Max(0, math.Max(py*cos-px*sin,
math.Max((W-px)*sin+py*cos,
math.Max((W-px)*sin-(H-py)*cos,
-px*sin-(H-py)*cos))))

contentHeight := cell.Height - textProp.Top - textProp.Bottom
if contentHeight > textHeight {
// place the rotated bbox top at the cell content top
y = cell.Y + textProp.Top + upExtent + fontHeight - pivotOffsetY
}
pivotX := x + alignOffsetX + pivotOffsetX + marginLeft
pivotY := y + (pivotOffsetY - fontHeight) + marginTop
s.pdf.TransformBegin()
s.pdf.TransformRotate(textProp.Rotation, pivotX, pivotY)
defer s.pdf.TransformEnd()
}

if len(lines) == 1 {
s.addLine(textProp, x, width, y, stringWidth, lines[0])
s.font.SetColor(originalColor)
return
}

accumulateOffsetY := 0.0
Expand All @@ -99,6 +168,13 @@ func (s *Text) Add(text string, cell *entity.Cell, textProp *props.Text) {
s.font.SetColor(originalColor)
}

// GetStringWidth returns the rendered width of the text after font selection
// and unicode translation.
func (s *Text) GetStringWidth(text string, textProp *props.Text) float64 {
s.font.SetFont(textProp.Family, textProp.Style, textProp.Size)
return s.pdf.GetStringWidth(s.textToUnicode(text, textProp))
}

// GetLinesQuantity retrieve the quantity of lines which a text will occupy to avoid that text to extrapolate a cell.
func (s *Text) GetLinesQuantity(text string, textProp *props.Text, colWidth float64) int {
s.font.SetFont(textProp.Family, textProp.Style, textProp.Size)
Expand Down
Loading