// logogen converts an image to a .bin file suitable for embedding in badge firmware. // // Usage: // // logogen -size -image [-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. _.png / .jpg — predefined resized variant (no resize needed) // 2. .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) } }