Files
uptop-vhs/crop/main.go
T
lerko 0cad80c352 chore: add VHS tooling for uptop TUI screenshots
Backfill tool, crop tool, demo tape, seed data, and setup script
extracted from the uptop repo for clean separation.
2026-05-29 12:33:39 -04:00

124 lines
2.8 KiB
Go

// Command crop trims the uniform background border around each VHS screenshot so the
// content fills the frame instead of floating in a large empty terminal. Sparse views
// (alerts, detail, nodes) would otherwise sit in a sea of dead space.
//
// Usage: crop [dir] (dir defaults to vhs/screenshots)
package main
import (
"fmt"
"image"
"image/color"
"image/png"
"os"
"path/filepath"
)
// pad is the margin (px) left around the detected content. tol is the per-channel
// colour distance (summed) above which a pixel counts as content rather than background.
const (
pad = 24
tol = 28
)
func main() {
dir := "vhs/screenshots"
if len(os.Args) > 1 {
dir = os.Args[1]
}
paths, err := filepath.Glob(filepath.Join(dir, "*.png"))
if err != nil {
fmt.Fprintf(os.Stderr, "glob: %v\n", err)
os.Exit(1)
}
if len(paths) == 0 {
fmt.Fprintf(os.Stderr, "no PNGs in %s\n", dir)
os.Exit(1)
}
for _, p := range paths {
w, h, err := cropFile(p)
if err != nil {
fmt.Fprintf(os.Stderr, "crop %s: %v\n", p, err)
os.Exit(1)
}
fmt.Printf("cropped %s -> %dx%d\n", filepath.Base(p), w, h)
}
}
func cropFile(path string) (int, int, error) {
f, err := os.Open(path) //nolint:gosec // dev tool: paths come from a trusted local glob
if err != nil {
return 0, 0, err
}
src, err := png.Decode(f)
_ = f.Close()
if err != nil {
return 0, 0, err
}
b := src.Bounds()
// Background colour sampled from a corner — always inside VHS's blank padding.
bgR, bgG, bgB := rgb(src.At(b.Min.X+2, b.Min.Y+2))
minX, minY := b.Max.X, b.Max.Y
maxX, maxY := b.Min.X, b.Min.Y
found := false
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
r, g, bl := rgb(src.At(x, y))
if abs(r-bgR)+abs(g-bgG)+abs(bl-bgB) > tol {
found = true
minX, minY = min(minX, x), min(minY, y)
maxX, maxY = max(maxX, x), max(maxY, y)
}
}
}
if !found {
return b.Dx(), b.Dy(), nil // blank frame — leave untouched
}
minX = clamp(minX-pad, b.Min.X, b.Max.X)
minY = clamp(minY-pad, b.Min.Y, b.Max.Y)
maxX = clamp(maxX+pad+1, b.Min.X, b.Max.X)
maxY = clamp(maxY+pad+1, b.Min.Y, b.Max.Y)
dst := image.NewRGBA(image.Rect(0, 0, maxX-minX, maxY-minY))
for y := minY; y < maxY; y++ {
for x := minX; x < maxX; x++ {
dst.Set(x-minX, y-minY, src.At(x, y))
}
}
out, err := os.Create(path) //nolint:gosec // dev tool: paths come from a trusted local glob
if err != nil {
return 0, 0, err
}
defer out.Close() //nolint:errcheck // best-effort close on write path
if err := png.Encode(out, dst); err != nil {
return 0, 0, err
}
return dst.Bounds().Dx(), dst.Bounds().Dy(), nil
}
func rgb(c color.Color) (int, int, int) {
r, g, b, _ := c.RGBA()
return int(r >> 8), int(g >> 8), int(b >> 8)
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
func clamp(v, lo, hi int) int {
if v < lo {
return lo
}
if v > hi {
return hi
}
return v
}