mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-24 14:47:23 +00:00
306 lines
7.7 KiB
Go
306 lines
7.7 KiB
Go
// Package main provides a utility to extract icons from Bose-branded TrueType fonts.
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"image/draw"
|
|
"image/png"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
|
|
"github.com/srwiley/rasterx"
|
|
"golang.org/x/image/font"
|
|
"golang.org/x/image/font/sfnt"
|
|
"golang.org/x/image/math/fixed"
|
|
)
|
|
|
|
type IconMapping struct {
|
|
Hex string `json:"hex"`
|
|
GlyphName string `json:"glyph_name"`
|
|
File string `json:"file"`
|
|
SVGFile string `json:"svg_file,omitempty"`
|
|
}
|
|
|
|
func main() {
|
|
fontPath := flag.String("font", "/path/to/bose.ttf", "Path to the TTF font file")
|
|
outputDir := flag.String("output", "extracted_icons", "Output directory for icons")
|
|
imgSize := flag.Int("size", 256, "Size of the PNG icons")
|
|
|
|
flag.Parse()
|
|
|
|
if err := os.MkdirAll(*outputDir, 0755); err != nil {
|
|
log.Fatalf("Failed to create output directory: %v", err)
|
|
}
|
|
|
|
data, err := os.ReadFile(*fontPath)
|
|
if err != nil {
|
|
log.Fatalf("Failed to read font file: %v", err)
|
|
}
|
|
|
|
f, err := sfnt.Parse(data)
|
|
if err != nil {
|
|
log.Fatalf("Failed to parse font: %v", err)
|
|
}
|
|
|
|
var (
|
|
buffer sfnt.Buffer
|
|
glyphIndex sfnt.GlyphIndex
|
|
glyphName string
|
|
segments sfnt.Segments
|
|
pngFile *os.File
|
|
)
|
|
|
|
unitsPerEm := f.UnitsPerEm()
|
|
ppem := fixed.Int26_6(unitsPerEm) << 6
|
|
|
|
m, err := f.Metrics(&buffer, ppem, font.HintingNone)
|
|
if err != nil {
|
|
log.Fatalf("Failed to get metrics: %v", err)
|
|
}
|
|
|
|
mapping := make(map[rune]IconMapping)
|
|
|
|
// Iterate through common ranges
|
|
ranges := []struct{ start, end rune }{
|
|
{0x20, 0x7E}, // Basic Latin
|
|
{0xA0, 0xFF}, // Latin-1 Supplement
|
|
{0xE000, 0xF8FF}, // Private Use Area
|
|
}
|
|
|
|
for _, rg := range ranges {
|
|
for r := rg.start; r <= rg.end; r++ {
|
|
glyphIndex, err = f.GlyphIndex(&buffer, r)
|
|
if err != nil || glyphIndex == 0 {
|
|
continue
|
|
}
|
|
|
|
glyphName, err = f.GlyphName(&buffer, glyphIndex)
|
|
if err != nil {
|
|
glyphName = fmt.Sprintf("uni%04X", r)
|
|
}
|
|
|
|
segments, err = f.LoadGlyph(&buffer, glyphIndex, ppem, nil)
|
|
if err != nil {
|
|
fmt.Printf("Failed to load glyph 0x%04X: %v\n", r, err)
|
|
continue
|
|
}
|
|
|
|
if len(segments) == 0 {
|
|
continue
|
|
}
|
|
|
|
charHex := fmt.Sprintf("%04X", r)
|
|
pngFilename := fmt.Sprintf("icon_%s.png", charHex)
|
|
svgFilename := fmt.Sprintf("icon_%s.svg", charHex)
|
|
|
|
// 1. Extract SVG
|
|
svgPath := segmentsToSVGPath(segments)
|
|
totalHeight := float64(m.Ascent+m.Descent) / 64.0
|
|
svgContent := fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 %g %d %g">
|
|
<g transform="scale(1, -1)">
|
|
<path d="%s" />
|
|
</g>
|
|
</svg>`, -float64(m.Ascent)/64.0, int(unitsPerEm), totalHeight, svgPath)
|
|
|
|
if err = os.WriteFile(filepath.Join(*outputDir, svgFilename), []byte(svgContent), 0644); err != nil {
|
|
fmt.Printf("Failed to write SVG 0x%s: %v\n", charHex, err)
|
|
}
|
|
|
|
// 2. Render PNG
|
|
img := renderGlyphToPNG(segments, int(unitsPerEm), int(m.Ascent), int(m.Descent), *imgSize)
|
|
|
|
pngFile, err = os.Create(filepath.Join(*outputDir, pngFilename))
|
|
if err == nil {
|
|
if err = png.Encode(pngFile, img); err != nil {
|
|
fmt.Printf("Failed to encode PNG 0x%s: %v\n", charHex, err)
|
|
}
|
|
|
|
pngFile.Close()
|
|
} else {
|
|
fmt.Printf("Failed to create PNG file 0x%s: %v\n", charHex, err)
|
|
}
|
|
|
|
mapping[r] = IconMapping{
|
|
Hex: fmt.Sprintf("0x%s", charHex),
|
|
GlyphName: glyphName,
|
|
File: pngFilename,
|
|
SVGFile: svgFilename,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save mapping.json
|
|
mappingList := make(map[string]IconMapping)
|
|
|
|
var keys []int
|
|
|
|
for r, m := range mapping {
|
|
mappingList[fmt.Sprintf("%d", r)] = m
|
|
keys = append(keys, int(r))
|
|
}
|
|
|
|
sort.Ints(keys)
|
|
|
|
jsonData, err := json.MarshalIndent(mappingList, "", " ")
|
|
if err != nil {
|
|
log.Fatalf("Failed to marshal mapping: %v", err)
|
|
}
|
|
|
|
_ = os.WriteFile(filepath.Join(*outputDir, "mapping.json"), jsonData, 0644)
|
|
|
|
// Save mapping.md
|
|
mdFile, _ := os.Create(filepath.Join(*outputDir, "mapping.md"))
|
|
fmt.Fprintln(mdFile, "# Bose Icons Mapping")
|
|
fmt.Fprintln(mdFile, "")
|
|
fmt.Fprintln(mdFile, "| Char Code | Glyph Name | PNG | SVG |")
|
|
fmt.Fprintln(mdFile, "| --- | --- | --- | --- |")
|
|
|
|
for _, k := range keys {
|
|
m := mapping[rune(k)]
|
|
fmt.Fprintf(mdFile, "| %s | %s |  | [SVG](%s) |\n", m.Hex, m.GlyphName, m.GlyphName, m.File, m.SVGFile)
|
|
}
|
|
|
|
mdFile.Close()
|
|
|
|
fmt.Printf("Extracted %d icons to %s\n", len(mapping), *outputDir)
|
|
}
|
|
|
|
func segmentsToSVGPath(segments sfnt.Segments) string {
|
|
var path string
|
|
|
|
for _, seg := range segments {
|
|
switch seg.Op {
|
|
case sfnt.SegmentOpMoveTo:
|
|
path += fmt.Sprintf("M%g %g ", float64(seg.Args[0].X)/64.0, -float64(seg.Args[0].Y)/64.0)
|
|
case sfnt.SegmentOpLineTo:
|
|
path += fmt.Sprintf("L%g %g ", float64(seg.Args[0].X)/64.0, -float64(seg.Args[0].Y)/64.0)
|
|
case sfnt.SegmentOpQuadTo:
|
|
path += fmt.Sprintf("Q%g %g %g %g ", float64(seg.Args[0].X)/64.0, -float64(seg.Args[0].Y)/64.0, float64(seg.Args[1].X)/64.0, -float64(seg.Args[1].Y)/64.0)
|
|
case sfnt.SegmentOpCubeTo:
|
|
path += fmt.Sprintf("C%g %g %g %g %g %g ", float64(seg.Args[0].X)/64.0, -float64(seg.Args[0].Y)/64.0, float64(seg.Args[1].X)/64.0, -float64(seg.Args[1].Y)/64.0, float64(seg.Args[2].X)/64.0, -float64(seg.Args[2].Y)/64.0)
|
|
}
|
|
}
|
|
|
|
return path
|
|
}
|
|
|
|
func renderGlyphToPNG(segments sfnt.Segments, _, _, _, imgSize int) image.Image {
|
|
rgba := image.NewRGBA(image.Rect(0, 0, imgSize, imgSize))
|
|
draw.Draw(rgba, rgba.Bounds(), image.Transparent, image.Point{}, draw.Src)
|
|
|
|
// Calculate glyph bounds
|
|
var xmin, ymin, xmax, ymax float64
|
|
|
|
initialized := false
|
|
|
|
for _, seg := range segments {
|
|
for _, arg := range seg.Args {
|
|
x, y := float64(arg.X)/64.0, float64(arg.Y)/64.0
|
|
if !initialized {
|
|
xmin, xmax = x, x
|
|
ymin, ymax = y, y
|
|
initialized = true
|
|
} else {
|
|
if x < xmin {
|
|
xmin = x
|
|
}
|
|
|
|
if x > xmax {
|
|
xmax = x
|
|
}
|
|
|
|
if y < ymin {
|
|
ymin = y
|
|
}
|
|
|
|
if y > ymax {
|
|
ymax = y
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
w := xmax - xmin
|
|
h := ymax - ymin
|
|
|
|
// If no width/height, return empty image
|
|
if w <= 0 || h <= 0 {
|
|
return rgba
|
|
}
|
|
|
|
// Calculate scale to fit in imgSize with padding
|
|
padding := 20.0
|
|
available := float64(imgSize) - 2*padding
|
|
|
|
scale := available / w
|
|
if h*scale > available {
|
|
scale = available / h
|
|
}
|
|
|
|
// Center the glyph
|
|
// X: center of image (imgSize/2) - (center of glyph (xmin+xmax)/2) * scale
|
|
offsetX := float64(imgSize)/2.0 - (xmin+xmax)/2.0*scale
|
|
// Y: center of image (imgSize/2) - (center of glyph (ymin+ymax)/2) * scale
|
|
offsetY := float64(imgSize)/2.0 - (ymin+ymax)/2.0*scale
|
|
|
|
scanner := rasterx.NewScannerGV(imgSize, imgSize, rgba, rgba.Bounds())
|
|
filler := rasterx.NewFiller(imgSize, imgSize, scanner)
|
|
filler.SetColor(color.Black)
|
|
|
|
for _, seg := range segments {
|
|
switch seg.Op {
|
|
case sfnt.SegmentOpMoveTo:
|
|
filler.Start(fixedP(
|
|
offsetX+float64(seg.Args[0].X)/64.0*scale,
|
|
offsetY+float64(seg.Args[0].Y)/64.0*scale,
|
|
))
|
|
case sfnt.SegmentOpLineTo:
|
|
filler.Line(fixedP(
|
|
offsetX+float64(seg.Args[0].X)/64.0*scale,
|
|
offsetY+float64(seg.Args[0].Y)/64.0*scale,
|
|
))
|
|
case sfnt.SegmentOpQuadTo:
|
|
filler.QuadBezier(
|
|
fixedP(
|
|
offsetX+float64(seg.Args[0].X)/64.0*scale,
|
|
offsetY+float64(seg.Args[0].Y)/64.0*scale,
|
|
),
|
|
fixedP(
|
|
offsetX+float64(seg.Args[1].X)/64.0*scale,
|
|
offsetY+float64(seg.Args[1].Y)/64.0*scale,
|
|
),
|
|
)
|
|
case sfnt.SegmentOpCubeTo:
|
|
filler.CubeBezier(
|
|
fixedP(
|
|
offsetX+float64(seg.Args[0].X)/64.0*scale,
|
|
offsetY+float64(seg.Args[0].Y)/64.0*scale,
|
|
),
|
|
fixedP(
|
|
offsetX+float64(seg.Args[1].X)/64.0*scale,
|
|
offsetY+float64(seg.Args[1].Y)/64.0*scale,
|
|
),
|
|
fixedP(
|
|
offsetX+float64(seg.Args[2].X)/64.0*scale,
|
|
offsetY+float64(seg.Args[2].Y)/64.0*scale,
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
filler.Stop(true)
|
|
filler.Draw()
|
|
|
|
return rgba
|
|
}
|
|
|
|
func fixedP(x, y float64) fixed.Point26_6 {
|
|
return fixed.Point26_6{X: fixed.Int26_6(x * 64), Y: fixed.Int26_6(y * 64)}
|
|
}
|