mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2026-02-13 21:00:00 +00:00
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>
This commit is contained in:
@@ -14,33 +14,47 @@
|
||||
|
||||
package badges
|
||||
|
||||
import "go.woodpecker-ci.org/woodpecker/v3/server/model"
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
// cspell:words Verdana
|
||||
|
||||
var (
|
||||
badgeSuccess = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="106" height="20" role="img" aria-label="pipeline: success"><title>pipeline: success</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="106" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="53" height="20" fill="#555"/><rect x="53" width="53" height="20" fill="#44cc11"/><rect width="106" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="275" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">pipeline</text><text x="275" y="140" transform="scale(.1)" fill="#fff" textLength="430">pipeline</text><text aria-hidden="true" x="785" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">success</text><text x="785" y="140" transform="scale(.1)" fill="#fff" textLength="430">success</text></g></svg>`
|
||||
badgeFailure = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="98" height="20" role="img" aria-label="pipeline: failure"><title>pipeline: failure</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="98" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="53" height="20" fill="#555"/><rect x="53" width="45" height="20" fill="#e05d44"/><rect width="98" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="275" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">pipeline</text><text x="275" y="140" transform="scale(.1)" fill="#fff" textLength="430">pipeline</text><text aria-hidden="true" x="745" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">failure</text><text x="745" y="140" transform="scale(.1)" fill="#fff" textLength="350">failure</text></g></svg>`
|
||||
badgeStarted = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="102" height="20" role="img" aria-label="pipeline: started"><title>pipeline: started</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="102" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="53" height="20" fill="#555"/><rect x="53" width="49" height="20" fill="#dfb317"/><rect width="102" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="275" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">pipeline</text><text x="275" y="140" transform="scale(.1)" fill="#fff" textLength="430">pipeline</text><text aria-hidden="true" x="765" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="390">started</text><text x="765" y="140" transform="scale(.1)" fill="#fff" textLength="390">started</text></g></svg>`
|
||||
badgeError = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20" role="img" aria-label="pipeline: error"><title>pipeline: error</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="90" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="53" height="20" fill="#555"/><rect x="53" width="37" height="20" fill="#9f9f9f"/><rect width="90" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="275" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">pipeline</text><text x="275" y="140" transform="scale(.1)" fill="#fff" textLength="430">pipeline</text><text aria-hidden="true" x="705" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">error</text><text x="705" y="140" transform="scale(.1)" fill="#fff" textLength="270">error</text></g></svg>`
|
||||
badgeNone = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20" role="img" aria-label="pipeline: none"><title>pipeline: none</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="90" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="53" height="20" fill="#555"/><rect x="53" width="37" height="20" fill="#9f9f9f"/><rect width="90" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="275" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">pipeline</text><text x="275" y="140" transform="scale(.1)" fill="#fff" textLength="430">pipeline</text><text aria-hidden="true" x="705" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">none</text><text x="705" y="140" transform="scale(.1)" fill="#fff" textLength="270">none</text></g></svg>`
|
||||
"go.woodpecker-ci.org/woodpecker/v3/server/model"
|
||||
)
|
||||
|
||||
// Generate an SVG badge based on a pipeline.
|
||||
func Generate(pipeline *model.Pipeline) string {
|
||||
if pipeline == nil {
|
||||
return badgeNone
|
||||
var (
|
||||
// Status labels.
|
||||
badgeStatusSuccess = "success"
|
||||
badgeStatusFailure = "failure"
|
||||
badgeStatusStarted = "started"
|
||||
badgeStatusError = "error"
|
||||
badgeStatusNone = "none"
|
||||
)
|
||||
|
||||
func getBadgeStatusLabelAndColor(status *model.StatusValue) (string, Color) {
|
||||
if status == nil {
|
||||
return badgeStatusNone, ColorGray
|
||||
}
|
||||
switch pipeline.Status {
|
||||
|
||||
switch *status {
|
||||
case model.StatusSuccess:
|
||||
return badgeSuccess
|
||||
return badgeStatusSuccess, ColorGreen
|
||||
case model.StatusFailure:
|
||||
return badgeFailure
|
||||
case model.StatusError, model.StatusKilled:
|
||||
return badgeError
|
||||
return badgeStatusFailure, ColorRed
|
||||
case model.StatusPending, model.StatusRunning:
|
||||
return badgeStarted
|
||||
return badgeStatusStarted, ColorYellow
|
||||
case model.StatusError, model.StatusKilled:
|
||||
return badgeStatusError, ColorGray
|
||||
default:
|
||||
return badgeNone
|
||||
return badgeStatusNone, ColorGray
|
||||
}
|
||||
}
|
||||
|
||||
// Generate an SVG badge based on a pipeline.
|
||||
func Generate(name string, status *model.StatusValue) (string, error) {
|
||||
label, color := getBadgeStatusLabelAndColor(status)
|
||||
bytes, err := RenderBytes(name, label, color)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("could not render badge")
|
||||
return "", err
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
package badges
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -22,13 +26,89 @@ import (
|
||||
"go.woodpecker-ci.org/woodpecker/v3/server/model"
|
||||
)
|
||||
|
||||
var (
|
||||
badgeNone = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="20"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="round"><rect width="82" height="20" rx="3" fill="#fff"/></mask><g mask="url(#round)"><rect width="49" height="20" fill="#555"/><rect x="49" width="33" height="20" fill="#9f9f9f"/><rect width="82" height="20" fill="url(#smooth)"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="25.5" y="15" fill="#010101" fill-opacity=".3">pipeline</text><text x="25.5" y="14">pipeline</text><text x="64.5" y="15" fill="#010101" fill-opacity=".3">none</text><text x="64.5" y="14">none</text></g></svg>`
|
||||
badgeSuccess = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="101" height="20"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="round"><rect width="101" height="20" rx="3" fill="#fff"/></mask><g mask="url(#round)"><rect width="49" height="20" fill="#555"/><rect x="49" width="52" height="20" fill="#44cc11"/><rect width="101" height="20" fill="url(#smooth)"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="25.5" y="15" fill="#010101" fill-opacity=".3">pipeline</text><text x="25.5" y="14">pipeline</text><text x="74" y="15" fill="#010101" fill-opacity=".3">success</text><text x="74" y="14">success</text></g></svg>`
|
||||
badgeFailure = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="round"><rect width="90" height="20" rx="3" fill="#fff"/></mask><g mask="url(#round)"><rect width="49" height="20" fill="#555"/><rect x="49" width="41" height="20" fill="#e05d44"/><rect width="90" height="20" fill="url(#smooth)"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="25.5" y="15" fill="#010101" fill-opacity=".3">pipeline</text><text x="25.5" y="14">pipeline</text><text x="68.5" y="15" fill="#010101" fill-opacity=".3">failure</text><text x="68.5" y="14">failure</text></g></svg>`
|
||||
badgeError = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="83" height="20"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="round"><rect width="83" height="20" rx="3" fill="#fff"/></mask><g mask="url(#round)"><rect width="49" height="20" fill="#555"/><rect x="49" width="34" height="20" fill="#9f9f9f"/><rect width="83" height="20" fill="url(#smooth)"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="25.5" y="15" fill="#010101" fill-opacity=".3">pipeline</text><text x="25.5" y="14">pipeline</text><text x="65" y="15" fill="#010101" fill-opacity=".3">error</text><text x="65" y="14">error</text></g></svg>`
|
||||
badgeStarted = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="95" height="20"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="round"><rect width="95" height="20" rx="3" fill="#fff"/></mask><g mask="url(#round)"><rect width="49" height="20" fill="#555"/><rect x="49" width="46" height="20" fill="#dfb317"/><rect width="95" height="20" fill="url(#smooth)"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="25.5" y="15" fill="#010101" fill-opacity=".3">pipeline</text><text x="25.5" y="14">pipeline</text><text x="71" y="15" fill="#010101" fill-opacity=".3">started</text><text x="71" y="14">started</text></g></svg>`
|
||||
)
|
||||
|
||||
// Generate an SVG badge based on a pipeline.
|
||||
func TestGenerate(t *testing.T) {
|
||||
assert.Equal(t, badgeNone, Generate(nil))
|
||||
assert.Equal(t, badgeSuccess, Generate(&model.Pipeline{Status: model.StatusSuccess}))
|
||||
assert.Equal(t, badgeFailure, Generate(&model.Pipeline{Status: model.StatusFailure}))
|
||||
assert.Equal(t, badgeError, Generate(&model.Pipeline{Status: model.StatusError}))
|
||||
assert.Equal(t, badgeError, Generate(&model.Pipeline{Status: model.StatusKilled}))
|
||||
assert.Equal(t, badgeStarted, Generate(&model.Pipeline{Status: model.StatusPending}))
|
||||
assert.Equal(t, badgeStarted, Generate(&model.Pipeline{Status: model.StatusRunning}))
|
||||
status := model.StatusDeclined
|
||||
badge, err := Generate("pipeline", &status)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, badgeNone, badge)
|
||||
status = model.StatusSuccess
|
||||
badge, err = Generate("pipeline", &status)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, badgeSuccess, badge)
|
||||
status = model.StatusFailure
|
||||
badge, err = Generate("pipeline", &status)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, badgeFailure, badge)
|
||||
status = model.StatusError
|
||||
badge, err = Generate("pipeline", &status)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, badgeError, badge)
|
||||
status = model.StatusKilled
|
||||
badge, err = Generate("pipeline", &status)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, badgeError, badge)
|
||||
status = model.StatusPending
|
||||
badge, err = Generate("pipeline", &status)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, badgeStarted, badge)
|
||||
status = model.StatusRunning
|
||||
badge, err = Generate("pipeline", &status)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, badgeStarted, badge)
|
||||
}
|
||||
|
||||
func TestBadgeDrawerRender(t *testing.T) {
|
||||
mockTemplate := strings.TrimSpace(`
|
||||
{{.Subject}},{{.Status}},{{.Color}},{{with .Bounds}}{{.SubjectX}},{{.SubjectDx}},{{.StatusX}},{{.StatusDx}},{{.Dx}}{{end}}
|
||||
`)
|
||||
mockFontSize := 11.0
|
||||
mockDPI := 72.0
|
||||
|
||||
fd, err := mustNewFontDrawer(mockFontSize, mockDPI)
|
||||
assert.NoError(t, err)
|
||||
|
||||
d := &badgeDrawer{
|
||||
fd: fd,
|
||||
tmpl: template.Must(template.New("mock-template").Parse(mockTemplate)),
|
||||
mutex: &sync.Mutex{},
|
||||
}
|
||||
|
||||
output := "XXX,YYY,#c0c0c0,14,26,38,26,52"
|
||||
|
||||
var buf bytes.Buffer
|
||||
assert.NoError(t, d.Render("XXX", "YYY", "#c0c0c0", &buf))
|
||||
assert.Equal(t, output, buf.String())
|
||||
}
|
||||
|
||||
func TestBadgeDrawerRenderBytes(t *testing.T) {
|
||||
mockTemplate := strings.TrimSpace(`
|
||||
{{.Subject}},{{.Status}},{{.Color}},{{with .Bounds}}{{.SubjectX}},{{.SubjectDx}},{{.StatusX}},{{.StatusDx}},{{.Dx}}{{end}}
|
||||
`)
|
||||
mockFontSize := 11.0
|
||||
mockDPI := 72.0
|
||||
|
||||
fd, err := mustNewFontDrawer(mockFontSize, mockDPI)
|
||||
assert.NoError(t, err)
|
||||
|
||||
d := &badgeDrawer{
|
||||
fd: fd,
|
||||
tmpl: template.Must(template.New("mock-template").Parse(mockTemplate)),
|
||||
mutex: &sync.Mutex{},
|
||||
}
|
||||
|
||||
output := "XXX,YYY,#c0c0c0,14,26,38,26,52"
|
||||
|
||||
bytes, err := d.RenderBytes("XXX", "YYY", "#c0c0c0")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, output, string(bytes))
|
||||
}
|
||||
|
||||
15
server/badges/color.go
Normal file
15
server/badges/color.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright 2023 The narqo/go-badge Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT.
|
||||
|
||||
package badges
|
||||
|
||||
// Color represents color of the badge.
|
||||
type Color string
|
||||
|
||||
// Standard colors.
|
||||
const (
|
||||
ColorGreen = "#44cc11"
|
||||
ColorYellow = "#dfb317"
|
||||
ColorRed = "#e05d44"
|
||||
ColorGray = "#9f9f9f"
|
||||
)
|
||||
130
server/badges/drawer.go
Normal file
130
server/badges/drawer.go
Normal file
@@ -0,0 +1,130 @@
|
||||
// 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
|
||||
}
|
||||
BIN
server/badges/fonts/DejaVuSans.ttf
Normal file
BIN
server/badges/fonts/DejaVuSans.ttf
Normal file
Binary file not shown.
13
server/badges/fonts/dejavusans.go
Normal file
13
server/badges/fonts/dejavusans.go
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright 2023 The narqo/go-badge Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT.
|
||||
|
||||
package fonts
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
// DejaVuSans is DejaVuSans.ttf font inlined to the bytes slice.
|
||||
//
|
||||
//go:embed DejaVuSans.ttf
|
||||
var DejaVuSans []byte
|
||||
8
server/badges/style.go
Normal file
8
server/badges/style.go
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright 2023 The narqo/go-badge Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT.
|
||||
|
||||
package badges
|
||||
|
||||
// cspell:words Verdana
|
||||
|
||||
var flatTemplate = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{{.Bounds.Dx}}" height="20"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="round"><rect width="{{.Bounds.Dx}}" height="20" rx="3" fill="#fff"/></mask><g mask="url(#round)"><rect width="{{.Bounds.SubjectDx}}" height="20" fill="#555"/><rect x="{{.Bounds.SubjectDx}}" width="{{.Bounds.StatusDx}}" height="20" fill="{{or .Color "#4c1" | html}}"/><rect width="{{.Bounds.Dx}}" height="20" fill="url(#smooth)"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="{{.Bounds.SubjectX}}" y="15" fill="#010101" fill-opacity=".3">{{.Subject | html}}</text><text x="{{.Bounds.SubjectX}}" y="14">{{.Subject | html}}</text><text x="{{.Bounds.StatusX}}" y="15" fill="#010101" fill-opacity=".3">{{.Status | html}}</text><text x="{{.Bounds.StatusX}}" y="14">{{.Status | html}}</text></g></svg>`
|
||||
Reference in New Issue
Block a user