Additional information on label, dynamic label layouting (#522)

This commit is contained in:
fidoriel
2025-02-10 02:39:23 +01:00
committed by GitHub
parent 7ddfa72936
commit 9a57ada534
4 changed files with 155 additions and 95 deletions

View File

@@ -16,7 +16,7 @@ import (
)
func generateOrPrint(ctrl *V1Controller, w http.ResponseWriter, r *http.Request, title string, description string, url string) error {
params := labelmaker.NewGenerateParams(int(ctrl.config.LabelMaker.Width), int(ctrl.config.LabelMaker.Height), int(ctrl.config.LabelMaker.Margin), int(ctrl.config.LabelMaker.Padding), ctrl.config.LabelMaker.FontSize, title, description, url)
params := labelmaker.NewGenerateParams(int(ctrl.config.LabelMaker.Width), int(ctrl.config.LabelMaker.Height), int(ctrl.config.LabelMaker.Margin), int(ctrl.config.LabelMaker.Padding), ctrl.config.LabelMaker.FontSize, title, description, url, ctrl.config.LabelMaker.DynamicLength, ctrl.config.LabelMaker.AdditionalInformation)
print := queryBool(r.URL.Query().Get("print"))

View File

@@ -51,12 +51,14 @@ type WebConfig struct {
}
type LabelMakerConf struct {
Width int64 `yaml:"width" conf:"default:526"`
Height int64 `yaml:"height" conf:"default:200"`
Padding int64 `yaml:"padding" conf:"default:32"`
Margin int64 `yaml:"margin" conf:"default:32"`
FontSize float64 `yaml:"font_size" conf:"default:32.0"`
PrintCommand *string `yaml:"string"`
Width int64 `yaml:"width" conf:"default:526"`
Height int64 `yaml:"height" conf:"default:200"`
Padding int64 `yaml:"padding" conf:"default:32"`
Margin int64 `yaml:"margin" conf:"default:32"`
FontSize float64 `yaml:"font_size" conf:"default:32.0"`
PrintCommand *string `yaml:"string"`
AdditionalInformation *string `yaml:"string"`
DynamicLength bool `yaml:"bool" conf:"default:true"`
}
// New parses the CLI/Config file and returns a Config struct. If the file argument is an empty string, the

View File

@@ -26,17 +26,19 @@ import (
)
type GenerateParameters struct {
Width int
Height int
QrSize int
Margin int
ComponentPadding int
TitleText string
TitleFontSize float64
DescriptionText string
DescriptionFontSize float64
Dpi float64
URL string
Width int
Height int
QrSize int
Margin int
ComponentPadding int
TitleText string
TitleFontSize float64
DescriptionText string
DescriptionFontSize float64
AdditionalInformation *string
Dpi float64
URL string
DynamicLength bool
}
func (p *GenerateParameters) Validate() error {
@@ -55,19 +57,21 @@ func (p *GenerateParameters) Validate() error {
return nil
}
func NewGenerateParams(width int, height int, margin int, padding int, fontSize float64, title string, description string, url string) GenerateParameters {
func NewGenerateParams(width int, height int, margin int, padding int, fontSize float64, title string, description string, url string, dynamicLength bool, additionalInformation *string) GenerateParameters {
return GenerateParameters{
Width: width,
Height: height,
QrSize: height - (padding * 2),
Margin: margin,
ComponentPadding: padding,
TitleText: title,
DescriptionText: description,
TitleFontSize: fontSize,
DescriptionFontSize: fontSize * 0.8,
Dpi: 72,
URL: url,
Width: width,
Height: height,
QrSize: height - (padding * 2),
Margin: margin,
ComponentPadding: padding,
TitleText: title,
DescriptionText: description,
TitleFontSize: fontSize,
DescriptionFontSize: fontSize * 0.8,
Dpi: 72,
URL: url,
AdditionalInformation: additionalInformation,
DynamicLength: dynamicLength,
}
}
@@ -80,15 +84,24 @@ func measureString(text string, face font.Face, ctx *freetype.Context) int {
return ctx.PointToFixed(float64(width)).Round()
}
// wrapText breaks text into lines that fit within maxWidth
func wrapText(text string, face font.Face, maxWidth int, ctx *freetype.Context) []string {
func wrapText(text string, face font.Face, maxWidth int, maxHeight int, lineHeight int, ctx *freetype.Context) ([]string, string) {
lines := strings.Split(text, "\n")
unlimitedHeight := maxHeight == -1
var wrappedLines []string
currentHeight := 0
processedChars := 0
for _, line := range lines {
words := strings.Fields(line)
if len(words) == 0 {
wrappedLines = append(wrappedLines, "")
processedChars += 1
if !unlimitedHeight {
currentHeight += lineHeight
if currentHeight > maxHeight {
return wrappedLines[:len(wrappedLines)-1], text[processedChars:]
}
}
continue
}
@@ -101,34 +114,28 @@ func wrapText(text string, face font.Face, maxWidth int, ctx *freetype.Context)
currentLine = testLine
} else {
wrappedLines = append(wrappedLines, currentLine)
processedChars += len(currentLine) + 1
if !unlimitedHeight {
currentHeight += lineHeight
if currentHeight > maxHeight {
return wrappedLines[:len(wrappedLines)-1], text[processedChars-len(currentLine)-1:]
}
}
currentLine = word
}
}
wrappedLines = append(wrappedLines, currentLine)
}
// Handle lines that are too long and have no spaces
for i, line := range wrappedLines {
width := measureString(line, face, ctx)
if width > maxWidth {
var splitLines []string
currentLine := ""
for _, r := range line {
testLine := currentLine + string(r)
width := measureString(testLine, face, ctx)
if width <= maxWidth {
currentLine = testLine
} else {
splitLines = append(splitLines, currentLine)
currentLine = string(r)
}
wrappedLines = append(wrappedLines, currentLine)
processedChars += len(currentLine) + 1
if !unlimitedHeight {
currentHeight += lineHeight
if currentHeight > maxHeight {
return wrappedLines[:len(wrappedLines)-1], text[processedChars-len(currentLine)-1:]
}
splitLines = append(splitLines, currentLine)
wrappedLines = append(wrappedLines[:i], append(splitLines, wrappedLines[i+1:]...)...)
}
}
return wrappedLines
return wrappedLines, ""
}
func GenerateLabel(w io.Writer, params *GenerateParameters) error {
@@ -136,6 +143,11 @@ func GenerateLabel(w io.Writer, params *GenerateParameters) error {
return err
}
bodyText := params.DescriptionText
if params.AdditionalInformation != nil {
bodyText = bodyText + "\n" + *params.AdditionalInformation
}
// Create QR code
qr, err := qrcode.New(params.URL, qrcode.Medium)
if err != nil {
@@ -144,18 +156,6 @@ func GenerateLabel(w io.Writer, params *GenerateParameters) error {
qr.DisableBorder = true
qrImage := qr.Image(params.QrSize)
// Create a new white background image
bounds := image.Rect(0, 0, params.Width, params.Height)
img := image.NewRGBA(bounds)
draw.Draw(img, bounds, &image.Uniform{color.White}, image.Point{}, draw.Src)
// Draw QR code onto the image
draw.Draw(img,
image.Rect(params.Margin, params.Margin, params.QrSize+params.Margin, params.QrSize+params.Margin),
qrImage,
image.Point{},
draw.Over)
regularFont, err := truetype.Parse(gomedium.TTF)
if err != nil {
return err
@@ -175,54 +175,108 @@ func GenerateLabel(w io.Writer, params *GenerateParameters) error {
DPI: params.Dpi,
})
createContext := func(font *truetype.Font, size float64) *freetype.Context {
c := freetype.NewContext()
c.SetDPI(params.Dpi)
c.SetFont(font)
c.SetFontSize(size)
c.SetClip(img.Bounds())
c.SetDst(img)
c.SetSrc(image.NewUniform(color.Black))
return c
// Calculate text area dimensions
maxWidth := params.Width - (params.Margin * 2) - params.ComponentPadding
// Create temporary contexts for text measurement
tmpImg := image.NewRGBA(image.Rect(0, 0, 1, 1))
boldContext := createContext(boldFont, params.TitleFontSize, tmpImg, params.Dpi)
regularContext := createContext(regularFont, params.DescriptionFontSize, tmpImg, params.Dpi)
// Calculate total height needed
totalHeight := params.Margin
titleLineSpacing := boldContext.PointToFixed(params.TitleFontSize).Round()
titleLines, _ := wrapText(params.TitleText, boldFace, maxWidth-params.QrSize, -1, titleLineSpacing, boldContext)
titleHeight := titleLineSpacing * len(titleLines)
totalHeight += titleHeight
totalHeight += params.ComponentPadding / 4
regularLineSpacing := regularContext.PointToFixed(params.DescriptionFontSize).Round()
descriptionLinesRight, descriptionRemaining := wrapText(bodyText, regularFace, maxWidth-params.QrSize, params.QrSize-titleHeight, regularLineSpacing, regularContext)
totalHeight += regularLineSpacing * len(descriptionLinesRight)
var textYBottomText int
var descriptionLinesBottom []string
hasBottomText := descriptionRemaining != ""
if hasBottomText {
totalHeight = max(params.Margin+params.QrSize+params.ComponentPadding/2, totalHeight)
textYBottomText = totalHeight
descriptionLinesBottom, _ = wrapText(descriptionRemaining, regularFace, maxWidth, -1, regularLineSpacing, regularContext)
totalHeight += regularLineSpacing * len(descriptionLinesBottom)
totalHeight += params.Margin
}
boldContext := createContext(boldFont, params.TitleFontSize)
regularContext := createContext(regularFont, params.DescriptionFontSize)
var requiredHeight int
if params.DynamicLength {
requiredHeight = max(totalHeight, params.QrSize+(params.Margin*2))
} else {
requiredHeight = params.Height
}
maxWidth := params.Width - (params.Margin * 2) - params.QrSize - params.ComponentPadding
lineSpacing := boldContext.PointToFixed(params.TitleFontSize).Round()
textX := params.Margin + params.ComponentPadding + params.QrSize
// Create the actual image with calculated height
bounds := image.Rect(0, 0, params.Width, requiredHeight)
img := image.NewRGBA(bounds)
draw.Draw(img, bounds, &image.Uniform{color.White}, image.Point{}, draw.Src)
// Draw QR code onto the image
draw.Draw(img,
image.Rect(params.Margin, params.Margin, params.QrSize+params.Margin, params.QrSize+params.Margin),
qrImage,
image.Point{},
draw.Over)
// Create final drawing contexts
boldContext = createContext(boldFont, params.TitleFontSize, img, params.Dpi)
regularContext = createContext(regularFont, params.DescriptionFontSize, img, params.Dpi)
textXRight := params.Margin + params.ComponentPadding + params.QrSize
textY := params.Margin - 8
titleLines := wrapText(params.TitleText, boldFace, maxWidth, boldContext)
// Draw title
for _, line := range titleLines {
pt := freetype.Pt(textX, textY+lineSpacing)
_, err = boldContext.DrawString(line, pt)
if err != nil {
pt := freetype.Pt(textXRight, textY+titleLineSpacing)
if _, err = boldContext.DrawString(line, pt); err != nil {
return err
}
textY += lineSpacing
textY += titleLineSpacing
}
// Draw description right from QR Code
textY += params.ComponentPadding / 4
lineSpacing = regularContext.PointToFixed(params.DescriptionFontSize).Round()
descriptionLines := wrapText(params.DescriptionText, regularFace, maxWidth, regularContext)
for _, line := range descriptionLines {
pt := freetype.Pt(textX, textY+lineSpacing)
_, err = regularContext.DrawString(line, pt)
if err != nil {
for _, line := range descriptionLinesRight {
pt := freetype.Pt(textXRight, textY+regularLineSpacing)
if _, err = regularContext.DrawString(line, pt); err != nil {
return err
}
textY += lineSpacing
textY += regularLineSpacing
}
err = png.Encode(w, img)
if err != nil {
return err
// Draw description below QR Code
if hasBottomText {
for _, line := range descriptionLinesBottom {
pt := freetype.Pt(params.Margin, textYBottomText+regularLineSpacing)
if _, err = regularContext.DrawString(line, pt); err != nil {
return err
}
textYBottomText += regularLineSpacing
}
}
return nil
return png.Encode(w, img)
}
// Helper function to create freetype context
func createContext(font *truetype.Font, size float64, img *image.RGBA, dpi float64) *freetype.Context {
c := freetype.NewContext()
c.SetDPI(dpi)
c.SetFont(font)
c.SetFontSize(size)
c.SetClip(img.Bounds())
c.SetDst(img)
c.SetSrc(image.NewUniform(color.Black))
return c
}
func PrintLabel(cfg *config.Config, params *GenerateParameters) error {

View File

@@ -32,6 +32,8 @@
| HBOX_LABEL_MAKER_MARGIN | 32 | space between the label content and edges of the label |
| HBOX_LABEL_MAKER_FONT_SIZE | 32.0 | the size of the labels font |
| HBOX_LABEL_MAKER_PRINT_COMMAND | | the command to use for printing labels. if empty, label printing is disabled. `{{.FileName}}` in the command will be replaced with the png filename of the label |
| HBOX_LABEL_MAKER_DYNAMIC_LENGTH | true | allow label generation with open length. `HBOX_LABEL_MAKER_HEIGHT` is still used for layout and minimal height. If not used, long text may be cut off, but all labels have the same size. |
| HBOX_LABEL_MAKER_ADDITIONAL_INFORMATION | | Additional information added to the label like name or phone number |
::: tip "CLI Arguments"
If you're deploying without docker you can use command line arguments to configure the application. Run `homebox --help` for more information.
@@ -68,6 +70,8 @@ OPTIONS
--label-maker-margin/$HBOX_LABEL_MAKER_MARGIN <int> (default: 32)
--label-maker-font-size/$HBOX_LABEL_MAKER_FONT_SIZE <float> (default: 32.0)
--label-maker-print-command/$HBOX_LABEL_MAKER_PRINT_COMMAND <string>
--label-maker-additional-information/$HBOX_LABEL_MAKER_DYNAMIC_LENGTH <string> (default: true)
--label-maker-additional-information/$HBOX_LABEL_MAKER_ADDITIONAL_INFORMATION <string>
--help/-h display this help message
```
:::