diff --git a/cmd/td-cli/main.go b/cmd/td-cli/main.go index bb147ff..cac866a 100644 --- a/cmd/td-cli/main.go +++ b/cmd/td-cli/main.go @@ -203,7 +203,8 @@ Commands: disconnect Disconnect operators dat read Read DAT content dat write Write DAT content - screenshot [path] [-o file] Capture TOP as PNG + screenshot [path] [-o file] + [--opaque] Capture TOP as PNG (--opaque forces alpha=255) project info Project metadata project save [path] Save project backup list [--limit N] List recent backup artifacts diff --git a/cmd/td-cli/routes.go b/cmd/td-cli/routes.go index abcd20e..fb025eb 100644 --- a/cmd/td-cli/routes.go +++ b/cmd/td-cli/routes.go @@ -55,15 +55,18 @@ func runCommand(c *client.Client, command string, args []string, jsonOutput bool case "screenshot": path := "" outputFile := "" + opaque := false for i := 0; i < len(args); i++ { if args[i] == "-o" && i+1 < len(args) { outputFile = args[i+1] i++ + } else if args[i] == "--opaque" { + opaque = true } else if path == "" { path = args[i] } } - return commands.Screenshot(c, path, outputFile, jsonOutput) + return commands.Screenshot(c, path, outputFile, opaque, jsonOutput) case "project": return runProject(c, args, jsonOutput) diff --git a/internal/commands/screenshot.go b/internal/commands/screenshot.go index b44f686..0d6693e 100644 --- a/internal/commands/screenshot.go +++ b/internal/commands/screenshot.go @@ -1,9 +1,13 @@ package commands import ( + "bytes" "encoding/base64" "encoding/json" "fmt" + "image" + "image/color" + "image/png" "os" "github.com/0dot77/td-cli/internal/client" @@ -15,8 +19,8 @@ type screenshotResult struct { Height int `json:"height"` } -// Screenshot captures a TOP output as PNG. -func Screenshot(c *client.Client, path, outputFile string, jsonOutput bool) error { +// Screenshot captures a TOP output as PNG. When opaque is true, alpha is forced to 255. +func Screenshot(c *client.Client, path, outputFile string, opaque, jsonOutput bool) error { payload := map[string]string{} if path != "" { payload["path"] = path @@ -44,18 +48,68 @@ func Screenshot(c *client.Client, path, outputFile string, jsonOutput bool) erro } } - if outputFile != "" { - data, err := base64.StdEncoding.DecodeString(result.Image) + data, err := base64.StdEncoding.DecodeString(result.Image) + if err != nil { + return fmt.Errorf("failed to decode image: %w", err) + } + + if opaque { + flattened, err := flattenAlpha(data) if err != nil { - return fmt.Errorf("failed to decode image: %w", err) + return fmt.Errorf("failed to flatten alpha: %w", err) } + data = flattened + } + + if outputFile != "" { if err := os.WriteFile(outputFile, data, 0644); err != nil { return fmt.Errorf("failed to write file: %w", err) } fmt.Printf("Screenshot saved to %s (%dx%d)\n", outputFile, result.Width, result.Height) } else { - fmt.Print(result.Image) + if opaque { + fmt.Print(base64.StdEncoding.EncodeToString(data)) + } else { + fmt.Print(result.Image) + } } return nil } + +func flattenAlpha(pngBytes []byte) ([]byte, error) { + img, err := png.Decode(bytes.NewReader(pngBytes)) + if err != nil { + return nil, fmt.Errorf("decode png: %w", err) + } + bounds := img.Bounds() + out := image.NewNRGBA(bounds) + if src, ok := img.(*image.NRGBA); ok { + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + si := src.PixOffset(x, y) + di := out.PixOffset(x, y) + out.Pix[di+0] = src.Pix[si+0] + out.Pix[di+1] = src.Pix[si+1] + out.Pix[di+2] = src.Pix[si+2] + out.Pix[di+3] = 255 + } + } + } else { + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA) + di := out.PixOffset(x, y) + out.Pix[di+0] = c.R + out.Pix[di+1] = c.G + out.Pix[di+2] = c.B + out.Pix[di+3] = 255 + } + } + } + var buf bytes.Buffer + if err := png.Encode(&buf, out); err != nil { + return nil, fmt.Errorf("encode png: %w", err) + } + return buf.Bytes(), nil +}