├── .gitignore ├── .godir ├── .travis.yml ├── LICENSE ├── README.md ├── assets ├── edge.xcf ├── favicon.xcf └── gradient.xcf ├── data ├── edge.png ├── gradient.png └── opensanssemibold.ttf ├── main.go ├── png.go ├── png_test.go ├── resources.go ├── static ├── favicon.png └── index.html └── test ├── use-buckler-blue.png └── vendor.data /.gitignore: -------------------------------------------------------------------------------- 1 | buckler 2 | buckler.test 3 | *.sw? 4 | -------------------------------------------------------------------------------- /.godir: -------------------------------------------------------------------------------- 1 | github.com/badges/buckler 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 1.2 3 | 4 | script: 5 | - go test -v ./... 6 | 7 | git: 8 | depth: 10 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 James Bowes 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 13 | all 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 21 | THE SOFTWARE. 22 | 23 | Buckler makes use of the OpenSans font, which is ASL v2.0 Licensed: 24 | 25 | Apache License 26 | Version 2.0, January 2004 27 | http://www.apache.org/licenses/ 28 | 29 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 30 | 31 | 1. Definitions. 32 | 33 | "License" shall mean the terms and conditions for use, reproduction, 34 | and distribution as defined by Sections 1 through 9 of this document. 35 | 36 | "Licensor" shall mean the copyright owner or entity authorized by 37 | the copyright owner that is granting the License. 38 | 39 | "Legal Entity" shall mean the union of the acting entity and all 40 | other entities that control, are controlled by, or are under common 41 | control with that entity. For the purposes of this definition, 42 | "control" means (i) the power, direct or indirect, to cause the 43 | direction or management of such entity, whether by contract or 44 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 45 | outstanding shares, or (iii) beneficial ownership of such entity. 46 | 47 | "You" (or "Your") shall mean an individual or Legal Entity 48 | exercising permissions granted by this License. 49 | 50 | "Source" form shall mean the preferred form for making modifications, 51 | including but not limited to software source code, documentation 52 | source, and configuration files. 53 | 54 | "Object" form shall mean any form resulting from mechanical 55 | transformation or translation of a Source form, including but 56 | not limited to compiled object code, generated documentation, 57 | and conversions to other media types. 58 | 59 | "Work" shall mean the work of authorship, whether in Source or 60 | Object form, made available under the License, as indicated by a 61 | copyright notice that is included in or attached to the work 62 | (an example is provided in the Appendix below). 63 | 64 | "Derivative Works" shall mean any work, whether in Source or Object 65 | form, that is based on (or derived from) the Work and for which the 66 | editorial revisions, annotations, elaborations, or other modifications 67 | represent, as a whole, an original work of authorship. For the purposes 68 | of this License, Derivative Works shall not include works that remain 69 | separable from, or merely link (or bind by name) to the interfaces of, 70 | the Work and Derivative Works thereof. 71 | 72 | "Contribution" shall mean any work of authorship, including 73 | the original version of the Work and any modifications or additions 74 | to that Work or Derivative Works thereof, that is intentionally 75 | submitted to Licensor for inclusion in the Work by the copyright owner 76 | or by an individual or Legal Entity authorized to submit on behalf of 77 | the copyright owner. For the purposes of this definition, "submitted" 78 | means any form of electronic, verbal, or written communication sent 79 | to the Licensor or its representatives, including but not limited to 80 | communication on electronic mailing lists, source code control systems, 81 | and issue tracking systems that are managed by, or on behalf of, the 82 | Licensor for the purpose of discussing and improving the Work, but 83 | excluding communication that is conspicuously marked or otherwise 84 | designated in writing by the copyright owner as "Not a Contribution." 85 | 86 | "Contributor" shall mean Licensor and any individual or Legal Entity 87 | on behalf of whom a Contribution has been received by Licensor and 88 | subsequently incorporated within the Work. 89 | 90 | 2. Grant of Copyright License. Subject to the terms and conditions of 91 | this License, each Contributor hereby grants to You a perpetual, 92 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 93 | copyright license to reproduce, prepare Derivative Works of, 94 | publicly display, publicly perform, sublicense, and distribute the 95 | Work and such Derivative Works in Source or Object form. 96 | 97 | 3. Grant of Patent License. Subject to the terms and conditions of 98 | this License, each Contributor hereby grants to You a perpetual, 99 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 100 | (except as stated in this section) patent license to make, have made, 101 | use, offer to sell, sell, import, and otherwise transfer the Work, 102 | where such license applies only to those patent claims licensable 103 | by such Contributor that are necessarily infringed by their 104 | Contribution(s) alone or by combination of their Contribution(s) 105 | with the Work to which such Contribution(s) was submitted. If You 106 | institute patent litigation against any entity (including a 107 | cross-claim or counterclaim in a lawsuit) alleging that the Work 108 | or a Contribution incorporated within the Work constitutes direct 109 | or contributory patent infringement, then any patent licenses 110 | granted to You under this License for that Work shall terminate 111 | as of the date such litigation is filed. 112 | 113 | 4. Redistribution. You may reproduce and distribute copies of the 114 | Work or Derivative Works thereof in any medium, with or without 115 | modifications, and in Source or Object form, provided that You 116 | meet the following conditions: 117 | 118 | (a) You must give any other recipients of the Work or 119 | Derivative Works a copy of this License; and 120 | 121 | (b) You must cause any modified files to carry prominent notices 122 | stating that You changed the files; and 123 | 124 | (c) You must retain, in the Source form of any Derivative Works 125 | that You distribute, all copyright, patent, trademark, and 126 | attribution notices from the Source form of the Work, 127 | excluding those notices that do not pertain to any part of 128 | the Derivative Works; and 129 | 130 | (d) If the Work includes a "NOTICE" text file as part of its 131 | distribution, then any Derivative Works that You distribute must 132 | include a readable copy of the attribution notices contained 133 | within such NOTICE file, excluding those notices that do not 134 | pertain to any part of the Derivative Works, in at least one 135 | of the following places: within a NOTICE text file distributed 136 | as part of the Derivative Works; within the Source form or 137 | documentation, if provided along with the Derivative Works; or, 138 | within a display generated by the Derivative Works, if and 139 | wherever such third-party notices normally appear. The contents 140 | of the NOTICE file are for informational purposes only and 141 | do not modify the License. You may add Your own attribution 142 | notices within Derivative Works that You distribute, alongside 143 | or as an addendum to the NOTICE text from the Work, provided 144 | that such additional attribution notices cannot be construed 145 | as modifying the License. 146 | 147 | You may add Your own copyright statement to Your modifications and 148 | may provide additional or different license terms and conditions 149 | for use, reproduction, or distribution of Your modifications, or 150 | for any such Derivative Works as a whole, provided Your use, 151 | reproduction, and distribution of the Work otherwise complies with 152 | the conditions stated in this License. 153 | 154 | 5. Submission of Contributions. Unless You explicitly state otherwise, 155 | any Contribution intentionally submitted for inclusion in the Work 156 | by You to the Licensor shall be under the terms and conditions of 157 | this License, without any additional terms or conditions. 158 | Notwithstanding the above, nothing herein shall supersede or modify 159 | the terms of any separate license agreement you may have executed 160 | with Licensor regarding such Contributions. 161 | 162 | 6. Trademarks. This License does not grant permission to use the trade 163 | names, trademarks, service marks, or product names of the Licensor, 164 | except as required for reasonable and customary use in describing the 165 | origin of the Work and reproducing the content of the NOTICE file. 166 | 167 | 7. Disclaimer of Warranty. Unless required by applicable law or 168 | agreed to in writing, Licensor provides the Work (and each 169 | Contributor provides its Contributions) on an "AS IS" BASIS, 170 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 171 | implied, including, without limitation, any warranties or conditions 172 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 173 | PARTICULAR PURPOSE. You are solely responsible for determining the 174 | appropriateness of using or redistributing the Work and assume any 175 | risks associated with Your exercise of permissions under this License. 176 | 177 | 8. Limitation of Liability. In no event and under no legal theory, 178 | whether in tort (including negligence), contract, or otherwise, 179 | unless required by applicable law (such as deliberate and grossly 180 | negligent acts) or agreed to in writing, shall any Contributor be 181 | liable to You for damages, including any direct, indirect, special, 182 | incidental, or consequential damages of any character arising as a 183 | result of this License or out of the use or inability to use the 184 | Work (including but not limited to damages for loss of goodwill, 185 | work stoppage, computer failure or malfunction, or any and all 186 | other commercial damages or losses), even if such Contributor 187 | has been advised of the possibility of such damages. 188 | 189 | 9. Accepting Warranty or Additional Liability. While redistributing 190 | the Work or Derivative Works thereof, You may choose to offer, 191 | and charge a fee for, acceptance of support, warranty, indemnity, 192 | or other liability obligations and/or rights consistent with this 193 | License. However, in accepting such obligations, You may act only 194 | on Your own behalf and on Your sole responsibility, not on behalf 195 | of any other Contributor, and only if You agree to indemnify, 196 | defend, and hold each Contributor harmless for any liability 197 | incurred by, or claims asserted against, such Contributor by reason 198 | of your accepting any such warranty or additional liability. 199 | 200 | END OF TERMS AND CONDITIONS 201 | 202 | APPENDIX: How to apply the Apache License to your work. 203 | 204 | To apply the Apache License to your work, attach the following 205 | boilerplate notice, with the fields enclosed by brackets "[]" 206 | replaced with your own identifying information. (Don't include 207 | the brackets!) The text should be enclosed in the appropriate 208 | comment syntax for the file format. We also recommend that a 209 | file or class name and description of purpose be included on the 210 | same "printed page" as the copyright notice for easier 211 | identification within third-party archives. 212 | 213 | Copyright [yyyy] [name of copyright owner] 214 | 215 | Licensed under the Apache License, Version 2.0 (the "License"); 216 | you may not use this file except in compliance with the License. 217 | You may obtain a copy of the License at 218 | 219 | http://www.apache.org/licenses/LICENSE-2.0 220 | 221 | Unless required by applicable law or agreed to in writing, software 222 | distributed under the License is distributed on an "AS IS" BASIS, 223 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 224 | See the License for the specific language governing permissions and 225 | limitations under the License. 226 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⛨ Buckler ⛨ 2 | 3 | [![deprecated](http://badges.github.io/stability-badges/dist/deprecated.svg)](http://github.com/badges/stability-badges) 4 | [![Build Status](https://travis-ci.org/badges/buckler.png)](https://travis-ci.org/badges/buckler) 5 | [![Buckler Shield](http://b.repl.ca/v1/use-buckler-blue.png)](http://buckler.repl.ca) 6 | [![Get Hype](http://b.repl.ca/v1/GET-HYPE!-orange.png)](http://buckler.repl.ca) 7 | [![MIT License](http://b.repl.ca/v1/License-MIT-red.png)](LICENSE) 8 | [![CLI interface](http://b.repl.ca/v1/command-line-blue.png)](#command-line) 9 | 10 | Buckler is [Shields](https://github.com/badges/shields) as a Service (ShaaS, or alternatively, Badges as a Service) 11 | for use in GitHub READMEs, or anywhere else. Use buckler with your favorite continuous integration tool, performance 12 | monitoring service API, or ridiculous in-joke to surface information. 13 | 14 | Buckler is available hosted at [b.repl.ca](http://buckler.repl.ca). You may use the [API](#API) to generate shields at runtime, 15 | pregenerate them and host them on your own service, or run your own copy of Buckler to protect important company secrets. 16 | 17 | # API 18 | 19 | Buckler tries to make creating shields easy. Each shield request is a url that has three parts: 20 | - `subject` 21 | - `status` 22 | - `colour` 23 | 24 | Parts are separated by a hyphen. The request is suffixed by `.png` and prefixed with the Buckler host and API version, likely 25 | `b.repl.ca/v1/`. Requests will take the form: `http://b.repl.ca/v1/$SUBJECT-$STATUS-$COLOR.png` 26 | 27 | ## Examples 28 | 29 | - http://b.repl.ca/v1/build-passing-brightgreen.png ⇨ ![](http://b.repl.ca/v1/build-passing-brightgreen.png) 30 | - http://b.repl.ca/v1/downloads-3.4K-blue.png ⇨ ![](http://b.repl.ca/v1/downloads-3.4K-blue.png) 31 | - http://b.repl.ca/v1/coverage-unknown-lightgrey.png ⇨ ![](http://b.repl.ca/v1/coverage-unknown-lightgrey.png) 32 | - http://b.repl.ca/v1/review-NACKED-red.png ⇨ ![](http://b.repl.ca/v1/review-NACKED-red.png) 33 | - http://b.repl.ca/v1/enterprise-ready-ff69b4.png ⇨ ![](http://b.repl.ca/v1/enterprise-ready-ff69b4.png) 34 | 35 | ## Valid Colours 36 | 37 | - `brightgreen` ⇨ ![](http://b.repl.ca/v1/colour-brightgreen-brightgreen.png) 38 | - `green` ⇨ ![](http://b.repl.ca/v1/colour-green-green.png) 39 | - `yellowgreen` ⇨ ![](http://b.repl.ca/v1/colour-yellowgreen-yellowgreen.png) 40 | - `yellow` ⇨ ![](http://b.repl.ca/v1/colour-yellow-yellow.png) 41 | - `orange` ⇨ ![](http://b.repl.ca/v1/colour-orange-orange.png) 42 | - `red` ⇨ ![](http://b.repl.ca/v1/colour-red-red.png) 43 | - `grey` ⇨ ![](http://b.repl.ca/v1/colour-grey-grey.png) 44 | - `lightgrey` ⇨ ![](http://b.repl.ca/v1/colour-lightgrey-lightgrey.png) 45 | - `blue` ⇨ ![](http://b.repl.ca/v1/colour-blue-blue.png) 46 | 47 | Six digit RGB hexidecimal colour values work as well: 48 | 49 | - `804000` - ![](http://b.repl.ca/v1/colour-brown-804000.png) 50 | 51 | ### Grey? 52 | 53 | Don't worry; `gray` and `lightgray` work too. 54 | 55 | ## Escaping Underscores and Hyphens 56 | 57 | Hyphens (`-`) are used to delimit individual fields in your shield request. To include a literal hyphen, use two hyphens (`--`): 58 | 59 | http://b.repl.ca/v1/really--cool-status-yellow.png ⇨ ![](http://b.repl.ca/v1/really--cool-status-yellow.png) 60 | 61 | Similarly, underscores (`_`) are used to indicated spaces. To include a literal underscore, use two underscores (`__`): 62 | 63 | http://b.repl.ca/v1/__private-method_name-lightgrey.png ⇨ ![](http://b.repl.ca/v1/__private-method_name-lightgrey.png) 64 | 65 | ## URL Safe 66 | 67 | Buckler API requests are just HTTP GETs, so remember to URL encode! 68 | 69 | http://b.repl.ca/v1/uptime-99.99%25-yellowgreen.png ⇨ ![](http://b.repl.ca/v1/uptime-99.99%25-yellowgreen.png) 70 | 71 | # Try It Out 72 | 73 | Play around with the simple form on [b.repl.ca](http://b.repl.ca) 74 | 75 | # Installing 76 | 77 | ```bash 78 | go get github.com/badges/buckler 79 | ``` 80 | 81 | Alternatively, `git clone` and `go build` to run from source. 82 | 83 | # Command Line 84 | 85 | Buckler also provides a command line interface: 86 | 87 | ```bash 88 | # writes to build-passing-brightgreen.png 89 | buckler -v build -s passing -c brightgreen 90 | 91 | # writes to my-custom-filename.png 92 | buckler -v build -s passing -c green my-custom-filename.png 93 | 94 | # writes to standard out 95 | buckler -v license -s MIT -c blue - 96 | 97 | # writes 2 shields 98 | buckler build-passing-brightgreen.png license-MIT-blue.png 99 | ``` 100 | 101 | # Thanks 102 | 103 | - Olivier Lacan for the [shields](https://github.com/badges/shields) repo 104 | - Steve Matteson for [Open Sans](http://opensans.com/) 105 | -------------------------------------------------------------------------------- /assets/edge.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badges/buckler/b3ebdfc892f052d908c4e0468738389be071b1bd/assets/edge.xcf -------------------------------------------------------------------------------- /assets/favicon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badges/buckler/b3ebdfc892f052d908c4e0468738389be071b1bd/assets/favicon.xcf -------------------------------------------------------------------------------- /assets/gradient.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badges/buckler/b3ebdfc892f052d908c4e0468738389be071b1bd/assets/gradient.xcf -------------------------------------------------------------------------------- /data/edge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badges/buckler/b3ebdfc892f052d908c4e0468738389be071b1bd/data/edge.png -------------------------------------------------------------------------------- /data/gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badges/buckler/b3ebdfc892f052d908c4e0468738389be071b1bd/data/gradient.png -------------------------------------------------------------------------------- /data/opensanssemibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badges/buckler/b3ebdfc892f052d908c4e0468738389be071b1bd/data/opensanssemibold.ttf -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | "github.com/droundy/goopt" 15 | ) 16 | 17 | var ( 18 | wsReplacer = strings.NewReplacer("__", "_", "_", " ") 19 | revWsReplacer = strings.NewReplacer(" ", "_", "_", "__", "-", "--") 20 | 21 | // set last modifed to server startup. close enough to release. 22 | lastModified = time.Now() 23 | lastModifiedStr = lastModified.UTC().Format(http.TimeFormat) 24 | oneYear = time.Duration(8700) * time.Hour 25 | 26 | staticPath, _ = resourcePaths() 27 | ) 28 | 29 | func shift(s []string) ([]string, string) { 30 | return s[1:], s[0] 31 | } 32 | 33 | func invalidRequest(w http.ResponseWriter, r *http.Request) { 34 | log.Println("bad request", r.URL.String()) 35 | http.Error(w, "bad request", 400) 36 | } 37 | 38 | func parseFileName(name string) (d Data, err error) { 39 | imageName := wsReplacer.Replace(name) 40 | imageParts := strings.Split(imageName, "-") 41 | 42 | newParts := []string{} 43 | for len(imageParts) > 0 { 44 | var head, right string 45 | imageParts, head = shift(imageParts) 46 | 47 | // if starts with - append to previous 48 | if len(head) == 0 && len(newParts) > 0 { 49 | left := "" 50 | if len(newParts) > 0 { 51 | left = newParts[len(newParts)-1] 52 | newParts = newParts[:len(newParts)-1] 53 | } 54 | 55 | // trailing -- is going to break color anyways so don't worry 56 | imageParts, right = shift(imageParts) 57 | 58 | head = strings.Join([]string{left, right}, "-") 59 | } 60 | 61 | newParts = append(newParts, head) 62 | } 63 | 64 | if len(newParts) != 3 { 65 | err = errors.New("Invalid file name") 66 | return 67 | } 68 | 69 | if !strings.HasSuffix(newParts[2], ".png") { 70 | err = errors.New("Unknown file type") 71 | return 72 | } 73 | 74 | cp := newParts[2][0 : len(newParts[2])-4] 75 | c, err := getColor(cp) 76 | if err != nil { 77 | return 78 | } 79 | 80 | d = Data{newParts[0], newParts[1], c} 81 | return 82 | } 83 | 84 | func buckle(w http.ResponseWriter, r *http.Request) { 85 | parts := strings.Split(r.URL.Path, "/") 86 | 87 | if len(parts) != 3 { 88 | invalidRequest(w, r) 89 | return 90 | } 91 | 92 | d, err := parseFileName(parts[2]) 93 | if err != nil { 94 | invalidRequest(w, r) 95 | return 96 | } 97 | 98 | t, err := time.Parse(time.RFC1123, r.Header.Get("if-modified-since")) 99 | if err == nil && lastModified.Before(t.Add(1*time.Second)) { 100 | w.WriteHeader(http.StatusNotModified) 101 | return 102 | } 103 | 104 | w.Header().Add("content-type", "image/png") 105 | w.Header().Add("expires", time.Now().Add(oneYear).Format(time.RFC1123)) 106 | w.Header().Add("cache-control", "public") 107 | w.Header().Add("last-modified", lastModifiedStr) 108 | 109 | makePngShield(w, d) 110 | } 111 | 112 | const basePkg = "github.com/badges/buckler" 113 | 114 | func index(w http.ResponseWriter, r *http.Request) { 115 | http.ServeFile(w, r, filepath.Join(staticPath, "index.html")) 116 | } 117 | 118 | func favicon(w http.ResponseWriter, r *http.Request) { 119 | http.ServeFile(w, r, filepath.Join(staticPath, "favicon.png")) 120 | } 121 | 122 | func fatal(msg string) { 123 | fmt.Println(msg) 124 | os.Exit(1) 125 | } 126 | 127 | func cliMode(vendor string, status string, color string, args []string) { 128 | // if any of vendor, status or color is given, all must be 129 | if (vendor != "" || status != "" || color != "") && 130 | !(vendor != "" && status != "" && color != "") { 131 | fatal("You must specify all of vendor, status, and color") 132 | } 133 | 134 | if vendor != "" { 135 | c, err := getColor(color) 136 | if err != nil { 137 | fatal("Invalid color: " + color) 138 | } 139 | d := Data{vendor, status, c} 140 | 141 | name := fmt.Sprintf("%s-%s-%s.png", revWsReplacer.Replace(vendor), 142 | revWsReplacer.Replace(status), color) 143 | 144 | if len(args) > 1 { 145 | fatal("You can only specify one output file name") 146 | } 147 | 148 | if len(args) == 1 { 149 | name = args[0] 150 | } 151 | 152 | // default to standard out 153 | f := os.Stdout 154 | if name != "-" { 155 | f, err = os.Create(name) 156 | if err != nil { 157 | fatal("Unable to create file: " + name) 158 | } 159 | } 160 | 161 | makePngShield(f, d) 162 | return 163 | } 164 | 165 | // generate based on command line file names 166 | for i := range args { 167 | name := args[i] 168 | d, err := parseFileName(name) 169 | if err != nil { 170 | fatal(err.Error()) 171 | } 172 | 173 | f, err := os.Create(name) 174 | if err != nil { 175 | fatal(err.Error()) 176 | } 177 | makePngShield(f, d) 178 | } 179 | } 180 | 181 | func usage() string { 182 | u := `Usage: %s [-h HOST] [-p PORT] 183 | %s [-v VENDOR -s STATUS -c COLOR] 184 | 185 | %s` 186 | return fmt.Sprintf(u, os.Args[0], os.Args[0], goopt.Help()) 187 | } 188 | 189 | func main() { 190 | hostEnv := os.Getenv("HOST") 191 | portEnv := os.Getenv("PORT") 192 | 193 | // default to environment variable values (changes the help string :( ) 194 | if hostEnv == "" { 195 | hostEnv = "*" 196 | } 197 | 198 | p := 8080 199 | if portEnv != "" { 200 | p, _ = strconv.Atoi(portEnv) 201 | } 202 | 203 | goopt.Usage = usage 204 | 205 | // server mode options 206 | host := goopt.String([]string{"-h", "--host"}, hostEnv, "host ip address to bind to") 207 | port := goopt.Int([]string{"-p", "--port"}, p, "port to listen on") 208 | 209 | // cli mode 210 | vendor := goopt.String([]string{"-v", "--vendor"}, "", "vendor for cli generation") 211 | status := goopt.String([]string{"-s", "--status"}, "", "status for cli generation") 212 | color := goopt.String([]string{"-c", "--color", "--colour"}, "", "color for cli generation") 213 | goopt.Parse(nil) 214 | 215 | args := goopt.Args 216 | 217 | // if any of the cli args are given, or positional args remain, assume cli 218 | // mode. 219 | if len(args) > 0 || *vendor != "" || *status != "" || *color != "" { 220 | cliMode(*vendor, *status, *color, args) 221 | return 222 | } 223 | // normalize for http serving 224 | if *host == "*" { 225 | *host = "" 226 | } 227 | 228 | http.HandleFunc("/v1/", buckle) 229 | http.HandleFunc("/favicon.png", favicon) 230 | http.HandleFunc("/", index) 231 | 232 | log.Println("Listening on port", *port) 233 | http.ListenAndServe(*host+":"+strconv.Itoa(*port), nil) 234 | } 235 | -------------------------------------------------------------------------------- /png.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "image" 6 | "image/color" 7 | "image/draw" 8 | "image/png" 9 | "io" 10 | "io/ioutil" 11 | "log" 12 | "math" 13 | "os" 14 | "path/filepath" 15 | "strconv" 16 | 17 | "code.google.com/p/freetype-go/freetype" 18 | "code.google.com/p/freetype-go/freetype/raster" 19 | ) 20 | 21 | type Data struct { 22 | Vendor string 23 | Status string 24 | Color color.RGBA 25 | } 26 | 27 | var ( 28 | Grey = color.RGBA{74, 74, 74, 255} 29 | BrightGreen = color.RGBA{69, 203, 20, 255} 30 | Green = color.RGBA{124, 166, 0, 255} 31 | YellowGreen = color.RGBA{156, 158, 9, 255} 32 | Yellow = color.RGBA{184, 148, 19, 255} 33 | Orange = color.RGBA{184, 113, 37, 255} 34 | Red = color.RGBA{186, 77, 56, 255} 35 | LightGrey = color.RGBA{131, 131, 131, 255} 36 | Blue = color.RGBA{0, 126, 198, 255} 37 | 38 | Colors = map[string]color.RGBA{ 39 | "grey": Grey, 40 | "brightgreen": BrightGreen, 41 | "green": Green, 42 | "yellowgreen": YellowGreen, 43 | "yellow": Yellow, 44 | "orange": Orange, 45 | "red": Red, 46 | "lightgrey": LightGrey, 47 | "blue": Blue, 48 | 49 | // US spelling 50 | "gray": Grey, 51 | "lightgray": LightGrey, 52 | } 53 | 54 | shadow = color.RGBA{0, 0, 0, 125} 55 | 56 | edge image.Image 57 | gradient image.Image 58 | c *freetype.Context 59 | ) 60 | 61 | const ( 62 | h = 18 63 | op = 4 64 | ip = 4 65 | ) 66 | 67 | func init() { 68 | _, dataPath := resourcePaths() 69 | fi, err := os.Open(filepath.Join(dataPath, "edge.png")) 70 | if err != nil { 71 | log.Fatal(err) 72 | } 73 | defer fi.Close() 74 | 75 | edge, err = png.Decode(fi) 76 | if err != nil { 77 | log.Fatal(err) 78 | } 79 | 80 | fi, err = os.Open(filepath.Join(dataPath, "gradient.png")) 81 | if err != nil { 82 | log.Fatal(err) 83 | } 84 | defer fi.Close() 85 | 86 | gradient, err = png.Decode(fi) 87 | if err != nil { 88 | log.Fatal(err) 89 | } 90 | 91 | fontBytes, err := ioutil.ReadFile(filepath.Join(dataPath, "opensanssemibold.ttf")) 92 | if err != nil { 93 | log.Fatal(err) 94 | } 95 | 96 | font, err := freetype.ParseFont(fontBytes) 97 | if err != nil { 98 | log.Fatal(err) 99 | } 100 | 101 | c = freetype.NewContext() 102 | c.SetDPI(72) 103 | c.SetFont(font) 104 | c.SetFontSize(10) 105 | } 106 | 107 | func hexColor(c string) (color.RGBA, bool) { 108 | if len(c) != 6 { 109 | return color.RGBA{}, false 110 | } 111 | 112 | r, rerr := strconv.ParseInt(c[0:2], 16, 16) 113 | g, gerr := strconv.ParseInt(c[2:4], 16, 16) 114 | b, berr := strconv.ParseInt(c[4:6], 16, 16) 115 | 116 | if rerr != nil || gerr != nil || berr != nil { 117 | return color.RGBA{}, false 118 | } 119 | 120 | return color.RGBA{uint8(r), uint8(g), uint8(b), 255}, true 121 | } 122 | 123 | func getColor(cs string) (c color.RGBA, err error) { 124 | c, ok := Colors[cs] 125 | if !ok { 126 | c, ok = hexColor(cs) 127 | if !ok { 128 | err = errors.New("Unknown colour") 129 | return 130 | } 131 | } 132 | 133 | return 134 | } 135 | 136 | func getTextOffset(pt raster.Point) int { 137 | return int(math.Floor(float64(float32(pt.X)/256 + 0.5))) 138 | } 139 | 140 | func renderString(s string, c *freetype.Context) (*image.RGBA, int) { 141 | estWidth := 8 * len(s) 142 | dst := image.NewRGBA(image.Rect(0, 0, estWidth, h)) 143 | 144 | c.SetDst(dst) 145 | c.SetClip(dst.Bounds()) 146 | 147 | c.SetSrc(&image.Uniform{C: shadow}) 148 | pt := freetype.Pt(0, 13) 149 | c.DrawString(s, pt) 150 | 151 | c.SetSrc(image.White) 152 | 153 | pt = freetype.Pt(0, 12) 154 | pt, _ = c.DrawString(s, pt) 155 | 156 | return dst, getTextOffset(pt) 157 | } 158 | 159 | func buildMask(mask *image.RGBA, imageWidth int, tmpl image.Image, imgOp draw.Op) { 160 | draw.Draw(mask, tmpl.Bounds(), tmpl, image.ZP, imgOp) 161 | 162 | sr := image.Rect(2, 0, 3, h) 163 | for i := 3; i <= imageWidth-3; i++ { 164 | dp := image.Point{i, 0} 165 | r := sr.Sub(sr.Min).Add(dp) 166 | draw.Draw(mask, r, tmpl, sr.Min, imgOp) 167 | } 168 | 169 | sr = image.Rect(0, 0, 1, h) 170 | dp := image.Point{imageWidth - 1, 0} 171 | r := sr.Sub(sr.Min).Add(dp) 172 | draw.Draw(mask, r, tmpl, sr.Min, imgOp) 173 | 174 | sr = image.Rect(1, 0, 2, h) 175 | dp = image.Point{imageWidth - 2, 0} 176 | r = sr.Sub(sr.Min).Add(dp) 177 | draw.Draw(mask, r, tmpl, sr.Min, imgOp) 178 | } 179 | 180 | func makePngShield(w io.Writer, d Data) { 181 | // render text to determine how wide the image has to be 182 | // we leave 6 pixels at the start and end, and 3 for each in the middle 183 | v, vw := renderString(d.Vendor, c) 184 | s, sw := renderString(d.Status, c) 185 | imageWidth := op + vw + ip*2 + sw + op 186 | 187 | img := image.NewRGBA(image.Rect(0, 0, imageWidth, h)) 188 | draw.Draw(img, img.Bounds(), &image.Uniform{C: d.Color}, image.ZP, draw.Src) 189 | 190 | rect := image.Rect(0, 0, op+vw+ip, h) 191 | draw.Draw(img, rect, &image.Uniform{C: Grey}, image.ZP, draw.Src) 192 | 193 | dst := image.NewRGBA(image.Rect(0, 0, imageWidth, h)) 194 | 195 | mask := image.NewRGBA(image.Rect(0, 0, imageWidth, h)) 196 | buildMask(mask, imageWidth, edge, draw.Src) 197 | draw.DrawMask(dst, dst.Bounds(), img, image.ZP, mask, image.ZP, draw.Over) 198 | 199 | buildMask(dst, imageWidth, gradient, draw.Over) 200 | 201 | draw.Draw(dst, dst.Bounds(), v, image.Point{-op, 0}, draw.Over) 202 | 203 | draw.Draw(dst, dst.Bounds(), s, image.Point{-(op + vw + ip*2), 0}, draw.Over) 204 | 205 | png.Encode(w, dst) 206 | } 207 | -------------------------------------------------------------------------------- /png_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | func TestRenderString(t *testing.T) { 11 | i, _ := os.Open("test/vendor.data") 12 | e, _ := ioutil.ReadAll(i) 13 | 14 | r, _ := renderString("Vendor", c) 15 | if !bytes.Equal(r.Pix, e) { 16 | t.Error("make png shield 'use buckler blue' bytes not equal") 17 | } 18 | } 19 | 20 | // simple regression test 21 | func TestMakePngShield(t *testing.T) { 22 | i, _ := os.Open("test/use-buckler-blue.png") 23 | e, _ := ioutil.ReadAll(i) 24 | 25 | var b bytes.Buffer 26 | makePngShield(&b, Data{"use", "buckler", Blue}) 27 | if !bytes.Equal(b.Bytes(), e) { 28 | t.Error("render string 'Vendor' bytes not equal") 29 | } 30 | } 31 | 32 | func BenchmarkRenderString(b *testing.B) { 33 | // c, the freetype context, is set up in png.go's init 34 | for i := 0; i < b.N; i++ { 35 | renderString("test string", c) 36 | } 37 | } 38 | 39 | func BenchmarkMakePngShield(b *testing.B) { 40 | d := Data{"test", "output", Blue} 41 | for i := 0; i < b.N; i++ { 42 | makePngShield(ioutil.Discard, d) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /resources.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "go/build" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | 9 | "bitbucket.org/kardianos/osext" 10 | ) 11 | 12 | // exists returns whether the given file or directory exists or not 13 | func exists(path string) bool { 14 | _, err := os.Stat(path) 15 | if err == nil { 16 | return true 17 | } 18 | if os.IsNotExist(err) { 19 | return false 20 | } 21 | return false 22 | } 23 | 24 | func resourcePaths() (staticPath string, dataPath string) { 25 | base, err := osext.ExecutableFolder() 26 | if err != nil { 27 | log.Fatal("Could not read base dir") 28 | } 29 | 30 | staticPath = filepath.Join(base, "static") 31 | dataPath = filepath.Join(base, "data") 32 | if exists(dataPath) && exists(staticPath) { 33 | return 34 | } 35 | 36 | p, err := build.Default.Import(basePkg, "", build.FindOnly) 37 | if err != nil { 38 | log.Fatal("Could not find package dir") 39 | } 40 | 41 | staticPath = filepath.Join(p.Dir, "static") 42 | dataPath = filepath.Join(p.Dir, "data") 43 | return 44 | } 45 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badges/buckler/b3ebdfc892f052d908c4e0468738389be071b1bd/static/favicon.png -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Buckler 7 | 8 | 9 | 10 | 11 | 12 |
13 | 16 |

17 | 18 | 19 |

20 | Buckler is 21 | Shields as a 22 | Service (ShaaS, or alternatively, Badges as a Service) for use in GitHub 23 | READMEs, or anywhere else. Use buckler with your favorite continuous 24 | integration tool, performance monitoring service API, or ridiculous 25 | in-joke to surface information. 26 |

27 | 28 |
29 |

Try it out

30 |
31 | 32 |     33 | ![play here](http://b.repl.ca/v1/play-here-yellow.png) 34 |

35 |
36 |
37 | 38 | 39 | 51 | 52 | 53 |
54 |
55 | 56 |
57 |

API

58 |
59 |

A full up-to-date description of the API is available in the 60 | README 61 | on GitHub. 62 |

63 |
64 | 65 | 103 | 104 | Fork me on GitHub 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /test/use-buckler-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badges/buckler/b3ebdfc892f052d908c4e0468738389be071b1bd/test/use-buckler-blue.png -------------------------------------------------------------------------------- /test/vendor.data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badges/buckler/b3ebdfc892f052d908c4e0468738389be071b1bd/test/vendor.data --------------------------------------------------------------------------------