PrintToVeraeTimeStamp/internal/pdfcert/pdfcert.go
George Lambert 38b902d5e7 fix(pdfcert): branded Registration Document without mcp.verae.ai
Match the official Verae Timestamp Registration Document layout (logo,
frames, field list, QR). Disclaimer: only the digest was submitted.
Lookup/footer use explorer.verae.ai — no mcp.verae.ai or “via MCP”.
2026-10-03 04:15:28 -04:00

218 lines
6 KiB
Go

package pdfcert
import (
"bytes"
_ "embed"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/phpdave11/gofpdf"
qrcode "github.com/skip2/go-qrcode"
)
//go:embed logo.png
var embeddedLogo []byte
// Input for the official Verae Timestamp Registration Document.
type Input struct {
DocumentName string
SHA256 string
ReceiptID string
CreatedAt string
Merkle string
Scope string
VerifyURL string // optional override; must not be mcp.verae.ai
Channel string
ByteLength int64
}
// LookupURL builds a public lookup link without mcp.verae.ai.
func LookupURL(sha256, receiptID string) string {
sha256 = strings.TrimSpace(sha256)
receiptID = strings.TrimSpace(receiptID)
// Explorer is the public surface; include digest (+ receipt when present).
u := "https://explorer.verae.ai/?sha256=" + sha256
if receiptID != "" {
u += "&receipt=" + receiptID
}
return u
}
// Build returns the branded one-page registration PDF (US Letter).
func Build(in Input) ([]byte, error) {
created := in.CreatedAt
if created == "" {
created = time.Now().UTC().Format(time.RFC3339Nano)
}
scope := strings.ToUpper(strings.TrimSpace(in.Scope))
if scope == "" {
scope = "GLOBAL"
}
lookup := in.VerifyURL
if lookup == "" || strings.Contains(strings.ToLower(lookup), "mcp.verae.ai") {
lookup = LookupURL(in.SHA256, in.ReceiptID)
}
merkle := in.Merkle
if merkle == "" {
merkle = "See ledger lookup via receipt CID"
}
docName := in.DocumentName
if docName == "" {
docName = "(digest only)"
}
pdf := gofpdf.New("P", "pt", "Letter", "")
pdf.SetCompression(false) // keep certificate text extractable / greppable
pdf.SetMargins(48, 48, 48)
pdf.SetAutoPageBreak(false, 0)
pdf.AddPage()
pageW, pageH := pdf.GetPageSize()
margin := 48.0
// Outer frames (purple + gold) matching register.html / app.html
pdf.SetDrawColor(42, 24, 56)
pdf.SetLineWidth(2)
pdf.Rect(28, 28, pageW-56, pageH-56, "D")
pdf.SetDrawColor(196, 168, 90)
pdf.SetLineWidth(0.6)
pdf.Rect(34, 34, pageW-68, pageH-68, "D")
// Logo
logoPath, cleanup, err := materializeLogo()
if err == nil {
pdf.ImageOptions(logoPath, margin, 48, 120, 36, false, gofpdf.ImageOptions{ImageType: "PNG", ReadDpi: true}, 0, "")
if cleanup != nil {
defer cleanup()
}
}
pdf.SetTextColor(42, 24, 56)
pdf.SetFont("Times", "B", 18)
pdf.SetXY(margin, 100)
pdf.CellFormat(pageW-margin*2, 20, "Verae Timestamp Registration Document", "", 0, "C", false, 0, "")
pdf.SetTextColor(90, 80, 100)
pdf.SetFont("Helvetica", "", 10)
pdf.SetXY(margin, 122)
pdf.CellFormat(pageW-margin*2, 12, "Official certificate of digital content registration on the VeraeTime global ledger", "", 0, "C", false, 0, "")
pdf.SetDrawColor(196, 168, 90)
pdf.SetLineWidth(1)
pdf.Line(margin, 142, pageW-margin, 142)
fields := [][2]string{
{"Document name", docName},
}
if in.ByteLength > 0 {
fields = append(fields, [2]string{"Document size", fmt.Sprintf("%d bytes", in.ByteLength)})
}
fields = append(fields,
[2]string{"Content SHA-256", in.SHA256},
[2]string{"Registration scope", scope},
[2]string{"Original timestamp (UTC)", created},
[2]string{"Receipt ID (CID)", orDash(in.ReceiptID)},
[2]string{"Merkle / block hash", merkle},
[2]string{"Ledger location", "VeraeTime global ledger - api.veraetime.net"},
[2]string{"Blockchain explorer", "https://explorer.verae.ai/"},
[2]string{"Public lookup URL", lookup},
)
y := 168.0
for _, fv := range fields {
label, value := fv[0], fv[1]
pdf.SetFont("Helvetica", "B", 10)
pdf.SetTextColor(42, 24, 56)
pdf.SetXY(margin, y)
pdf.Cell(pageW-margin*2, 12, label)
y += 14
pdf.SetFont("Helvetica", "", 10)
pdf.SetTextColor(40, 40, 50)
lines := pdf.SplitLines([]byte(value), pageW-margin*2-10)
for _, ln := range lines {
pdf.SetXY(margin, y)
pdf.Cell(pageW-margin*2-10, 12, string(ln))
y += 12
}
y += 10
if y > pageH-220 {
break
}
}
// QR code → explorer lookup (not mcp.verae.ai)
qrPNG, err := qrcode.Encode(lookup, qrcode.Medium, 280)
if err == nil {
qrFile, qclean, qerr := writeTempPNG(qrPNG)
if qerr == nil {
qrSize := 110.0
qrX := pageW - margin - qrSize
qrY := pageH - margin - qrSize - 36
pdf.ImageOptions(qrFile, qrX, qrY, qrSize, qrSize, false, gofpdf.ImageOptions{ImageType: "PNG"}, 0, "")
pdf.SetFont("Helvetica", "", 8)
pdf.SetTextColor(90, 80, 100)
pdf.SetXY(qrX, qrY+qrSize+4)
pdf.CellFormat(qrSize, 10, "Scan to look up this registration", "", 0, "C", false, 0, "")
if qclean != nil {
defer qclean()
}
}
}
disclaimer := "This document certifies that the SHA-256 digest above was registered with Verae. File bytes were not uploaded; only the digest was submitted."
pdf.SetFont("Helvetica", "", 8)
pdf.SetTextColor(90, 80, 100)
pdf.SetXY(margin, pageH-64)
pdf.MultiCell(pageW-margin*2-130, 10, disclaimer, "", "L", false)
pdf.SetFont("Helvetica", "B", 9)
pdf.SetTextColor(42, 24, 56)
pdf.SetXY(margin, pageH-36)
pdf.Cell(pageW-margin*2, 12, "Verae | verae.com | explorer.verae.ai")
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func orDash(s string) string {
if strings.TrimSpace(s) == "" {
return "—"
}
return s
}
func materializeLogo() (path string, cleanup func(), err error) {
// Prefer module assets path next to source; fall back to embed
candidates := []string{
filepath.Join("assets", "VeraeFullLogo.png"),
"/Users/marchon/research/PrintToVeraeTimeStamp/assets/VeraeFullLogo.png",
}
for _, c := range candidates {
if st, e := os.Stat(c); e == nil && st.Size() > 0 {
return c, nil, nil
}
}
if len(embeddedLogo) == 0 {
return "", nil, fmt.Errorf("logo not found")
}
return writeTempPNG(embeddedLogo)
}
func writeTempPNG(b []byte) (path string, cleanup func(), err error) {
f, err := os.CreateTemp("", "verae-logo-*.png")
if err != nil {
return "", nil, err
}
if _, err := f.Write(b); err != nil {
f.Close()
os.Remove(f.Name())
return "", nil, err
}
f.Close()
return f.Name(), func() { _ = os.Remove(f.Name()) }, nil
}