This repository has no description
1package main
2
3import (
4 "bytes"
5 _ "embed"
6 "flag"
7 "fmt"
8 "image"
9 "image/color"
10 "image/png"
11 "os"
12 "path/filepath"
13 "strconv"
14 "strings"
15 "text/template"
16
17 "github.com/srwiley/oksvg"
18 "github.com/srwiley/rasterx"
19 "golang.org/x/image/draw"
20 "tangled.org/core/ico"
21)
22
23func main() {
24 var (
25 size string
26 fillColor string
27 output string
28 templatePath string
29 favicon bool
30 )
31
32 flag.StringVar(&templatePath, "template", "", "Path to dolly go-html template")
33 flag.StringVar(&size, "size", "512x512", "Output size in format WIDTHxHEIGHT (e.g., 512x512)")
34 flag.StringVar(&fillColor, "color", "#000000", "Fill color in hex format (e.g., #FF5733)")
35 flag.StringVar(&output, "output", "dolly.svg", "Output file path (format detected from extension: .svg, .png, or .ico)")
36 flag.BoolVar(&favicon, "favicon", false, "Embed a prefers-color-scheme style block so the SVG reacts to dark mode (SVG output only)")
37 flag.Parse()
38
39 if templatePath == "" {
40 fmt.Fprintf(os.Stderr, "Empty template path")
41 os.Exit(1)
42 }
43
44 width, height, err := parseSize(size)
45 if err != nil {
46 fmt.Fprintf(os.Stderr, "Error parsing size: %v\n", err)
47 os.Exit(1)
48 }
49
50 // Detect format from file extension
51 ext := strings.ToLower(filepath.Ext(output))
52 format := strings.TrimPrefix(ext, ".")
53
54 if format != "svg" && format != "png" && format != "ico" {
55 fmt.Fprintf(os.Stderr, "Invalid file extension: %s. Must be .svg, .png, or .ico\n", ext)
56 os.Exit(1)
57 }
58
59 if fillColor != "currentColor" && !isValidHexColor(fillColor) {
60 fmt.Fprintf(os.Stderr, "Invalid color format: %s. Use hex format like #FF5733\n", fillColor)
61 os.Exit(1)
62 }
63
64 tpl, err := os.ReadFile(templatePath)
65 if err != nil {
66 fmt.Fprintf(os.Stderr, "Failed to read template from path %s: %v\n", templatePath, err)
67 os.Exit(1)
68 }
69
70 if favicon && format != "svg" {
71 fmt.Fprintf(os.Stderr, "-favicon is only supported for .svg output\n")
72 os.Exit(1)
73 }
74
75 svgData, err := dolly(string(tpl), fillColor, favicon)
76 if err != nil {
77 fmt.Fprintf(os.Stderr, "Error generating SVG: %v\n", err)
78 os.Exit(1)
79 }
80
81 // Create output directory if it doesn't exist
82 dir := filepath.Dir(output)
83 if dir != "" && dir != "." {
84 if err := os.MkdirAll(dir, 0755); err != nil {
85 fmt.Fprintf(os.Stderr, "Error creating output directory: %v\n", err)
86 os.Exit(1)
87 }
88 }
89
90 switch format {
91 case "svg":
92 err = saveSVG(svgData, output, width, height)
93 case "png":
94 err = savePNG(svgData, output, width, height)
95 case "ico":
96 err = saveICO(svgData, output, width, height)
97 }
98
99 if err != nil {
100 fmt.Fprintf(os.Stderr, "Error saving file: %v\n", err)
101 os.Exit(1)
102 }
103
104 fmt.Printf("Successfully generated %s (%dx%d)\n", output, width, height)
105}
106
107func dolly(tplString, hexColor string, favicon bool) ([]byte, error) {
108 tpl, err := template.New("dolly").Parse(tplString)
109 if err != nil {
110 return nil, err
111 }
112
113 var svgData bytes.Buffer
114 if err := tpl.ExecuteTemplate(&svgData, "fragments/dolly/logo", map[string]any{
115 "FillColor": hexColor,
116 "Classes": "",
117 "Favicon": favicon,
118 }); err != nil {
119 return nil, err
120 }
121
122 return svgData.Bytes(), nil
123}
124
125func svgToImage(svgData []byte, w, h int) (image.Image, error) {
126 icon, err := oksvg.ReadIconStream(bytes.NewReader(svgData))
127 if err != nil {
128 return nil, fmt.Errorf("error parsing SVG: %v", err)
129 }
130
131 icon.SetTarget(0, 0, float64(w), float64(h))
132 rgba := image.NewRGBA(image.Rect(0, 0, w, h))
133 draw.Draw(rgba, rgba.Bounds(), &image.Uniform{color.Transparent}, image.Point{}, draw.Src)
134 scanner := rasterx.NewScannerGV(w, h, rgba, rgba.Bounds())
135 raster := rasterx.NewDasher(w, h, scanner)
136 icon.Draw(raster, 1.0)
137
138 return rgba, nil
139}
140
141func parseSize(size string) (int, int, error) {
142 parts := strings.Split(size, "x")
143 if len(parts) != 2 {
144 return 0, 0, fmt.Errorf("invalid size format, use WIDTHxHEIGHT")
145 }
146
147 width, err := strconv.Atoi(parts[0])
148 if err != nil {
149 return 0, 0, fmt.Errorf("invalid width: %v", err)
150 }
151
152 height, err := strconv.Atoi(parts[1])
153 if err != nil {
154 return 0, 0, fmt.Errorf("invalid height: %v", err)
155 }
156
157 if width <= 0 || height <= 0 {
158 return 0, 0, fmt.Errorf("width and height must be positive")
159 }
160
161 return width, height, nil
162}
163
164func isValidHexColor(hex string) bool {
165 if len(hex) != 7 || hex[0] != '#' {
166 return false
167 }
168 _, err := strconv.ParseUint(hex[1:], 16, 32)
169 return err == nil
170}
171
172func saveSVG(svgData []byte, filepath string, _, _ int) error {
173 return os.WriteFile(filepath, svgData, 0644)
174}
175
176func savePNG(svgData []byte, filepath string, width, height int) error {
177 img, err := svgToImage(svgData, width, height)
178 if err != nil {
179 return err
180 }
181
182 f, err := os.Create(filepath)
183 if err != nil {
184 return err
185 }
186 defer f.Close()
187
188 return png.Encode(f, img)
189}
190
191func saveICO(svgData []byte, filepath string, width, height int) error {
192 img, err := svgToImage(svgData, width, height)
193 if err != nil {
194 return err
195 }
196
197 icoData, err := ico.ImageToIco(img)
198 if err != nil {
199 return err
200 }
201
202 return os.WriteFile(filepath, icoData, 0644)
203}