├── LICENSE.md ├── README.md ├── assets └── syumai.gif ├── cmd └── syumaigen │ └── main.go ├── colormap.go ├── example └── server │ └── main.go ├── go.mod ├── go.sum ├── image.go ├── pattern.go ├── pattern_test.go └── svg.go /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2019-present [syumai](https://github.com/syumai/) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # syumaigen 2 | 3 | ![syumai.gif](assets/syumai.gif) 4 | 5 | * A CLI tool to generate syumai's avatar image. 6 | * The avatar used in this command was designed by [@tanakaworld](https://github.com/tanakaworld). 7 | 8 | ## Features 9 | 10 | * Change scale of avatar image 11 | * Randomize color generation 12 | * Generate PNG / SVG 13 | * Generate animated GIF 14 | 15 | ## Installation 16 | 17 | ```console 18 | go install github.com/syumai/syumaigen/cmd/syumaigen@latest 19 | ``` 20 | 21 | ## Usage 22 | 23 | ```sh 24 | # Generate image (default: PNG) 25 | syumaigen > syumai.png 26 | 27 | # Show usage 28 | syumaigen -help 29 | 30 | # Upscale (default: 10) and stop randomize color generation (default: true) 31 | syumaigen -scale=100 -random=false > syumai.png 32 | 33 | # generate animated GIF 34 | syumaigen -animated > syumai.gif 35 | 36 | # generate SVG 37 | syumaigen -svg > syumai.svg 38 | ``` 39 | 40 | ### Example HTTP Server 41 | 42 | * `go run example/server/main.go` 43 | * This generates random avator image. 44 | 45 | ## License 46 | 47 | MIT 48 | 49 | ## Author 50 | 51 | syumai 52 | -------------------------------------------------------------------------------- /assets/syumai.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syumai/syumaigen/a55a3f76f8b4211e05e8b9000ccc3d2a7d5dcc5e/assets/syumai.gif -------------------------------------------------------------------------------- /cmd/syumaigen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "image/color" 6 | "image/gif" 7 | "image/png" 8 | "io" 9 | "log" 10 | "os" 11 | "strings" 12 | 13 | "github.com/lucasb-eyer/go-colorful" 14 | "github.com/syumai/syumaigen" 15 | ) 16 | 17 | var ( 18 | scale = flag.Int("scale", 10, "specify image scale") 19 | code = flag.String("code", "", "use color code") 20 | bgCode = flag.String("bgcode", "", "use background color code") 21 | norandom = flag.Bool("norandom", false, "stop randomize color generation") 22 | animated = flag.Bool("animated", false, "generate animated GIF") 23 | outputSVG = flag.Bool("svg", false, "generate SVG") 24 | ) 25 | 26 | func main() { 27 | flag.Parse() 28 | 29 | if *animated { 30 | img, err := syumaigen.GenerateAnimatedSyumaiGIF(*scale) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | if err := gif.EncodeAll(os.Stdout, img); err != nil { 35 | log.Fatal(err) 36 | } 37 | return 38 | } 39 | 40 | colorMap := syumaigen.DefaultColorMap 41 | if *code != "" { 42 | colorMap = syumaigen.GenerateColorMapByColorCode(*code) 43 | } else if !*norandom { 44 | colorMap = syumaigen.GenerateRandomColorMap() 45 | } 46 | 47 | if *bgCode != "" { 48 | c, err := parseColorCode(*bgCode) 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | colorMap[0] = c 53 | } 54 | 55 | if *outputSVG { 56 | img, err := syumaigen.GenerateSVG( 57 | syumaigen.Pattern, 58 | colorMap, 59 | *scale, 60 | ) 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | if _, err := io.Copy(os.Stdout, img); err != nil { 65 | log.Fatal(err) 66 | } 67 | return 68 | } 69 | 70 | img, err := syumaigen.GenerateImage( 71 | syumaigen.Pattern, 72 | colorMap, 73 | *scale, 74 | ) 75 | if err != nil { 76 | log.Fatal(err) 77 | } 78 | if err := png.Encode(os.Stdout, img); err != nil { 79 | log.Fatal(err) 80 | } 81 | } 82 | 83 | func parseColorCode(cc string) (color.Color, error) { 84 | if !strings.HasPrefix(cc, "#") { 85 | cc = "#" + cc 86 | } 87 | col, err := colorful.Hex(cc) 88 | if err != nil { 89 | return nil, err 90 | } 91 | h, c, l := col.Hcl() 92 | return colorful.Hcl(h, c, l).Clamped(), nil 93 | } 94 | -------------------------------------------------------------------------------- /colormap.go: -------------------------------------------------------------------------------- 1 | package syumaigen 2 | 3 | import ( 4 | "image/color" 5 | "math/rand" 6 | "strings" 7 | "time" 8 | 9 | colorful "github.com/lucasb-eyer/go-colorful" 10 | ) 11 | 12 | type ColorMap map[int]color.Color 13 | 14 | var DefaultColorMap = ColorMap{ 15 | 0: color.Transparent, 16 | 1: color.Black, 17 | 2: color.RGBA{66, 66, 66, 255}, 18 | 3: color.RGBA{255, 255, 240, 255}, 19 | 4: color.RGBA{222, 222, 203, 255}, 20 | 5: color.RGBA{255, 121, 0, 255}, 21 | 6: color.RGBA{28, 214, 1, 255}, 22 | 7: color.RGBA{25, 179, 3, 255}, 23 | 8: color.RGBA{126, 214, 113, 255}, 24 | 9: color.RGBA{191, 214, 188, 255}, 25 | } 26 | 27 | func GenerateRandomColorMap() ColorMap { 28 | rand.Seed(time.Now().UnixNano()) 29 | h := rand.Float64() * 360.0 30 | c := 0.4 + rand.Float64()*0.6 31 | return GenerateColorMapByHCL(h, c) 32 | } 33 | 34 | func GenerateColorMapByHCL(h float64, c float64) ColorMap { 35 | return ColorMap{ 36 | 0: DefaultColorMap[0], 37 | 1: DefaultColorMap[1], 38 | 2: DefaultColorMap[2], 39 | 3: DefaultColorMap[3], 40 | 4: DefaultColorMap[4], 41 | 5: DefaultColorMap[5], 42 | 6: colorful.Hcl(h, c, 0.5).Clamped(), 43 | 7: colorful.Hcl(h, c, 0.3).Clamped(), 44 | 8: colorful.Hcl(h, c, 0.7).Clamped(), 45 | 9: colorful.Hcl(h, c, 0.9).Clamped(), 46 | } 47 | } 48 | 49 | func GenerateColorMapByColorCode(cc string) ColorMap { 50 | if !strings.HasPrefix(cc, "#") { 51 | cc = "#" + cc 52 | } 53 | col, err := colorful.Hex(cc) 54 | if err != nil { 55 | return DefaultColorMap 56 | } 57 | h, c, l := col.Hcl() 58 | return ColorMap{ 59 | 0: DefaultColorMap[0], 60 | 1: DefaultColorMap[1], 61 | 2: DefaultColorMap[2], 62 | 3: DefaultColorMap[3], 63 | 4: DefaultColorMap[4], 64 | 5: DefaultColorMap[5], 65 | 6: colorful.Hcl(h, c, l).Clamped(), 66 | 7: colorful.Hcl(h, c, l*0.8).Clamped(), 67 | 8: colorful.Hcl(h, c, 1.0-(1.0-l)*0.8).Clamped(), 68 | 9: colorful.Hcl(h, c, 1.0-(1.0-l)*0.2).Clamped(), 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /example/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "image/png" 7 | "io" 8 | "log" 9 | "net/http" 10 | 11 | "github.com/syumai/syumaigen" 12 | ) 13 | 14 | func main() { 15 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 16 | w.Header().Set("Content-Type", "image/png") 17 | img, err := syumaigen.GenerateImage( 18 | syumaigen.Pattern, 19 | syumaigen.GenerateRandomColorMap(), 20 | 10, 21 | ) 22 | if err != nil { 23 | w.Header().Set("Content-Type", "text/plain") 24 | w.WriteHeader(http.StatusInternalServerError) 25 | fmt.Fprintf(w, "Internal Server Error") 26 | return 27 | } 28 | var buf bytes.Buffer 29 | err = png.Encode(&buf, img) 30 | if err != nil { 31 | w.Header().Set("Content-Type", "text/plain") 32 | w.WriteHeader(http.StatusInternalServerError) 33 | fmt.Fprintf(w, "Internal Server Error") 34 | return 35 | } 36 | w.WriteHeader(http.StatusOK) 37 | if _, err := io.Copy(w, &buf); err != nil { 38 | log.Fatal(err) 39 | } 40 | }) 41 | port := "8080" 42 | fmt.Printf("listening on http://localhost:%s\n", port) 43 | log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil)) 44 | } 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/syumai/syumaigen 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/ajstarks/svgo v0.0.0-20200725142600-7a3c8b57fecb 7 | github.com/google/go-cmp v0.3.1 8 | github.com/lucasb-eyer/go-colorful v1.0.3 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/ajstarks/svgo v0.0.0-20200725142600-7a3c8b57fecb h1:EVl3FJLQCzSbgBezKo/1A4ADnJ4mtJZ0RvnNzDJ44nY= 2 | github.com/ajstarks/svgo v0.0.0-20200725142600-7a3c8b57fecb/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= 3 | github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= 4 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 5 | github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= 6 | github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 7 | -------------------------------------------------------------------------------- /image.go: -------------------------------------------------------------------------------- 1 | package syumaigen 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/color" 7 | "image/color/palette" 8 | "image/draw" 9 | "image/gif" 10 | ) 11 | 12 | var transparentPalette []color.Color 13 | 14 | func init() { 15 | transparentPalette = append(transparentPalette, palette.WebSafe...) 16 | transparentPalette = append(transparentPalette, color.RGBA{0, 0, 0, 0}) 17 | } 18 | 19 | func assertData(data [][]int) error { 20 | if data == nil || len(data) == 0 { 21 | return fmt.Errorf("data is blank") 22 | } 23 | lineLen := len(data[0]) 24 | for _, line := range data { 25 | if lineLen != len(line) { 26 | return fmt.Errorf("line length is not equal, want: %d, got: %d", lineLen, len(line)) 27 | } 28 | } 29 | return nil 30 | } 31 | 32 | func GenerateImage(data [][]int, cmap ColorMap, scale int) (image.Image, error) { 33 | if err := assertData(data); err != nil { 34 | return nil, err 35 | } 36 | if scale < 1 { 37 | return nil, fmt.Errorf("scale must be >= 1") 38 | } 39 | width := len(data[0]) * scale 40 | height := len(data) * scale 41 | img := image.NewRGBA(image.Rect(0, 0, width, height)) 42 | for i, line := range data { 43 | for j, n := range line { 44 | c, ok := cmap[n] 45 | if !ok { 46 | return nil, fmt.Errorf("color not found: %d", n) 47 | } 48 | for xs := 0; xs < scale; xs++ { 49 | for ys := 0; ys < scale; ys++ { 50 | img.Set(j*scale+ys, i*scale+xs, c) 51 | } 52 | } 53 | } 54 | } 55 | return img, nil 56 | } 57 | 58 | func GenerateAnimatedSyumaiGIF(scale int) (*gif.GIF, error) { 59 | g := &gif.GIF{} 60 | frames := 30 61 | for i := 0; i < frames; i++ { 62 | h := float64(i) / float64(frames) * 360.0 63 | img, err := GenerateImage( 64 | Pattern, 65 | GenerateColorMapByHCL(h, 0.95), 66 | scale, 67 | ) 68 | if err != nil { 69 | return nil, err 70 | } 71 | palettedImg := image.NewPaletted(img.Bounds(), transparentPalette) 72 | draw.FloydSteinberg.Draw(palettedImg, img.Bounds(), img, image.ZP) 73 | g.Image = append(g.Image, palettedImg) 74 | g.Delay = append(g.Delay, 10) 75 | } 76 | return g, nil 77 | } 78 | -------------------------------------------------------------------------------- /pattern.go: -------------------------------------------------------------------------------- 1 | package syumaigen 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | const patternStr = `00000000000000000000 8 | 00000000666600000000 9 | 00000006688670000000 10 | 00002266998667220000 11 | 00023366998667432000 12 | 00233366888667433200 13 | 00233336666674333200 14 | 02233333666743333220 15 | 02322333344433322420 16 | 02333222222222233420 17 | 02333333333333333420 18 | 02333313333313333420 19 | 02333313333313333420 20 | 02333333333333333420 21 | 02333355555553333420 22 | 02333335555533333420 23 | 02333333333333334420 24 | 00223333333334442200 25 | 00002222222222220000 26 | 00000000000000000000` 27 | 28 | var Pattern [][]int 29 | 30 | func init() { 31 | Pattern = parsePattern(patternStr) 32 | } 33 | 34 | func parsePattern(s string) [][]int { 35 | lines := strings.Split(s, "\n") 36 | p := make([][]int, len(lines)) 37 | for i, line := range lines { 38 | p[i] = make([]int, len(line)) 39 | for j, r := range line { 40 | p[i][j] = int(r - '0') 41 | } 42 | } 43 | return p 44 | } 45 | -------------------------------------------------------------------------------- /pattern_test.go: -------------------------------------------------------------------------------- 1 | package syumaigen 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | ) 8 | 9 | func Test_parsePattern(t *testing.T) { 10 | const value = "012\n345" 11 | want := [][]int{{0, 1, 2}, {3, 4, 5}} 12 | got := parsePattern(value) 13 | if d := cmp.Diff(want, got); d != "" { 14 | t.Errorf(d) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /svg.go: -------------------------------------------------------------------------------- 1 | package syumaigen 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | 8 | svg "github.com/ajstarks/svgo" 9 | "github.com/lucasb-eyer/go-colorful" 10 | ) 11 | 12 | func GenerateSVG(data [][]int, cmap ColorMap, scale int) (io.Reader, error) { 13 | if err := assertData(data); err != nil { 14 | return nil, err 15 | } 16 | if scale < 1 { 17 | return nil, fmt.Errorf("scale must be >= 1") 18 | } 19 | width := len(data[0]) * scale 20 | height := len(data) * scale 21 | 22 | var buf bytes.Buffer 23 | canvas := svg.New(&buf) 24 | canvas.Start(width, height) 25 | 26 | for i, line := range data { 27 | for j, n := range line { 28 | c, ok := cmap[n] 29 | if !ok { 30 | return nil, fmt.Errorf("color not found: %d", n) 31 | } 32 | cc, ok := colorful.MakeColor(c) 33 | if !ok { 34 | continue 35 | } 36 | canvas.Square(j*scale, i*scale, scale, fmt.Sprintf("fill: %s", cc.Hex())) 37 | } 38 | } 39 | canvas.End() 40 | return &buf, nil 41 | } 42 | --------------------------------------------------------------------------------