Merge branch 'main' of code.madriguera.me:GoEducation/badges
This commit is contained in:
commit
8fc41e4eff
8 changed files with 286 additions and 0 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -25,3 +25,6 @@ go.work.sum
|
|||
# env file
|
||||
.env
|
||||
|
||||
# logogen cache
|
||||
cmd/logogen/cache/*.bin
|
||||
|
||||
|
|
|
|||
68
README.md
68
README.md
|
|
@ -1,2 +1,70 @@
|
|||
# badges
|
||||
|
||||
TinyGo badge firmware for multiple hardware targets (GopherBadge, NiceBadge, GoBadge/PyBadge).
|
||||
|
||||
## Hardware targets
|
||||
|
||||
| Target | Build tag | Resolution |
|
||||
|--------|-----------|------------|
|
||||
| GopherBadge | `gopher_badge` | 320×240 |
|
||||
| NiceBadge | `nicenano` | 240×135 |
|
||||
| GoBadge / PyBadge | `pybadge` | 160×128 |
|
||||
|
||||
## Generating logo.bin
|
||||
|
||||
The badge firmware embeds a `logo.bin` file displayed at boot (`showLogoBin` in `badge.go`).
|
||||
Use the `logogen` command to convert any image to the required hex format.
|
||||
|
||||
### Setup
|
||||
|
||||
```sh
|
||||
cd cmd/logogen
|
||||
go mod download
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```sh
|
||||
# By target name
|
||||
go run . -size nicebadge -image tinygo
|
||||
go run . -size gopherbadge -image gopherconeu
|
||||
go run . -size gobadge -image golab
|
||||
|
||||
# By explicit resolution
|
||||
go run . -size 320x240 -image /path/to/logo.jpg
|
||||
|
||||
# Write directly to the project root logo.bin
|
||||
go run . -size nicebadge -image tinygo -output ../../logo.bin
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `-size` | *(required)* | Target name (`nicebadge`, `gopherbadge`, `gobadge`, `pybadge`) or explicit resolution (`320x240`) |
|
||||
| `-image` | *(required)* | File path (relative or absolute) or short name |
|
||||
| `-output` | `logo.bin` | Output file |
|
||||
| `-images-dir` | `images` | Directory with source images |
|
||||
| `-cache-dir` | `cache` | Directory for cached `.bin` files |
|
||||
|
||||
### Adding images
|
||||
|
||||
Place source images inside `cmd/logogen/images/`:
|
||||
|
||||
```
|
||||
cmd/logogen/images/
|
||||
tinygo.jpg ← source image for short name "tinygo"
|
||||
tinygo_240x135.png ← predefined variant for nicebadge (skip resize)
|
||||
tinygo_320x240.png ← predefined variant for gopherbadge (skip resize)
|
||||
gopherconeu.jpg
|
||||
golab.png
|
||||
...
|
||||
```
|
||||
|
||||
**Resolution priority** for a given short name + target:
|
||||
|
||||
1. `<name>_<WxH>.png` / `.jpg` — predefined resized variant, used as-is (no resize)
|
||||
2. `<name>.png` / `.jpg` / `.jpeg` — base image, resized with BiLinear interpolation
|
||||
3. Cache hit (`cache/<name>_<WxH>.bin`) — skips all image processing on repeat runs
|
||||
|
||||
Generated `.bin` files in `cache/` are gitignored.
|
||||
|
|
|
|||
0
cmd/logogen/cache/.gitkeep
vendored
Normal file
0
cmd/logogen/cache/.gitkeep
vendored
Normal file
5
cmd/logogen/go.mod
Normal file
5
cmd/logogen/go.mod
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module github.com/conejoninja/gopherbadge/cmd/logogen
|
||||
|
||||
go 1.22.1
|
||||
|
||||
require golang.org/x/image v0.23.0
|
||||
2
cmd/logogen/go.sum
Normal file
2
cmd/logogen/go.sum
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||
0
cmd/logogen/images/.gitkeep
Normal file
0
cmd/logogen/images/.gitkeep
Normal file
BIN
cmd/logogen/logogen
Executable file
BIN
cmd/logogen/logogen
Executable file
Binary file not shown.
208
cmd/logogen/main.go
Normal file
208
cmd/logogen/main.go
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
// 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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue