wraith/internal/ai/screenshot.go
Vantz Stockwell 7ee5321d69
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Has been cancelled
feat: AI copilot backend — OAuth PKCE, Claude API streaming, 16 tools, conversations
- OAuth PKCE flow for Max subscription auth (no API key needed)
- Claude API client with SSE streaming (Messages API v1)
- 16 tool definitions: terminal, SFTP, RDP, session management
- Tool dispatch router mapping to existing Wraith services
- Conversation manager with SQLite persistence
- Terminal output ring buffer for AI context
- RDP screenshot encoder (RGBA → JPEG with downscaling)
- Wired into Wails app as AIService

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 09:09:23 -04:00

80 lines
2.2 KiB
Go

package ai
import (
"bytes"
"fmt"
"image"
"image/jpeg"
)
// EncodeScreenshot converts raw RGBA pixel data to a JPEG image.
// If the source dimensions exceed maxWidth x maxHeight, the image is
// downscaled using nearest-neighbor sampling (fast, no external deps).
// Returns the JPEG bytes.
func EncodeScreenshot(rgba []byte, srcWidth, srcHeight, maxWidth, maxHeight, quality int) ([]byte, error) {
expectedLen := srcWidth * srcHeight * 4
if len(rgba) < expectedLen {
return nil, fmt.Errorf("RGBA buffer too small: got %d bytes, expected %d for %dx%d", len(rgba), expectedLen, srcWidth, srcHeight)
}
if quality <= 0 || quality > 100 {
quality = 75
}
// Create source image from RGBA buffer
src := image.NewRGBA(image.Rect(0, 0, srcWidth, srcHeight))
copy(src.Pix, rgba[:expectedLen])
// Determine output dimensions
dstWidth, dstHeight := srcWidth, srcHeight
if srcWidth > maxWidth || srcHeight > maxHeight {
dstWidth, dstHeight = fitDimensions(srcWidth, srcHeight, maxWidth, maxHeight)
}
var img image.Image = src
// Downscale if needed using nearest-neighbor sampling
if dstWidth != srcWidth || dstHeight != srcHeight {
dst := image.NewRGBA(image.Rect(0, 0, dstWidth, dstHeight))
for y := 0; y < dstHeight; y++ {
srcY := y * srcHeight / dstHeight
for x := 0; x < dstWidth; x++ {
srcX := x * srcWidth / dstWidth
srcIdx := (srcY*srcWidth + srcX) * 4
dstIdx := (y*dstWidth + x) * 4
dst.Pix[dstIdx+0] = src.Pix[srcIdx+0] // R
dst.Pix[dstIdx+1] = src.Pix[srcIdx+1] // G
dst.Pix[dstIdx+2] = src.Pix[srcIdx+2] // B
dst.Pix[dstIdx+3] = src.Pix[srcIdx+3] // A
}
}
img = dst
}
// Encode to JPEG
var buf bytes.Buffer
if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: quality}); err != nil {
return nil, fmt.Errorf("encode JPEG: %w", err)
}
return buf.Bytes(), nil
}
// fitDimensions calculates the largest dimensions that fit within max bounds
// while preserving aspect ratio.
func fitDimensions(srcW, srcH, maxW, maxH int) (int, int) {
ratio := float64(srcW) / float64(srcH)
w, h := maxW, int(float64(maxW)/ratio)
if h > maxH {
h = maxH
w = int(float64(maxH) * ratio)
}
if w <= 0 {
w = 1
}
if h <= 0 {
h = 1
}
return w, h
}