Brush up the go code

This commit is contained in:
2022-04-19 21:22:59 +02:00
parent b88208ba3d
commit 63875a6f59
7 changed files with 161 additions and 46 deletions

View File

@@ -2,20 +2,24 @@ package covergen
import "github.com/go-pdf/fpdf"
// Color represents a simple RGB color
type Color struct {
R uint8
G uint8
B uint8
}
// Text sets the text color of the given PDF to the called-on color instance
func (c Color) Text(pdf *fpdf.Fpdf) {
pdf.SetTextColor(int(c.R), int(c.G), int(c.B))
}
// Draw sets the draw color of the given PDF to the called-on color instance
func (c Color) Draw(pdf *fpdf.Fpdf) {
pdf.SetDrawColor(int(c.R), int(c.G), int(c.B))
}
// Fill sets the fill color of the given PDF to the called-on color instance
func (c Color) Fill(pdf *fpdf.Fpdf) {
pdf.SetFillColor(int(c.R), int(c.G), int(c.B))
}

View File

@@ -8,16 +8,32 @@ import (
"github.com/go-pdf/fpdf"
)
// page margins
const margin = 20
const miwebbLogoName = "miwebbLogo"
const miwebbLogoPath = "miwebb.white.png"
const logoScale = 0.6
const gridXTicks = 10
const gridYTicks = 6
const gridMargin = 5
const lineHeight = 1.2
const maxFontSize = 100
// constants for the logo
const (
// pdf internal name to reference the logo
logoName = "miwebbLogo"
// this should match
logoPath = "miwebb.white.png"
// Scale-up / down factor for the logo
logoScale = 0.6
)
// constants for the grid
const (
// Number of lines on the X axis of the grid behind the customer name
gridXTicks = 10
// Number of lines on the Y axis of the grid
gridYTicks = 6
// Internal padding for the text inside the grid, in mm
gridPadding = 5
// default line height used for the grid
lineHeight = 1.2
// maximum font size for the grid, in pt
maxFontSize = 100
)
var bgColor = Color{25, 25, 25}
var textColor = Color{255, 255, 255}
@@ -27,12 +43,18 @@ var gridNoduleColor = Color{255, 255, 255}
//go:embed miwebb.white.png font_embed
var fs embed.FS
const fontsEmbedPrefix = "font_embed/"
const (
fontsEmbedPrefix = "font_embed/"
fontLight = "Mark Light"
fontMedium = "Mark Medium"
)
// Copied from fPDF's source, can't get it dynamically
// constant for mm is just to ensure you don't change it without noting the scale factor that depends on it
const scaleUnit = "mm"
const scaleFactor = 72.0 / 25.4
const (
// Copied from FPDF's source, can't get it dynamically
// constant for mm is just to ensure you don't change it without noting the scale factor that depends on it
scaleUnit = "mm"
scaleFactor = 72.0 / 25.4
)
type CoverSettings struct {
Number string
@@ -42,12 +64,16 @@ type CoverSettings struct {
HLColor Color
}
// GenerateInvoice generates a fpdf.Fpdf that contains two pages, the first being the front page, (see GenerateFrontCover)
// and the second page being the closing page (see GenerateBackCover)
func GenerateInvoice(settings CoverSettings) (*fpdf.Fpdf, error) {
pdf, err := GenerateFrontCover(settings)
if err != nil {
return nil, err
}
// Generating the back cover from the front cover is just 'drawing the customer on a new page'
// as all the other things are automatically done using the page setup handlers
pdf.AddPage()
drawCustomerName(pdf, settings)
@@ -58,6 +84,8 @@ func GenerateInvoice(settings CoverSettings) (*fpdf.Fpdf, error) {
return pdf, nil
}
// GenerateFrontCover generates a fpdf.Fpdf representing the front cover of the invoice, containing the customer name,
// the link to the MiWebb website, and the invoice number.
func GenerateFrontCover(settings CoverSettings) (*fpdf.Fpdf, error) {
pdf, err := generateBaseInvoice(settings)
if err != nil {
@@ -74,6 +102,7 @@ func GenerateFrontCover(settings CoverSettings) (*fpdf.Fpdf, error) {
return pdf, nil
}
// GenerateBackCover generates a very plain cover containing just the customer name
func GenerateBackCover(settings CoverSettings) (*fpdf.Fpdf, error) {
pdf, err := generateBaseInvoice(settings)
if err != nil {
@@ -83,6 +112,12 @@ func GenerateBackCover(settings CoverSettings) (*fpdf.Fpdf, error) {
return pdf, err
}
// generateBaseInvoice takes care of setting up the initial fpdf.Fpdf instance by:
// - registering all the fonts,
// - registering the logo
// - setting up the page settings (margins)
// - setting up the header func that renders the logo on every page
// After the initialization it will also render the customer name on the first page
func generateBaseInvoice(settings CoverSettings) (*fpdf.Fpdf, error) {
pdf := fpdf.New("P", scaleUnit, "A4", "")
@@ -92,7 +127,7 @@ func generateBaseInvoice(settings CoverSettings) (*fpdf.Fpdf, error) {
if err := addEmbeddedFont(pdf, "Mark Light", "", "Mark-Light"); err != nil {
return nil, fmt.Errorf("error adding font: %w", err)
}
miwebbLogo, err := addEmbeddedLogo(pdf, miwebbLogoName, "miwebb.white.png")
logo, err := addEmbeddedLogo(pdf, logoName, logoPath)
if err != nil {
return nil, fmt.Errorf("error adding logo: %w", err)
}
@@ -102,9 +137,11 @@ func generateBaseInvoice(settings CoverSettings) (*fpdf.Fpdf, error) {
pdf.SetAutoPageBreak(false, 0)
pdf.SetHeaderFunc(func() {
// Fills the entire page with the background color
bgColor.Fill(pdf)
pdf.Rect(0, 0, pageWidth, pageHeight, "F")
pdf.Image(miwebbLogoName, margin-5, margin, miwebbLogo.Width()*logoScale, miwebbLogo.Height()*logoScale, false, "", 0, "")
// Draws the MiWebb logo
pdf.Image(logoName, margin-5, margin, logo.Width()*logoScale, logo.Height()*logoScale, false, "", 0, "")
})
pdf.AddPage()
@@ -117,11 +154,14 @@ func generateBaseInvoice(settings CoverSettings) (*fpdf.Fpdf, error) {
return pdf, nil
}
// drawInvoiceNumber does what it says on the tin, and draws the number prefix and number on the invoice in the bottom-left corner
// this moves the cursor to the top right corner of the text
func drawInvoiceNumber(pdf *fpdf.Fpdf, settings CoverSettings) {
_, pageHeight := pdf.GetPageSize()
// Bottom left corner of the page, given margin
pdf.MoveTo(margin, pageHeight-margin)
pdf.SetFont("Mark Light", "", 9)
pdf.SetFont(fontLight, "", 9)
textColor.Text(pdf)
pdf.TransformBegin()
@@ -130,37 +170,44 @@ func drawInvoiceNumber(pdf *fpdf.Fpdf, settings CoverSettings) {
pdf.CellFormat(0, 1, invoiceText, "", 0, "LT", false, 0, "")
pdf.TransformEnd()
// Calculate where the text has gone, so we can figure out the position of the line behind it
_, h := pdf.GetFontSize()
// X is 'margin + half text height'
// X is 'margin + half text height' (so centered on the line height)
x := float64(margin) + h/2
// Y is 'where the text ended' + some gap
// Y is 'where the text ended' + some gap (vertically)
y := pageHeight - margin - pdf.GetStringWidth(invoiceText) - 5
textColor.Draw(pdf)
pdf.SetLineWidth(0.01)
// the 10 here is the length of the line segment, chosen arbitrarily
pdf.Line(x, y, x, y-10)
}
// drawMiwebbLink draws a link to the MiWebb site in the bottom right corner of the page
func drawMiwebbLink(pdf *fpdf.Fpdf) {
pageWidth, pageHeight := pdf.GetPageSize()
pdf.MoveTo(pageWidth-margin, pageHeight-margin)
pdf.SetFont("Mark Light", "", 9)
pdf.SetFont(fontLight, "", 9)
textColor.Text(pdf)
pdf.CellFormat(0, 0, "WWW.MIWEBB.COM", "", 0, "RB", false, 0, "https://www.miwebb.com")
}
// drawCustomerName renders the customer name and the grid underneath it, fully centered on the page
func drawCustomerName(pdf *fpdf.Fpdf, settings CoverSettings) {
ml, _, mr, _ := pdf.GetMargins()
pw, ph := pdf.GetPageSize()
// Calculate the width and height of the grid, as we'll need it to position it
width := pw - ml - mr
// ticks are square, so the height is fully determined by the width of each grid segment
height := (width/gridXTicks - 1) * gridYTicks
text := strings.ToUpper(settings.CustomerName)
// Draw the grid with a weird gray color
gridColor.Draw(pdf)
pdf.SetLineWidth(0.01)
// Calculates the top left corner of the grid
gridX, gridY := ml, (ph/2)-(height/2)
drawGrid(pdf, gridX, gridY, width, height, gridXTicks, gridYTicks)
@@ -172,22 +219,25 @@ func drawCustomerName(pdf *fpdf.Fpdf, settings CoverSettings) {
1.2, gridNoduleColor,
0.25, settings.HLColor)
pdf.SetFont("Mark Medium", "", 10)
pdf.SetFont(fontMedium, "", 10)
// Estimate font size, and calculate line height immediately after
pdf.SetFontSize(estimateFontSize(pdf, text, width, height, gridMargin))
_, sz := pdf.GetFontSize()
sz *= lineHeight
pdf.SetFontSize(estimateFontSize(pdf, text, width, height, gridPadding))
_, fontHeight := pdf.GetFontSize()
fontHeight *= lineHeight
_, textHeight := stringSize(pdf, text, sz, 0)
// Determine the height of the full string (multiline aware)
_, textHeight := stringSize(pdf, text, fontHeight, 0)
// Centered vertically:
// x = <gridX> + <margin>
// y = <gridY> + <gridH>/2 - textHeight/2
pdf.MoveTo(ml+gridMargin, ph/2-(textHeight/2))
pdf.MoveTo(ml+gridPadding, ph/2-(textHeight/2))
textColor.Text(pdf)
pdf.MultiCell(width-2*gridMargin, sz, text, "", "ML", false)
// finally, we can draw the text
pdf.MultiCell(width-2*gridPadding, fontHeight, text, "", "ML", false)
}
// drawGrid ... draws a grid at the given x,y with given width and height,
// xs ticks on the x-axis, and ys ticks on the y-axis.
func drawGrid(pdf *fpdf.Fpdf, x, y, w, h float64, xs, ys int) {
// Horizontal lines
for i := 0; i < ys; i++ {
@@ -195,13 +245,14 @@ func drawGrid(pdf *fpdf.Fpdf, x, y, w, h float64, xs, ys int) {
pdf.Line(x, yOff, x+w, yOff)
}
// Vertical line(pw/2)-(width/2)s
// Vertical lines
for i := 0; i < xs; i++ {
xOff := x + (w/float64(xs-1))*float64(i)
pdf.Line(xOff, y, xOff, y+h)
}
}
// drawNoduleLine draws the colored line on the right side of the grid, with little squares at each end
func drawNoduleLine(pdf *fpdf.Fpdf, x1, y1, x2, y2, squareSize float64, squareColor Color, lineWidth float64, lineColor Color) {
lineColor.Draw(pdf)
pdf.SetLineWidth(lineWidth)
@@ -212,14 +263,20 @@ func drawNoduleLine(pdf *fpdf.Fpdf, x1, y1, x2, y2, squareSize float64, squareCo
pdf.Rect(x2-(squareSize/2), y2-(squareSize/2), squareSize, squareSize, "F")
}
// estimateFontSize attempts to determine the optimal font size for a given piece of text in a given container (with margin)
// this works by progressively making the font size larger and larger (up to maxFontSize) and calculating both the
// width and height at each iteration.
// It returns the biggest font size that still fits in the container (as in, 1pt larger will no longer fit, unless
// the returned font size is equal to maxFontSize)
func estimateFontSize(pdf *fpdf.Fpdf, text string, containerWidth, containerHeight, margin float64) float64 {
// Margin is on both sides haha, but I add an extra just for 'safety'
// Margin is on both sides haha, but I add an extra just for 'safety' on the right side
maxWidth := containerWidth - 3*margin
maxHeight := containerHeight - 3*margin
// Probably smallest we can start with
for fontSize := 10.; fontSize <= maxFontSize; fontSize += 1 {
width, height := stringSize(pdf, text, (fontSize/scaleFactor)*lineHeight, fontSize)
// Return the last font size if this one overflows
if width >= maxWidth || height >= maxHeight {
return fontSize - 1
}
@@ -228,6 +285,9 @@ func estimateFontSize(pdf *fpdf.Fpdf, text string, containerWidth, containerHeig
return maxFontSize
}
// stringSize builds on fpdf's `GetStringSymbolWidth` to return both the width of the largest line (split on `\n`)
// and the height of all lines put on top of each other.
// this effectively calculates the bounding box of the given text fragment
func stringSize(pdf *fpdf.Fpdf, text string, lineHeight, fontSizePt float64) (width, height float64) {
if fontSizePt == 0 {
fontSizePt, _ = pdf.GetFontSize()
@@ -245,6 +305,8 @@ func stringSize(pdf *fpdf.Fpdf, text string, lineHeight, fontSizePt float64) (wi
return
}
// addEmbeddedFont takes a reference a `go:embed`ed font and embeds it into the pdf so people
// will actually see the font if they don't magically have the specific font installed
func addEmbeddedFont(pdf *fpdf.Fpdf, family, style, path string) error {
jsonData, err := fs.ReadFile(fontsEmbedPrefix + path + ".json")
if err != nil {
@@ -263,6 +325,8 @@ func addEmbeddedFont(pdf *fpdf.Fpdf, family, style, path string) error {
return nil
}
// addEmbeddedLogo reads a `go:embed`ed image and adds it to the pdf, returning the handle for you to use
// to add it to the page
func addEmbeddedLogo(pdf *fpdf.Fpdf, name, path string) (*fpdf.ImageInfoType, error) {
reader, err := fs.Open(path)
if err != nil {