package main import ( "fmt" "io" "log/slog" "os" "codeberg.org/go-pdf/fpdf" "github.com/yeqown/go-qrcode/v2" "github.com/yeqown/go-qrcode/writer/standard" ) var textTopPadding = 0.0 // typically none is required var qrTopPadding = 5.0 // below the baseline of the text, remember the descenders like 'y', 'q' var requiredCellPadding = 3.0 // half on each side, effectively func main() { f, _ := os.Create("hello.pdf") err := generatePage(f, PageOptions{ qrURL: "https://nanocat.net/dom/sdfih/dsifj/sidhof/sdifho/sdfiuoh/safiuho", qrLabel: "Á[Hjqy]|", qrSize: 54, font: "Helvetica", fontStyle: "B", fontSize: 34.0, rows: 4, cols: 3, borders: true, }) if err != nil { slog.Error("could not generate pdf", "error", err) os.Exit(1) } } type PageOptions struct { qrURL string qrLabel string qrSize float64 font string fontStyle string fontSize float64 rows int cols int borders bool } func generatePage(w io.Writer, po PageOptions) error { fontSizeMM := po.fontSize / 72 * 25.4 qrc, err := qrcode.New(po.qrURL) if err != nil { return fmt.Errorf("could not generate QRCode: %v", err) } b := newBWC() qrBytes := standard.NewWithWriter(&b, // standard.WithQRWidth(uint8(qrSize)), standard.WithBorderWidth(0), standard.WithBuiltinImageEncoder(standard.PNG_FORMAT), // standard.WithCircleShape(), // standard.WithFgGradient(&standard.LinearGradient{ // Stops: []standard.ColorStop{{ // T: 0.0, // Color: color.RGBA{ // R: 255, // G: 0, // B: 0, // A: 0, // }, // }, { // T: 1.0, // Color: color.RGBA{ // R: 0, // G: 255, // B: 0, // A: 0, // }, // }}, // Angle: 45, // }), ) // save file if err = qrc.Save(qrBytes); err != nil { fmt.Printf("could not save image: %v", err) } // dim := qrc.Dimension() pdf := fpdf.New("P", "mm", "A4", "") pdf.SetMargins(0, 0, 0) pageWidth, pageHeight := pdf.GetPageSize() // register the qr pdf.RegisterImageOptionsReader("qr", fpdf.ImageOptions{ ImageType: "png", ReadDpi: false, AllowNegativePosition: false, }, b) pdf.AddPage() pdf.SetFont(po.font, po.fontStyle, float64(po.fontSize)) textWidth := pdf.GetStringWidth(po.qrLabel) colWidth := pageWidth / float64(po.cols) rowHeight := pageHeight / float64(po.rows) cellHeight := textTopPadding + fontSizeMM + qrTopPadding + po.qrSize cellWidth := po.qrSize if cellHeight > rowHeight-requiredCellPadding { slog.Warn("qr is too big for row", "cell_height", cellHeight, "row_height", rowHeight) return fmt.Errorf("qr is too big for row size") } // slog.Info("check width", "cell_width", cellWidth, "col_width", colWidth) if cellWidth > colWidth-requiredCellPadding { slog.Warn("qr is too big for column", "cell_width", cellWidth, "col_width", colWidth) return fmt.Errorf("qr is too big for col size") } if textWidth > colWidth { slog.Warn("text probably too wide") return fmt.Errorf("text is too wide for col size") } for rowIdx := range po.rows { for colIdx := range po.cols { topLeftX := colWidth * float64(colIdx) topLeftY := rowHeight * float64(rowIdx) // draw box if po.borders { pdf.SetDashPattern([]float64{1, 1}, 0) pdf.Rect(topLeftX, topLeftY, colWidth, rowHeight, "D") pdf.SetDashPattern([]float64{}, 0) } // label at the top pdf.Text( topLeftX+colWidth/2-textWidth/2, topLeftY+fontSizeMM+textTopPadding, po.qrLabel, // +fmt.Sprintf("%d/%d", rowIdx, colIdx) ) // qr at the bottom pdf.ImageOptions("qr", topLeftX+colWidth/2-po.qrSize/2, topLeftY+textTopPadding+fontSizeMM+qrTopPadding, po.qrSize, po.qrSize, false, fpdf.ImageOptions{AllowNegativePosition: true}, 0, "") } } err = pdf.Output(w) if err != nil { return fmt.Errorf("could not output PDF: %w", err) } return nil }