Brush up the go code
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user