Files
covergen/pkg/covergen/render.go

283 lines
7.0 KiB
Go

package covergen
import (
"embed"
"fmt"
"strings"
"github.com/go-pdf/fpdf"
)
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
var bgColor = Color{25, 25, 25}
var textColor = Color{255, 255, 255}
var gridColor = Color{80, 80, 80}
var gridNoduleColor = Color{255, 255, 255}
//go:embed miwebb.white.png font_embed
var fs embed.FS
const fontsEmbedPrefix = "font_embed/"
// 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
type CoverSettings struct {
Number string
NumberPrefix string
CustomerName string
HLColor Color
}
func GenerateInvoice(settings CoverSettings) (*fpdf.Fpdf, error) {
pdf, err := GenerateFrontCover(settings)
if err != nil {
return nil, err
}
pdf.AddPage()
drawCustomerName(pdf, settings)
if pdf.Err() {
return nil, pdf.Error()
}
return pdf, nil
}
func GenerateFrontCover(settings CoverSettings) (*fpdf.Fpdf, error) {
pdf, err := generateBaseInvoice(settings)
if err != nil {
return nil, err
}
drawInvoiceNumber(pdf, settings)
drawMiwebbLink(pdf)
if pdf.Err() {
return nil, pdf.Error()
}
return pdf, nil
}
func GenerateBackCover(settings CoverSettings) (*fpdf.Fpdf, error) {
pdf, err := generateBaseInvoice(settings)
if err != nil {
return nil, err
}
return pdf, err
}
func generateBaseInvoice(settings CoverSettings) (*fpdf.Fpdf, error) {
pdf := fpdf.New("P", scaleUnit, "A4", "")
if err := addEmbeddedFont(pdf, "Mark Medium", "", "Mark-Medium"); err != nil {
return nil, fmt.Errorf("error adding font: %w", err)
}
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")
if err != nil {
return nil, fmt.Errorf("error adding logo: %w", err)
}
pdf.SetMargins(margin, margin, margin)
pageWidth, pageHeight := pdf.GetPageSize()
pdf.SetAutoPageBreak(false, 0)
pdf.SetHeaderFunc(func() {
bgColor.Fill(pdf)
pdf.Rect(0, 0, pageWidth, pageHeight, "F")
pdf.Image(miwebbLogoName, margin-5, margin, miwebbLogo.Width()*logoScale, miwebbLogo.Height()*logoScale, false, "", 0, "")
})
pdf.AddPage()
drawCustomerName(pdf, settings)
if pdf.Err() {
return nil, fmt.Errorf("error generating invoice: %w", pdf.Error())
}
return pdf, nil
}
func drawInvoiceNumber(pdf *fpdf.Fpdf, settings CoverSettings) {
_, pageHeight := pdf.GetPageSize()
pdf.MoveTo(margin, pageHeight-margin)
pdf.SetFont("Mark Light", "", 9)
textColor.Text(pdf)
pdf.TransformBegin()
pdf.TransformRotate(90, margin, pageHeight-margin)
invoiceText := strings.ToUpper(fmt.Sprintf("%s %s", settings.NumberPrefix, settings.Number))
pdf.CellFormat(0, 1, invoiceText, "", 0, "LT", false, 0, "")
pdf.TransformEnd()
_, h := pdf.GetFontSize()
// X is 'margin + half text height'
x := float64(margin) + h/2
// Y is 'where the text ended' + some gap
y := pageHeight - margin - pdf.GetStringWidth(invoiceText) - 5
textColor.Draw(pdf)
pdf.SetLineWidth(0.01)
pdf.Line(x, y, x, y-10)
}
func drawMiwebbLink(pdf *fpdf.Fpdf) {
pageWidth, pageHeight := pdf.GetPageSize()
pdf.MoveTo(pageWidth-margin, pageHeight-margin)
pdf.SetFont("Mark Light", "", 9)
textColor.Text(pdf)
pdf.CellFormat(0, 0, "WWW.MIWEBB.COM", "", 0, "RB", false, 0, "https://www.miwebb.com")
}
func drawCustomerName(pdf *fpdf.Fpdf, settings CoverSettings) {
ml, _, mr, _ := pdf.GetMargins()
pw, ph := pdf.GetPageSize()
width := pw - ml - mr
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)
gridX, gridY := ml, (ph/2)-(height/2)
drawGrid(pdf, gridX, gridY, width, height, gridXTicks, gridYTicks)
// Draw the highlight on the right
drawNoduleLine(
pdf,
gridX+width, gridY,
gridX+width, gridY+height,
1.2, gridNoduleColor,
0.25, settings.HLColor)
pdf.SetFont("Mark Medium", "", 10)
// Estimate font size, and calculate line height immediately after
pdf.SetFontSize(estimateFontSize(pdf, text, width, height, gridMargin))
_, sz := pdf.GetFontSize()
sz *= lineHeight
_, textHeight := stringSize(pdf, text, sz, 0)
// Centered vertically:
// x = <gridX> + <margin>
// y = <gridY> + <gridH>/2 - textHeight/2
pdf.MoveTo(ml+gridMargin, ph/2-(textHeight/2))
textColor.Text(pdf)
pdf.MultiCell(width-2*gridMargin, sz, text, "", "ML", false)
}
func drawGrid(pdf *fpdf.Fpdf, x, y, w, h float64, xs, ys int) {
// Horizontal lines
for i := 0; i < ys; i++ {
yOff := y + (h/float64(ys-1))*float64(i)
pdf.Line(x, yOff, x+w, yOff)
}
// Vertical line(pw/2)-(width/2)s
for i := 0; i < xs; i++ {
xOff := x + (w/float64(xs-1))*float64(i)
pdf.Line(xOff, y, xOff, y+h)
}
}
func drawNoduleLine(pdf *fpdf.Fpdf, x1, y1, x2, y2, squareSize float64, squareColor Color, lineWidth float64, lineColor Color) {
lineColor.Draw(pdf)
pdf.SetLineWidth(lineWidth)
pdf.Line(x1, y1, x2, y2)
squareColor.Fill(pdf)
pdf.Rect(x1-(squareSize/2), y1-(squareSize/2), squareSize, squareSize, "F")
pdf.Rect(x2-(squareSize/2), y2-(squareSize/2), squareSize, squareSize, "F")
}
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'
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)
if width >= maxWidth || height >= maxHeight {
return fontSize - 1
}
}
return maxFontSize
}
func stringSize(pdf *fpdf.Fpdf, text string, lineHeight, fontSizePt float64) (width, height float64) {
if fontSizePt == 0 {
fontSizePt, _ = pdf.GetFontSize()
}
lines := strings.Split(text, "\n")
height = float64(len(lines)) * lineHeight
for _, line := range lines {
lineWidth := float64(pdf.GetStringSymbolWidth(line)) * fontSizePt / scaleFactor / 1000
if lineWidth > width {
width = lineWidth
}
}
return
}
func addEmbeddedFont(pdf *fpdf.Fpdf, family, style, path string) error {
jsonData, err := fs.ReadFile(fontsEmbedPrefix + path + ".json")
if err != nil {
return err
}
zData, err := fs.ReadFile(fontsEmbedPrefix + path + ".z")
if err != nil {
return err
}
pdf.AddFontFromBytes(family, style, jsonData, zData)
if pdf.Err() {
return pdf.Error()
}
return nil
}
func addEmbeddedLogo(pdf *fpdf.Fpdf, name, path string) (*fpdf.ImageInfoType, error) {
reader, err := fs.Open(path)
if err != nil {
return nil, err
}
handle := pdf.RegisterImageOptionsReader(name, fpdf.ImageOptions{
ImageType: "PNG",
ReadDpi: true,
}, reader)
if pdf.Err() {
return nil, pdf.Error()
}
return handle, nil
}