Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Has been cancelled
- 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>
80 lines
2.2 KiB
Go
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
|
|
}
|