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”.
This commit is contained in:
parent
b40e862082
commit
38b902d5e7
1 changed files with 279 additions and 82 deletions
|
|
@ -1,103 +1,218 @@
|
|||
package pdfcert
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/phpdave11/gofpdf"
|
||||
qrcode "github.com/skip2/go-qrcode"
|
||||
)
|
||||
|
||||
// Input for a simple one-page Verae registration certificate PDF.
|
||||
//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
|
||||
VerifyURL string
|
||||
Scope string
|
||||
VerifyURL string // optional override; must not be mcp.verae.ai
|
||||
Channel string
|
||||
ByteLength int64
|
||||
}
|
||||
|
||||
// Build returns a minimal PDF (no external deps) describing the stamp.
|
||||
// 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.RFC3339)
|
||||
created = time.Now().UTC().Format(time.RFC3339Nano)
|
||||
}
|
||||
local := ""
|
||||
if t, err := time.Parse(time.RFC3339, created); err == nil {
|
||||
local = t.Local().Format(time.RFC1123)
|
||||
scope := strings.ToUpper(strings.TrimSpace(in.Scope))
|
||||
if scope == "" {
|
||||
scope = "GLOBAL"
|
||||
}
|
||||
verify := in.VerifyURL
|
||||
if verify == "" && in.SHA256 != "" {
|
||||
verify = fmt.Sprintf("https://mcp.verae.ai/register.html?sha256=%s&receipt=%s", in.SHA256, in.ReceiptID)
|
||||
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)"
|
||||
}
|
||||
|
||||
lines := []string{
|
||||
"Verae Timestamp Registration",
|
||||
"",
|
||||
"Document: " + sanitize(in.DocumentName),
|
||||
"SHA-256: " + in.SHA256,
|
||||
"Receipt ID: " + in.ReceiptID,
|
||||
"UTC: " + created,
|
||||
}
|
||||
if local != "" {
|
||||
lines = append(lines, "Local: "+local)
|
||||
}
|
||||
if in.Merkle != "" {
|
||||
lines = append(lines, "Merkle/block: "+in.Merkle)
|
||||
}
|
||||
if in.Channel != "" {
|
||||
lines = append(lines, "Channel: "+in.Channel)
|
||||
}
|
||||
lines = append(lines, "Verify: "+verify)
|
||||
lines = append(lines, "", "Digests only — document bytes were not uploaded.")
|
||||
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
|
||||
|
||||
return buildTextPDF(lines)
|
||||
// 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 sanitize(s string) string {
|
||||
s = strings.ReplaceAll(s, "(", "\\(")
|
||||
s = strings.ReplaceAll(s, ")", "\\)")
|
||||
s = strings.ReplaceAll(s, "\\", "\\\\")
|
||||
func orDash(s string) string {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return "—"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Minimal PDF 1.4 with Helvetica text lines.
|
||||
func buildTextPDF(lines []string) ([]byte, error) {
|
||||
var content strings.Builder
|
||||
content.WriteString("BT /F1 12 Tf 50 750 Td 14 TL\n")
|
||||
for i, line := range lines {
|
||||
if i > 0 {
|
||||
content.WriteString("T*\n")
|
||||
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
|
||||
}
|
||||
content.WriteString("(")
|
||||
content.WriteString(sanitize(line))
|
||||
content.WriteString(") Tj\n")
|
||||
}
|
||||
content.WriteString("ET\n")
|
||||
stream := content.String()
|
||||
|
||||
objects := []string{
|
||||
"<< /Type /Catalog /Pages 2 0 R >>",
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
|
||||
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>",
|
||||
fmt.Sprintf("<< /Length %d >>\nstream\n%s\nendstream", len(stream), stream),
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
|
||||
if len(embeddedLogo) == 0 {
|
||||
return "", nil, fmt.Errorf("logo not found")
|
||||
}
|
||||
return writeTempPNG(embeddedLogo)
|
||||
}
|
||||
|
||||
var body strings.Builder
|
||||
body.WriteString("%PDF-1.4\n")
|
||||
offsets := make([]int, len(objects)+1)
|
||||
for i, obj := range objects {
|
||||
offsets[i+1] = body.Len()
|
||||
fmt.Fprintf(&body, "%d 0 obj\n%s\nendobj\n", i+1, obj)
|
||||
func writeTempPNG(b []byte) (path string, cleanup func(), err error) {
|
||||
f, err := os.CreateTemp("", "verae-logo-*.png")
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
xrefStart := body.Len()
|
||||
fmt.Fprintf(&body, "xref\n0 %d\n", len(objects)+1)
|
||||
body.WriteString("0000000000 65535 f \n")
|
||||
for i := 1; i <= len(objects); i++ {
|
||||
fmt.Fprintf(&body, "%010d 00000 n \n", offsets[i])
|
||||
if _, err := f.Write(b); err != nil {
|
||||
f.Close()
|
||||
os.Remove(f.Name())
|
||||
return "", nil, err
|
||||
}
|
||||
fmt.Fprintf(&body, "trailer << /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF\n", len(objects)+1, xrefStart)
|
||||
return []byte(body.String()), nil
|
||||
f.Close()
|
||||
return f.Name(), func() { _ = os.Remove(f.Name()) }, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue