package labels import ( "fmt" "image" "image/color" "image/draw" "regexp" "github.com/skip2/go-qrcode" "golang.org/x/image/font" "golang.org/x/image/font/basicfont" "golang.org/x/image/math/fixed" ) const ( LabelWidth = 384 // pixels at 203 DPI ≈ 48mm usable LabelHeight = 120 // pixels ≈ 15mm QRSize = 96 // pixels QROffsetX = 8 QROffsetY = 12 TextOffsetX = 112 BaseURL = "http://mac-mini.mg:8080/hw/" ) // hwIDPattern validates the HW-XXXXX format (T-04-07 mitigation). var hwIDPattern = regexp.MustCompile(`^HW-\d{5}$`) // LabelData holds the fields needed to render a standard item label. type LabelData struct { HWID string // e.g. "HW-00001" Name string // item name from NetBox SpecLine string // key spec summary, e.g. "Intel NUC i5 2019" } // RenderStandard produces a 384x120 label bitmap (black on white) for a standard item. // Layout: QR code left (96x96 at x=8,y=12), text right (HW ID bold top, name middle, spec bottom). // Returns an error if HWID does not match HW-\d{5} (T-04-07 mitigation). func RenderStandard(d LabelData) (image.Image, error) { if !hwIDPattern.MatchString(d.HWID) { return nil, fmt.Errorf("labels: invalid HWID %q: must match HW-NNNNN", d.HWID) } if d.Name == "" { d.Name = "Unknown Item" } img := image.NewNRGBA(image.Rect(0, 0, LabelWidth, LabelHeight)) // Fill white background draw.Draw(img, img.Bounds(), image.NewUniform(color.NRGBA{R: 255, G: 255, B: 255, A: 255}), image.Point{}, draw.Src) // QR code qr, err := qrcode.New(BaseURL+d.HWID, qrcode.Low) if err != nil { return nil, fmt.Errorf("labels: QR generation failed: %w", err) } qr.DisableBorder = true qrImg := qr.Image(QRSize) qrDst := image.Rect(QROffsetX, QROffsetY, QROffsetX+QRSize, QROffsetY+QRSize) draw.Draw(img, qrDst, qrImg, image.Point{}, draw.Over) // Text: HW ID (y=26), Name (y=60), SpecLine (y=92) drawText(img, TextOffsetX, 26, d.HWID) drawText(img, TextOffsetX, 60, truncate(d.Name, 30)) drawText(img, TextOffsetX, 92, truncate(d.SpecLine, 30)) return img, nil } // drawText renders text onto a draw.Image at (x, y) using basicfont.Face7x13. func drawText(img draw.Image, x, y int, text string) { d := &font.Drawer{ Dst: img, Src: image.NewUniform(color.Black), Face: basicfont.Face7x13, Dot: fixed.Point26_6{X: fixed.I(x), Y: fixed.I(y)}, } d.DrawString(text) } // truncate caps s at max runes, appending "…" if truncated. func truncate(s string, max int) string { runes := []rune(s) if len(runes) <= max { return s } return string(runes[:max-1]) + "…" }