package renderer import ( "bytes" "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 := "Document integrity hash: " + 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 }