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
}
}