package pdfcert import ( "bytes" "fmt" "log" "os" "path/filepath" "strconv" "strings" "github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/statuslang" "github.com/phpdave11/gofpdf" qrcode "github.com/skip2/go-qrcode" ) // brandTitleFamily is the gofpdf family name when a brand TTF/OTF registers. const brandTitleFamily = "VeraeBrand" // Brand tokens (CERT-01 / BrandingBook): plum, gold, ink, muted, ok. const ( plumR, plumG, plumB = 83, 27, 89 // #531B59 goldR, goldG, goldB = 196, 168, 90 // #C4A85A goldDR, goldDG, goldDB = 110, 87, 23 // #6E5717 inkR, inkG, inkB = 29, 21, 52 // #1D1534 mutedR, mutedG, mutedB = 94, 84, 112 // #5E5470 okR, okG, okB = 14, 107, 58 // #0E6B3A okBgR, okBgG, okBgB = 227, 245, 234 // #E3F5EA okBdR, okBdG, okBdB = 185, 226, 200 // #B9E2C8 linkR, linkG, linkB = 59, 42, 140 // #3B2A8C info ) // Minimal fallback when Node/jsPDF is unavailable. // // Fonts: PDF core faces by default (Times title, Helvetica body). // When EmbedBrandFonts is on and BrandFontsDir has a .ttf/.otf, attempt // gofpdf AddUTF8FontFromBytes for the title; on failure log and keep Times. 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 titleFamily := tryRegisterBrandTitleFont(pdf, in) // CERT-01 double frame: plum outer, gold inner pdf.SetDrawColor(plumR, plumG, plumB) pdf.SetLineWidth(1.5) pdf.Rect(28, 28, pageW-56, pageH-56, "D") pdf.SetDrawColor(goldR, goldG, goldB) pdf.SetLineWidth(0.5) 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() } } // Gold-d eyebrow (BrandingBook §12) — Helvetica core font (not Fraunces) pdf.SetTextColor(goldDR, goldDG, goldDB) pdf.SetFont("Helvetica", "B", 9) pdf.SetXY(margin, 92) pdf.CellFormat(pageW-margin*2, 11, "TIMESTAMP CERTIFICATE", "", 0, "C", false, 0, "") // Title — brand face when registered; else Times core serif stand-in pdf.SetTextColor(inkR, inkG, inkB) if titleFamily != "" { pdf.SetFont(titleFamily, "B", 22) } else { pdf.SetFont("Times", "B", 22) } pdf.SetXY(margin, 106) pdf.CellFormat(pageW-margin*2, 24, "Verae Timestamp Certificate", "", 0, "C", false, 0, "") pdf.SetTextColor(mutedR, mutedG, mutedB) pdf.SetFont("Helvetica", "", 10) pdf.SetXY(margin, 132) pdf.CellFormat(pageW-margin*2, 12, "Proof that this exact document existed at the time below,", "", 0, "C", false, 0, "") pdf.SetXY(margin, 144) pdf.CellFormat(pageW-margin*2, 12, "recorded on the "+statuslang.ChainName+" (digests only)", "", 0, "C", false, 0, "") // Short centered gold rule (CERT-01) ruleW := 120.0 pdf.SetDrawColor(goldR, goldG, goldB) pdf.SetLineWidth(2) pdf.Line(pageW/2-ruleW/2, 162, pageW/2+ruleW/2, 162) // FINAL-only status banner (receipt-final / statuslang wording) bannerY := 174.0 bannerH := 40.0 pdf.SetFillColor(okBgR, okBgG, okBgB) pdf.SetDrawColor(okBdR, okBdG, okBdB) pdf.SetLineWidth(1.5) pdf.Rect(margin, bannerY, pageW-margin*2, bannerH, "FD") pdf.SetTextColor(okR, okG, okB) pdf.SetFont("Helvetica", "B", 12) pdf.SetXY(margin+12, bannerY+8) pdf.Cell(pageW-margin*2-24, 14, finalBannerTitle(merkle)) pdf.SetFont("Helvetica", "", 9) pdf.SetXY(margin+12, bannerY+24) pdf.Cell(pageW-margin*2-24, 12, statuslang.EvidenceNote) if created == "" { created = "—" } mh, b3, b3mh := ResolveDigests(in) fields := [][2]string{ {"Document", docName}, } if in.ByteLength > 0 { fields = append(fields, [2]string{"Document size", fmt.Sprintf("%d bytes", in.ByteLength)}) } // CERT-01: digest lines only when data is available (never invent). if mh != "" { fields = append(fields, [2]string{"Content multihash", mh}) } if strings.TrimSpace(in.SHA256) != "" { fields = append(fields, [2]string{"SHA-256 (hex)", in.SHA256}) } if b3mh != "" { fields = append(fields, [2]string{"BLAKE3 multihash", b3mh}) } if b3 != "" { fields = append(fields, [2]string{"BLAKE3 (hex)", b3}) } fields = append(fields, [2]string{"Registration scope", scope}, [2]string{"Original timestamp (UTC)", created}, [2]string{"VeraeTime receipt (CID)", orDash(in.ReceiptID)}, [2]string{"Merkle / block", merkle}, [2]string{"Network", statuslang.ChainName + " (" + statuslang.NetworkName + ")"}, [2]string{"Blockchain explorer", statuslang.ExplorerURL}, [2]string{"Public lookup URL", lookup}, ) isHTTP := func(s string) bool { return strings.HasPrefix(strings.ToLower(strings.TrimSpace(s)), "http://") || strings.HasPrefix(strings.ToLower(strings.TrimSpace(s)), "https://") } y := bannerY + bannerH + 18 for _, fv := range fields { pdf.SetFont("Helvetica", "B", 9) pdf.SetTextColor(mutedR, mutedG, mutedB) pdf.SetXY(margin, y) pdf.Cell(pageW-margin*2, 11, fv[0]) y += 12 pdf.SetFont("Helvetica", "", 10) linkURL := "" if isHTTP(fv[1]) { linkURL = strings.TrimSpace(fv[1]) pdf.SetTextColor(linkR, linkG, linkB) } else { pdf.SetTextColor(inkR, inkG, inkB) } 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 += 8 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 - 48 pdf.ImageOptions(qrFile, qrX, qrY, qrSize, qrSize, false, gofpdf.ImageOptions{ImageType: "PNG"}, 0, "") pdf.LinkString(qrX, qrY, qrSize, qrSize, lookup) pdf.SetFont("Helvetica", "B", 8) pdf.SetTextColor(linkR, linkG, linkB) pdf.SetXY(qrX, qrY+qrSize+4) pdf.CellFormat(qrSize, 10, "Scan to verify", "", 0, "C", false, 0, lookup) if qclean != nil { defer qclean() } } } // Final-only legal + digests-only (CERT-01 footer / BrandingBook) pdf.SetFont("Helvetica", "", 8) pdf.SetTextColor(mutedR, mutedG, mutedB) pdf.SetXY(margin, pageH-72) pdf.MultiCell(pageW-margin*2-130, 10, "Only the document's SHA-256 digest was submitted; the file was not uploaded. This certificate is final receipt evidence for the digest above. A jobId or pending hold is not proof. Anyone can verify this receipt for free — no account needed.", "", "L", false) pdf.SetFont("Helvetica", "B", 9) pdf.SetTextColor(plumR, plumG, plumB) pdf.SetXY(margin, pageH-36) pdf.Cell(55, 12, "Verae LLC · ") pdf.SetTextColor(linkR, linkG, linkB) pdf.CellFormat(40, 12, "verae.ai", "", 0, "L", false, 0, "https://verae.ai") pdf.SetTextColor(plumR, plumG, plumB) pdf.Cell(12, 12, " · ") pdf.SetTextColor(linkR, linkG, linkB) 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 } // tryRegisterBrandTitleFont attempts gofpdf UTF-8 embed of a licensed brand // serif when EmbedBrandFonts is on and BrandFontsDir has .ttf/.otf. // Returns brandTitleFamily on success, "" on skip/failure (caller keeps Times). // Never errors the PDF build — registration failure is a logged no-op. func tryRegisterBrandTitleFont(pdf *gofpdf.Fpdf, in Input) string { if pdf == nil || !EmbedBrandFontsEnabled(in) { return "" } dir := BrandFontsDir() face := findBrandFaceFile(dir) if face == "" { log.Printf("pdfcert: EmbedBrandFonts on but no .ttf/.otf in %q — keeping Times", dir) return "" } ext := strings.ToLower(filepath.Ext(face)) // gofpdf AddUTF8FontFromBytes parses TrueType tables; OTF-CFF often fails. b, err := os.ReadFile(face) if err != nil { log.Printf("pdfcert: brand font read %s: %v — keeping Times", face, err) return "" } pdf.AddUTF8FontFromBytes(brandTitleFamily, "B", b) if err := pdf.Error(); err != nil { log.Printf("pdfcert: gofpdf could not register brand font %s (%s): %v — keeping Times", face, ext, err) pdf.ClearError() return "" } // Regular style alias so SetFont(family, "", …) would also work if needed. pdf.AddUTF8FontFromBytes(brandTitleFamily, "", b) if err := pdf.Error(); err != nil { pdf.ClearError() // Bold alone is enough for the title. } log.Printf("pdfcert: registered brand title font from %s", face) return brandTitleFamily } // finalBannerTitle uses statuslang.Label("final", block) — BrandingBook chip text. // Certificates are issued only for evidence states (IsEvidence); hold ≠ evidence. func finalBannerTitle(merkle string) string { s := strings.TrimSpace(merkle) if n, err := strconv.Atoi(s); err == nil && n > 0 { return statuslang.Label("final", n) } // Block unknown: BrandingBook #N placeholder (still final / evidence wording). return statuslang.Label("final") } func orDash(s string) string { if s == "" { return "—" } return s } func materializeLogo() (path string, cleanup func(), err error) { // Prefer tree-local assets, then walk for go.mod (packaging / tests). var candidates []string candidates = append(candidates, filepath.Join("assets", "VeraeFullLogo.png")) if wd, e := os.Getwd(); e == nil { dir := wd for i := 0; i < 8; i++ { candidates = append(candidates, filepath.Join(dir, "assets", "VeraeFullLogo.png")) if st, se := os.Stat(filepath.Join(dir, "go.mod")); se == nil && !st.IsDir() { break } parent := filepath.Dir(dir) if parent == dir { break } dir = parent } } 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-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 }