Add favicon-gen tool to generate PNG and ICO favicons from SVG sources (#22)

This commit is contained in:
Tobias Gesellchen
2026-03-21 13:18:16 +01:00
committed by GitHub
parent e74d2e0fc3
commit a0833c113c
4 changed files with 173 additions and 1 deletions
+9 -1
View File
@@ -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"
+152
View File
@@ -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
}
+4
View File
@@ -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
)
+8
View File
@@ -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=