package pdfcert import ( "bytes" "fmt" "os" "path/filepath" "strings" "github.com/phpdave11/gofpdf" qrcode "github.com/skip2/go-qrcode" ) // Minimal fallback when Node/jsPDF is unavailable. func buildFallbackGofpdf(in Input, docName, created, scope, merkle, lookup string) ([]byte, error) { pdf := gofpdf.New("P", "pt", "Letter", "") pdf.SetCompression(false) pdf.SetMargins(48, 48, 48) pdf.SetAutoPageBreak(false, 0) pdf.AddPage() pageW, pageH := pdf.GetPageSize() margin := 48.0 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") if p, cleanup, err := materializeLogo(); err == nil { pdf.ImageOptions(p, 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) if created == "" { created = "—" } fields := [][2]string{ {"Document name", docName}, {"Content SHA-256", in.SHA256}, {"Registration scope", scope}, {"Original timestamp (UTC)", created}, {"Receipt ID (CID)", orDash(in.ReceiptID)}, {"Merkle / block hash", merkle}, {"Ledger location", "VeraeTime global ledger - api.veraetime.net"}, {"Blockchain explorer", "https://explorer.verae.ai/"}, {"Public lookup URL", lookup}, } if in.ByteLength > 0 { fields = append([][2]string{ {"Document name", docName}, {"Document size", fmt.Sprintf("%d bytes", in.ByteLength)}, }, fields[1:]...) } isHTTP := func(s string) bool { return strings.HasPrefix(strings.ToLower(strings.TrimSpace(s)), "http://") || strings.HasPrefix(strings.ToLower(strings.TrimSpace(s)), "https://") } y := 168.0 for _, fv := range fields { pdf.SetFont("Helvetica", "B", 10) pdf.SetTextColor(42, 24, 56) pdf.SetXY(margin, y) pdf.Cell(pageW-margin*2, 12, fv[0]) y += 14 pdf.SetFont("Helvetica", "", 10) linkURL := "" if isHTTP(fv[1]) { linkURL = strings.TrimSpace(fv[1]) pdf.SetTextColor(74, 46, 140) } else { pdf.SetTextColor(40, 40, 50) } for _, ln := range pdf.SplitLines([]byte(fv[1]), pageW-margin*2-10) { pdf.SetXY(margin, y) if linkURL != "" { pdf.CellFormat(pageW-margin*2-10, 12, string(ln), "", 0, "L", false, 0, linkURL) } else { pdf.Cell(pageW-margin*2-10, 12, string(ln)) } y += 12 } y += 10 if y > pageH-220 { break } } if qrPNG, err := qrcode.Encode(lookup, qrcode.Medium, 280); err == nil { if qrFile, qclean, qerr := writeTempPNG(qrPNG); 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.LinkString(qrX, qrY, qrSize, qrSize, lookup) pdf.SetFont("Helvetica", "", 8) pdf.SetTextColor(74, 46, 140) pdf.SetXY(qrX, qrY+qrSize+4) pdf.CellFormat(qrSize, 10, "Scan to look up this registration", "", 0, "C", false, 0, lookup) if qclean != nil { defer qclean() } } } pdf.SetFont("Helvetica", "", 8) pdf.SetTextColor(90, 80, 100) pdf.SetXY(margin, pageH-64) pdf.MultiCell(pageW-margin*2-130, 10, "This document certifies that the SHA-256 digest above was registered with Verae. File bytes were not uploaded; only the digest was submitted.", "", "L", false) pdf.SetFont("Helvetica", "B", 9) pdf.SetTextColor(42, 24, 56) pdf.SetXY(margin, pageH-36) pdf.Cell(40, 12, "Verae | ") pdf.SetTextColor(74, 46, 140) pdf.CellFormat(55, 12, "verae.com", "", 0, "L", false, 0, "https://verae.com") pdf.SetTextColor(42, 24, 56) pdf.Cell(12, 12, " | ") pdf.SetTextColor(74, 46, 140) pdf.CellFormat(90, 12, "explorer.verae.ai", "", 0, "L", false, 0, "https://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 s == "" { return "—" } return s } func materializeLogo() (path string, cleanup func(), err error) { for _, c := range []string{ filepath.Join("assets", "VeraeFullLogo.png"), "/Users/marchon/research/PrintToVeraeTimeStamp/assets/VeraeFullLogo.png", } { 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-png-*.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 }