Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cmd/td-cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@ Commands:
disconnect <src> <dst> Disconnect operators
dat read <path> Read DAT content
dat write <path> <content> 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
Expand Down
5 changes: 4 additions & 1 deletion cmd/td-cli/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
66 changes: 60 additions & 6 deletions internal/commands/screenshot.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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
}