master
John Lauer rust: Windows window-control layer (enumerate, process, messages, menu, focus, capture, UIA), all cross-process calls h… 3ecdded 2mo ago
//! Window pixels for the canvas-painted probe: PrintWindow(PW_RENDERFULLCONTENT) into a
//! compatible bitmap, read back with GetDIBits. Only the capture is ported; the image goes
//! to ab for the evidence pipeline and the PNG encoding stays out of this crate.
//! Python: kicad_ui.py `_canvas_probe` and the local half of `_screenshot_hwnd`.

use windows::Win32::Foundation::HWND;
use windows::Win32::Graphics::Gdi::{
    CreateCompatibleBitmap, CreateCompatibleDC, DeleteDC, DeleteObject, GetDC, GetDIBits, ReleaseDC, SelectObject, BITMAPINFO,
    BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, HGDIOBJ,
};
use windows::Win32::Storage::Xps::{PrintWindow, PRINT_WINDOW_FLAGS};
use windows::Win32::UI::WindowsAndMessaging::PW_RENDERFULLCONTENT;

use super::enumerate::{minimized, rect};
use super::{hwnd_u64, require_responsive};
use crate::Capture;

/// Windows renders the whole window (including the DWM-composed GL canvas) into the
/// memory DC; `rgb` comes back top-down, 3 bytes per pixel.
pub fn capture(h: HWND) -> Result<Capture, String> {
    if minimized(h) {
        return Err(format!("window {} is minimized; PrintWindow renders nothing for a minimized window (call show_no_activate first)", hwnd_u64(h)));
    }
    // PrintWindow sends WM_PRINT to the target synchronously; a hung KiCad would hold us.
    require_responsive(h, "capture_window", 500)?;
    let (_, _, width, height) = rect(h);
    if width <= 0 || height <= 0 {
        return Err(format!("window {} has an empty rect", hwnd_u64(h)));
    }

    // SAFETY: every GDI object created here is released on every path below; the pixel
    // buffer is sized exactly width * height * 4 and GetDIBits is told the same size
    // through the header. The window is responsive, so PrintWindow returns.
    unsafe {
        let hdc = GetDC(Some(h));
        if hdc.is_invalid() {
            return Err("GetDC failed".into());
        }
        let mem = CreateCompatibleDC(Some(hdc));
        let bmp = CreateCompatibleBitmap(hdc, width, height);
        if mem.is_invalid() || bmp.is_invalid() {
            if !bmp.is_invalid() {
                let _ = DeleteObject(HGDIOBJ(bmp.0));
            }
            if !mem.is_invalid() {
                let _ = DeleteDC(mem);
            }
            ReleaseDC(Some(h), hdc);
            return Err("CreateCompatibleDC/Bitmap failed".into());
        }
        let old = SelectObject(mem, HGDIOBJ(bmp.0));
        let printed = PrintWindow(h, mem, PRINT_WINDOW_FLAGS(PW_RENDERFULLCONTENT)).as_bool();

        let mut bmi = BITMAPINFO::default();
        bmi.bmiHeader = BITMAPINFOHEADER {
            biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
            biWidth: width,
            biHeight: -height, // negative: top-down rows, no flip needed
            biPlanes: 1,
            biBitCount: 32,
            biCompression: BI_RGB.0,
            ..Default::default()
        };
        let mut bgra = vec![0u8; (width as usize) * (height as usize) * 4];
        let lines = GetDIBits(mem, bmp, 0, height as u32, Some(bgra.as_mut_ptr() as *mut _), &mut bmi, DIB_RGB_COLORS);

        SelectObject(mem, old);
        let _ = DeleteObject(HGDIOBJ(bmp.0));
        let _ = DeleteDC(mem);
        ReleaseDC(Some(h), hdc);

        if !printed {
            return Err(format!("PrintWindow returned FALSE for {}", hwnd_u64(h)));
        }
        if lines <= 0 {
            return Err("GetDIBits returned no scan lines".into());
        }
        let mut rgb = Vec::with_capacity((width as usize) * (height as usize) * 3);
        for px in bgra.chunks_exact(4) {
            rgb.extend_from_slice(&[px[2], px[1], px[0]]);
        }
        Ok(Capture { width: width as u32, height: height as u32, rgb })
    }
}