Fixes + stable gateway ids

This commit is contained in:
Stephan D
2026-02-18 20:38:08 +01:00
parent 4dc182bfa2
commit 770c7b9da9
119 changed files with 3000 additions and 734 deletions

View File

@@ -27,6 +27,7 @@ func drawHeader(pdf *gofpdf.Fpdf, issuer Issuer, marginLeft, marginTop float64)
if logoWidth > 0 {
textX = startX + logoWidth + 6
}
pdf.SetXY(textX, startY)
pdf.SetFont("Helvetica", "B", 12)
pdf.CellFormat(0, 5, issuer.LegalName, "", 1, "L", false, 0, "")
@@ -39,6 +40,7 @@ func drawHeader(pdf *gofpdf.Fpdf, issuer Issuer, marginLeft, marginTop float64)
}
currentY := pdf.GetY()
if logoWidth > 0 {
logoBottom := startY + logoWidth
if logoBottom > currentY {

View File

@@ -2,7 +2,6 @@ package renderer
import (
"bytes"
"fmt"
"math"
"strings"
@@ -39,18 +38,22 @@ func (r Renderer) Render(blocks []Block, footerHash string) ([]byte, error) {
pdf.SetFooterFunc(func() {
pdf.SetY(-15)
pdf.SetFont("Helvetica", "", 8)
footer := fmt.Sprintf("Document integrity hash: %s", footerHash)
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()
}
@@ -60,6 +63,7 @@ func (r Renderer) Render(blocks []Block, footerHash string) ([]byte, error) {
if err := pdf.Output(buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
@@ -69,47 +73,64 @@ func renderBlock(pdf *gofpdf.Fpdf, block Block) {
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)
@@ -119,12 +140,14 @@ func renderBlock(pdf *gofpdf.Fpdf, block Block) {
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)
@@ -133,6 +156,7 @@ func renderBlock(pdf *gofpdf.Fpdf, block Block) {
func renderKeyValue(pdf *gofpdf.Fpdf, block Block) {
pdf.SetFont("Helvetica", "", 10)
usable := usableWidth(pdf)
keyWidth := math.Round(usable * 0.35)
valueWidth := usable - keyWidth
@@ -142,11 +166,14 @@ func renderKeyValue(pdf *gofpdf.Fpdf, block Block) {
if len(row) == 0 {
continue
}
key := row[0]
value := ""
if len(row) > 1 {
value = row[1]
}
x := pdf.GetX()
y := pdf.GetY()
@@ -162,6 +189,7 @@ func renderKeyValue(pdf *gofpdf.Fpdf, block Block) {
pdf.SetY(maxFloat(leftY, rightY))
}
pdf.Ln(1)
}
@@ -169,6 +197,7 @@ 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
@@ -176,9 +205,11 @@ func renderTable(pdf *gofpdf.Fpdf, block Block) {
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 {
@@ -186,15 +217,19 @@ func renderTable(pdf *gofpdf.Fpdf, block Block) {
}
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)
@@ -204,12 +239,14 @@ func renderTable(pdf *gofpdf.Fpdf, block Block) {
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
}
@@ -217,5 +254,6 @@ func maxFloat(a, b float64) float64 {
if a > b {
return a
}
return b
}

View File

@@ -26,11 +26,13 @@ func TestRenderer_RenderContainsText(t *testing.T) {
if err != nil {
t.Fatalf("Render: %v", err)
}
if len(pdfBytes) == 0 {
t.Fatalf("expected PDF bytes")
}
checks := []string{"Sendico Ltd", "Jane Doe", "100 USD", "Document integrity hash"}
for _, token := range checks {
if !containsPDFText(pdfBytes, token) {
t.Fatalf("expected PDF to contain %q", token)
@@ -42,22 +44,29 @@ func containsPDFText(pdfBytes []byte, text string) bool {
if bytes.Contains(pdfBytes, []byte(text)) {
return true
}
hexText := hex.EncodeToString([]byte(text))
if bytes.Contains(pdfBytes, []byte(strings.ToUpper(hexText))) {
return true
}
if bytes.Contains(pdfBytes, []byte(strings.ToLower(hexText))) {
return true
}
utf16Bytes := encodeUTF16BE(text, false)
if bytes.Contains(pdfBytes, utf16Bytes) {
return true
}
utf16Hex := hex.EncodeToString(utf16Bytes)
if bytes.Contains(pdfBytes, []byte(strings.ToUpper(utf16Hex))) {
return true
}
if bytes.Contains(pdfBytes, []byte(strings.ToLower(utf16Hex))) {
return true
}
@@ -66,25 +75,33 @@ func containsPDFText(pdfBytes []byte, text string) bool {
if bytes.Contains(pdfBytes, utf16BytesBOM) {
return true
}
utf16HexBOM := hex.EncodeToString(utf16BytesBOM)
if bytes.Contains(pdfBytes, []byte(strings.ToUpper(utf16HexBOM))) {
return true
}
return bytes.Contains(pdfBytes, []byte(strings.ToLower(utf16HexBOM)))
}
func encodeUTF16BE(text string, withBOM bool) []byte {
encoded := utf16.Encode([]rune(text))
length := len(encoded) * 2
if withBOM {
length += 2
}
out := make([]byte, 0, length)
if withBOM {
out = append(out, 0xFE, 0xFF)
}
for _, v := range encoded {
out = append(out, byte(v>>8), byte(v))
}
return out
}

View File

@@ -32,6 +32,7 @@ type Block struct {
func ParseBlocks(input string) ([]Block, error) {
scanner := bufio.NewScanner(strings.NewReader(input))
blocks := make([]Block, 0)
var current *Block
flush := func() {
@@ -44,17 +45,24 @@ func ParseBlocks(input string) ([]Block, error) {
for scanner.Scan() {
line := strings.TrimRight(scanner.Text(), "\r")
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "#") {
flush()
tag := Tag(strings.TrimSpace(strings.TrimPrefix(trimmed, "#")))
if tag == "" {
continue
}
if tag == TagSpacer {
blocks = append(blocks, Block{Tag: TagSpacer})
continue
}
current = &Block{Tag: tag}
continue
}
@@ -62,16 +70,19 @@ func ParseBlocks(input string) ([]Block, error) {
continue
}
switch current.Tag {
switch current.Tag { //nolint:exhaustive // only KV and Table need row parsing
case TagKV, TagTable:
if trimmed == "" {
continue
}
parts := strings.Split(line, "|")
row := make([]string, 0, len(parts))
for _, part := range parts {
row = append(row, strings.TrimSpace(part))
}
current.Rows = append(current.Rows, row)
default:
current.Lines = append(current.Lines, line)
@@ -79,9 +90,10 @@ func ParseBlocks(input string) ([]Block, error) {
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("Parse blocks: %w", err)
return nil, fmt.Errorf("parse blocks: %w", err)
}
flush()
return blocks, nil
}