mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
Add favicon-gen tool to generate PNG and ICO favicons from SVG sources (#22)
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
Reference in New Issue
Block a user