283 lines
7.0 KiB
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
|
|
}
|