Files
aiops/backend/pkg/rdp/bitmap_decode.go
T
LoganandClaude 76ec9387f0 feat(rdp): 新增 RDP 远程桌面/SMB/账单同步能力 + 清理编译产物
主要变更:
- RDP 远程桌面:新增 backend/pkg/rdp(RDP 引擎 + credssp/display/rle/trace)、
  backend/controller/rdp*(连接/凭证/文件/会话/实时)、frontend RDP 组件与 API
- Windows SMB 文件传输:backend/pkg/winsmb
- 云账单同步:backend/pkg/billingsync + billing_sync/aliyun_billing,前端账单详情
- Agent CLI Kubernetes 工具:kubernetes_action/get、pod_execute/logs
- 服务器导入助手:server_import_helpers、ImportCredentialSettings
- 国际化与组件重构(cloud/deploy/monitor/server/system 等多模块)
- 清理编译产物二进制:移除 backend/server、backend/agent/bin/aiops-agent-*
- .gitignore:新增 backend/agent/ 与 .cache/(避免 334MB go-build 缓存误入库)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-04 01:50:36 +08:00

298 lines
6.8 KiB
Go

package rdp
import (
"bytes"
"encoding/base64"
"encoding/binary"
"image"
"image/png"
"io"
"ezops/pkg/rdp/rle"
"ezops/pkg/rdp/trace"
)
const (
bitmapCompression = 0x0001
noBitmapCompressionHdr = 0x0400
bitmapNoProcessing = 0x8000
fastPathUpdateBitmap = 0x1
fastPathUpdateSurfaceCmds = 0x4
pduType2Update = 0x02
)
// BitmapRect 表示一块待绘制到 Canvas 的位图区域 // Logan.Li
type BitmapRect struct {
DestLeft int
DestTop int
Width int
Height int
BitsPerPixel int
Data []byte
}
func bppFromWire(v uint16) int {
switch v {
case 8:
return 1
case 15, 16:
return 2
case 24:
return 3
case 32:
return 4
default:
if v <= 4 {
return int(v)
}
return 4
}
}
type wireBitmap struct {
DestLeft uint16
DestTop uint16
DestRight uint16
DestBottom uint16
Width uint16
Height uint16
BitsPerPixel uint16
Flags uint16
BitmapLength uint16
BitmapDataStream []byte
}
func readWireBitmap(r *bytes.Reader) (wireBitmap, error) {
var rect wireBitmap
var err error
rect.DestLeft, err = readLE16(r)
if err != nil {
return rect, err
}
rect.DestTop, err = readLE16(r)
if err != nil {
return rect, err
}
rect.DestRight, err = readLE16(r)
if err != nil {
return rect, err
}
rect.DestBottom, err = readLE16(r)
if err != nil {
return rect, err
}
rect.Width, err = readLE16(r)
if err != nil {
return rect, err
}
rect.Height, err = readLE16(r)
if err != nil {
return rect, err
}
rect.BitsPerPixel, err = readLE16(r)
if err != nil {
return rect, err
}
rect.Flags, err = readLE16(r)
if err != nil {
return rect, err
}
rect.BitmapLength, err = readLE16(r)
if err != nil {
return rect, err
}
ln := int(rect.BitmapLength)
if rect.Flags&bitmapCompression != 0 && rect.Flags&noBitmapCompressionHdr == 0 {
if _, err = readLE16(r); err != nil {
return rect, err
}
mainBody, err := readLE16(r)
if err != nil {
return rect, err
}
if _, err = readLE16(r); err != nil {
return rect, err
}
if _, err = readLE16(r); err != nil {
return rect, err
}
ln = int(mainBody)
}
rect.BitmapDataStream = make([]byte, ln)
_, err = io.ReadFull(r, rect.BitmapDataStream)
return rect, err
}
func parseFastPathBitmap(payload []byte) ([]BitmapRect, error) {
r := bytes.NewReader(payload)
if _, err := readLE16(r); err != nil {
return nil, err
}
count, err := readLE16(r)
if err != nil {
return nil, err
}
out := make([]BitmapRect, 0, count)
for i := 0; i < int(count); i++ {
wb, err := readWireBitmap(r)
if err != nil {
return out, err
}
if rect, ok := decodeWireBitmap(wb); ok {
out = append(out, rect)
}
}
return out, nil
}
func parseSlowPathBitmap(payload []byte) ([]BitmapRect, error) {
if len(payload) < 15 {
return nil, nil
}
pduType := binaryLittleUint16(payload[2:4])
if pduType != pduTypeData {
return nil, nil
}
type2 := payload[14]
if type2 != pduType2Update {
return nil, nil
}
body := payload[18:]
if len(body) < 2 {
return nil, nil
}
updateType := binaryLittleUint16(body[0:2])
if updateType != fastPathUpdateBitmap {
return nil, nil
}
return parseFastPathBitmap(body[2:])
}
func decodeWireBitmap(wb wireBitmap) (rect BitmapRect, ok bool) {
defer func() {
if recover() != nil {
rect = BitmapRect{}
ok = false
}
}()
bpp := bppFromWire(wb.BitsPerPixel)
data := wb.BitmapDataStream
originalLen := len(data)
if wb.Flags&bitmapNoProcessing != 0 {
// Surface command 已是自上而下 BGRA // Logan.Li
} else if wb.Flags&bitmapCompression != 0 {
data = rle.DecompressInto(data, nil, int(wb.Width), int(wb.Height), bpp)
if len(data) == 0 {
trace.Logf("bitmap decode failed compressed rect=%dx%d@%d,%d wireBpp=%d bpp=%d flags=0x%04x original=%d", wb.Width, wb.Height, wb.DestLeft, wb.DestTop, wb.BitsPerPixel, bpp, wb.Flags, originalLen)
return BitmapRect{}, false
}
} else {
data = flipBitmapVertical(data, int(wb.Width), bpp, int(wb.Height))
}
if len(data) < int(wb.Width)*int(wb.Height)*bpp {
trace.Logf("bitmap decode short rect=%dx%d@%d,%d wireBpp=%d bpp=%d flags=0x%04x original=%d decoded=%d", wb.Width, wb.Height, wb.DestLeft, wb.DestTop, wb.BitsPerPixel, bpp, wb.Flags, originalLen, len(data))
return BitmapRect{}, false
}
if isMostlyBlack(data, bpp) && int(wb.Width) >= 128 && int(wb.Height) >= 128 {
trace.Logf("bitmap decode mostly black rect=%dx%d@%d,%d wireBpp=%d bpp=%d flags=0x%04x original=%d decoded=%d", wb.Width, wb.Height, wb.DestLeft, wb.DestTop, wb.BitsPerPixel, bpp, wb.Flags, originalLen, len(data))
}
return BitmapRect{
DestLeft: int(wb.DestLeft),
DestTop: int(wb.DestTop),
Width: int(wb.Width),
Height: int(wb.Height),
BitsPerPixel: bpp,
Data: data,
}, true
}
func isMostlyBlack(data []byte, bpp int) bool {
if len(data) == 0 || bpp <= 0 {
return false
}
limit := len(data)
if limit > 256*1024 {
limit = 256 * 1024
}
nonBlack := 0
for i := 0; i+bpp-1 < limit; i += bpp {
switch bpp {
case 1:
if data[i] > 4 {
nonBlack++
}
case 2:
if data[i] > 0 || data[i+1] > 0 {
nonBlack++
}
default:
if data[i] > 4 || data[i+1] > 4 || data[i+2] > 4 {
nonBlack++
}
}
if nonBlack > 32 {
return false
}
}
return true
}
func flipBitmapVertical(data []byte, width, bpp, height int) []byte {
out := append([]byte(nil), data...)
stride := width * bpp
tmp := make([]byte, stride)
for y := 0; y < height/2; y++ {
top := y * stride
bot := (height - 1 - y) * stride
copy(tmp, out[top:top+stride])
copy(out[top:top+stride], out[bot:bot+stride])
copy(out[bot:bot+stride], tmp)
}
return out
}
func bitmapRectPNG(rect BitmapRect) (string, error) {
img := image.NewRGBA(image.Rect(0, 0, rect.Width, rect.Height))
pix := img.Pix
data := rect.Data
switch rect.BitsPerPixel {
case 1, 2:
n := rect.Width * rect.Height
for i := 0; i < n && i*2+1 < len(data); i++ {
// 保持 PNG 兼容路径与 native display 的 16bpp 大端 RGB565 解析一致。 // Logan.Li
c := binary.BigEndian.Uint16(data[i*2 : i*2+2])
r := uint8((c >> 11) & 0x1F)
g := uint8((c >> 5) & 0x3F)
b := uint8(c & 0x1F)
off := i * 4
pix[off] = (r << 3) | (r >> 2)
pix[off+1] = (g << 2) | (g >> 4)
pix[off+2] = (b << 3) | (b >> 2)
pix[off+3] = 0xFF
}
default:
stride := rect.BitsPerPixel
n := rect.Width * rect.Height
for i := 0; i < n; i++ {
s := i * stride
if s+2 >= len(data) {
break
}
off := i * 4
pix[off+2] = data[s]
pix[off+1] = data[s+1]
pix[off] = data[s+2]
pix[off+3] = 0xFF
}
}
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
return "", err
}
return "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes()), nil
}
func binaryLittleUint16(b []byte) uint16 {
return uint16(b[0]) | uint16(b[1])<<8
}