// Package badge provides methods for generating SVG badges package badge // ////////////////////////////////////////////////////////////////////////////////// // // // // Copyright (c) 2023 ESSENTIAL KAOS // // Apache License, Version 2.0 // // // // ////////////////////////////////////////////////////////////////////////////////// // import ( "fmt" "io/ioutil" "math" "strconv" "strings" "github.com/golang/freetype/truetype" "golang.org/x/image/font" ) // ////////////////////////////////////////////////////////////////////////////////// // const ( COLOR_BLUE = "#007ec6" COLOR_BRIGHTGREEN = "#4c1" COLOR_GREEN = "#97ca00" COLOR_GREY = "#555" COLOR_LIGHTGREY = "#9f9f9f" COLOR_ORANGE = "#fe7d37" COLOR_RED = "#e05d44" COLOR_YELLOW = "#dfb317" COLOR_YELLOWGREEN = "#a4a61d" COLOR_SUCCESS = "#4c1" COLOR_IMPORTANT = "#fe7d37" COLOR_CRITICAL = "#e05d44" COLOR_INFORMATIONAL = "#007ec6" COLOR_INACTIVE = "#9f9f9f" ) const ( DEFAULT_OFFSET = 9 // default font offset DEFAULT_SPACING = 0 // default letter spacing ) // ////////////////////////////////////////////////////////////////////////////////// // const _TEMPLATE_PLASTIC = `{LABEL}: {MESSAGE}{LABEL}{MESSAGE}` const _TEMPLATE_FLAT = `{LABEL}: {MESSAGE}{LABEL}{MESSAGE}` const _TEMPLATE_FLAT_SQUARE = `{LABEL}: {MESSAGE}{LABEL}{MESSAGE}` const _TEMPLATE_FLAT_SIMPLE = `{MESSAGE}{MESSAGE}` const _TEMPLATE_PLASTIC_SIMPLE = `TESTTEST{MESSAGE}` const _TEMPLATE_FLAT_SQUARE_SIMPLE = `{MESSAGE}{MESSAGE}` // ////////////////////////////////////////////////////////////////////////////////// // // Generator is badge generator type Generator struct { Offset int // Text offset Spacing float64 // Letter spacing fontSize int fontName string drawer *font.Drawer } // ////////////////////////////////////////////////////////////////////////////////// // // NewGenerator creates new badge generator with given font func NewGenerator(fontFile string, fontSize int) (*Generator, error) { fontData, err := ioutil.ReadFile(fontFile) if err != nil { return nil, err } fontTTF, err := truetype.Parse(fontData) if err != nil { return nil, err } return &Generator{ Offset: DEFAULT_OFFSET, Spacing: DEFAULT_SPACING, fontSize: fontSize, fontName: fontTTF.Name(truetype.NameIDFontFullName), drawer: &font.Drawer{ Face: truetype.NewFace(fontTTF, &truetype.Options{ Size: float64(fontSize), DPI: 72, Hinting: font.HintingFull, }), }, }, nil } // ////////////////////////////////////////////////////////////////////////////////// // // GeneratePlastic generates SVG badge in plastic style func (g *Generator) GeneratePlastic(label, message, color string) []byte { if label == "" { return g.generateBadgeSimple(_TEMPLATE_PLASTIC_SIMPLE, message, color) } return g.generateBadge(_TEMPLATE_PLASTIC, label, message, color) } // GenerateFlat generates SVG badge in flat style func (g *Generator) GenerateFlat(label, message, color string) []byte { if label == "" { return g.generateBadgeSimple(_TEMPLATE_FLAT_SIMPLE, message, color) } return g.generateBadge(_TEMPLATE_FLAT, label, message, color) } // GenerateFlatSquare generates SVG badge in flat-square style func (g *Generator) GenerateFlatSquare(label, message, color string) []byte { if label == "" { return g.generateBadgeSimple(_TEMPLATE_FLAT_SQUARE_SIMPLE, message, color) } return g.generateBadge(_TEMPLATE_FLAT_SQUARE, label, message, color) } // GeneratePlasticSimple generates SVG simple badge in plastic style func (g *Generator) GeneratePlasticSimple(message, color string) []byte { return g.generateBadgeSimple(_TEMPLATE_PLASTIC_SIMPLE, message, color) } // GenerateFlatSimple generates SVG simple badge in flat style func (g *Generator) GenerateFlatSimple(message, color string) []byte { return g.generateBadgeSimple(_TEMPLATE_FLAT_SIMPLE, message, color) } // GenerateFlatSquareSimple generates SVG simple badge in flat-square style func (g *Generator) GenerateFlatSquareSimple(message, color string) []byte { return g.generateBadgeSimple(_TEMPLATE_FLAT_SQUARE_SIMPLE, message, color) } // ////////////////////////////////////////////////////////////////////////////////// // // generateBadge generates badge with given template func (g *Generator) generateBadge(template, label, message, color string) []byte { c := parseColor(color) gF := float64(g.Offset) lW := float64(g.drawer.MeasureString(label)>>6) + gF mW := float64(g.drawer.MeasureString(message)>>6) + gF fW := lW + mW lX := (lW/2 + 1) * 10 mX := (lW + (mW / 2) - 1) * 10 lL := (lW - gF) * (10.0 + g.Spacing - 0.5) mL := (mW - gF) * (10.0 + g.Spacing - 0.5) fS := g.fontSize * 10 mC, mS := getMessageColors(c) badge := strings.ReplaceAll(template, "{LABEL}", label) badge = strings.ReplaceAll(badge, "{MESSAGE}", message) badge = strings.ReplaceAll(badge, "{COLOR}", formatColor(c)) badge = strings.ReplaceAll(badge, "{WIDTH}", formatFloat(fW)) badge = strings.ReplaceAll(badge, "{LABEL_WIDTH}", formatFloat(lW)) badge = strings.ReplaceAll(badge, "{MESSAGE_WIDTH}", formatFloat(mW)) badge = strings.ReplaceAll(badge, "{LABEL_X}", formatFloat(lX)) badge = strings.ReplaceAll(badge, "{MESSAGE_X}", formatFloat(mX)) badge = strings.ReplaceAll(badge, "{LABEL_LENGTH}", formatFloat(lL)) badge = strings.ReplaceAll(badge, "{MESSAGE_LENGTH}", formatFloat(mL)) badge = strings.ReplaceAll(badge, "{MESSAGE_COLOR}", mC) badge = strings.ReplaceAll(badge, "{MESSAGE_SHADOW}", mS) badge = strings.ReplaceAll(badge, "{FONT}", g.fontName) badge = strings.ReplaceAll(badge, "{FONT_SIZE}", strconv.Itoa(fS)) return []byte(badge) } // generateBadgeSimple generates badge with given template func (g *Generator) generateBadgeSimple(template, message, color string) []byte { c := parseColor(color) gF := float64(g.Offset) fW := float64(g.drawer.MeasureString(message)>>6) + gF mX := (fW / 2) * 10 mL := (fW - gF) * (10.0 + g.Spacing) fS := g.fontSize * 10 mC, mS := getMessageColors(c) badge := strings.ReplaceAll(template, "{MESSAGE}", message) badge = strings.ReplaceAll(badge, "{COLOR}", formatColor(c)) badge = strings.ReplaceAll(badge, "{WIDTH}", formatFloat(fW)) badge = strings.ReplaceAll(badge, "{MESSAGE_X}", formatFloat(mX)) badge = strings.ReplaceAll(badge, "{MESSAGE_LENGTH}", formatFloat(mL)) badge = strings.ReplaceAll(badge, "{MESSAGE_COLOR}", mC) badge = strings.ReplaceAll(badge, "{MESSAGE_SHADOW}", mS) badge = strings.ReplaceAll(badge, "{FONT}", g.fontName) badge = strings.ReplaceAll(badge, "{FONT_SIZE}", strconv.Itoa(fS)) return []byte(badge) } // ////////////////////////////////////////////////////////////////////////////////// // // parseColor parses hex color func parseColor(c string) int64 { if strings.HasPrefix(c, "#") { c = strings.TrimLeft(c, "#") } // Shorthand hex color if len(c) == 3 { c = strings.Repeat(string(c[0]), 2) + strings.Repeat(string(c[1]), 2) + strings.Repeat(string(c[2]), 2) } i, _ := strconv.ParseInt(c, 16, 32) return i } // formatColor formats color func formatColor(c int64) string { k := fmt.Sprintf("%06x", c) if k[0] == k[1] && k[2] == k[3] && k[4] == k[5] { k = k[0:1] + k[2:3] + k[4:5] } return "#" + k } // formatFloat formats float values func formatFloat(v float64) string { return strconv.FormatFloat(v, 'f', 0, 64) } // getMessageColors returns message text and shadow colors based on color of badge func getMessageColors(c int64) (string, string) { if c == 0 || calcLuminance(c) < 0.65 { return "#fff", "#010101" } return "#333", "#ccc" } // calcLuminance calculates relative luminance func calcLuminance(color int64) float64 { r := calcLumColor(float64(color>>16&0xFF) / 255) g := calcLumColor(float64(color>>8&0xFF) / 255) b := calcLumColor(float64(color&0xFF) / 255) return 0.2126*r + 0.7152*g + 0.0722*b } // calcLumColor calculates luminance for one color func calcLumColor(c float64) float64 { if c <= 0.03928 { return c / 12.92 } return math.Pow(((c + 0.055) / 1.055), 2.4) }