goext/wpdf/wpdfTable.go

342 lines
8.6 KiB
Go
Raw Normal View History

2024-08-07 13:57:29 +02:00
package wpdf
import (
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/rext"
"regexp"
"strconv"
)
// Column specifier:
//
// - `{number}`: Use this amount of space
// - `auto`: Use the needed space for the content
// - `*` / `fr`: Use the remaining space, evenly distributed, shrink down to auto
// - `{num}fr` / `{num}*`: Use the remaining space, evenly distributed (weighted), shrink down to auto
//
// # TableBuilder
// - PadX/PadY: Padding between cells
// - DefaultStyle: Default style for cells
//
// # TableCellStyleOpt
// - MultiCell: Use wpdf.MultiCell() instead of wpdf.Cell() --> supports linebreaks
// - Ellipsize: Ellipsize text if too long
// - PaddingHorz: Additional horizontal padding inside of cell to space text around
// - PDFCellOpt: Normal styling options (evtl not all are supported, depending on MultiCell: true/false)
var regexTableColumnSpecFr = rext.W(regexp.MustCompile(`^(?P<num>[0-9]*)(fr|\*)$`))
type TableBuilder struct {
builder *WPDFBuilder
padx float64
pady float64
rows []tableRow
defaultCellStyle *TableCellStyleOpt
columnWidths *[]string
2024-08-07 15:34:06 +02:00
debug *bool
2024-08-07 13:57:29 +02:00
}
func (r tableRow) maxFontSize(defaultFontSize float64) float64 {
mfs := defaultFontSize
for _, cell := range r.cells {
if cell.Style.fontSizeOverride != nil {
mfs = max(mfs, *cell.Style.fontSizeOverride)
}
}
return mfs
}
func (b *TableBuilder) Widths(v ...string) *TableBuilder {
b.columnWidths = &v
return b
}
2024-08-07 19:35:23 +02:00
func (b *TableBuilder) DefaultStyle(s *TableCellStyleOpt) *TableBuilder {
b.defaultCellStyle = s
2024-08-07 13:57:29 +02:00
return b
}
func (b *TableBuilder) PadX(v float64) *TableBuilder {
b.padx = v
return b
}
func (b *TableBuilder) PadY(v float64) *TableBuilder {
b.pady = v
return b
}
2024-08-07 15:34:06 +02:00
func (b *TableBuilder) AddRow(cells ...TableCell) *TableBuilder {
2024-08-07 13:57:29 +02:00
b.rows = append(b.rows, tableRow{cells: cells})
2024-08-07 15:34:06 +02:00
return b
2024-08-07 13:57:29 +02:00
}
2024-08-07 19:30:38 +02:00
func (b *TableBuilder) AddRowWithStyle(style *TableCellStyleOpt, cells ...string) *TableBuilder {
2024-08-07 13:57:29 +02:00
tcels := make([]TableCell, 0, len(cells))
for _, cell := range cells {
2024-08-07 19:30:38 +02:00
tcels = append(tcels, TableCell{Content: cell, Style: *style})
2024-08-07 13:57:29 +02:00
}
b.rows = append(b.rows, tableRow{cells: tcels})
2024-08-07 15:34:06 +02:00
return b
2024-08-07 13:57:29 +02:00
}
2024-08-07 15:34:06 +02:00
func (b *TableBuilder) AddRowDefaultStyle(cells ...string) *TableBuilder {
2024-08-07 13:57:29 +02:00
tcels := make([]TableCell, 0, len(cells))
for _, cell := range cells {
tcels = append(tcels, TableCell{Content: cell, Style: langext.Coalesce(b.defaultCellStyle, TableCellStyleOpt{})})
}
b.rows = append(b.rows, tableRow{cells: tcels})
2024-08-07 15:34:06 +02:00
return b
2024-08-07 13:57:29 +02:00
}
2024-08-07 19:30:38 +02:00
func (b *TableBuilder) BuildRow() *TableRowBuilder {
return &TableRowBuilder{tabbuilder: b, cells: make([]TableCell, 0)}
}
2024-08-07 13:57:29 +02:00
func (b *TableBuilder) Build() {
builder := b.builder
2024-08-07 15:34:06 +02:00
debug := langext.Coalesce(b.debug, b.builder.debug)
2024-08-07 13:57:29 +02:00
if len(b.rows) == 0 {
return // nothing to do
}
_, pageHeight := builder.FPDF().GetPageSize()
pbEnabled, pbMargin := builder.FPDF().GetAutoPageBreak()
builder.FPDF().SetAutoPageBreak(false, 0) // manually handle pagebreak in tables
defer func() { builder.FPDF().SetAutoPageBreak(pbEnabled, pbMargin) }()
columnWidths := b.calculateColumns()
columnCount := len(columnWidths)
for i, dat := range b.rows {
if len(dat.cells) != columnCount {
builder.FPDF().SetError(exerr.New(exerr.TypeInternal, "data must have the same length as header").Int("idx", i).Build())
return
}
}
defaultFontSize, _ := builder.FPDF().GetFontSize()
for rowIdx, row := range b.rows {
nextY := builder.GetY()
for cellIdx, cell := range row.cells {
str := cell.Content
style := cell.Style
2024-08-07 19:30:38 +02:00
ellipsize := langext.Coalesce(style.ellipsize, true)
cellPaddingHorz := langext.Coalesce(style.paddingHorz, 2)
2024-08-07 13:57:29 +02:00
2024-08-07 19:44:45 +02:00
fillHeight := langext.Coalesce(style.fillHeight, false)
2024-08-07 13:57:29 +02:00
bx := builder.GetX()
by := builder.GetY()
cellWidth := columnWidths[cellIdx]
2024-08-07 19:44:45 +02:00
_ = fillHeight // TODO implement, but how?? ( cells with fillHeight=true should have a border of the full column height, even if another column is growing it, but we do not know teh height beforehand ... )
2024-08-07 19:30:38 +02:00
if langext.Coalesce(style.multiCell, true) {
2024-08-07 13:57:29 +02:00
2024-08-07 15:34:06 +02:00
builder.MultiCell(str, style.PDFCellOpt.Copy().ToMulti().Width(cellWidth).Debug(debug))
2024-08-07 13:57:29 +02:00
} else {
if ellipsize {
if builder.GetStringWidth(str, style.PDFCellOpt) > (cellWidth - cellPaddingHorz) {
for builder.GetStringWidth(str+"...", style.PDFCellOpt) > (cellWidth-cellPaddingHorz) && len(str) > 0 {
str = str[:len(str)-1]
}
str += "..."
}
}
2024-08-07 15:34:06 +02:00
builder.Cell(str, style.PDFCellOpt.Copy().Width(cellWidth).Debug(debug))
2024-08-07 13:57:29 +02:00
}
nextY = max(nextY, builder.GetY())
builder.SetXY(bx+cellWidth+b.padx, by)
}
builder.SetY(nextY + b.pady)
if rowIdx < len(b.rows)-1 && pbEnabled && (builder.GetY()+b.rows[rowIdx+1].maxFontSize(defaultFontSize)) > (pageHeight-pbMargin) {
builder.FPDF().AddPage()
}
}
}
func (b *TableBuilder) calculateColumns() []float64 {
pageWidthTotal, _ := b.builder.FPDF().GetPageSize()
marginLeft, _, marginRight, _ := b.builder.FPDF().GetMargins()
pageWidth := pageWidthTotal - marginLeft - marginRight
columnDef := make([]string, 0)
if b.columnWidths != nil {
columnDef = *b.columnWidths
} else if len(b.rows) > 0 {
columnDef = make([]string, len(b.rows[0].cells))
for i := range columnDef {
columnDef[i] = "*"
}
} else {
return []float64{}
}
columnWidths := make([]float64, len(columnDef))
frColumnWidthCount := 0
frColumnWeights := make([]float64, len(columnDef))
remainingWidth := pageWidth - (float64(len(columnDef)-1) * b.padx)
autoWidths := make([]float64, len(columnDef))
for colIdx := range columnDef {
w := float64(0)
for _, row := range b.rows {
if len(row.cells) > colIdx {
w = max(w, b.builder.GetStringWidth(row.cells[colIdx].Content, row.cells[colIdx].Style.PDFCellOpt))
}
}
autoWidths[colIdx] = w
}
for colIdx, col := range columnDef {
maxPadHorz := float64(0)
minWidth := float64(0)
for _, row := range b.rows {
if len(row.cells) > colIdx {
2024-08-07 19:30:38 +02:00
ph := langext.Coalesce(row.cells[colIdx].Style.paddingHorz, 2)
mw := langext.Coalesce(row.cells[colIdx].Style.minWidth, 0)
2024-08-07 13:57:29 +02:00
minWidth = max(minWidth, ph+mw)
maxPadHorz = max(maxPadHorz, ph)
}
}
if col == "auto" {
w := max(autoWidths[colIdx]+maxPadHorz, minWidth)
columnWidths[colIdx] = w
remainingWidth -= w
} else if match, ok := regexTableColumnSpecFr.MatchFirst(col); ok {
if match.GroupByName("num").Value() == "" {
w := minWidth
frColumnWidthCount += 1
frColumnWeights[colIdx] = 1
columnWidths[colIdx] = w
remainingWidth -= w
} else {
w := minWidth
n, _ := strconv.Atoi(match.GroupByName("num").Value())
frColumnWidthCount += n
frColumnWeights[colIdx] = float64(n)
columnWidths[colIdx] = w
remainingWidth -= w
}
} else {
if w, err := strconv.ParseFloat(col, 64); err == nil {
w = max(w, minWidth)
columnWidths[colIdx] = w
remainingWidth -= w
} else {
b.builder.FPDF().SetError(exerr.New(exerr.TypeInternal, "invalid column width").Str("width", col).Build())
w = max(w, minWidth)
columnWidths[colIdx] = w
remainingWidth -= w
return nil
}
}
}
if remainingWidth < 0 {
// no remaining space to distribute
return columnWidths
}
2024-08-07 17:04:59 +02:00
{
rmSub := 0.0
for i := range columnDef {
if frColumnWeights[i] != 0 {
w := min(autoWidths[i], (remainingWidth/float64(frColumnWidthCount))*frColumnWeights[i])
rmSub += w - columnWidths[i]
columnWidths[i] = w
}
2024-08-07 13:57:29 +02:00
}
2024-08-07 17:04:59 +02:00
remainingWidth -= rmSub
2024-08-07 13:57:29 +02:00
}
2024-08-07 17:04:59 +02:00
if remainingWidth > 0.01 {
rmSub := 0.0
2024-08-07 13:57:29 +02:00
for i, _ := range columnDef {
if frColumnWeights[i] != 0 {
addW := (remainingWidth / float64(frColumnWidthCount)) * frColumnWeights[i]
2024-08-07 17:04:59 +02:00
rmSub += addW
2024-08-07 13:57:29 +02:00
columnWidths[i] += addW
}
}
2024-08-07 17:04:59 +02:00
remainingWidth -= rmSub
2024-08-07 13:57:29 +02:00
}
return columnWidths
}
func (b *TableBuilder) RowCount() int {
return len(b.rows)
}
2024-08-07 15:34:06 +02:00
func (b *TableBuilder) Debug(v bool) *TableBuilder {
b.debug = &v
return b
}
2024-08-07 13:57:29 +02:00
func (b *WPDFBuilder) Table() *TableBuilder {
return &TableBuilder{
builder: b,
rows: make([]tableRow, 0),
pady: 2,
padx: 2,
defaultCellStyle: defaultTableStyle(),
}
}
func defaultTableStyle() *TableCellStyleOpt {
return &TableCellStyleOpt{
PDFCellOpt: *NewPDFCellOpt().
FontSize(float64(8)).
2024-08-07 15:34:06 +02:00
Border(BorderFull).
BorderColorHex(uint32(0x666666)).
FillColorHex(uint32(0xF0F0F0)).
2024-08-07 13:57:29 +02:00
TextColorHex(uint32(0x000000)).
2024-08-07 15:34:06 +02:00
FillBackground(true),
2024-08-07 19:30:38 +02:00
minWidth: langext.Ptr(float64(5)),
ellipsize: langext.PTrue,
multiCell: langext.PFalse,
2024-08-07 13:57:29 +02:00
}
}