├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── badge.go ├── badge_test.go ├── color.go ├── example ├── .gitignore └── example.go ├── fonts ├── Bitstream Vera License.txt ├── vera.go └── vera.ttf ├── go.mod ├── go.sum ├── style.go ├── utils.go ├── utils_test.go └── version.go /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.6 4 | - tip 5 | script: go test -v -bench=Render -benchmem 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Vladimir Varankin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-badge [![GoDoc](https://godoc.org/github.com/narqo/go-badge?status.svg)](https://godoc.org/github.com/narqo/go-badge) 2 | 3 | go-badge is a library to render shield badges to SVG. 4 | 5 | ## Installation 6 | 7 | Using `go get` 8 | 9 | ``` 10 | go get github.com/narqo/go-badge 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```go 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | 22 | "github.com/narqo/go-badge" 23 | ) 24 | 25 | func main() { 26 | err := badge.Render("godoc", "reference", "#5272B4", os.Stdout) 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | badge, err := badge.RenderBytes("godoc", "reference", "#5272B4") 32 | if err != nil { 33 | panic(err) 34 | } 35 | fmt.Printf("%s", badge) 36 | } 37 | 38 | ``` 39 | 40 | Hope `example/` directory will have more examples in future. 41 | 42 | ## Contribution and Feedback 43 | 44 | Contributing is more than welcome. Create an issue if you see any problem in the code or send a PR with fixes if you'd like. 45 | 46 | ## License 47 | 48 | MIT 49 | 50 | --- 51 | 52 | All the kudos should go to the great [Shields.io](https://github.com/badges/shields) specification project. 53 | -------------------------------------------------------------------------------- /badge.go: -------------------------------------------------------------------------------- 1 | package badge 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "io" 7 | "sync" 8 | 9 | "github.com/golang/freetype/truetype" 10 | "github.com/narqo/go-badge/fonts" 11 | "golang.org/x/image/font" 12 | ) 13 | 14 | type badge struct { 15 | Subject string 16 | Status string 17 | Color Color 18 | Bounds bounds 19 | } 20 | 21 | type bounds struct { 22 | // SubjectDx is the width of subject string of the badge. 23 | SubjectDx float64 24 | SubjectX float64 25 | // StatusDx is the width of status string of the badge. 26 | StatusDx float64 27 | StatusX float64 28 | } 29 | 30 | func (b bounds) Dx() float64 { 31 | return b.SubjectDx + b.StatusDx 32 | } 33 | 34 | type badgeDrawer struct { 35 | fd *font.Drawer 36 | tmpl *template.Template 37 | mutex *sync.Mutex 38 | } 39 | 40 | func (d *badgeDrawer) Render(subject, status string, color Color, w io.Writer) error { 41 | d.mutex.Lock() 42 | subjectDx := d.measureString(subject) 43 | statusDx := d.measureString(status) 44 | d.mutex.Unlock() 45 | 46 | bdg := badge{ 47 | Subject: subject, 48 | Status: status, 49 | Color: color, 50 | Bounds: bounds{ 51 | SubjectDx: subjectDx, 52 | SubjectX: subjectDx/2.0 + 1, 53 | StatusDx: statusDx, 54 | StatusX: subjectDx + statusDx/2.0 - 1, 55 | }, 56 | } 57 | return d.tmpl.Execute(w, bdg) 58 | } 59 | 60 | func (d *badgeDrawer) RenderBytes(subject, status string, color Color) ([]byte, error) { 61 | buf := &bytes.Buffer{} 62 | err := d.Render(subject, status, color, buf) 63 | return buf.Bytes(), err 64 | } 65 | 66 | // shield.io uses Verdana.ttf to measure text width with an extra 10px. 67 | // As we use Vera.ttf, we have to tune this value a little. 68 | const extraDx = 13 69 | 70 | func (d *badgeDrawer) measureString(s string) float64 { 71 | return float64(d.fd.MeasureString(s)>>6) + extraDx 72 | } 73 | 74 | // Render renders a badge of the given color, with given subject and status to w. 75 | func Render(subject, status string, color Color, w io.Writer) error { 76 | return drawer.Render(subject, status, color, w) 77 | } 78 | 79 | // RenderBytes renders a badge of the given color, with given subject and status to bytes. 80 | func RenderBytes(subject, status string, color Color) ([]byte, error) { 81 | return drawer.RenderBytes(subject, status, color) 82 | } 83 | 84 | const ( 85 | dpi = 72 86 | fontsize = 11 87 | ) 88 | 89 | var drawer *badgeDrawer 90 | 91 | func init() { 92 | drawer = &badgeDrawer{ 93 | fd: mustNewFontDrawer(fontsize, dpi), 94 | tmpl: template.Must(template.New("flat-template").Parse(flatTemplate)), 95 | mutex: &sync.Mutex{}, 96 | } 97 | } 98 | 99 | func mustNewFontDrawer(size, dpi float64) *font.Drawer { 100 | ttf, err := truetype.Parse(fonts.VeraSans) 101 | if err != nil { 102 | panic(err) 103 | } 104 | return &font.Drawer{ 105 | Face: truetype.NewFace(ttf, &truetype.Options{ 106 | Size: size, 107 | DPI: dpi, 108 | Hinting: font.HintingFull, 109 | }), 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /badge_test.go: -------------------------------------------------------------------------------- 1 | package badge 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "io/ioutil" 7 | "strings" 8 | "sync" 9 | "testing" 10 | ) 11 | 12 | func TestBadgeDrawerRender(t *testing.T) { 13 | mockTemplate := strings.TrimSpace(` 14 | {{.Subject}},{{.Status}},{{.Color}},{{with .Bounds}}{{.SubjectX}},{{.SubjectDx}},{{.StatusX}},{{.StatusDx}},{{.Dx}}{{end}} 15 | `) 16 | mockFontSize := 11.0 17 | mockDPI := 72.0 18 | 19 | d := &badgeDrawer{ 20 | fd: mustNewFontDrawer(mockFontSize, mockDPI), 21 | tmpl: template.Must(template.New("mock-template").Parse(mockTemplate)), 22 | mutex: &sync.Mutex{}, 23 | } 24 | 25 | output := "XXX,YYY,#c0c0c0,18,34,50,34,68" 26 | 27 | var buf bytes.Buffer 28 | err := d.Render("XXX", "YYY", "#c0c0c0", &buf) 29 | if err != nil { 30 | t.Errorf("unexpected error: %s", err) 31 | } 32 | result := buf.String() 33 | if result != output { 34 | t.Errorf("expect %q got %q", output, result) 35 | } 36 | } 37 | 38 | func TestBadgeDrawerRenderBytes(t *testing.T) { 39 | mockTemplate := strings.TrimSpace(` 40 | {{.Subject}},{{.Status}},{{.Color}},{{with .Bounds}}{{.SubjectX}},{{.SubjectDx}},{{.StatusX}},{{.StatusDx}},{{.Dx}}{{end}} 41 | `) 42 | mockFontSize := 11.0 43 | mockDPI := 72.0 44 | 45 | d := &badgeDrawer{ 46 | fd: mustNewFontDrawer(mockFontSize, mockDPI), 47 | tmpl: template.Must(template.New("mock-template").Parse(mockTemplate)), 48 | mutex: &sync.Mutex{}, 49 | } 50 | 51 | output := "XXX,YYY,#c0c0c0,18,34,50,34,68" 52 | 53 | bytes, err := d.RenderBytes("XXX", "YYY", "#c0c0c0") 54 | if err != nil { 55 | t.Errorf("unexpected error: %s", err) 56 | } 57 | if string(bytes) != output { 58 | t.Errorf("expect %q got %q", output, string(bytes)) 59 | } 60 | } 61 | 62 | func BenchmarkRender(b *testing.B) { 63 | // warm up 64 | Render("XXX", "YYY", ColorBlue, ioutil.Discard) 65 | b.ResetTimer() 66 | for i := 0; i < b.N; i++ { 67 | err := Render("XXX", "YYY", ColorBlue, ioutil.Discard) 68 | if err != nil { 69 | b.Fatal(err) 70 | } 71 | } 72 | } 73 | 74 | func BenchmarkRenderParallel(b *testing.B) { 75 | b.RunParallel(func(pb *testing.PB) { 76 | for pb.Next() { 77 | err := Render("XXX", "YYY", ColorBlue, ioutil.Discard) 78 | if err != nil { 79 | b.Fatal(err) 80 | } 81 | } 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /color.go: -------------------------------------------------------------------------------- 1 | package badge 2 | 3 | // Color represents color of the badge. 4 | type Color string 5 | 6 | // ColorScheme contains named colors that could be used to render the badge. 7 | var ColorScheme = map[string]string{ 8 | "brightgreen": "#4c1", 9 | "green": "#97ca00", 10 | "yellow": "#dfb317", 11 | "yellowgreen": "#a4a61d", 12 | "orange": "#fe7d37", 13 | "red": "#e05d44", 14 | "blue": "#007ec6", 15 | "grey": "#555", 16 | "gray": "#555", 17 | "lightgrey": "#9f9f9f", 18 | "lightgray": "#9f9f9f", 19 | } 20 | 21 | // Standard colors. 22 | const ( 23 | ColorBrightgreen = Color("brightgreen") 24 | ColorGreen = Color("green") 25 | ColorYellow = Color("yellow") 26 | ColorYellowgreen = Color("yellowgreen") 27 | ColorOrange = Color("orange") 28 | ColorRed = Color("red") 29 | ColorBlue = Color("blue") 30 | ColorGrey = Color("grey") 31 | ColorGray = Color("gray") 32 | ColorLightgrey = Color("lightgrey") 33 | ColorLightgray = Color("lightgray") 34 | ) 35 | 36 | func (c Color) String() string { 37 | color, ok := ColorScheme[string(c)] 38 | if ok { 39 | return color 40 | } else { 41 | return string(c) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | /example 2 | /example.svg 3 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/narqo/go-badge" 10 | ) 11 | 12 | var ( 13 | subject = flag.String("subject", "", "Badge subject") 14 | status = flag.String("status", "", "Badge status") 15 | color = flag.String("color", "blue", "Badge color") 16 | ) 17 | 18 | func main() { 19 | flag.Parse() 20 | err := badge.Render(*subject, *status, badge.Color(*color), os.Stdout) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | 25 | badge, err := badge.RenderBytes(*subject, *status, badge.Color(*color)) 26 | if err != nil { 27 | panic(err) 28 | } 29 | fmt.Printf("%s", badge) 30 | } 31 | -------------------------------------------------------------------------------- /fonts/Bitstream Vera License.txt: -------------------------------------------------------------------------------- 1 | Bitstream Vera Fonts Copyright 2 | 3 | The fonts have a generous copyright, allowing derivative works (as 4 | long as "Bitstream" or "Vera" are not in the names), and full 5 | redistribution (so long as they are not *sold* by themselves). They 6 | can be be bundled, redistributed and sold with any software. 7 | 8 | The fonts are distributed under the following copyright: 9 | 10 | Copyright 11 | ========= 12 | 13 | Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream 14 | Vera is a trademark of Bitstream, Inc. 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining 17 | a copy of the fonts accompanying this license ("Fonts") and associated 18 | documentation files (the "Font Software"), to reproduce and distribute 19 | the Font Software, including without limitation the rights to use, 20 | copy, merge, publish, distribute, and/or sell copies of the Font 21 | Software, and to permit persons to whom the Font Software is furnished 22 | to do so, subject to the following conditions: 23 | 24 | The above copyright and trademark notices and this permission notice 25 | shall be included in all copies of one or more of the Font Software 26 | typefaces. 27 | 28 | The Font Software may be modified, altered, or added to, and in 29 | particular the designs of glyphs or characters in the Fonts may be 30 | modified and additional glyphs or characters may be added to the 31 | Fonts, only if the fonts are renamed to names not containing either 32 | the words "Bitstream" or the word "Vera". 33 | 34 | This License becomes null and void to the extent applicable to Fonts 35 | or Font Software that has been modified and is distributed under the 36 | "Bitstream Vera" names. 37 | 38 | The Font Software may be sold as part of a larger software package but 39 | no copy of one or more of the Font Software typefaces may be sold by 40 | itself. 41 | 42 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 43 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 44 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 45 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL 46 | BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR 47 | OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, 48 | OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR 49 | OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT 50 | SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. 51 | 52 | Except as contained in this notice, the names of Gnome, the Gnome 53 | Foundation, and Bitstream Inc., shall not be used in advertising or 54 | otherwise to promote the sale, use or other dealings in this Font 55 | Software without prior written authorization from the Gnome Foundation 56 | or Bitstream Inc., respectively. For further information, contact: 57 | fonts at gnome dot org. 58 | 59 | Copyright FAQ 60 | ============= 61 | 62 | 1. I don't understand the resale restriction... What gives? 63 | 64 | Bitstream is giving away these fonts, but wishes to ensure its 65 | competitors can't just drop the fonts as is into a font sale system 66 | and sell them as is. It seems fair that if Bitstream can't make money 67 | from the Bitstream Vera fonts, their competitors should not be able to 68 | do so either. You can sell the fonts as part of any software package, 69 | however. 70 | 71 | 2. I want to package these fonts separately for distribution and 72 | sale as part of a larger software package or system. Can I do so? 73 | 74 | Yes. A RPM or Debian package is a "larger software package" to begin 75 | with, and you aren't selling them independently by themselves. 76 | See 1. above. 77 | 78 | 3. Are derivative works allowed? 79 | Yes! 80 | 81 | 4. Can I change or add to the font(s)? 82 | Yes, but you must change the name(s) of the font(s). 83 | 84 | 5. Under what terms are derivative works allowed? 85 | 86 | You must change the name(s) of the fonts. This is to ensure the 87 | quality of the fonts, both to protect Bitstream and Gnome. We want to 88 | ensure that if an application has opened a font specifically of these 89 | names, it gets what it expects (though of course, using fontconfig, 90 | substitutions could still could have occurred during font 91 | opening). You must include the Bitstream copyright. Additional 92 | copyrights can be added, as per copyright law. Happy Font Hacking! 93 | 94 | 6. If I have improvements for Bitstream Vera, is it possible they might get 95 | adopted in future versions? 96 | 97 | Yes. The contract between the Gnome Foundation and Bitstream has 98 | provisions for working with Bitstream to ensure quality additions to 99 | the Bitstream Vera font family. Please contact us if you have such 100 | additions. Note, that in general, we will want such additions for the 101 | entire family, not just a single font, and that you'll have to keep 102 | both Gnome and Jim Lyles, Vera's designer, happy! To make sense to add 103 | glyphs to the font, they must be stylistically in keeping with Vera's 104 | design. Vera cannot become a "ransom note" font. Jim Lyles will be 105 | providing a document describing the design elements used in Vera, as a 106 | guide and aid for people interested in contributing to Vera. 107 | 108 | 7. I want to sell a software package that uses these fonts: Can I do so? 109 | 110 | Sure. Bundle the fonts with your software and sell your software 111 | with the fonts. That is the intent of the copyright. 112 | 113 | 8. If applications have built the names "Bitstream Vera" into them, 114 | can I override this somehow to use fonts of my choosing? 115 | 116 | This depends on exact details of the software. Most open source 117 | systems and software (e.g., Gnome, KDE, etc.) are now converting to 118 | use fontconfig (see www.fontconfig.org) to handle font configuration, 119 | selection and substitution; it has provisions for overriding font 120 | names and subsituting alternatives. An example is provided by the 121 | supplied local.conf file, which chooses the family Bitstream Vera for 122 | "sans", "serif" and "monospace". Other software (e.g., the XFree86 123 | core server) has other mechanisms for font substitution. -------------------------------------------------------------------------------- /fonts/vera.go: -------------------------------------------------------------------------------- 1 | package fonts 2 | 3 | import ( 4 | _ "embed" 5 | ) 6 | 7 | // VeraSans is vera.ttf font inlined to the bytes slice. 8 | // 9 | //go:embed vera.ttf 10 | var VeraSans []byte 11 | -------------------------------------------------------------------------------- /fonts/vera.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/narqo/go-badge/c9a75c019a59408f64f8ebcfa2419bb4ea707231/fonts/vera.ttf -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/narqo/go-badge 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 7 | golang.org/x/image v0.0.0-20211028202545-6944b10bf410 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/golang/freetype v0.0.0-20160221112527-9ce4eec92a4b h1:g/ACZ897P/JC5EnPl0nas+CTm7BAXv6QA/5sBxm2Umw= 2 | github.com/golang/freetype v0.0.0-20160221112527-9ce4eec92a4b/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 3 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 4 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 5 | golang.org/x/image v0.0.0-20160107224602-7c492694a644 h1:gDX87IHVAWzhS+GuswKlPHa5jnZxbtVTojgccecF6Pw= 6 | golang.org/x/image v0.0.0-20160107224602-7c492694a644/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 7 | golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= 8 | golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= 9 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 10 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 11 | -------------------------------------------------------------------------------- /style.go: -------------------------------------------------------------------------------- 1 | package badge 2 | 3 | var flatTemplate = stripXmlWhitespace(` 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {{.Subject | html}} 22 | {{.Subject | html}} 23 | {{.Status | html}} 24 | {{.Status | html}} 25 | 26 | 27 | `) 28 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package badge 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | // https://github.com/badges/shields/blob/b6be37d277b64a90bde98ca10446c9d433a56681/badge-maker/lib/xml.js#L7 9 | func stripXmlWhitespace(xml string) string { 10 | return strings.TrimSpace(regexp.MustCompile(`<\s+`).ReplaceAllString(regexp.MustCompile(`>\s+`).ReplaceAllString(xml, ">"), "<")) 11 | } 12 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package badge 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // Keep newlines, but clean up whitespace 8 | func TestStripXmlWhitespace(t *testing.T) { 9 | const mock = ` 10 | 11 | 12 | aaa 13 | 14 | aaaa 15 | 16 | ` 17 | const expected = `aaa 18 | aaaa 19 | ` 20 | 21 | if stripXmlWhitespace(mock) != expected { 22 | t.Errorf("stripXmlWhitespace failed") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package badge 2 | 3 | const VersionString = "v0.3" 4 | --------------------------------------------------------------------------------