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.
This commit is contained in:
+123
@@ -0,0 +1,123 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user