Files
woodpecker/server/badges/drawer.go
David Loewe e2270ae95c Per-Workflow and Per-Workflow-Step badge generation (#5977)
Co-authored-by: qwerty287 <80460567+qwerty287@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Robert Kaussow <mail@thegeeklab.de>
Co-authored-by: qwerty287 <qwerty287@posteo.de>
2026-02-01 16:44:09 +01:00

131 lines
2.7 KiB
Go

// Copyright 2023 The narqo/go-badge Authors. All rights reserved.
// SPDX-License-Identifier: MIT.
package badges
// cspell:words Verdana
import (
"bytes"
"html/template"
"io"
"sync"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font"
"go.woodpecker-ci.org/woodpecker/v3/server/badges/fonts"
)
type badge struct {
Subject string
Status string
Color Color
Bounds bounds
}
type bounds struct {
// SubjectDx is the width of subject string of the badge.
SubjectDx float64
SubjectX float64
// StatusDx is the width of status string of the badge.
StatusDx float64
StatusX float64
}
func (b bounds) Dx() float64 {
return b.SubjectDx + b.StatusDx
}
type badgeDrawer struct {
fd *font.Drawer
tmpl *template.Template
mutex *sync.Mutex
}
func (d *badgeDrawer) Render(subject, status string, color Color, w io.Writer) error {
d.mutex.Lock()
subjectDx := d.measureString(subject)
statusDx := d.measureString(status)
d.mutex.Unlock()
bdg := badge{
Subject: subject,
Status: status,
Color: color,
Bounds: bounds{
SubjectDx: subjectDx,
SubjectX: subjectDx/2.0 + 1,
StatusDx: statusDx,
StatusX: subjectDx + statusDx/2.0 - 1,
},
}
return d.tmpl.Execute(w, bdg)
}
func (d *badgeDrawer) RenderBytes(subject, status string, color Color) ([]byte, error) {
buf := &bytes.Buffer{}
err := d.Render(subject, status, color, buf)
return buf.Bytes(), err
}
// shields.io uses Verdana.ttf to measure text width with an extra 10px.
// As we use DejaVuSans.ttf, we have to tune this value a little.
const extraDx = 5
func (d *badgeDrawer) measureString(s string) float64 {
SHIFT := 6
return float64(d.fd.MeasureString(s)>>SHIFT) + extraDx
}
// RenderBytes renders a badge of the given color, with given subject and status to bytes.
func RenderBytes(subject, status string, color Color) ([]byte, error) {
drawer, err := initDrawer()
if err != nil {
return nil, err
}
return drawer.RenderBytes(subject, status, color)
}
const (
dpi = 72
fontSize = 11
)
var (
drawer *badgeDrawer
initError error
initOnce sync.Once
)
func initDrawer() (*badgeDrawer, error) {
initOnce.Do(func() {
fd, err := mustNewFontDrawer(fontSize, dpi)
if err != nil {
initError = err
return
}
drawer = &badgeDrawer{
fd: fd,
tmpl: template.Must(template.New("flat-template").Parse(flatTemplate)),
mutex: &sync.Mutex{},
}
initError = nil
})
return drawer, initError
}
func mustNewFontDrawer(size, dpi float64) (*font.Drawer, error) {
ttf, err := truetype.Parse(fonts.DejaVuSans)
if err != nil {
return nil, err
}
return &font.Drawer{
Face: truetype.NewFace(ttf, &truetype.Options{
Size: size,
DPI: dpi,
Hinting: font.HintingFull,
}),
}, nil
}