diff --git a/Makefile b/Makefile index e13a27c..ab40c99 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,8 @@ EXAMPLE_UPNP_NAME=example-upnp EXAMPLE_UPNP_PATH=./cmd/$(EXAMPLE_UPNP_NAME) SCANNER_NAME=mdns-scanner SCANNER_PATH=./cmd/$(SCANNER_NAME) +FAVICON_GEN_NAME=favicon-gen +FAVICON_GEN_PATH=./cmd/$(FAVICON_GEN_NAME) BUILD_DIR=./build # Version info @@ -27,7 +29,7 @@ BUILD_DIR=./build all: check build -build: build-cli build-service build-examples +build: build-cli build-service build-examples build-favicon-gen build-cli: @echo "Building $(BINARY_NAME)..." @@ -48,6 +50,11 @@ build-examples: @echo "Building $(SCANNER_NAME)..." $(GOBUILD) -o $(BUILD_DIR)/$(SCANNER_NAME) $(SCANNER_PATH) +build-favicon-gen: + @echo "Building $(FAVICON_GEN_NAME)..." + @mkdir -p $(BUILD_DIR) + $(GOBUILD) -o $(BUILD_DIR)/$(FAVICON_GEN_NAME) $(FAVICON_GEN_PATH) + build-all: build-linux build-darwin build-windows build-examples-all build-linux: @@ -226,6 +233,7 @@ help: @echo " build - Build the CLI tool, service, and examples" @echo " build-cli - Build only the CLI tool" @echo " build-service - Build only the service" + @echo " build-favicon-gen - Build the favicon generator" @echo " build-examples - Build only the example programs" @echo " build-all - Build for all platforms" @echo " test - Run tests" diff --git a/cmd/favicon-gen/main.go b/cmd/favicon-gen/main.go new file mode 100644 index 0000000..834519a --- /dev/null +++ b/cmd/favicon-gen/main.go @@ -0,0 +1,152 @@ +// Package main provides a utility to generate PNG and ICO favicons from SVG source files. +package main + +import ( + "bufio" + "bytes" + "encoding/binary" + "fmt" + "image" + "image/png" + "log" + "os" + "path/filepath" + + "github.com/srwiley/oksvg" + "github.com/srwiley/rasterx" +) + +func main() { + mediaDir := "pkg/service/handlers/web/img" + files := []string{"favicon-braille", "favicon-morse"} + + for _, name := range files { + svgPath := filepath.Join(mediaDir, name+".svg") + pngPath := filepath.Join(mediaDir, name+".png") + icoPath := filepath.Join(mediaDir, name+".ico") + + fmt.Printf("Processing %s...\n", name) + + // 1. Render SVG to PNG + img, err := renderSVG(svgPath, 32, 32) + if err != nil { + log.Fatalf("Failed to render %s: %v", svgPath, err) + } + + f, err := os.Create(pngPath) + if err != nil { + log.Fatalf("Failed to create %s: %v", pngPath, err) + } + + if err := png.Encode(f, img); err != nil { + f.Close() + log.Fatalf("Failed to encode PNG %s: %v", pngPath, err) + } + + f.Close() + fmt.Printf("Created %s\n", pngPath) + + // 2. Create ICO (containing multiple sizes) + sizes := []int{16, 32, 48} + + var images []image.Image + + for _, s := range sizes { + m, err := renderSVG(svgPath, s, s) + if err != nil { + log.Fatalf("Failed to render %s at size %d: %v", svgPath, s, err) + } + + images = append(images, m) + } + + if err := writeICO(icoPath, images); err != nil { + log.Fatalf("Failed to write ICO %s: %v", icoPath, err) + } + + fmt.Printf("Created %s\n", icoPath) + } +} + +func renderSVG(path string, w, h int) (image.Image, error) { + in, err := os.Open(path) + if err != nil { + return nil, err + } + defer in.Close() + + icon, err := oksvg.ReadIconStream(in) + if err != nil { + return nil, err + } + + icon.SetTarget(0, 0, float64(w), float64(h)) + rgba := image.NewRGBA(image.Rect(0, 0, w, h)) + gv := rasterx.NewScannerGV(w, h, rgba, rgba.Bounds()) + dasher := rasterx.NewDasher(w, h, gv) + icon.Draw(dasher, 1.0) + + return rgba, nil +} + +// Simple ICO encoder that wraps PNGs +func writeICO(path string, images []image.Image) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + + bw := bufio.NewWriter(f) + defer bw.Flush() + + // ICONDIR header + // Reserved (2), Type (2), Count (2) + binary.Write(bw, binary.LittleEndian, uint16(0)) + binary.Write(bw, binary.LittleEndian, uint16(1)) // 1 = ICO + binary.Write(bw, binary.LittleEndian, uint16(len(images))) + + var pngData [][]byte + + for _, img := range images { + var buf bytes.Buffer + if err := png.Encode(&buf, img); err != nil { + return err + } + + pngData = append(pngData, buf.Bytes()) + } + + offset := uint32(6 + len(images)*16) + for i, img := range images { + b := img.Bounds() + + width := uint8(b.Dx()) + if b.Dx() >= 256 { + width = 0 + } + + height := uint8(b.Dy()) + if b.Dy() >= 256 { + height = 0 + } + + // ICONDIRENTRY + bw.WriteByte(width) + bw.WriteByte(height) + bw.WriteByte(0) // Color count + bw.WriteByte(0) // Reserved + binary.Write(bw, binary.LittleEndian, uint16(1)) // Planes (1) + binary.Write(bw, binary.LittleEndian, uint16(32)) // Bits per pixel (32) + binary.Write(bw, binary.LittleEndian, uint32(len(pngData[i]))) + binary.Write(bw, binary.LittleEndian, offset) + + offset += uint32(len(pngData[i])) + } + + for _, data := range pngData { + bw.Write(data) + } + + return nil +} diff --git a/go.mod b/go.mod index e952621..86e005c 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,8 @@ require ( github.com/hashicorp/mdns v1.0.6 github.com/miekg/dns v1.1.72 github.com/russross/blackfriday/v2 v2.1.0 + github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c + github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef github.com/urfave/cli/v2 v2.27.7 golang.org/x/crypto v0.49.0 ) @@ -15,9 +17,11 @@ require ( require ( github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect + golang.org/x/image v0.37.0 // indirect golang.org/x/mod v0.34.0 // indirect golang.org/x/net v0.52.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect golang.org/x/tools v0.43.0 // indirect ) diff --git a/go.sum b/go.sum index 4ffded9..fa47316 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,10 @@ github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE= +github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= +github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ= +github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg= @@ -26,6 +30,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA= +golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -91,6 +97,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=