This commit is contained in:
2026-04-29 11:00:44 +09:30
parent 3fbd1871b6
commit 15f7ce6d8f
4 changed files with 260 additions and 152 deletions

172
main.go
View File

@@ -1,168 +1,38 @@
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"
"code.ppl.town/justin/qr_labels/qr_labels"
)
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,
err := qr_labels.GeneratePage(f,
`https://signal.me/#eu/tardisx.81`,
"Wow Nic is cool :-)",
qr_labels.WithQRSize(55),
// qr_labels.WithFont("Helvetica", 16, ""),
// qr_labels.WithBorders(false),
// qr_labels.WithGrid(4, 3),
)
// err := qr_labels.GeneratePage(f, qr_labels.PageOptions{
// // qrURL: "geo:-34.9285,138.6007",
// qrURL: `https://signal.me/#eu/tardisx.81`,
// qrLabel: "Á[Hjqy]|",
// qrSize: 54,
font: "Helvetica",
fontStyle: "B",
fontSize: 34.0,
rows: 4,
cols: 3,
borders: true,
})
// 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
}

68
qr_labels/options.go Normal file
View File

@@ -0,0 +1,68 @@
package qr_labels
type Option interface {
apply(*pageOptions)
}
type optWithQRSize struct {
size float64
}
func (o optWithQRSize) apply(po *pageOptions) {
po.qrSize = o.size
}
func WithQRSize(size float64) optWithQRSize {
return optWithQRSize{size: size}
}
type optWithFont struct {
font, style string
size float64
}
func (o optWithFont) apply(po *pageOptions) {
po.font = o.font
po.fontSize = o.size
po.fontStyle = o.style
}
// WithFont defines the font, size and style to use.
// The font should be a PDF font (Helvetica, Courier, Times), the size is in
// points and the style can an empty string, or any combination of the characters
// U, I, B, S to apply Underline, Italics, Bold and Strikethrough effects.
func WithFont(font string, size float64, style string) optWithFont {
return optWithFont{
font: font,
style: style,
size: size,
}
}
type optWithBorders struct {
borders bool
}
func (o optWithBorders) apply(po *pageOptions) {
po.borders = o.borders
}
func WithBorders(borders bool) optWithBorders {
return optWithBorders{borders: borders}
}
type optWithGrid struct {
rows, cols int
}
func (o optWithGrid) apply(po *pageOptions) {
po.rows = o.rows
po.cols = o.cols
}
func WithGrid(rows, cols int) optWithGrid {
return optWithGrid{
rows: rows,
cols: cols,
}
}

170
qr_labels/qr_labels.go Normal file
View File

@@ -0,0 +1,170 @@
package qr_labels
import (
"fmt"
"io"
"log/slog"
"codeberg.org/go-pdf/fpdf"
"github.com/yeqown/go-qrcode/v2"
"github.com/yeqown/go-qrcode/writer/standard"
)
var textTopPadding = 2.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
type pageOptions struct {
qrSize float64
font string
fontStyle string
fontSize float64
rows int
cols int
borders bool
}
func defaultPageOptions() []Option {
return []Option{
WithBorders(true),
WithQRSize(40),
WithFont("Courier", 16.0, "B"),
WithGrid(4, 3),
}
}
// GeneratePage writes the labels as a PDF to the supplied io.Writer.
// The default options are:
// - 40mm QR code
// - 4x3 grid
// - 20pt Courier font for the label
// - borders around each cell
//
// Supply options to override these.
func GeneratePage(w io.Writer, url, label string, opts ...Option) error {
po := pageOptions{}
for _, o := range defaultPageOptions() {
o.apply(&po)
}
for _, o := range opts {
o.apply(&po)
}
fontSizeMM := po.fontSize / 72 * 25.4
qrc, err := qrcode.New(url)
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(label)
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,
label, // +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
}

View File

@@ -1,4 +1,4 @@
package main
package qr_labels
import "bytes"