#include "VisualProbeCommon.h" #include #include #include #include #include #include namespace VisualProbe { namespace { using SetProcessDpiAwarenessContextFn = BOOL(WINAPI*)(DPI_AWARENESS_CONTEXT); using GetDpiForWindowFn = UINT(WINAPI*)(HWND); using GetThreadDpiAwarenessContextFn = DPI_AWARENESS_CONTEXT(WINAPI*)(); using GetWindowDpiAwarenessContextFn = DPI_AWARENESS_CONTEXT(WINAPI*)(HWND); using GetAwarenessFromDpiAwarenessContextFn = DPI_AWARENESS(WINAPI*)(DPI_AWARENESS_CONTEXT); using GetProcessDpiAwarenessFn = HRESULT(WINAPI*)(HANDLE, int*); std::string awarenessName(int awareness) { switch (awareness) { case 0: return "unaware"; case 1: return "system"; case 2: return "per-monitor"; default: return "unknown"; } } std::string contextAwarenessName(DPI_AWARENESS_CONTEXT context) noexcept { HMODULE user32 = GetModuleHandleW(L"user32.dll"); const auto getAwareness = user32 ? reinterpret_cast( GetProcAddress(user32, "GetAwarenessFromDpiAwarenessContext")) : nullptr; if (!getAwareness || !context) return "not_available"; return awarenessName(static_cast(getAwareness(context))); } DPI_AWARENESS_CONTEXT requestedContext(const std::string& requested) noexcept { if (requested == "unaware") return DPI_AWARENESS_CONTEXT_UNAWARE; if (requested == "system") return DPI_AWARENESS_CONTEXT_SYSTEM_AWARE; if (requested == "per-monitor") return DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE; if (requested == "per-monitor-v2") return DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2; return nullptr; } std::uint64_t fnv1a(const std::vector& pixels) noexcept { std::uint64_t value = 1469598103934665603ull; for (const std::uint32_t pixel : pixels) { for (unsigned int shift = 0; shift < 32; shift += 8) { value ^= static_cast((pixel >> shift) & 0xFFu); value *= 1099511628211ull; } } return value; } std::vector readPixels( HDC hdc, int width, int height, int sourceX = 0, int sourceY = 0) noexcept { std::vector pixels; if (!hdc || width <= 0 || height <= 0) return pixels; BITMAPINFO info{}; info.bmiHeader.biSize = sizeof(info.bmiHeader); info.bmiHeader.biWidth = width; info.bmiHeader.biHeight = -height; info.bmiHeader.biPlanes = 1; info.bmiHeader.biBitCount = 32; info.bmiHeader.biCompression = BI_RGB; void* bits = nullptr; HDC memoryDc = CreateCompatibleDC(hdc); if (!memoryDc) return pixels; HBITMAP bitmap = CreateDIBSection(hdc, &info, DIB_RGB_COLORS, &bits, nullptr, 0); if (!bitmap || !bits) { if (bitmap) DeleteObject(bitmap); DeleteDC(memoryDc); return pixels; } HGDIOBJ previous = SelectObject(memoryDc, bitmap); const bool copied = previous && BitBlt(memoryDc, 0, 0, width, height, hdc, sourceX, sourceY, SRCCOPY) != FALSE; if (copied) { try { const auto* first = static_cast(bits); pixels.assign(first, first + static_cast(width) * height); } catch (...) { pixels.clear(); } } if (previous) SelectObject(memoryDc, previous); DeleteObject(bitmap); DeleteDC(memoryDc); return pixels; } bool xformEqual(const XFORM& left, const XFORM& right) noexcept { constexpr float Epsilon = 0.0001f; return std::fabs(left.eM11 - right.eM11) < Epsilon && std::fabs(left.eM12 - right.eM12) < Epsilon && std::fabs(left.eM21 - right.eM21) < Epsilon && std::fabs(left.eM22 - right.eM22) < Epsilon && std::fabs(left.eDx - right.eDx) < Epsilon && std::fabs(left.eDy - right.eDy) < Epsilon; } std::string captureOsBuild() noexcept { using RtlGetVersionFn = LONG(WINAPI*)(OSVERSIONINFOW*); HMODULE ntdll = GetModuleHandleW(L"ntdll.dll"); const auto rtlGetVersion = ntdll ? reinterpret_cast( GetProcAddress(ntdll, "RtlGetVersion")) : nullptr; OSVERSIONINFOW version{}; version.dwOSVersionInfoSize = sizeof(version); if (!rtlGetVersion || rtlGetVersion(&version) != 0) return "not_available"; std::ostringstream output; output << version.dwMajorVersion << '.' << version.dwMinorVersion << '.' << version.dwBuildNumber; return output.str(); } } bool parseOptions(int argc, char** argv, Options& options, std::string& error) { for (int index = 1; index < argc; ++index) { const std::string argument = argv[index]; if ((argument == "--mode" || argument == "--output" || argument == "--dpi-awareness" || argument == "--ime-name" || argument == "--auto-close-ms") && index + 1 >= argc) { error = "Missing value for " + argument; return false; } if (argument == "--mode") options.mode = argv[++index]; else if (argument == "--output") options.outputDirectory = argv[++index]; else if (argument == "--dpi-awareness") options.requestedDpiAwareness = argv[++index]; else if (argument == "--ime-name") options.expectedImeName = argv[++index]; else if (argument == "--auto-close-ms") { try { options.autoCloseMilliseconds = static_cast(std::stoul(argv[++index])); } catch (...) { error = "Invalid auto-close duration"; return false; } } else { error = "Unknown argument: " + argument; return false; } } if (options.mode != "wide-text" && options.mode != "dpi-coordinate" && options.mode != "ime-position") { error = "Mode must be wide-text, dpi-coordinate, or ime-position"; return false; } if (options.outputDirectory.empty()) { error = "Output directory is required"; return false; } const std::vector awarenessValues{ "inherit", "unaware", "system", "per-monitor", "per-monitor-v2" }; if (std::find(awarenessValues.begin(), awarenessValues.end(), options.requestedDpiAwareness) == awarenessValues.end()) { error = "Unsupported DPI awareness: " + options.requestedDpiAwareness; return false; } const std::vector imeNames{ "unspecified", "MicrosoftPinyin", "Sogou" }; if (std::find(imeNames.begin(), imeNames.end(), options.expectedImeName) == imeNames.end()) { error = "Unsupported IME name: " + options.expectedImeName; return false; } std::error_code fileError; std::filesystem::create_directories(options.outputDirectory / "evidence", fileError); if (fileError) { error = "Cannot create output directory: " + fileError.message(); return false; } return true; } DpiConfiguration applyRequestedDpiAwareness(const std::string& requested) noexcept { DpiConfiguration result; result.requested = requested; if (requested == "inherit") { result.applied = true; return result; } result.attempted = true; HMODULE user32 = GetModuleHandleW(L"user32.dll"); const auto setContext = user32 ? reinterpret_cast( GetProcAddress(user32, "SetProcessDpiAwarenessContext")) : nullptr; if (!setContext) { result.error = ERROR_PROC_NOT_FOUND; return result; } SetLastError(ERROR_SUCCESS); result.applied = setContext(requestedContext(requested)) != FALSE; result.error = result.applied ? ERROR_SUCCESS : GetLastError(); return result; } EnvironmentSnapshot captureEnvironment(HWND hwnd) noexcept { EnvironmentSnapshot snapshot; snapshot.processId = GetCurrentProcessId(); snapshot.threadId = GetCurrentThreadId(); snapshot.windowUnicode = hwnd && IsWindowUnicode(hwnd) != FALSE; snapshot.osBuild = captureOsBuild(); const TCHAR* easyxVersion = GetEasyXVer(); snapshot.easyxVersion = easyxVersion ? easyxVersion : "not_available"; if (hwnd) { GetClientRect(hwnd, &snapshot.clientRect); GetWindowRect(hwnd, &snapshot.windowRect); } HMODULE user32 = GetModuleHandleW(L"user32.dll"); const auto getDpi = user32 ? reinterpret_cast( GetProcAddress(user32, "GetDpiForWindow")) : nullptr; const auto getThreadContext = user32 ? reinterpret_cast( GetProcAddress(user32, "GetThreadDpiAwarenessContext")) : nullptr; const auto getWindowContext = user32 ? reinterpret_cast( GetProcAddress(user32, "GetWindowDpiAwarenessContext")) : nullptr; snapshot.dpiForWindow = getDpi && hwnd ? getDpi(hwnd) : 0; snapshot.threadAwareness = contextAwarenessName(getThreadContext ? getThreadContext() : nullptr); snapshot.windowAwareness = contextAwarenessName( getWindowContext && hwnd ? getWindowContext(hwnd) : nullptr); HMODULE shcore = LoadLibraryW(L"shcore.dll"); const auto getProcessAwareness = shcore ? reinterpret_cast( GetProcAddress(shcore, "GetProcessDpiAwareness")) : nullptr; int processAwareness = -1; if (getProcessAwareness && SUCCEEDED(getProcessAwareness(nullptr, &processAwareness))) snapshot.processAwareness = awarenessName(processAwareness); else snapshot.processAwareness = "not_available"; if (shcore) FreeLibrary(shcore); HDC screen = GetDC(nullptr); if (screen) { snapshot.deviceDpiX = GetDeviceCaps(screen, LOGPIXELSX); snapshot.deviceDpiY = GetDeviceCaps(screen, LOGPIXELSY); ReleaseDC(nullptr, screen); } return snapshot; } GdiState captureGdiState(HDC hdc) noexcept { GdiState state; if (!hdc) return state; state.mapMode = GetMapMode(hdc); GetViewportOrgEx(hdc, &state.viewportOrigin); GetWindowOrgEx(hdc, &state.windowOrigin); GetViewportExtEx(hdc, &state.viewportExtent); GetWindowExtEx(hdc, &state.windowExtent); state.graphicsMode = GetGraphicsMode(hdc); state.worldTransformAvailable = GetWorldTransform(hdc, &state.worldTransform) != FALSE; state.backgroundMode = GetBkMode(hdc); state.textColor = GetTextColor(hdc); state.backgroundColor = GetBkColor(hdc); state.textAlign = GetTextAlign(hdc); state.textCharacterExtra = GetTextCharacterExtra(hdc); state.clipType = GetClipBox(hdc, &state.clipBox); return state; } bool sameGdiState(const GdiState& left, const GdiState& right) noexcept { return left.mapMode == right.mapMode && left.viewportOrigin.x == right.viewportOrigin.x && left.viewportOrigin.y == right.viewportOrigin.y && left.windowOrigin.x == right.windowOrigin.x && left.windowOrigin.y == right.windowOrigin.y && left.viewportExtent.cx == right.viewportExtent.cx && left.viewportExtent.cy == right.viewportExtent.cy && left.windowExtent.cx == right.windowExtent.cx && left.windowExtent.cy == right.windowExtent.cy && left.graphicsMode == right.graphicsMode && left.worldTransformAvailable == right.worldTransformAvailable && (!left.worldTransformAvailable || xformEqual(left.worldTransform, right.worldTransform)) && left.backgroundMode == right.backgroundMode && left.textColor == right.textColor && left.backgroundColor == right.backgroundColor && left.textAlign == right.textAlign && left.textCharacterExtra == right.textCharacterExtra && left.clipType == right.clipType && EqualRect(&left.clipBox, &right.clipBox) != FALSE; } SurfaceFingerprint captureSurfaceFingerprint(HDC hdc, int width, int height) noexcept { SurfaceFingerprint fingerprint; fingerprint.hdc = reinterpret_cast(hdc); fingerprint.hbitmap = reinterpret_cast( hdc ? GetCurrentObject(hdc, OBJ_BITMAP) : nullptr); fingerprint.hfont = reinterpret_cast( hdc ? GetCurrentObject(hdc, OBJ_FONT) : nullptr); if (fingerprint.hfont) { fingerprint.logFontAvailable = GetObjectW( reinterpret_cast(fingerprint.hfont), sizeof(fingerprint.logFont), &fingerprint.logFont) == sizeof(fingerprint.logFont); } if (fingerprint.hbitmap) { BITMAP bitmap{}; if (GetObject(reinterpret_cast(fingerprint.hbitmap), sizeof(bitmap), &bitmap) == sizeof(bitmap)) { fingerprint.bitmapInfoAvailable = true; fingerprint.bitmapWidth = bitmap.bmWidth; fingerprint.bitmapHeight = bitmap.bmHeight; fingerprint.planes = bitmap.bmPlanes; fingerprint.bitsPerPixel = bitmap.bmBitsPixel; fingerprint.stride = bitmap.bmWidthBytes; } DIBSECTION section{}; fingerprint.dibSection = GetObject(reinterpret_cast(fingerprint.hbitmap), sizeof(section), §ion) == sizeof(section); } const auto pixels = readPixels(hdc, width, height); fingerprint.pixelDigest = fnv1a(pixels); fingerprint.gdiState = captureGdiState(hdc); return fingerprint; } std::uint64_t capturePixelRegionDigest(HDC hdc, const RECT& region) noexcept { const int width = region.right - region.left; const int height = region.bottom - region.top; return fnv1a(readPixels(hdc, width, height, region.left, region.top)); } PixelBounds findColorBounds(HDC hdc, const RECT& region, COLORREF color) noexcept { PixelBounds bounds; for (int y = region.top; y < region.bottom; ++y) { for (int x = region.left; x < region.right; ++x) { if (GetPixel(hdc, x, y) != color) continue; if (!bounds.present) { bounds.present = true; bounds.left = bounds.right = x; bounds.top = bounds.bottom = y; } else { bounds.left = (std::min)(bounds.left, x); bounds.top = (std::min)(bounds.top, y); bounds.right = (std::max)(bounds.right, x); bounds.bottom = (std::max)(bounds.bottom, y); } ++bounds.pixelCount; } } return bounds; } PixelBounds findChangedBounds(HDC hdc, const RECT& region, COLORREF background) noexcept { PixelBounds bounds; for (int y = region.top; y < region.bottom; ++y) { for (int x = region.left; x < region.right; ++x) { const COLORREF pixel = GetPixel(hdc, x, y); if (pixel == CLR_INVALID || pixel == background) continue; if (!bounds.present) { bounds.present = true; bounds.left = bounds.right = x; bounds.top = bounds.bottom = y; } else { bounds.left = (std::min)(bounds.left, x); bounds.top = (std::min)(bounds.top, y); bounds.right = (std::max)(bounds.right, x); bounds.bottom = (std::max)(bounds.bottom, y); } ++bounds.pixelCount; } } return bounds; } bool saveHdcImage(HDC source, int width, int height, const std::filesystem::path& path) noexcept { if (!source || width <= 0 || height <= 0) return false; try { IMAGE image(width, height); HDC destination = GetImageHDC(&image); if (!destination || BitBlt(destination, 0, 0, width, height, source, 0, 0, SRCCOPY) == FALSE) return false; saveimage(path.string().c_str(), &image); return std::filesystem::exists(path) && std::filesystem::file_size(path) > 0; } catch (...) { return false; } } bool captureClientImage( HWND hwnd, const std::filesystem::path& path, std::string* captureBackend) noexcept { if (!hwnd) return false; RECT client{}; if (!GetClientRect(hwnd, &client)) return false; const int width = client.right - client.left; const int height = client.bottom - client.top; try { IMAGE image(width, height); HDC destination = GetImageHDC(&image); if (!destination) return false; PatBlt(destination, 0, 0, width, height, BLACKNESS); bool captured = false; HDC clientDc = GetDC(hwnd); if (clientDc) { captured = BitBlt(destination, 0, 0, width, height, clientDc, 0, 0, SRCCOPY) != FALSE; ReleaseDC(hwnd, clientDc); } if (captured) { if (captureBackend) *captureBackend = "client_getdc_bitblt"; } else { captured = PrintWindow(hwnd, destination, PW_CLIENTONLY) != FALSE; if (captured && captureBackend) *captureBackend = "printwindow_fallback"; } if (!captured) return false; saveimage(path.string().c_str(), &image); return std::filesystem::exists(path) && std::filesystem::file_size(path) > 0; } catch (...) { return false; } } std::wstring makeUtf16(const std::vector& codePoints, bool& valid) { std::wstring text; valid = true; for (const std::uint32_t codePoint : codePoints) { if (codePoint > 0x10FFFFu || (codePoint >= 0xD800u && codePoint <= 0xDFFFu)) { valid = false; continue; } if (codePoint <= 0xFFFFu) text.push_back(static_cast(codePoint)); else { const std::uint32_t adjusted = codePoint - 0x10000u; text.push_back(static_cast(0xD800u + (adjusted >> 10))); text.push_back(static_cast(0xDC00u + (adjusted & 0x3FFu))); } } return text; } std::string utf16UnitsJson(const std::wstring& text) { std::ostringstream output; output << '['; for (std::size_t index = 0; index < text.size(); ++index) { if (index) output << ','; output << static_cast(static_cast(text[index])); } output << ']'; return output.str(); } std::string codePointsJson(const std::vector& codePoints) { std::ostringstream output; output << '['; for (std::size_t index = 0; index < codePoints.size(); ++index) { if (index) output << ','; output << codePoints[index]; } output << ']'; return output.str(); } std::string jsonString(const std::string& value) { std::ostringstream output; output << '"'; for (const unsigned char character : value) { switch (character) { case '"': output << "\\\""; break; case '\\': output << "\\\\"; break; case '\b': output << "\\b"; break; case '\f': output << "\\f"; break; case '\n': output << "\\n"; break; case '\r': output << "\\r"; break; case '\t': output << "\\t"; break; default: if (character < 0x20u) output << "\\u" << std::hex << std::setw(4) << std::setfill('0') << static_cast(character) << std::dec; else output << static_cast(character); break; } } output << '"'; return output.str(); } std::string hexValue(std::uint64_t value) { std::ostringstream output; output << "0x" << std::hex << std::uppercase << value; return output.str(); } void writeBoolean(std::ostream& output, bool value) { output << (value ? "true" : "false"); } void writeRectJson(std::ostream& output, const RECT& rect) { output << "{\"left\":" << rect.left << ",\"top\":" << rect.top << ",\"right\":" << rect.right << ",\"bottom\":" << rect.bottom << '}'; } void writePointJson(std::ostream& output, const POINT& point) { output << "{\"x\":" << point.x << ",\"y\":" << point.y << '}'; } void writeSizeJson(std::ostream& output, const SIZE& size) { output << "{\"cx\":" << size.cx << ",\"cy\":" << size.cy << '}'; } void writeBoundsJson(std::ostream& output, const PixelBounds& bounds) { output << "{\"present\":"; writeBoolean(output, bounds.present); output << ",\"left\":" << bounds.left << ",\"top\":" << bounds.top << ",\"right\":" << bounds.right << ",\"bottom\":" << bounds.bottom << ",\"pixelCount\":" << bounds.pixelCount << '}'; } void writeGdiStateJson(std::ostream& output, const GdiState& state) { output << "{\"mapMode\":" << state.mapMode << ",\"viewportOrigin\":"; writePointJson(output, state.viewportOrigin); output << ",\"windowOrigin\":"; writePointJson(output, state.windowOrigin); output << ",\"viewportExtent\":"; writeSizeJson(output, state.viewportExtent); output << ",\"windowExtent\":"; writeSizeJson(output, state.windowExtent); output << ",\"graphicsMode\":" << state.graphicsMode << ",\"worldTransformAvailable\":"; writeBoolean(output, state.worldTransformAvailable); output << ",\"worldTransform\":{\"m11\":" << state.worldTransform.eM11 << ",\"m12\":" << state.worldTransform.eM12 << ",\"m21\":" << state.worldTransform.eM21 << ",\"m22\":" << state.worldTransform.eM22 << ",\"dx\":" << state.worldTransform.eDx << ",\"dy\":" << state.worldTransform.eDy << "}" << ",\"backgroundMode\":" << state.backgroundMode << ",\"textColor\":" << state.textColor << ",\"backgroundColor\":" << state.backgroundColor << ",\"textAlign\":" << state.textAlign << ",\"textCharacterExtra\":" << state.textCharacterExtra << ",\"clipType\":" << state.clipType << ",\"clipBox\":"; writeRectJson(output, state.clipBox); output << '}'; } void writeSurfaceFingerprintJson(std::ostream& output, const SurfaceFingerprint& fingerprint) { output << "{\"hdc\":" << jsonString(hexValue(fingerprint.hdc)) << ",\"hbitmap\":" << jsonString(hexValue(fingerprint.hbitmap)) << ",\"hfont\":" << jsonString(hexValue(fingerprint.hfont)) << ",\"bitmapInfoAvailable\":"; writeBoolean(output, fingerprint.bitmapInfoAvailable); output << ",\"bitmapWidth\":" << fingerprint.bitmapWidth << ",\"bitmapHeight\":" << fingerprint.bitmapHeight << ",\"planes\":" << fingerprint.planes << ",\"bitsPerPixel\":" << fingerprint.bitsPerPixel << ",\"stride\":" << fingerprint.stride << ",\"dibSection\":"; writeBoolean(output, fingerprint.dibSection); output << ",\"logFontAvailable\":"; writeBoolean(output, fingerprint.logFontAvailable); output << ",\"logFont\":{" << "\"height\":" << fingerprint.logFont.lfHeight << ",\"width\":" << fingerprint.logFont.lfWidth << ",\"weight\":" << fingerprint.logFont.lfWeight << ",\"italic\":" << static_cast(fingerprint.logFont.lfItalic) << ",\"charset\":" << static_cast(fingerprint.logFont.lfCharSet) << ",\"faceNameUtf16\":["; for (std::size_t index = 0; index < LF_FACESIZE && fingerprint.logFont.lfFaceName[index]; ++index) { if (index) output << ','; output << static_cast(fingerprint.logFont.lfFaceName[index]); } output << "]}"; output << ",\"pixelDigest\":" << jsonString(hexValue(fingerprint.pixelDigest)) << ",\"gdiState\":"; writeGdiStateJson(output, fingerprint.gdiState); output << '}'; } bool writeRawResult( const Options& options, const DpiConfiguration& dpiConfiguration, const EnvironmentSnapshot& environment, bool completed, const std::vector& evidence, const std::string& resultJson, std::string& error) { const auto path = options.outputDirectory / "raw-result.json"; std::ofstream output(path, std::ios::binary | std::ios::trunc); if (!output) { error = "Cannot open raw result: " + path.string(); return false; } output << "{\n \"rawSchemaVersion\":1,\n \"mode\":" << jsonString(options.mode) << ",\n \"completed\":"; writeBoolean(output, completed); output << ",\n \"dpiConfiguration\":{\"requested\":" << jsonString(dpiConfiguration.requested) << ",\"attempted\":"; writeBoolean(output, dpiConfiguration.attempted); output << ",\"applied\":"; writeBoolean(output, dpiConfiguration.applied); output << ",\"error\":" << dpiConfiguration.error << "},\n \"environment\":{" << "\"processId\":" << environment.processId << ",\"threadId\":" << environment.threadId << ",\"windowUnicode\":"; writeBoolean(output, environment.windowUnicode); output << ",\"dpiForWindow\":" << environment.dpiForWindow << ",\"deviceDpiX\":" << environment.deviceDpiX << ",\"deviceDpiY\":" << environment.deviceDpiY << ",\"processAwareness\":" << jsonString(environment.processAwareness) << ",\"threadAwareness\":" << jsonString(environment.threadAwareness) << ",\"windowAwareness\":" << jsonString(environment.windowAwareness) << ",\"osBuild\":" << jsonString(environment.osBuild) << ",\"easyxVersion\":" << jsonString(environment.easyxVersion) << ",\"clientRect\":"; writeRectJson(output, environment.clientRect); output << ",\"windowRect\":"; writeRectJson(output, environment.windowRect); output << "},\n \"evidenceFiles\":["; for (std::size_t index = 0; index < evidence.size(); ++index) { if (index) output << ','; output << "{\"path\":" << jsonString(evidence[index].relativePath) << ",\"purpose\":" << jsonString(evidence[index].purpose) << ",\"captureBackend\":" << jsonString(evidence[index].captureBackend) << '}'; } output << "],\n \"result\":" << resultJson << "\n}\n"; if (!output.good()) { error = "Failed writing raw result: " + path.string(); return false; } return true; } }