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[0-9]*)(fr|\*)$`)) type TableBuilder struct { builder *WPDFBuilder padx float64 pady float64 rows []tableRow defaultCellStyle *TableCellStyleOpt columnWidths *[]string debug *bool } 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) *TableBuilder { b.rows = append(b.rows, tableRow{cells: cells}) return b } func (b *TableBuilder) AddRowWithStyle(style TableCellStyleOpt, cells ...string) *TableBuilder { 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}) return b } func (b *TableBuilder) AddRowDefaultStyle(cells ...string) *TableBuilder { 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}) return b } func (b *TableBuilder) Build() { builder := b.builder debug := langext.Coalesce(b.debug, b.builder.debug) 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).Debug(debug)) } 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).Debug(debug)) } 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 *TableBuilder) Debug(v bool) *TableBuilder { b.debug = &v return b } 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)). Border(BorderFull). BorderColorHex(uint32(0x666666)). FillColorHex(uint32(0xF0F0F0)). TextColorHex(uint32(0x000000)). FillBackground(true), MinWidth: langext.Ptr(float64(5)), Ellipsize: langext.PTrue, MultiCell: langext.PFalse, } }