using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
namespace WindowsBridge;
///
/// Screenshot capture and OCR.
/// Equivalent to macOS CoreGraphicsBridge (capture) + VisionBridge (OCR).
/// Uses GDI+ for screenshots and Windows.Media.Ocr for text recognition.
///
class ScreenCapture
{
[DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")]
private static extern bool PrintWindow(IntPtr hWnd, IntPtr hdcBlt, uint nFlags);
[DllImport("user32.dll")]
private static extern IntPtr GetDesktopWindow();
[DllImport("user32.dll")]
private static extern IntPtr GetWindowDC(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("gdi32.dll")]
private static extern IntPtr CreateCompatibleDC(IntPtr hdc);
[DllImport("gdi32.dll")]
private static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight);
[DllImport("gdi32.dll")]
private static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
[DllImport("gdi32.dll")]
private static extern bool BitBlt(IntPtr hdcDest, int xDest, int yDest, int wDest, int hDest,
IntPtr hdcSrc, int xSrc, int ySrc, uint rop);
[DllImport("gdi32.dll")]
private static extern bool DeleteDC(IntPtr hdc);
[DllImport("gdi32.dll")]
private static extern bool DeleteObject(IntPtr hObject);
[DllImport("user32.dll")]
private static extern int GetSystemMetrics(int nIndex);
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
public int Left, Top, Right, Bottom;
}
private const int SM_CXSCREEN = 0;
private const int SM_CYSCREEN = 1;
private const int SM_XVIRTUALSCREEN = 76;
private const int SM_YVIRTUALSCREEN = 77;
private const int SM_CXVIRTUALSCREEN = 78;
private const int SM_CYVIRTUALSCREEN = 79;
private const uint SRCCOPY = 0x00CC0020;
private const uint PW_RENDERFULLCONTENT = 0x00000002;
private static readonly string _tempDir = Path.Combine(Path.GetTempPath(), "screenhand");
static ScreenCapture()
{
Directory.CreateDirectory(_tempDir);
}
///
/// Capture the full screen or a region.
///
public Dictionary CaptureScreen(Dictionary? region)
{
int x, y, width, height;
if (region != null)
{
x = (int)region.GetValueOrDefault("x", 0);
y = (int)region.GetValueOrDefault("y", 0);
width = (int)region.GetValueOrDefault("width", GetSystemMetrics(SM_CXSCREEN));
height = (int)region.GetValueOrDefault("height", GetSystemMetrics(SM_CYSCREEN));
}
else
{
// Capture virtual screen (all monitors)
x = GetSystemMetrics(SM_XVIRTUALSCREEN);
y = GetSystemMetrics(SM_YVIRTUALSCREEN);
width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
// Fallback to primary monitor
if (width == 0 || height == 0)
{
x = 0;
y = 0;
width = GetSystemMetrics(SM_CXSCREEN);
height = GetSystemMetrics(SM_CYSCREEN);
}
}
using var bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb);
using var graphics = Graphics.FromImage(bitmap);
graphics.CopyFromScreen(x, y, 0, 0, new Size(width, height), CopyPixelOperation.SourceCopy);
var filePath = Path.Combine(_tempDir, $"screen_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.png");
bitmap.Save(filePath, ImageFormat.Png);
return new Dictionary
{
["path"] = filePath,
["width"] = width,
["height"] = height,
};
}
///
/// Capture a specific window by its window handle (passed as windowId).
///
public Dictionary CaptureWindow(int windowId)
{
var hWnd = new IntPtr(windowId);
GetWindowRect(hWnd, out RECT rect);
int width = rect.Right - rect.Left;
int height = rect.Bottom - rect.Top;
if (width <= 0 || height <= 0)
throw new BridgeException($"Window {windowId} has invalid dimensions");
using var bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb);
using var graphics = Graphics.FromImage(bitmap);
// Try PrintWindow first (works for off-screen windows)
var hdc = graphics.GetHdc();
bool success = PrintWindow(hWnd, hdc, PW_RENDERFULLCONTENT);
graphics.ReleaseHdc(hdc);
if (!success)
{
// Fallback to screen capture of the window area
graphics.CopyFromScreen(rect.Left, rect.Top, 0, 0,
new Size(width, height), CopyPixelOperation.SourceCopy);
}
var filePath = Path.Combine(_tempDir, $"window_{windowId}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.png");
bitmap.Save(filePath, ImageFormat.Png);
return new Dictionary
{
["path"] = filePath,
["width"] = width,
["height"] = height,
};
}
///
/// Capture a specific window in-memory, return base64 PNG (no disk I/O).
/// Equivalent to macOS captureWindowBuffer.
///
public Dictionary CaptureWindowBuffer(int windowId)
{
var hWnd = new IntPtr(windowId);
GetWindowRect(hWnd, out RECT rect);
int width = rect.Right - rect.Left;
int height = rect.Bottom - rect.Top;
if (width <= 0 || height <= 0)
throw new BridgeException($"Window {windowId} has invalid dimensions");
using var bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb);
using var graphics = Graphics.FromImage(bitmap);
var hdc = graphics.GetHdc();
bool success = PrintWindow(hWnd, hdc, PW_RENDERFULLCONTENT);
graphics.ReleaseHdc(hdc);
if (!success)
{
graphics.CopyFromScreen(rect.Left, rect.Top, 0, 0,
new Size(width, height), CopyPixelOperation.SourceCopy);
}
using var ms = new MemoryStream();
bitmap.Save(ms, ImageFormat.Png);
var base64 = Convert.ToBase64String(ms.ToArray());
return new Dictionary
{
["base64"] = base64,
["width"] = width,
["height"] = height,
};
}
///
/// OCR a specific region of a window. Captures window, crops to ROI, runs OCR,
/// then translates bounds back to window coordinates.
/// Equivalent to macOS vision.ocrRegion.
///
public Dictionary OcrRegion(int windowId, Dictionary region)
{
var hWnd = new IntPtr(windowId);
GetWindowRect(hWnd, out RECT rect);
int winWidth = rect.Right - rect.Left;
int winHeight = rect.Bottom - rect.Top;
if (winWidth <= 0 || winHeight <= 0)
throw new BridgeException($"Window {windowId} has invalid dimensions");
int roiX = (int)region.GetValueOrDefault("x", 0);
int roiY = (int)region.GetValueOrDefault("y", 0);
int roiW = (int)region.GetValueOrDefault("width", winWidth);
int roiH = (int)region.GetValueOrDefault("height", winHeight);
// Clamp ROI to window bounds
roiX = Math.Max(0, Math.Min(roiX, winWidth));
roiY = Math.Max(0, Math.Min(roiY, winHeight));
roiW = Math.Min(roiW, winWidth - roiX);
roiH = Math.Min(roiH, winHeight - roiY);
if (roiW <= 0 || roiH <= 0)
throw new BridgeException("ROI has zero or negative area after clamping");
// Capture full window
using var fullBitmap = new Bitmap(winWidth, winHeight, PixelFormat.Format32bppArgb);
using (var graphics = Graphics.FromImage(fullBitmap))
{
var hdc = graphics.GetHdc();
bool success = PrintWindow(hWnd, hdc, PW_RENDERFULLCONTENT);
graphics.ReleaseHdc(hdc);
if (!success)
{
graphics.CopyFromScreen(rect.Left, rect.Top, 0, 0,
new Size(winWidth, winHeight), CopyPixelOperation.SourceCopy);
}
}
// Crop to ROI
using var cropped = fullBitmap.Clone(
new Rectangle(roiX, roiY, roiW, roiH), fullBitmap.PixelFormat);
// Save cropped to temp file for OCR
var tempPath = Path.Combine(_tempDir, $"ocr_region_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.png");
cropped.Save(tempPath, ImageFormat.Png);
try
{
var ocrResult = Ocr(tempPath);
// Translate bounds back to window coordinates
if (ocrResult["regions"] is List