222 lines
4.9 KiB
Go
222 lines
4.9 KiB
Go
package renderer
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
|
|
"github.com/jung-kurt/gofpdf"
|
|
)
|
|
|
|
const (
|
|
pageMarginLeft = 20.0
|
|
pageMarginRight = 20.0
|
|
pageMarginTop = 20.0
|
|
pageMarginBottom = 22.0
|
|
)
|
|
|
|
// Renderer builds a PDF document from tagged blocks.
|
|
type Renderer struct {
|
|
Issuer Issuer
|
|
OwnerPassword string
|
|
}
|
|
|
|
// Render generates the PDF bytes for the provided blocks and footer hash.
|
|
func (r Renderer) Render(blocks []Block, footerHash string) ([]byte, error) {
|
|
pdf := gofpdf.New("P", "mm", "A4", "")
|
|
pdf.SetMargins(pageMarginLeft, pageMarginTop, pageMarginRight)
|
|
pdf.SetAutoPageBreak(true, pageMarginBottom)
|
|
pdf.SetCompression(false)
|
|
pdf.SetAuthor(r.Issuer.LegalName, false)
|
|
pdf.SetTitle("Act of Acceptance", false)
|
|
|
|
owner := strings.TrimSpace(r.OwnerPassword)
|
|
if owner != "" {
|
|
pdf.SetProtection(gofpdf.CnProtectPrint, "", owner)
|
|
}
|
|
|
|
pdf.SetFooterFunc(func() {
|
|
pdf.SetY(-15)
|
|
pdf.SetFont("Helvetica", "", 8)
|
|
footer := fmt.Sprintf("Document integrity hash: %s", footerHash)
|
|
pdf.CellFormat(0, 5, footer, "", 0, "L", false, 0, "")
|
|
})
|
|
|
|
pdf.AddPage()
|
|
if _, err := drawHeader(pdf, r.Issuer, pageMarginLeft, pageMarginTop); err != nil {
|
|
return nil, err
|
|
}
|
|
pdf.Ln(6)
|
|
|
|
for _, block := range blocks {
|
|
renderBlock(pdf, block)
|
|
if pdf.Error() != nil {
|
|
return nil, pdf.Error()
|
|
}
|
|
}
|
|
|
|
buf := &bytes.Buffer{}
|
|
if err := pdf.Output(buf); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func renderBlock(pdf *gofpdf.Fpdf, block Block) {
|
|
switch block.Tag {
|
|
case TagSpacer:
|
|
pdf.Ln(6)
|
|
case TagTitle:
|
|
pdf.SetFont("Helvetica", "B", 14)
|
|
for _, line := range block.Lines {
|
|
if strings.TrimSpace(line) == "" {
|
|
pdf.Ln(4)
|
|
continue
|
|
}
|
|
pdf.CellFormat(0, 7, line, "", 1, "C", false, 0, "")
|
|
}
|
|
pdf.Ln(2)
|
|
case TagSubtitle:
|
|
pdf.SetFont("Helvetica", "", 11)
|
|
for _, line := range block.Lines {
|
|
if strings.TrimSpace(line) == "" {
|
|
pdf.Ln(3)
|
|
continue
|
|
}
|
|
pdf.CellFormat(0, 6, line, "", 1, "C", false, 0, "")
|
|
}
|
|
pdf.Ln(2)
|
|
case TagMeta:
|
|
pdf.SetFont("Helvetica", "", 9)
|
|
for _, line := range block.Lines {
|
|
if strings.TrimSpace(line) == "" {
|
|
pdf.Ln(2)
|
|
continue
|
|
}
|
|
pdf.CellFormat(0, 4.5, line, "", 1, "R", false, 0, "")
|
|
}
|
|
pdf.Ln(2)
|
|
case TagSection:
|
|
pdf.Ln(2)
|
|
pdf.SetFont("Helvetica", "B", 11)
|
|
for _, line := range block.Lines {
|
|
if strings.TrimSpace(line) == "" {
|
|
pdf.Ln(3)
|
|
continue
|
|
}
|
|
pdf.CellFormat(0, 6, line, "", 1, "L", false, 0, "")
|
|
}
|
|
pdf.Ln(1)
|
|
case TagText:
|
|
pdf.SetFont("Helvetica", "", 10)
|
|
text := strings.Join(block.Lines, "\n")
|
|
pdf.MultiCell(0, 5, text, "", "L", false)
|
|
pdf.Ln(1)
|
|
case TagKV:
|
|
renderKeyValue(pdf, block)
|
|
case TagTable:
|
|
renderTable(pdf, block)
|
|
case TagSign:
|
|
pdf.SetFont("Helvetica", "", 10)
|
|
text := strings.Join(block.Lines, "\n")
|
|
pdf.MultiCell(0, 6, text, "", "L", false)
|
|
pdf.Ln(2)
|
|
default:
|
|
// Unknown tag: treat as plain text for resilience.
|
|
pdf.SetFont("Helvetica", "", 10)
|
|
text := strings.Join(block.Lines, "\n")
|
|
pdf.MultiCell(0, 5, text, "", "L", false)
|
|
pdf.Ln(1)
|
|
}
|
|
}
|
|
|
|
func renderKeyValue(pdf *gofpdf.Fpdf, block Block) {
|
|
pdf.SetFont("Helvetica", "", 10)
|
|
usable := usableWidth(pdf)
|
|
keyWidth := math.Round(usable * 0.35)
|
|
valueWidth := usable - keyWidth
|
|
lineHeight := 5.0
|
|
|
|
for _, row := range block.Rows {
|
|
if len(row) == 0 {
|
|
continue
|
|
}
|
|
key := row[0]
|
|
value := ""
|
|
if len(row) > 1 {
|
|
value = row[1]
|
|
}
|
|
x := pdf.GetX()
|
|
y := pdf.GetY()
|
|
|
|
pdf.SetXY(x, y)
|
|
pdf.SetFont("Helvetica", "B", 10)
|
|
pdf.MultiCell(keyWidth, lineHeight, key, "", "L", false)
|
|
leftY := pdf.GetY()
|
|
|
|
pdf.SetXY(x+keyWidth, y)
|
|
pdf.SetFont("Helvetica", "", 10)
|
|
pdf.MultiCell(valueWidth, lineHeight, value, "", "L", false)
|
|
rightY := pdf.GetY()
|
|
|
|
pdf.SetY(maxFloat(leftY, rightY))
|
|
}
|
|
pdf.Ln(1)
|
|
}
|
|
|
|
func renderTable(pdf *gofpdf.Fpdf, block Block) {
|
|
if len(block.Rows) == 0 {
|
|
return
|
|
}
|
|
usable := usableWidth(pdf)
|
|
col1 := math.Round(usable * 0.7)
|
|
col2 := usable - col1
|
|
lineHeight := 6.0
|
|
|
|
header := block.Rows[0]
|
|
pdf.SetFont("Helvetica", "B", 10)
|
|
if len(header) > 0 {
|
|
pdf.CellFormat(col1, lineHeight, header[0], "1", 0, "L", false, 0, "")
|
|
}
|
|
if len(header) > 1 {
|
|
pdf.CellFormat(col2, lineHeight, header[1], "1", 1, "R", false, 0, "")
|
|
} else {
|
|
pdf.CellFormat(col2, lineHeight, "", "1", 1, "R", false, 0, "")
|
|
}
|
|
|
|
pdf.SetFont("Helvetica", "", 10)
|
|
for _, row := range block.Rows[1:] {
|
|
colA := ""
|
|
colB := ""
|
|
if len(row) > 0 {
|
|
colA = row[0]
|
|
}
|
|
if len(row) > 1 {
|
|
colB = row[1]
|
|
}
|
|
x := pdf.GetX()
|
|
y := pdf.GetY()
|
|
pdf.MultiCell(col1, lineHeight, colA, "1", "L", false)
|
|
leftY := pdf.GetY()
|
|
pdf.SetXY(x+col1, y)
|
|
pdf.MultiCell(col2, lineHeight, colB, "1", "R", false)
|
|
rightY := pdf.GetY()
|
|
pdf.SetY(maxFloat(leftY, rightY))
|
|
}
|
|
pdf.Ln(2)
|
|
}
|
|
|
|
func usableWidth(pdf *gofpdf.Fpdf) float64 {
|
|
pageW, _ := pdf.GetPageSize()
|
|
left, _, right, _ := pdf.GetMargins()
|
|
return pageW - left - right
|
|
}
|
|
|
|
func maxFloat(a, b float64) float64 {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|