208 lines
6.4 KiB
Go
208 lines
6.4 KiB
Go
// logogen converts an image to a .bin file suitable for embedding in badge firmware.
|
|
//
|
|
// Usage:
|
|
//
|
|
// logogen -size <target|WxH> -image <path|shortname> [-output logo.bin] [-images-dir images] [-cache-dir cache]
|
|
//
|
|
// Examples:
|
|
//
|
|
// logogen -size nicebadge -image tinygo
|
|
// logogen -size gopherbadge -image gopherconeu
|
|
// logogen -size 320x240 -image /path/to/logo.jpg
|
|
// logogen -size gobadge -image ./my-logo.png -output logo.bin
|
|
//
|
|
// Short names resolve to image files inside -images-dir.
|
|
// If a predefined resized variant exists (e.g. tinygo_320x240.png), it is used directly.
|
|
// Generated .bin files are cached in -cache-dir to avoid reprocessing.
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"image"
|
|
_ "image/jpeg"
|
|
_ "image/png"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
draw2 "golang.org/x/image/draw"
|
|
)
|
|
|
|
type resolution struct {
|
|
width, height int
|
|
}
|
|
|
|
func (r resolution) String() string {
|
|
return fmt.Sprintf("%dx%d", r.width, r.height)
|
|
}
|
|
|
|
var predefinedTargets = map[string]resolution{
|
|
"nicebadge": {240, 135},
|
|
"gopherbadge": {320, 240},
|
|
"gobadge": {160, 128},
|
|
"pybadge": {160, 128},
|
|
}
|
|
|
|
func main() {
|
|
size := flag.String("size", "", "Resolution: WxH or target name (nicebadge, gopherbadge, gobadge, pybadge)")
|
|
imageArg := flag.String("image", "", "Image: file path (relative/absolute) or short name (tinygo, gopherconeu, golab, ...)")
|
|
output := flag.String("output", "logo.bin", "Output .bin file path")
|
|
imagesDir := flag.String("images-dir", "images", "Directory with source images and predefined variants")
|
|
cacheDir := flag.String("cache-dir", "cache", "Directory for cached .bin files")
|
|
flag.Parse()
|
|
|
|
if *size == "" {
|
|
flag.Usage()
|
|
log.Fatal("\nMust specify -size (e.g. -size nicebadge or -size 320x240)")
|
|
}
|
|
if *imageArg == "" {
|
|
flag.Usage()
|
|
log.Fatal("\nMust specify -image (file path or short name)")
|
|
}
|
|
|
|
res := parseSize(*size)
|
|
|
|
// Determine whether -image is a file path or a short name.
|
|
// Heuristic: contains path separator, starts with '.' or '..' , or has a file extension → path.
|
|
isPath := strings.ContainsAny(*imageArg, "/\\") ||
|
|
strings.HasPrefix(*imageArg, ".") ||
|
|
filepath.Ext(*imageArg) != ""
|
|
|
|
var baseName string // used as cache/predefined key
|
|
var srcPath string // resolved source image file
|
|
|
|
if isPath {
|
|
absPath, err := filepath.Abs(*imageArg)
|
|
if err != nil {
|
|
log.Fatalf("invalid image path %q: %v", *imageArg, err)
|
|
}
|
|
srcPath = absPath
|
|
base := filepath.Base(absPath)
|
|
baseName = strings.TrimSuffix(base, filepath.Ext(base))
|
|
} else {
|
|
baseName = *imageArg
|
|
srcPath = findSourceImage(*imageArg, res, *imagesDir)
|
|
}
|
|
|
|
cacheFile := filepath.Join(*cacheDir, fmt.Sprintf("%s_%s.bin", baseName, res))
|
|
|
|
// 1. Check cache.
|
|
if data, err := os.ReadFile(cacheFile); err == nil {
|
|
fmt.Printf("Using cached: %s\n", cacheFile)
|
|
writeOutput(*output, data)
|
|
fmt.Printf("Written: %s (%s)\n", *output, res)
|
|
return
|
|
}
|
|
|
|
// 2. Load, resize if needed, convert.
|
|
img := loadAndResize(srcPath, res)
|
|
binData := convertToBin(img, res)
|
|
|
|
// 3. Save cache.
|
|
if err := os.MkdirAll(*cacheDir, 0755); err != nil {
|
|
log.Printf("Warning: cannot create cache dir %s: %v", *cacheDir, err)
|
|
} else if err := os.WriteFile(cacheFile, binData, 0644); err != nil {
|
|
log.Printf("Warning: cannot save cache %s: %v", cacheFile, err)
|
|
} else {
|
|
fmt.Printf("Cached: %s\n", cacheFile)
|
|
}
|
|
|
|
writeOutput(*output, binData)
|
|
fmt.Printf("Generated: %s (%s, %d bytes)\n", *output, res, len(binData))
|
|
}
|
|
|
|
// parseSize accepts either a target name (nicebadge, gopherbadge, ...) or WxH format.
|
|
func parseSize(s string) resolution {
|
|
if res, ok := predefinedTargets[strings.ToLower(s)]; ok {
|
|
return res
|
|
}
|
|
parts := strings.SplitN(strings.ToLower(s), "x", 2)
|
|
if len(parts) != 2 {
|
|
log.Fatalf("invalid size %q: must be WxH (e.g. 320x240) or a target name (nicebadge, gopherbadge, gobadge, pybadge)", s)
|
|
}
|
|
w, err1 := strconv.Atoi(parts[0])
|
|
h, err2 := strconv.Atoi(parts[1])
|
|
if err1 != nil || err2 != nil || w <= 0 || h <= 0 {
|
|
log.Fatalf("invalid size %q: width and height must be positive integers", s)
|
|
}
|
|
return resolution{w, h}
|
|
}
|
|
|
|
// findSourceImage looks for the best source image for a short name in imagesDir.
|
|
// Priority:
|
|
// 1. <name>_<WxH>.png / .jpg — predefined resized variant (no resize needed)
|
|
// 2. <name>.png / .jpg / .jpeg — base source image (will be resized)
|
|
func findSourceImage(name string, res resolution, imagesDir string) string {
|
|
resStr := res.String()
|
|
extensions := []string{".png", ".jpg", ".jpeg"}
|
|
|
|
// Try predefined resized variant first.
|
|
for _, ext := range extensions {
|
|
p := filepath.Join(imagesDir, fmt.Sprintf("%s_%s%s", name, resStr, ext))
|
|
if _, err := os.Stat(p); err == nil {
|
|
fmt.Printf("Using predefined: %s\n", p)
|
|
return p
|
|
}
|
|
}
|
|
|
|
// Try base source image.
|
|
for _, ext := range extensions {
|
|
p := filepath.Join(imagesDir, name+ext)
|
|
if _, err := os.Stat(p); err == nil {
|
|
return p
|
|
}
|
|
}
|
|
|
|
log.Fatalf("image not found for short name %q in directory %q\n"+
|
|
"Expected one of: %s_%s.png, %s.png, %s.jpg, ...",
|
|
name, imagesDir, name, resStr, name, name)
|
|
return ""
|
|
}
|
|
|
|
// loadAndResize opens a JPEG or PNG and scales it to the target resolution.
|
|
// If the image is already the exact target size, scaling is skipped.
|
|
func loadAndResize(path string, res resolution) image.Image {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
log.Fatalf("cannot open image %s: %v", path, err)
|
|
}
|
|
defer f.Close()
|
|
|
|
img, format, err := image.Decode(f)
|
|
if err != nil {
|
|
log.Fatalf("cannot decode image %s: %v", path, err)
|
|
}
|
|
fmt.Printf("Loaded: %s (%s, %dx%d)\n", path, format, img.Bounds().Dx(), img.Bounds().Dy())
|
|
|
|
if img.Bounds().Dx() == res.width && img.Bounds().Dy() == res.height {
|
|
return img
|
|
}
|
|
|
|
fmt.Printf("Resizing to %s...\n", res)
|
|
dst := image.NewRGBA(image.Rect(0, 0, res.width, res.height))
|
|
draw2.BiLinear.Scale(dst, dst.Bounds(), img, img.Bounds(), draw2.Over, nil)
|
|
return dst
|
|
}
|
|
|
|
// convertToBin encodes the image pixels as a hex string: "RRGGBB" per pixel, row-major.
|
|
// This matches the format expected by showLogoBin() in badge.go.
|
|
func convertToBin(img image.Image, res resolution) []byte {
|
|
var sb strings.Builder
|
|
sb.Grow(res.width * res.height * 6)
|
|
for y := 0; y < res.height; y++ {
|
|
for x := 0; x < res.width; x++ {
|
|
r, g, b, _ := img.At(x, y).RGBA()
|
|
fmt.Fprintf(&sb, "%02x%02x%02x", uint8(r>>8), uint8(g>>8), uint8(b>>8))
|
|
}
|
|
}
|
|
return []byte(sb.String())
|
|
}
|
|
|
|
func writeOutput(path string, data []byte) {
|
|
if err := os.WriteFile(path, data, 0644); err != nil {
|
|
log.Fatalf("cannot write output %s: %v", path, err)
|
|
}
|
|
}
|