Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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