333 lines
8.1 KiB
Go
333 lines
8.1 KiB
Go
|
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
|
||
|
}
|
||
|
|
||
|
type TableCell struct {
|
||
|
Content string
|
||
|
Style TableCellStyleOpt
|
||
|
}
|
||
|
|
||
|
type TableCellStyleOpt struct {
|
||
|
MultiCell *bool
|
||
|
Ellipsize *bool
|
||
|
PaddingHorz *float64
|
||
|
MinWidth *float64
|
||
|
|
||
|
PDFCellOpt
|
||
|
}
|
||
|
|
||
|
type tableRow struct {
|
||
|
cells []TableCell
|
||
|
}
|
||
|
|
||
|
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
|
||
|
}
|
||
|
|
||
|
func (b *TableBuilder) DefaultStyle(s TableCellStyleOpt) *TableBuilder {
|
||
|
b.defaultCellStyle = &s
|
||
|
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
|
||
|
}
|
||
|
|
||
|
func (b *TableBuilder) AddRow(cells ...TableCell) {
|
||
|
b.rows = append(b.rows, tableRow{cells: cells})
|
||
|
}
|
||
|
|
||
|
func (b *TableBuilder) AddRowWithStyle(style TableCellStyleOpt, cells ...string) {
|
||
|
tcels := make([]TableCell, 0, len(cells))
|
||
|
for _, cell := range cells {
|
||
|
tcels = append(tcels, TableCell{Content: cell, Style: style})
|
||
|
}
|
||
|
|
||
|
b.rows = append(b.rows, tableRow{cells: tcels})
|
||
|
}
|
||
|
|
||
|
func (b *TableBuilder) AddRowDefaultStyle(cells ...string) {
|
||
|
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})
|
||
|
}
|
||
|
|
||
|
func (b *TableBuilder) Build() {
|
||
|
builder := b.builder
|
||
|
|
||
|
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
|
||
|
|
||
|
ellipsize := langext.Coalesce(style.Ellipsize, true)
|
||
|
cellPaddingHorz := langext.Coalesce(style.PaddingHorz, 2)
|
||
|
|
||
|
bx := builder.GetX()
|
||
|
by := builder.GetY()
|
||
|
|
||
|
cellWidth := columnWidths[cellIdx]
|
||
|
|
||
|
if langext.Coalesce(style.MultiCell, true) {
|
||
|
|
||
|
builder.MultiCell(str, style.PDFCellOpt.Copy().ToMulti().Width(cellWidth))
|
||
|
|
||
|
} 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 += "..."
|
||
|
}
|
||
|
}
|
||
|
|
||
|
builder.Cell(str, style.PDFCellOpt.Copy().Width(cellWidth))
|
||
|
|
||
|
}
|
||
|
|
||
|
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 {
|
||
|
|
||
|
ph := langext.Coalesce(row.cells[colIdx].Style.PaddingHorz, 2)
|
||
|
mw := langext.Coalesce(row.cells[colIdx].Style.MinWidth, 0)
|
||
|
|
||
|
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
|
||
|
}
|
||
|
|
||
|
for i, _ := range columnDef {
|
||
|
if frColumnWeights[i] != 0 {
|
||
|
w := min(autoWidths[i], (remainingWidth/float64(frColumnWidthCount))*frColumnWeights[i])
|
||
|
remainingWidth += columnWidths[i]
|
||
|
columnWidths[i] = w
|
||
|
remainingWidth -= w
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if remainingWidth > 0 {
|
||
|
for i, _ := range columnDef {
|
||
|
if frColumnWeights[i] != 0 {
|
||
|
addW := (remainingWidth / float64(frColumnWidthCount)) * frColumnWeights[i]
|
||
|
columnWidths[i] += addW
|
||
|
remainingWidth -= addW
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return columnWidths
|
||
|
}
|
||
|
|
||
|
func (b *TableBuilder) RowCount() int {
|
||
|
return len(b.rows)
|
||
|
}
|
||
|
|
||
|
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)).
|
||
|
BorderColorHex(uint32(0x888888)).
|
||
|
FillColorHex(uint32(0xFFFFFF)).
|
||
|
TextColorHex(uint32(0x000000)).
|
||
|
FillBackground(false),
|
||
|
MinWidth: langext.Ptr(float64(5)),
|
||
|
Ellipsize: langext.PTrue,
|
||
|
MultiCell: langext.PFalse,
|
||
|
}
|
||
|
}
|