- Add internal/labels package with LabelData struct and RenderStandard() - QR encodes http://mac-mini.mg:8080/hw/HW-XXXXX (LBL-01) - Label is 384x120px NRGBA, white background, basicfont text - T-04-07 mitigation: validate HWID format before qrcode.New() - Install github.com/skip2/go-qrcode and golang.org/x/image - All 5 renderer tests pass
87 lines
2.5 KiB
Go
87 lines
2.5 KiB
Go
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]) + "…"
|
|
}
|