181 lines
5.1 KiB
Go
181 lines
5.1 KiB
Go
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
|
|
}
|