v0.0.494 add tables to wpdf
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled

This commit is contained in:
Mike Schwörer 2024-08-07 13:57:29 +02:00
parent f1b4480e0f
commit b78a468632
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
11 changed files with 646 additions and 13 deletions

View File

@ -1,5 +1,5 @@
package goext
const GoextVersion = "0.0.493"
const GoextVersion = "0.0.494"
const GoextVersionTimestamp = "2024-08-07T09:22:37+0200"
const GoextVersionTimestamp = "2024-08-07T13:57:29+0200"

View File

@ -169,7 +169,7 @@ func EncodeImage(img image.Image, compression ImageCompresson) (bytes.Buffer, st
}
}
func ObjectFitImage(img image.Image, bbw float64, bbh float64, fit ImageFit, fillColor color.Color) (image.Image, error) {
func ObjectFitImage(img image.Image, bbw float64, bbh float64, fit ImageFit, fillColor color.Color) (image.Image, PercentageRectangle, error) {
iw := img.Bounds().Size().X
ih := img.Bounds().Size().Y
@ -214,12 +214,12 @@ func ObjectFitImage(img image.Image, bbw float64, bbh float64, fit ImageFit, fil
draw.Draw(newImg, newImg.Bounds(), &image.Uniform{C: fillColor}, image.Pt(0, 0), draw.Src)
draw.Draw(newImg, newImg.Bounds(), img, image.Pt(0, 0), draw.Over)
return newImg, nil
return newImg, newImg.Bounds(), nil
}
if fit == ImageFitContainCenter || fit == ImageFitContainTopLeft || fit == ImageFitContainTopRight || fit == ImageFitContainBottomLeft || fit == ImageFitContainBottomRight {
// image-fit:cover fills the target-bounding-box with the image, there is potentially empty-space, it potentially cuts parts of the image away
// image-fit:contain fills the target-bounding-box with the image, there is potentially empty-space, it potentially cuts parts of the image away
// we use the bigger (!) value of facW and facH,
// because the image is made to fit the bounding-box, the bigger factor (= the dimension the image is stretched less) is relevant
@ -266,7 +266,7 @@ func ObjectFitImage(img image.Image, bbw float64, bbh float64, fit ImageFit, fil
draw.Draw(newImg, newImg.Bounds(), &image.Uniform{C: fillColor}, image.Pt(0, 0), draw.Src)
draw.Draw(newImg, destBounds, img, image.Pt(0, 0), draw.Over)
return newImg, nil
return newImg, destBounds, nil
}
if fit == ImageFitStretch {
@ -293,10 +293,10 @@ func ObjectFitImage(img image.Image, bbw float64, bbh float64, fit ImageFit, fil
draw.Draw(newImg, newImg.Bounds(), &image.Uniform{C: fillColor}, image.Pt(0, 0), draw.Src)
draw.Draw(newImg, newImg.Bounds(), img, image.Pt(0, 0), draw.Over)
return newImg, nil
return newImg, newImg.Bounds(), nil
}
return nil, exerr.New(exerr.TypeInternal, fmt.Sprintf("unknown image-fit: '%s'", fit)).Build()
return nil, image.Rectangle{}, exerr.New(exerr.TypeInternal, fmt.Sprintf("unknown image-fit: '%s'", fit)).Build()
}
func VerifyAndDecodeImage(data io.Reader, mime string) (image.Image, error) {

24
imageext/types.go Normal file
View File

@ -0,0 +1,24 @@
package imageext
type Rectangle struct {
X float64
Y float64
W float64
H float64
}
type PercentageRectangle struct {
X float64 // [0..1]
Y float64 // [0..1]
W float64 // [0..1]
H float64 // [0..1]
}
func (r PercentageRectangle) Of(ref Rectangle) Rectangle {
return Rectangle{
X: ref.X + r.X*ref.W,
Y: ref.Y + r.Y*ref.H,
W: r.W * ref.W,
H: r.H * ref.H,
}
}

View File

@ -3,6 +3,7 @@ package wpdf
import (
"bytes"
"github.com/jung-kurt/gofpdf"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
)
type WPDFBuilder struct {
@ -105,6 +106,18 @@ func (b *WPDFBuilder) SetFont(fontName PDFFontFamily, fontStyle PDFFontStyle, fo
b.cellHeight = b.b.PointConvert(fontSize)
}
func (b *WPDFBuilder) GetFontSize() float64 {
return b.fontSize
}
func (b *WPDFBuilder) GetFontFamily() PDFFontStyle {
return b.fontStyle
}
func (b *WPDFBuilder) GetFontStyle() float64 {
return b.fontSize
}
func (b *WPDFBuilder) SetCellSpacing(h float64) {
b.cellSpacing = h
}
@ -192,6 +205,40 @@ func (b *WPDFBuilder) GetWorkAreaWidth() float64 {
return b.GetPageWidth() - b.GetMarginLeft() - b.GetMarginRight()
}
func (b *WPDFBuilder) GetStringWidth(str string) float64 {
func (b *WPDFBuilder) SetAutoPageBreak(auto bool, margin float64) {
b.b.SetAutoPageBreak(auto, margin)
}
func (b *WPDFBuilder) SetFooterFunc(fnc func()) {
b.b.SetFooterFunc(fnc)
}
func (b *WPDFBuilder) PageNo() int {
return b.b.PageNo()
}
func (b *WPDFBuilder) GetStringWidth(str string, opts ...PDFCellOpt) float64 {
var fontNameOverride *PDFFontFamily
var fontStyleOverride *PDFFontStyle
var fontSizeOverride *float64
for _, opt := range opts {
fontNameOverride = langext.CoalesceOpt(opt.fontNameOverride, fontNameOverride)
fontStyleOverride = langext.CoalesceOpt(opt.fontStyleOverride, fontStyleOverride)
fontSizeOverride = langext.CoalesceOpt(opt.fontSizeOverride, fontSizeOverride)
}
if fontNameOverride != nil || fontStyleOverride != nil || fontSizeOverride != nil {
oldFontName := b.fontName
oldFontStyle := b.fontStyle
oldFontSize := b.fontSize
newFontName := langext.Coalesce(fontNameOverride, oldFontName)
newFontStyle := langext.Coalesce(fontStyleOverride, oldFontStyle)
newFontSize := langext.Coalesce(fontSizeOverride, oldFontSize)
b.SetFont(newFontName, newFontStyle, newFontSize)
defer func() { b.SetFont(oldFontName, oldFontStyle, oldFontSize) }()
}
return b.b.GetStringWidth(str)
}

View File

@ -1,6 +1,9 @@
package wpdf
import "gogs.mikescher.com/BlackForestBytes/goext/langext"
import (
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
)
type PDFCellOpt struct {
width *float64
@ -14,6 +17,7 @@ type PDFCellOpt struct {
fontNameOverride *PDFFontFamily
fontStyleOverride *PDFFontStyle
fontSizeOverride *float64
alphaOverride *dataext.Tuple[float64, PDFBlendMode]
extraLn *float64
x *float64
autoWidth *bool
@ -149,6 +153,34 @@ func (opt *PDFCellOpt) FillColorHex(c uint32) *PDFCellOpt {
return opt
}
func (opt *PDFCellOpt) Alpha(alpha float64, blendMode PDFBlendMode) *PDFCellOpt {
opt.alphaOverride = &dataext.Tuple[float64, PDFBlendMode]{V1: alpha, V2: blendMode}
return opt
}
func (opt *PDFCellOpt) Copy() *PDFCellOpt {
c := *opt
return &c
}
func (opt *PDFCellOpt) ToMulti() *PDFMultiCellOpt {
return &PDFMultiCellOpt{
width: opt.width,
height: opt.height,
border: opt.border,
align: opt.align,
fill: opt.fill,
fontNameOverride: opt.fontNameOverride,
fontStyleOverride: opt.fontStyleOverride,
fontSizeOverride: opt.fontSizeOverride,
extraLn: opt.extraLn,
x: opt.x,
textColor: opt.textColor,
borderColor: opt.borderColor,
fillColor: opt.fillColor,
}
}
func (b *WPDFBuilder) Cell(txt string, opts ...*PDFCellOpt) {
txtTR := b.tr(txt)
@ -164,6 +196,7 @@ func (b *WPDFBuilder) Cell(txt string, opts ...*PDFCellOpt) {
var fontNameOverride *PDFFontFamily
var fontStyleOverride *PDFFontStyle
var fontSizeOverride *float64
var alphaOverride *dataext.Tuple[float64, PDFBlendMode]
extraLn := float64(0)
var x *float64
autoWidth := false
@ -184,6 +217,7 @@ func (b *WPDFBuilder) Cell(txt string, opts ...*PDFCellOpt) {
fontNameOverride = langext.CoalesceOpt(opt.fontNameOverride, fontNameOverride)
fontStyleOverride = langext.CoalesceOpt(opt.fontStyleOverride, fontStyleOverride)
fontSizeOverride = langext.CoalesceOpt(opt.fontSizeOverride, fontSizeOverride)
alphaOverride = langext.CoalesceOpt(opt.alphaOverride, alphaOverride)
extraLn = langext.Coalesce(opt.extraLn, extraLn)
x = langext.CoalesceOpt(opt.x, x)
autoWidth = langext.Coalesce(opt.autoWidth, autoWidth)
@ -222,6 +256,12 @@ func (b *WPDFBuilder) Cell(txt string, opts ...*PDFCellOpt) {
defer func() { b.SetFillColor(oldColorR, oldColorG, oldColorB) }()
}
if alphaOverride != nil {
oldA, oldBMS := b.b.GetAlpha()
b.b.SetAlpha(alphaOverride.V1, string(alphaOverride.V2))
defer func() { b.b.SetAlpha(oldA, oldBMS) }()
}
if x != nil {
b.b.SetX(*x)
}

View File

@ -74,6 +74,35 @@ const (
RectFillOutline PDFRectStyle = "FD"
)
type PDFBlendMode string
const (
BlendNormal PDFBlendMode = "Normal"
BlendMultiply PDFBlendMode = "Multiply"
BlendScreen PDFBlendMode = "Screen"
BlendOverlay PDFBlendMode = "Overlay"
BlendDarken PDFBlendMode = "Darken"
BlendLighten PDFBlendMode = "Lighten"
BlendColorDodge PDFBlendMode = "ColorDodge"
BlendColorBurn PDFBlendMode = "ColorBurn"
BlendHardLight PDFBlendMode = "HardLight"
BlendSoftLight PDFBlendMode = "SoftLight"
BlendDifference PDFBlendMode = "Difference"
BlendExclusion PDFBlendMode = "Exclusion"
BlendHue PDFBlendMode = "Hue"
BlendSaturation PDFBlendMode = "Saturation"
BlendColor PDFBlendMode = "Color"
BlendLuminosity PDFBlendMode = "Luminosity"
)
type PDFLineCapStyle string
const (
CapButt PDFLineCapStyle = "butt"
CapRound PDFLineCapStyle = "round"
CapSquare PDFLineCapStyle = "square"
)
const (
BackgroundFill = true
BackgroundTransparent = false

View File

@ -3,6 +3,7 @@ package wpdf
import (
"bytes"
"github.com/jung-kurt/gofpdf"
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/imageext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"image"
@ -130,6 +131,8 @@ type PDFImageOpt struct {
compression *imageext.ImageCompresson
reEncodePixelPerMM *float64
crop *imageext.ImageCrop
alphaOverride *dataext.Tuple[float64, PDFBlendMode]
debug *bool
}
func NewPDFImageOpt() *PDFImageOpt {
@ -156,6 +159,11 @@ func (opt *PDFImageOpt) Height(v float64) *PDFImageOpt {
return opt
}
func (opt *PDFImageOpt) Debug(v bool) *PDFImageOpt {
opt.debug = &v
return opt
}
func (opt *PDFImageOpt) Flow(v bool) *PDFImageOpt {
opt.flow = &v
return opt
@ -217,6 +225,11 @@ func (opt *PDFImageOpt) Crop(cropX float64, cropY float64, cropWidth float64, cr
return opt
}
func (opt *PDFImageOpt) Alpha(alpha float64, blendMode PDFBlendMode) *PDFImageOpt {
opt.alphaOverride = &dataext.Tuple[float64, PDFBlendMode]{V1: alpha, V2: blendMode}
return opt
}
func (b *WPDFBuilder) Image(img *PDFImageRef, opts ...*PDFImageOpt) {
var err error
@ -234,7 +247,9 @@ func (b *WPDFBuilder) Image(img *PDFImageRef, opts ...*PDFImageOpt) {
var imageFit *imageext.ImageFit = nil
var fillColor color.Color = color.Transparent
compression := imageext.CompressionPNGSpeed
debug := false
var crop *imageext.ImageCrop = nil
var alphaOverride *dataext.Tuple[float64, PDFBlendMode]
for _, opt := range opts {
x = langext.Coalesce(opt.x, x)
@ -252,10 +267,14 @@ func (b *WPDFBuilder) Image(img *PDFImageRef, opts ...*PDFImageOpt) {
compression = langext.Coalesce(opt.compression, compression)
reEncodePixelPerMM = langext.Coalesce(opt.reEncodePixelPerMM, reEncodePixelPerMM)
crop = langext.CoalesceOpt(opt.crop, crop)
debug = langext.Coalesce(opt.debug, debug)
alphaOverride = langext.CoalesceOpt(opt.alphaOverride, alphaOverride)
}
regName := img.Name
var subImageBounds *imageext.PercentageRectangle = nil
if imageFit != nil || fillColor != nil || crop != nil {
var dataimg image.Image
@ -283,11 +302,14 @@ func (b *WPDFBuilder) Image(img *PDFImageRef, opts ...*PDFImageOpt) {
pxw := w * pdfPixelPerMillimeter
pxh := h * pdfPixelPerMillimeter
dataimg, err = imageext.ObjectFitImage(dataimg, pxw, pxh, *imageFit, fillColor)
var dataImgRect imageext.PercentageRectangle
dataimg, dataImgRect, err = imageext.ObjectFitImage(dataimg, pxw, pxh, *imageFit, fillColor)
if err != nil {
b.b.SetError(err)
return
}
subImageBounds = &dataImgRect
}
if dataimg.ColorModel() != color.RGBAModel && dataimg.ColorModel() != color.NRGBAModel {
@ -318,6 +340,12 @@ func (b *WPDFBuilder) Image(img *PDFImageRef, opts ...*PDFImageOpt) {
}
if alphaOverride != nil {
oldA, oldBMS := b.b.GetAlpha()
b.b.SetAlpha(alphaOverride.V1, string(alphaOverride.V2))
defer func() { b.b.SetAlpha(oldA, oldBMS) }()
}
fpdfOpt := gofpdf.ImageOptions{
ImageType: imageType,
ReadDpi: readDpi,
@ -325,4 +353,15 @@ func (b *WPDFBuilder) Image(img *PDFImageRef, opts ...*PDFImageOpt) {
}
b.b.ImageOptions(regName, x, y, w, h, flow, fpdfOpt, link, linkStr)
if debug {
b.Rect(w, h, RectOutline, NewPDFRectOpt().X(x).Y(y).LineWidth(2).DrawColor(255, 0, 0))
if subImageBounds != nil {
r := subImageBounds.Of(imageext.Rectangle{X: x, Y: y, W: w, H: h})
b.Rect(r.W, r.H, RectFill, NewPDFRectOpt().X(r.X).Y(r.Y).FillColor(255, 0, 0).Alpha(0.2, BlendNormal))
b.Line(r.X, r.Y, r.X+r.W, r.Y+r.H, NewPDFLineOpt().LineWidth(2).DrawColor(255, 0, 0))
b.Line(r.X+r.W, r.Y, r.X, r.Y+r.H, NewPDFLineOpt().LineWidth(2).DrawColor(255, 0, 0))
}
}
}

88
wpdf/wpdfLine.go Normal file
View File

@ -0,0 +1,88 @@
package wpdf
import (
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
)
type PDFLineOpt struct {
lineWidth *float64
drawColor *PDFColor
alpha *dataext.Tuple[float64, PDFBlendMode]
capStyle *PDFLineCapStyle
}
func NewPDFLineOpt() *PDFLineOpt {
return &PDFLineOpt{}
}
func (opt *PDFLineOpt) LineWidth(v float64) *PDFLineOpt {
opt.lineWidth = &v
return opt
}
func (opt *PDFLineOpt) DrawColor(cr, cg, cb int) *PDFLineOpt {
opt.drawColor = langext.Ptr(rgbToColor(cr, cg, cb))
return opt
}
func (opt *PDFLineOpt) DrawColorHex(c uint32) *PDFLineOpt {
opt.drawColor = langext.Ptr(hexToColor(c))
return opt
}
func (opt *PDFLineOpt) Alpha(alpha float64, blendMode PDFBlendMode) *PDFLineOpt {
opt.alpha = &dataext.Tuple[float64, PDFBlendMode]{V1: alpha, V2: blendMode}
return opt
}
func (opt *PDFLineOpt) CapButt() *PDFLineOpt {
opt.capStyle = langext.Ptr(CapButt)
return opt
}
func (opt *PDFLineOpt) CapSquare() *PDFLineOpt {
opt.capStyle = langext.Ptr(CapSquare)
return opt
}
func (opt *PDFLineOpt) CapRound() *PDFLineOpt {
opt.capStyle = langext.Ptr(CapRound)
return opt
}
func (b *WPDFBuilder) Line(x1 float64, y1 float64, x2 float64, y2 float64, opts ...*PDFLineOpt) {
var lineWidth *float64
var drawColor *PDFColor
var alphaOverride *dataext.Tuple[float64, PDFBlendMode]
capStyle := CapButt
for _, opt := range opts {
lineWidth = langext.CoalesceOpt(opt.lineWidth, lineWidth)
drawColor = langext.CoalesceOpt(opt.drawColor, drawColor)
alphaOverride = langext.CoalesceOpt(opt.alpha, alphaOverride)
capStyle = langext.Coalesce(opt.capStyle, capStyle)
}
if lineWidth != nil {
old := b.GetLineWidth()
b.SetLineWidth(*lineWidth)
defer func() { b.SetLineWidth(old) }()
}
if drawColor != nil {
oldR, oldG, oldB := b.GetDrawColor()
b.SetDrawColor(drawColor.R, drawColor.G, drawColor.B)
defer func() { b.SetDrawColor(oldR, oldG, oldB) }()
}
if alphaOverride != nil {
oldA, oldBMS := b.b.GetAlpha()
b.b.SetAlpha(alphaOverride.V1, string(alphaOverride.V2))
defer func() { b.b.SetAlpha(oldA, oldBMS) }()
}
b.b.SetLineCapStyle(string(capStyle))
b.b.Line(x1, y1, x2, y2)
}

View File

@ -1,6 +1,9 @@
package wpdf
import "gogs.mikescher.com/BlackForestBytes/goext/langext"
import (
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
)
type PDFMultiCellOpt struct {
width *float64
@ -11,6 +14,7 @@ type PDFMultiCellOpt struct {
fontNameOverride *PDFFontFamily
fontStyleOverride *PDFFontStyle
fontSizeOverride *float64
alphaOverride *dataext.Tuple[float64, PDFBlendMode]
extraLn *float64
x *float64
textColor *PDFColor
@ -119,6 +123,11 @@ func (opt *PDFMultiCellOpt) FillColorHex(c uint32) *PDFMultiCellOpt {
return opt
}
func (opt *PDFMultiCellOpt) Alpha(alpha float64, blendMode PDFBlendMode) *PDFMultiCellOpt {
opt.alphaOverride = &dataext.Tuple[float64, PDFBlendMode]{V1: alpha, V2: blendMode}
return opt
}
func (b *WPDFBuilder) MultiCell(txt string, opts ...*PDFMultiCellOpt) {
txtTR := b.tr(txt)
@ -131,6 +140,7 @@ func (b *WPDFBuilder) MultiCell(txt string, opts ...*PDFMultiCellOpt) {
var fontNameOverride *PDFFontFamily
var fontStyleOverride *PDFFontStyle
var fontSizeOverride *float64
var alphaOverride *dataext.Tuple[float64, PDFBlendMode]
extraLn := float64(0)
var x *float64
var textColor *PDFColor
@ -146,6 +156,7 @@ func (b *WPDFBuilder) MultiCell(txt string, opts ...*PDFMultiCellOpt) {
fontNameOverride = langext.CoalesceOpt(opt.fontNameOverride, fontNameOverride)
fontStyleOverride = langext.CoalesceOpt(opt.fontStyleOverride, fontStyleOverride)
fontSizeOverride = langext.CoalesceOpt(opt.fontSizeOverride, fontSizeOverride)
alphaOverride = langext.CoalesceOpt(opt.alphaOverride, alphaOverride)
extraLn = langext.Coalesce(opt.extraLn, extraLn)
x = langext.CoalesceOpt(opt.x, x)
textColor = langext.CoalesceOpt(opt.textColor, textColor)
@ -182,6 +193,12 @@ func (b *WPDFBuilder) MultiCell(txt string, opts ...*PDFMultiCellOpt) {
defer func() { b.SetFillColor(oldColorR, oldColorG, oldColorB) }()
}
if alphaOverride != nil {
oldA, oldBMS := b.b.GetAlpha()
b.b.SetAlpha(alphaOverride.V1, string(alphaOverride.V2))
defer func() { b.b.SetAlpha(oldA, oldBMS) }()
}
if x != nil {
b.b.SetX(*x)
}

View File

@ -1,6 +1,9 @@
package wpdf
import "gogs.mikescher.com/BlackForestBytes/goext/langext"
import (
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
)
type PDFRectOpt struct {
x *float64
@ -8,6 +11,7 @@ type PDFRectOpt struct {
lineWidth *float64
drawColor *PDFColor
fillColor *PDFColor
alpha *dataext.Tuple[float64, PDFBlendMode]
radiusTL *float64
radiusTR *float64
radiusBR *float64
@ -81,12 +85,18 @@ func (opt *PDFRectOpt) RadiusBR(radius float64) *PDFRectOpt {
return opt
}
func (opt *PDFRectOpt) Alpha(alpha float64, blendMode PDFBlendMode) *PDFRectOpt {
opt.alpha = &dataext.Tuple[float64, PDFBlendMode]{V1: alpha, V2: blendMode}
return opt
}
func (b *WPDFBuilder) Rect(w float64, h float64, styleStr PDFRectStyle, opts ...*PDFRectOpt) {
x := b.GetX()
y := b.GetY()
var lineWidth *float64
var drawColor *PDFColor
var fillColor *PDFColor
var alphaOverride *dataext.Tuple[float64, PDFBlendMode]
radiusTL := float64(0)
radiusTR := float64(0)
radiusBR := float64(0)
@ -98,6 +108,7 @@ func (b *WPDFBuilder) Rect(w float64, h float64, styleStr PDFRectStyle, opts ...
lineWidth = langext.CoalesceOpt(opt.lineWidth, lineWidth)
drawColor = langext.CoalesceOpt(opt.drawColor, drawColor)
fillColor = langext.CoalesceOpt(opt.fillColor, fillColor)
alphaOverride = langext.CoalesceOpt(opt.alpha, alphaOverride)
radiusTL = langext.Coalesce(opt.radiusTL, radiusTL)
radiusTR = langext.Coalesce(opt.radiusTR, radiusTR)
radiusBR = langext.Coalesce(opt.radiusBR, radiusBR)
@ -122,5 +133,11 @@ func (b *WPDFBuilder) Rect(w float64, h float64, styleStr PDFRectStyle, opts ...
defer func() { b.SetFillColor(oldR, oldG, oldB) }()
}
if alphaOverride != nil {
oldA, oldBMS := b.b.GetAlpha()
b.b.SetAlpha(alphaOverride.V1, string(alphaOverride.V2))
defer func() { b.b.SetAlpha(oldA, oldBMS) }()
}
b.b.RoundedRectExt(x, y, w, h, radiusTL, radiusTR, radiusBR, radiusBL, string(styleStr))
}

332
wpdf/wpdfTable.go Normal file
View File

@ -0,0 +1,332 @@
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,
}
}