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