WIP
This commit is contained in:
172
main.go
172
main.go
@@ -1,168 +1,38 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"codeberg.org/go-pdf/fpdf"
|
"code.ppl.town/justin/qr_labels/qr_labels"
|
||||||
"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() {
|
func main() {
|
||||||
f, _ := os.Create("hello.pdf")
|
f, _ := os.Create("hello.pdf")
|
||||||
err := generatePage(f, PageOptions{
|
err := qr_labels.GeneratePage(f,
|
||||||
qrURL: "https://nanocat.net/dom/sdfih/dsifj/sidhof/sdifho/sdfiuoh/safiuho",
|
`https://signal.me/#eu/tardisx.81`,
|
||||||
qrLabel: "Á[Hjqy]|",
|
"Wow Nic is cool :-)",
|
||||||
qrSize: 54,
|
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",
|
// font: "Helvetica",
|
||||||
fontStyle: "B",
|
// fontStyle: "B",
|
||||||
fontSize: 34.0,
|
// fontSize: 34.0,
|
||||||
rows: 4,
|
// rows: 4,
|
||||||
cols: 3,
|
// cols: 3,
|
||||||
borders: true,
|
// borders: true,
|
||||||
})
|
// })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("could not generate pdf", "error", err)
|
slog.Error("could not generate pdf", "error", err)
|
||||||
os.Exit(1)
|
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
68
qr_labels/options.go
Normal 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
170
qr_labels/qr_labels.go
Normal 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
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package qr_labels
|
||||||
|
|
||||||
import "bytes"
|
import "bytes"
|
||||||
|
|
||||||
Reference in New Issue
Block a user