diff --git a/main.go b/main.go index c3206df..a234c67 100644 --- a/main.go +++ b/main.go @@ -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 -} diff --git a/qr_labels/options.go b/qr_labels/options.go new file mode 100644 index 0000000..ce04fe4 --- /dev/null +++ b/qr_labels/options.go @@ -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, + } +} diff --git a/qr_labels/qr_labels.go b/qr_labels/qr_labels.go new file mode 100644 index 0000000..2ccd0fc --- /dev/null +++ b/qr_labels/qr_labels.go @@ -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 +} diff --git a/structs.go b/qr_labels/structs.go similarity index 92% rename from structs.go rename to qr_labels/structs.go index 2ebac90..8e6d752 100644 --- a/structs.go +++ b/qr_labels/structs.go @@ -1,4 +1,4 @@ -package main +package qr_labels import "bytes"