diff --git a/goextVersion.go b/goextVersion.go index 54060bc..38c1ab3 100644 --- a/goextVersion.go +++ b/goextVersion.go @@ -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" diff --git a/imageext/image.go b/imageext/image.go index 171b708..481d091 100644 --- a/imageext/image.go +++ b/imageext/image.go @@ -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) { diff --git a/imageext/types.go b/imageext/types.go new file mode 100644 index 0000000..f2b5cd7 --- /dev/null +++ b/imageext/types.go @@ -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, + } +} diff --git a/wpdf/wpdf.go b/wpdf/wpdf.go index 6e54c62..b3c7db0 100644 --- a/wpdf/wpdf.go +++ b/wpdf/wpdf.go @@ -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) } diff --git a/wpdf/wpdfCell.go b/wpdf/wpdfCell.go index 3b706a4..5835277 100644 --- a/wpdf/wpdfCell.go +++ b/wpdf/wpdfCell.go @@ -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) } diff --git a/wpdf/wpdfConstants.go b/wpdf/wpdfConstants.go index 2cd9e82..b2331b8 100644 --- a/wpdf/wpdfConstants.go +++ b/wpdf/wpdfConstants.go @@ -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 diff --git a/wpdf/wpdfImage.go b/wpdf/wpdfImage.go index 17bf2c4..c3547c0 100644 --- a/wpdf/wpdfImage.go +++ b/wpdf/wpdfImage.go @@ -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)) + } + } } diff --git a/wpdf/wpdfLine.go b/wpdf/wpdfLine.go new file mode 100644 index 0000000..c280792 --- /dev/null +++ b/wpdf/wpdfLine.go @@ -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) +} diff --git a/wpdf/wpdfMultiCell.go b/wpdf/wpdfMultiCell.go index 1261eed..c03e52c 100644 --- a/wpdf/wpdfMultiCell.go +++ b/wpdf/wpdfMultiCell.go @@ -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) } diff --git a/wpdf/wpdfRect.go b/wpdf/wpdfRect.go index 4fef78e..30b251f 100644 --- a/wpdf/wpdfRect.go +++ b/wpdf/wpdfRect.go @@ -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)) } diff --git a/wpdf/wpdfTable.go b/wpdf/wpdfTable.go new file mode 100644 index 0000000..dc77fc9 --- /dev/null +++ b/wpdf/wpdfTable.go @@ -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[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, + } +}