This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / cmd / dolly / main.go
5.0 kB 203 lines
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}