feat(diagnostic): 完成阶段 0C 视觉与 IME 坐标探测

This commit is contained in:
Codex
2026-10-06 01:59:41 +08:00
parent 225bc35219
commit 43cf4ac5f7
10 changed files with 2624 additions and 0 deletions
+270
View File
@@ -0,0 +1,270 @@
#include "VisualProbeCommon.h"
#include <algorithm>
#include <array>
#include <cstdlib>
#include <iostream>
#include <sstream>
namespace VisualProbe
{
namespace
{
constexpr COLORREF Background = RGB(238, 241, 246);
constexpr COLORREF EasyXColor = RGB(220, 45, 70);
constexpr COLORREF GdiColor = RGB(20, 150, 70);
struct RoundTripResult
{
POINT client{};
POINT screen{};
POINT clientRoundTrip{};
int errorX = 0;
int errorY = 0;
bool converted = false;
};
std::string normalizedRelative(const std::filesystem::path& path)
{
std::string value = path.generic_string();
std::replace(value.begin(), value.end(), '\\', '/');
return value;
}
bool addSurfaceEvidence(
const Options& options,
HDC hdc,
const std::string& fileName,
const std::string& purpose,
std::vector<EvidenceFile>& evidence)
{
const auto relative = std::filesystem::path("evidence") / fileName;
const bool saved = saveHdcImage(hdc, CanvasWidth, CanvasHeight,
options.outputDirectory / relative);
if (saved) evidence.push_back({ normalizedRelative(relative), purpose, "surface_bitblt" });
return saved;
}
bool addClientEvidence(
const Options& options,
HWND hwnd,
const std::string& fileName,
const std::string& purpose,
std::vector<EvidenceFile>& evidence)
{
const auto relative = std::filesystem::path("evidence") / fileName;
Sleep(30);
std::string backend;
const bool saved = captureClientImage(hwnd, options.outputDirectory / relative, &backend);
if (saved) evidence.push_back({ normalizedRelative(relative), purpose, backend });
return saved;
}
RoundTripResult roundTrip(HWND hwnd, POINT client)
{
RoundTripResult result;
result.client = client;
result.screen = client;
if (!ClientToScreen(hwnd, &result.screen)) return result;
result.clientRoundTrip = result.screen;
if (!ScreenToClient(hwnd, &result.clientRoundTrip)) return result;
result.converted = true;
result.errorX = result.clientRoundTrip.x - result.client.x;
result.errorY = result.clientRoundTrip.y - result.client.y;
return result;
}
int boundsWidth(const PixelBounds& bounds)
{
return bounds.present ? bounds.right - bounds.left + 1 : 0;
}
int boundsHeight(const PixelBounds& bounds)
{
return bounds.present ? bounds.bottom - bounds.top + 1 : 0;
}
void writeRoundTrip(std::ostream& output, const RoundTripResult& result)
{
output << "{\"converted\":";
writeBoolean(output, result.converted);
output << ",\"client\":";
writePointJson(output, result.client);
output << ",\"screen\":";
writePointJson(output, result.screen);
output << ",\"clientRoundTrip\":";
writePointJson(output, result.clientRoundTrip);
output << ",\"error\":{\"x\":" << result.errorX
<< ",\"y\":" << result.errorY << "}}";
}
}
int runDpiCoordinateProbe(const Options& options, const DpiConfiguration& dpiConfiguration)
{
HWND hwnd = initgraph(CanvasWidth, CanvasHeight, EX_SHOWCONSOLE);
if (!hwnd)
{
std::cerr << "initgraph failed\n";
return 3;
}
SetWindowTextA(hwnd, "StellarX 0C DPI Coordinate Probe");
ShowWindow(hwnd, SW_SHOWNORMAL);
SetForegroundWindow(hwnd);
Sleep(80);
const EnvironmentSnapshot environment = captureEnvironment(hwnd);
SetWorkingImage(nullptr);
HDC hdc = GetImageHDC(nullptr);
std::vector<EvidenceFile> evidence;
bool evidenceComplete = true;
BeginBatchDraw();
setbkcolor(Background);
cleardevice();
setlinestyle(PS_SOLID, 1);
setlinecolor(EasyXColor);
rectangle(100, 100, 240, 180);
line(100, 140, 240, 140);
line(170, 100, 170, 180);
settextcolor(EasyXColor);
setbkmode(TRANSPARENT);
settextstyle(28, 0, "Microsoft YaHei UI");
const char* easyXText = "StellarX DPI";
outtextxy(100, 250, easyXText);
const SIZE easyXLogicalExtent{ textwidth(easyXText), textheight(easyXText) };
const GdiState stateBefore = captureGdiState(hdc);
const int saved = SaveDC(hdc);
HPEN pen = CreatePen(PS_SOLID, 1, GdiColor);
HGDIOBJ previousPen = pen ? SelectObject(hdc, pen) : nullptr;
HBRUSH previousBrush = static_cast<HBRUSH>(SelectObject(hdc, GetStockObject(NULL_BRUSH)));
SetBkMode(hdc, TRANSPARENT);
SetTextColor(hdc, GdiColor);
Rectangle(hdc, 500, 100, 641, 181);
MoveToEx(hdc, 500, 140, nullptr);
LineTo(hdc, 641, 140);
MoveToEx(hdc, 570, 100, nullptr);
LineTo(hdc, 570, 181);
const std::wstring gdiText = L"StellarX DPI";
SIZE gdiLogicalExtent{};
const bool gdiExtentSuccess = GetTextExtentPoint32W(hdc, gdiText.data(),
static_cast<int>(gdiText.size()), &gdiLogicalExtent) != FALSE;
const bool gdiTextSuccess = TextOutW(hdc, 500, 250, gdiText.data(),
static_cast<int>(gdiText.size())) != FALSE;
if (previousBrush) SelectObject(hdc, previousBrush);
if (previousPen) SelectObject(hdc, previousPen);
if (saved) RestoreDC(hdc, saved);
if (pen) DeleteObject(pen);
const GdiState stateAfter = captureGdiState(hdc);
const bool gdiStateRestored = saved != 0 && sameGdiState(stateBefore, stateAfter);
const SurfaceFingerprint afterDraw = captureSurfaceFingerprint(hdc, CanvasWidth, CanvasHeight);
const RECT easyXMarkerRegion{ 90, 90, 250, 190 };
const RECT gdiMarkerRegion{ 490, 90, 650, 190 };
const RECT easyXTextRegion{ 90, 240, 400, 310 };
const RECT gdiTextRegion{ 490, 240, 800, 310 };
const PixelBounds easyXMarkerSurface = findColorBounds(hdc, easyXMarkerRegion, EasyXColor);
const PixelBounds gdiMarkerSurface = findColorBounds(hdc, gdiMarkerRegion, GdiColor);
const PixelBounds easyXTextSurface = findChangedBounds(hdc, easyXTextRegion, Background);
const PixelBounds gdiTextSurface = findChangedBounds(hdc, gdiTextRegion, Background);
evidenceComplete &= addSurfaceEvidence(options, hdc, "dpi_after_draw_surface.png",
"DPI surface after EasyX and raw GDI drawing", evidence);
evidenceComplete &= addClientEvidence(options, hwnd, "dpi_before_flush_client.png",
"DPI client before FlushBatchDraw", evidence);
FlushBatchDraw();
evidenceComplete &= addClientEvidence(options, hwnd, "dpi_after_flush_client.png",
"DPI client after FlushBatchDraw", evidence);
EndBatchDraw();
evidenceComplete &= addClientEvidence(options, hwnd, "dpi_after_end_client.png",
"DPI client after EndBatchDraw", evidence);
PixelBounds easyXMarkerClient;
PixelBounds gdiMarkerClient;
HDC clientDc = GetDC(hwnd);
if (clientDc)
{
easyXMarkerClient = findColorBounds(clientDc, easyXMarkerRegion, EasyXColor);
gdiMarkerClient = findColorBounds(clientDc, gdiMarkerRegion, GdiColor);
ReleaseDC(hwnd, clientDc);
}
const std::array<POINT, 4> anchorPoints{
POINT{ 0, 0 }, POINT{ 100, 100 }, POINT{ 500, 250 },
POINT{ CanvasWidth - 1, CanvasHeight - 1 }
};
std::vector<RoundTripResult> roundTrips;
bool roundTripsExact = true;
for (const POINT point : anchorPoints)
{
roundTrips.push_back(roundTrip(hwnd, point));
const auto& result = roundTrips.back();
roundTripsExact = roundTripsExact && result.converted &&
result.errorX == 0 && result.errorY == 0;
}
const bool surfaceMarkerSizesAgree = easyXMarkerSurface.present && gdiMarkerSurface.present &&
std::abs(boundsWidth(easyXMarkerSurface) - boundsWidth(gdiMarkerSurface)) <= 2 &&
std::abs(boundsHeight(easyXMarkerSurface) - boundsHeight(gdiMarkerSurface)) <= 2;
const bool clientMatchesSurface = easyXMarkerClient.present && gdiMarkerClient.present &&
std::abs(boundsWidth(easyXMarkerClient) - boundsWidth(easyXMarkerSurface)) <= 1 &&
std::abs(boundsHeight(easyXMarkerClient) - boundsHeight(easyXMarkerSurface)) <= 1 &&
std::abs(boundsWidth(gdiMarkerClient) - boundsWidth(gdiMarkerSurface)) <= 1 &&
std::abs(boundsHeight(gdiMarkerClient) - boundsHeight(gdiMarkerSurface)) <= 1;
const bool implicitScalingObserved = !surfaceMarkerSizesAgree || !clientMatchesSurface;
const bool completed = evidenceComplete && gdiStateRestored && gdiExtentSuccess &&
gdiTextSuccess && easyXMarkerSurface.present && gdiMarkerSurface.present &&
easyXTextSurface.present && gdiTextSurface.present && roundTripsExact;
std::ostringstream result;
result << "{\"batchState\":\"inside\",\"surfaceFingerprintAfterDraw\":";
writeSurfaceFingerprintJson(result, afterDraw);
result << ",\"gdiStateRestored\":";
writeBoolean(result, gdiStateRestored);
result << ",\"easyXLogicalExtent\":";
writeSizeJson(result, easyXLogicalExtent);
result << ",\"gdiLogicalExtent\":";
writeSizeJson(result, gdiLogicalExtent);
result << ",\"gdiExtentSuccess\":";
writeBoolean(result, gdiExtentSuccess);
result << ",\"gdiTextSuccess\":";
writeBoolean(result, gdiTextSuccess);
result << ",\"surfaceBounds\":{\"easyXMarker\":";
writeBoundsJson(result, easyXMarkerSurface);
result << ",\"gdiMarker\":";
writeBoundsJson(result, gdiMarkerSurface);
result << ",\"easyXText\":";
writeBoundsJson(result, easyXTextSurface);
result << ",\"gdiText\":";
writeBoundsJson(result, gdiTextSurface);
result << "},\"clientBounds\":{\"easyXMarker\":";
writeBoundsJson(result, easyXMarkerClient);
result << ",\"gdiMarker\":";
writeBoundsJson(result, gdiMarkerClient);
result << "},\"surfaceMarkerSizesAgree\":";
writeBoolean(result, surfaceMarkerSizesAgree);
result << ",\"clientMatchesSurface\":";
writeBoolean(result, clientMatchesSurface);
result << ",\"implicitScalingObserved\":";
writeBoolean(result, implicitScalingObserved);
result << ",\"roundTripsExact\":";
writeBoolean(result, roundTripsExact);
result << ",\"coordinateRoundTrips\":[";
for (std::size_t index = 0; index < roundTrips.size(); ++index)
{
if (index) result << ',';
writeRoundTrip(result, roundTrips[index]);
}
result << "]}";
SetWorkingImage(nullptr);
closegraph();
std::string error;
if (!writeRawResult(options, dpiConfiguration, environment, completed,
evidence, result.str(), error))
{
std::cerr << error << '\n';
return 4;
}
return completed ? 0 : 5;
}
}
+436
View File
@@ -0,0 +1,436 @@
#include "VisualProbeCommon.h"
#include <imm.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <iostream>
#include <mutex>
#include <sstream>
#pragma comment(lib, "Imm32.lib")
namespace VisualProbe
{
namespace
{
struct Anchor
{
const char* id;
POINT controlOrigin;
POINT localCaretTopLeft;
POINT localCaretBottom;
RECT localLineRect;
};
struct ImePositionEvent
{
std::uint64_t sequence = 0;
std::string stage;
std::string positionTrigger = "none";
std::string anchorId;
DWORD threadId = 0;
std::uintptr_t hkl = 0;
std::wstring imeDescription;
std::wstring imeFileName;
bool apiAttempted = false;
bool compositionActiveAtAttempt = false;
bool contextAvailable = false;
bool compositionSucceeded = false;
bool candidateSucceeded = false;
POINT hwndRelativePoint{};
RECT hwndRelativeLineRect{};
POINT screenPoint{};
};
struct ImePositionState
{
HWND hwnd = nullptr;
WNDPROC previousWndProc = nullptr;
std::atomic<int> activeAnchor{ 1 };
std::atomic<bool> compositionActive{ false };
std::atomic<bool> closeRequested{ false };
std::atomic<std::uint64_t> nextSequence{ 0 };
std::mutex eventsMutex;
std::vector<ImePositionEvent> events;
};
constexpr std::array<Anchor, 3> Anchors{
Anchor{ "anchor_top_left_safe", POINT{ 80, 125 }, POINT{ 30, 35 }, POINT{ 30, 61 }, RECT{ 10, 30, 250, 66 } },
Anchor{ "anchor_center", POINT{ 350, 305 }, POINT{ 130, 55 }, POINT{ 130, 81 }, RECT{ 10, 50, 260, 86 } },
Anchor{ "anchor_bottom_right_safe", POINT{ 650, 520 }, POINT{ 180, 45 }, POINT{ 180, 71 }, RECT{ 10, 40, 230, 76 } }
};
ImePositionState* activeState = nullptr;
POINT surfaceCaret(const Anchor& anchor) noexcept
{
return POINT{
anchor.controlOrigin.x + anchor.localCaretBottom.x,
anchor.controlOrigin.y + anchor.localCaretBottom.y
};
}
std::wstring imeDescription(HKL hkl)
{
const UINT length = ImmGetDescriptionW(hkl, nullptr, 0);
if (!length) return {};
std::wstring value(length + 1, L'\0');
const UINT written = ImmGetDescriptionW(hkl, value.data(), static_cast<UINT>(value.size()));
value.resize(written);
return value;
}
std::wstring imeFileName(HKL hkl)
{
const UINT length = ImmGetIMEFileNameW(hkl, nullptr, 0);
if (!length) return {};
std::wstring value(length + 1, L'\0');
const UINT written = ImmGetIMEFileNameW(hkl, value.data(), static_cast<UINT>(value.size()));
value.resize(written);
return value;
}
void appendEvent(ImePositionState& state, ImePositionEvent event)
{
event.sequence = state.nextSequence.fetch_add(1, std::memory_order_relaxed) + 1;
std::lock_guard<std::mutex> lock(state.eventsMutex);
state.events.push_back(std::move(event));
}
void recordLifecycle(ImePositionState& state, const char* stage)
{
const int anchorIndex = state.activeAnchor.load(std::memory_order_acquire);
ImePositionEvent event;
event.stage = stage;
event.anchorId = Anchors[static_cast<std::size_t>(anchorIndex)].id;
event.threadId = GetCurrentThreadId();
const HKL hkl = GetKeyboardLayout(0);
event.hkl = reinterpret_cast<std::uintptr_t>(hkl);
event.imeDescription = imeDescription(hkl);
event.imeFileName = imeFileName(hkl);
appendEvent(state, std::move(event));
}
void configureImePosition(ImePositionState& state, const char* stage, const char* positionTrigger)
{
const int anchorIndex = state.activeAnchor.load(std::memory_order_acquire);
const Anchor& anchor = Anchors[static_cast<std::size_t>(anchorIndex)];
ImePositionEvent event;
event.stage = stage;
event.positionTrigger = positionTrigger;
event.anchorId = anchor.id;
event.threadId = GetCurrentThreadId();
const HKL hkl = GetKeyboardLayout(0);
event.hkl = reinterpret_cast<std::uintptr_t>(hkl);
event.imeDescription = imeDescription(hkl);
event.imeFileName = imeFileName(hkl);
event.apiAttempted = true;
event.compositionActiveAtAttempt = state.compositionActive.load(std::memory_order_acquire);
event.hwndRelativePoint = surfaceCaret(anchor);
event.hwndRelativeLineRect = RECT{
anchor.controlOrigin.x + anchor.localLineRect.left,
anchor.controlOrigin.y + anchor.localLineRect.top,
anchor.controlOrigin.x + anchor.localLineRect.right,
anchor.controlOrigin.y + anchor.localLineRect.bottom
};
event.screenPoint = event.hwndRelativePoint;
ClientToScreen(state.hwnd, &event.screenPoint);
HIMC context = ImmGetContext(state.hwnd);
event.contextAvailable = context != 0;
if (context)
{
COMPOSITIONFORM composition{};
composition.dwStyle = CFS_POINT;
composition.ptCurrentPos = event.hwndRelativePoint;
event.compositionSucceeded = ImmSetCompositionWindow(context, &composition) != FALSE;
CANDIDATEFORM candidate{};
candidate.dwIndex = 0;
candidate.dwStyle = CFS_EXCLUDE;
candidate.ptCurrentPos = event.hwndRelativePoint;
candidate.rcArea = event.hwndRelativeLineRect;
event.candidateSucceeded = ImmSetCandidateWindow(context, &candidate) != FALSE;
ImmReleaseContext(state.hwnd, context);
}
appendEvent(state, std::move(event));
}
LRESULT CALLBACK ProbeWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
ImePositionState* state = activeState;
if (!state)
return DefWindowProcA(hwnd, message, wParam, lParam);
if (message == WM_KEYDOWN && wParam >= VK_F1 && wParam <= VK_F3)
{
state->activeAnchor.store(static_cast<int>(wParam - VK_F1), std::memory_order_release);
configureImePosition(*state, "anchor_preconfigure", "anchor_change");
return 0;
}
else if (message == WM_IME_STARTCOMPOSITION)
{
state->compositionActive.store(true, std::memory_order_release);
configureImePosition(*state, "ime_start", "ime_lifecycle");
}
else if (message == WM_IME_COMPOSITION)
{
configureImePosition(*state, "ime_composition", "ime_lifecycle");
}
else if (message == WM_IME_ENDCOMPOSITION)
{
recordLifecycle(*state, "ime_end");
state->compositionActive.store(false, std::memory_order_release);
}
else if (message == WM_INPUTLANGCHANGE)
{
recordLifecycle(*state, "input_language_change");
}
else if (message == WM_CLOSE)
{
recordLifecycle(*state, "close_requested");
state->closeRequested.store(true, std::memory_order_release);
return 0;
}
return state->previousWndProc
? CallWindowProcA(state->previousWndProc, hwnd, message, wParam, lParam)
: DefWindowProcA(hwnd, message, wParam, lParam);
}
void drawAnchorScreen(int activeAnchorIndex)
{
SetWorkingImage(nullptr);
BeginBatchDraw();
setbkcolor(RGB(238, 241, 246));
cleardevice();
setbkmode(TRANSPARENT);
settextstyle(22, 0, "Microsoft YaHei UI");
settextcolor(RGB(30, 45, 70));
outtextxy(30, 25, "IME position probe: F1/F2/F3 select anchor; close window when done");
outtextxy(30, 58, "Use only fixed non-sensitive input: ceshi; inspect candidate position; Esc cancels");
for (std::size_t index = 0; index < Anchors.size(); ++index)
{
const Anchor& anchor = Anchors[index];
const bool selected = static_cast<int>(index) == activeAnchorIndex;
const COLORREF color = selected ? RGB(220, 60, 45) : RGB(45, 125, 190);
const POINT caret = surfaceCaret(anchor);
setlinecolor(color);
setlinestyle(PS_SOLID, selected ? 3 : 1);
rectangle(anchor.controlOrigin.x, anchor.controlOrigin.y,
anchor.controlOrigin.x + anchor.localLineRect.right,
anchor.controlOrigin.y + anchor.localLineRect.bottom + 12);
line(caret.x, caret.y - 26, caret.x, caret.y + 4);
line(caret.x - 5, caret.y, caret.x + 5, caret.y);
settextcolor(color);
const std::string label = std::string("F") + std::to_string(index + 1) + " " + anchor.id;
outtextxy(anchor.controlOrigin.x + 8, anchor.controlOrigin.y + 5, label.c_str());
}
EndBatchDraw();
}
std::string normalizedRelative(const std::filesystem::path& path)
{
std::string value = path.generic_string();
std::replace(value.begin(), value.end(), '\\', '/');
return value;
}
void writeAnchor(std::ostream& output, const Anchor& anchor)
{
const POINT surfaceTopLeft{
anchor.controlOrigin.x + anchor.localCaretTopLeft.x,
anchor.controlOrigin.y + anchor.localCaretTopLeft.y
};
const POINT surfaceBottom = surfaceCaret(anchor);
RECT surfaceLine{
anchor.controlOrigin.x + anchor.localLineRect.left,
anchor.controlOrigin.y + anchor.localLineRect.top,
anchor.controlOrigin.x + anchor.localLineRect.right,
anchor.controlOrigin.y + anchor.localLineRect.bottom
};
output << "{\"syntheticAnchorId\":" << jsonString(anchor.id)
<< ",\"controlOrigin\":";
writePointJson(output, anchor.controlOrigin);
output << ",\"controlLocalCaretTopLeft\":";
writePointJson(output, anchor.localCaretTopLeft);
output << ",\"controlLocalCaretBottom\":";
writePointJson(output, anchor.localCaretBottom);
output << ",\"controlLocalLineRect\":";
writeRectJson(output, anchor.localLineRect);
output << ",\"easyxSurfaceCaretTopLeft\":";
writePointJson(output, surfaceTopLeft);
output << ",\"easyxSurfaceCaretBottom\":";
writePointJson(output, surfaceBottom);
output << ",\"easyxSurfaceLineRect\":";
writeRectJson(output, surfaceLine);
output << ",\"targetHwndRelativePoint\":";
writePointJson(output, surfaceBottom);
output << '}';
}
void writeEvent(std::ostream& output, const ImePositionEvent& event)
{
output << "{\"sequence\":" << event.sequence
<< ",\"stage\":" << jsonString(event.stage)
<< ",\"positionTrigger\":" << jsonString(event.positionTrigger)
<< ",\"syntheticAnchorId\":" << jsonString(event.anchorId)
<< ",\"threadId\":" << event.threadId
<< ",\"hkl\":" << jsonString(hexValue(event.hkl))
<< ",\"imeDescriptionUtf16\":" << utf16UnitsJson(event.imeDescription)
<< ",\"imeFileNameUtf16\":" << utf16UnitsJson(event.imeFileName)
<< ",\"apiAttempted\":";
writeBoolean(output, event.apiAttempted);
output << ",\"compositionActiveAtAttempt\":";
writeBoolean(output, event.compositionActiveAtAttempt);
output << ",\"contextAvailable\":";
writeBoolean(output, event.contextAvailable);
output << ",\"compositionForm\":{\"style\":" << CFS_POINT
<< ",\"apiResult\":";
writeBoolean(output, event.compositionSucceeded);
output << "},\"candidateForm\":{\"style\":" << CFS_EXCLUDE
<< ",\"index\":0,\"apiResult\":";
writeBoolean(output, event.candidateSucceeded);
output << ",\"excludeRect\":";
writeRectJson(output, event.hwndRelativeLineRect);
output << "},\"targetHwndRelativePoint\":";
writePointJson(output, event.hwndRelativePoint);
output << ",\"diagnosticScreenPoint\":";
writePointJson(output, event.screenPoint);
output << '}';
}
}
int runImePositionProbe(const Options& options, const DpiConfiguration& dpiConfiguration)
{
HWND hwnd = initgraph(CanvasWidth, CanvasHeight, EX_SHOWCONSOLE);
if (!hwnd)
{
std::cerr << "initgraph failed\n";
return 3;
}
SetWindowTextA(hwnd, "StellarX 0C IME Position Probe");
ShowWindow(hwnd, SW_SHOWNORMAL);
SetForegroundWindow(hwnd);
Sleep(80);
const EnvironmentSnapshot environment = captureEnvironment(hwnd);
DWORD windowProcessId = 0;
const DWORD windowOwnerThread = GetWindowThreadProcessId(hwnd, &windowProcessId);
ImePositionState state;
state.hwnd = hwnd;
activeState = &state;
SetLastError(ERROR_SUCCESS);
state.previousWndProc = reinterpret_cast<WNDPROC>(SetWindowLongPtrA(
hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&ProbeWndProc)));
const DWORD subclassError = state.previousWndProc ? ERROR_SUCCESS : GetLastError();
std::vector<EvidenceFile> evidence;
bool evidenceComplete = state.previousWndProc != nullptr;
for (std::size_t index = 0; index < Anchors.size(); ++index)
{
drawAnchorScreen(static_cast<int>(index));
Sleep(40);
const auto relative = std::filesystem::path("evidence") /
(std::string(Anchors[index].id) + "_client.png");
std::string backend;
const bool saved = captureClientImage(hwnd, options.outputDirectory / relative, &backend);
evidenceComplete = evidenceComplete && saved;
if (saved)
evidence.push_back({ normalizedRelative(relative), "synthetic anchor client evidence", backend });
}
state.activeAnchor.store(1, std::memory_order_release);
drawAnchorScreen(1);
int lastDrawnAnchor = 1;
const ULONGLONG loopStarted = GetTickCount64();
while (!state.closeRequested.load(std::memory_order_acquire) && IsWindow(hwnd))
{
if (options.autoCloseMilliseconds > 0 &&
GetTickCount64() - loopStarted >= options.autoCloseMilliseconds)
{
recordLifecycle(state, "auto_close");
state.closeRequested.store(true, std::memory_order_release);
break;
}
const int currentAnchor = state.activeAnchor.load(std::memory_order_acquire);
if (currentAnchor != lastDrawnAnchor)
{
drawAnchorScreen(currentAnchor);
lastDrawnAnchor = currentAnchor;
}
Sleep(16);
}
if (state.previousWndProc && IsWindow(hwnd))
SetWindowLongPtrA(hwnd, GWLP_WNDPROC,
reinterpret_cast<LONG_PTR>(state.previousWndProc));
activeState = nullptr;
if (IsWindow(hwnd)) closegraph();
std::vector<ImePositionEvent> events;
{
std::lock_guard<std::mutex> lock(state.eventsMutex);
events = state.events;
}
std::array<bool, Anchors.size()> anchorSucceeded{};
bool imeLifecycleObserved = false;
bool imeLifecycleSucceeded = false;
for (const auto& event : events)
{
if (event.positionTrigger == "ime_lifecycle") imeLifecycleObserved = true;
if (event.positionTrigger == "ime_lifecycle" && event.contextAvailable &&
event.compositionSucceeded && event.candidateSucceeded)
{
imeLifecycleSucceeded = true;
}
if (!event.apiAttempted || !event.compositionSucceeded || !event.candidateSucceeded)
continue;
for (std::size_t index = 0; index < Anchors.size(); ++index)
{
if (event.anchorId == Anchors[index].id) anchorSucceeded[index] = true;
}
}
bool allAnchorsSucceeded = true;
for (const bool value : anchorSucceeded) allAnchorsSucceeded = allAnchorsSucceeded && value;
const bool completed = evidenceComplete && allAnchorsSucceeded && imeLifecycleSucceeded;
std::ostringstream result;
result << "{\"expectedImeName\":" << jsonString(options.expectedImeName)
<< ",\"windowOwnerThreadId\":" << windowOwnerThread
<< ",\"windowProcessId\":" << windowProcessId
<< ",\"subclassInstalled\":";
writeBoolean(result, state.previousWndProc != nullptr);
result << ",\"subclassError\":" << subclassError
<< ",\"allAnchorsApiSucceeded\":";
writeBoolean(result, allAnchorsSucceeded);
result << ",\"imeLifecycleObserved\":";
writeBoolean(result, imeLifecycleObserved);
result << ",\"imeLifecycleSucceeded\":";
writeBoolean(result, imeLifecycleSucceeded);
result << ",\"anchors\":[";
for (std::size_t index = 0; index < Anchors.size(); ++index)
{
if (index) result << ',';
writeAnchor(result, Anchors[index]);
}
result << "],\"imeEvents\":[";
for (std::size_t index = 0; index < events.size(); ++index)
{
if (index) result << ',';
writeEvent(result, events[index]);
}
result << "]}";
std::string error;
if (!writeRawResult(options, dpiConfiguration, environment, completed,
evidence, result.str(), error))
{
std::cerr << error << '\n';
return 4;
}
return completed ? 0 : 5;
}
}
+722
View File
@@ -0,0 +1,722 @@
#include "VisualProbeCommon.h"
#include <algorithm>
#include <cmath>
#include <fstream>
#include <iomanip>
#include <limits>
#include <sstream>
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<GetAwarenessFromDpiAwarenessContextFn>(
GetProcAddress(user32, "GetAwarenessFromDpiAwarenessContext")) : nullptr;
if (!getAwareness || !context)
return "not_available";
return awarenessName(static_cast<int>(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<std::uint32_t>& 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<unsigned char>((pixel >> shift) & 0xFFu);
value *= 1099511628211ull;
}
}
return value;
}
std::vector<std::uint32_t> readPixels(
HDC hdc,
int width,
int height,
int sourceX = 0,
int sourceY = 0) noexcept
{
std::vector<std::uint32_t> 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<const std::uint32_t*>(bits);
pixels.assign(first, first + static_cast<std::size_t>(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<RtlGetVersionFn>(
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<unsigned int>(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<std::string> 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<std::string> 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<SetProcessDpiAwarenessContextFn>(
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<GetDpiForWindowFn>(
GetProcAddress(user32, "GetDpiForWindow")) : nullptr;
const auto getThreadContext = user32 ? reinterpret_cast<GetThreadDpiAwarenessContextFn>(
GetProcAddress(user32, "GetThreadDpiAwarenessContext")) : nullptr;
const auto getWindowContext = user32 ? reinterpret_cast<GetWindowDpiAwarenessContextFn>(
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<GetProcessDpiAwarenessFn>(
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<std::uintptr_t>(hdc);
fingerprint.hbitmap = reinterpret_cast<std::uintptr_t>(
hdc ? GetCurrentObject(hdc, OBJ_BITMAP) : nullptr);
fingerprint.hfont = reinterpret_cast<std::uintptr_t>(
hdc ? GetCurrentObject(hdc, OBJ_FONT) : nullptr);
if (fingerprint.hfont)
{
fingerprint.logFontAvailable = GetObjectW(
reinterpret_cast<HFONT>(fingerprint.hfont),
sizeof(fingerprint.logFont), &fingerprint.logFont) == sizeof(fingerprint.logFont);
}
if (fingerprint.hbitmap)
{
BITMAP bitmap{};
if (GetObject(reinterpret_cast<HBITMAP>(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<HBITMAP>(fingerprint.hbitmap),
sizeof(section), &section) == 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<std::uint32_t>& 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<wchar_t>(codePoint));
else
{
const std::uint32_t adjusted = codePoint - 0x10000u;
text.push_back(static_cast<wchar_t>(0xD800u + (adjusted >> 10)));
text.push_back(static_cast<wchar_t>(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<unsigned int>(static_cast<std::uint16_t>(text[index]));
}
output << ']';
return output.str();
}
std::string codePointsJson(const std::vector<std::uint32_t>& 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<unsigned int>(character) << std::dec;
else
output << static_cast<char>(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<unsigned int>(fingerprint.logFont.lfItalic)
<< ",\"charset\":" << static_cast<unsigned int>(fingerprint.logFont.lfCharSet)
<< ",\"faceNameUtf16\":[";
for (std::size_t index = 0;
index < LF_FACESIZE && fingerprint.logFont.lfFaceName[index]; ++index)
{
if (index) output << ',';
output << static_cast<unsigned int>(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<EvidenceFile>& 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;
}
}
+143
View File
@@ -0,0 +1,143 @@
#pragma once
#include <easyx.h>
#include <windows.h>
#include <cstdint>
#include <filesystem>
#include <ostream>
#include <string>
#include <vector>
namespace VisualProbe
{
constexpr int CanvasWidth = 960;
constexpr int CanvasHeight = 720;
struct Options
{
std::string mode;
std::filesystem::path outputDirectory;
std::string requestedDpiAwareness = "inherit";
std::string expectedImeName = "unspecified";
unsigned int autoCloseMilliseconds = 0;
};
struct DpiConfiguration
{
std::string requested = "inherit";
bool attempted = false;
bool applied = false;
DWORD error = ERROR_SUCCESS;
};
struct EvidenceFile
{
std::string relativePath;
std::string purpose;
std::string captureBackend;
};
struct PixelBounds
{
bool present = false;
int left = 0;
int top = 0;
int right = 0;
int bottom = 0;
std::uint64_t pixelCount = 0;
};
struct GdiState
{
int mapMode = 0;
POINT viewportOrigin{};
POINT windowOrigin{};
SIZE viewportExtent{};
SIZE windowExtent{};
int graphicsMode = 0;
bool worldTransformAvailable = false;
XFORM worldTransform{};
int backgroundMode = 0;
COLORREF textColor = CLR_INVALID;
COLORREF backgroundColor = CLR_INVALID;
UINT textAlign = GDI_ERROR;
int textCharacterExtra = 0;
RECT clipBox{};
int clipType = ERROR;
};
struct SurfaceFingerprint
{
std::uintptr_t hdc = 0;
std::uintptr_t hbitmap = 0;
std::uintptr_t hfont = 0;
int bitmapWidth = 0;
int bitmapHeight = 0;
int planes = 0;
int bitsPerPixel = 0;
int stride = 0;
bool bitmapInfoAvailable = false;
bool dibSection = false;
bool logFontAvailable = false;
LOGFONTW logFont{};
std::uint64_t pixelDigest = 0;
GdiState gdiState{};
};
struct EnvironmentSnapshot
{
DWORD processId = 0;
DWORD threadId = 0;
bool windowUnicode = false;
UINT dpiForWindow = 0;
int deviceDpiX = 0;
int deviceDpiY = 0;
std::string processAwareness;
std::string threadAwareness;
std::string windowAwareness;
std::string osBuild;
std::string easyxVersion;
RECT clientRect{};
RECT windowRect{};
};
bool parseOptions(int argc, char** argv, Options& options, std::string& error);
DpiConfiguration applyRequestedDpiAwareness(const std::string& requested) noexcept;
EnvironmentSnapshot captureEnvironment(HWND hwnd) noexcept;
GdiState captureGdiState(HDC hdc) noexcept;
bool sameGdiState(const GdiState& left, const GdiState& right) noexcept;
SurfaceFingerprint captureSurfaceFingerprint(HDC hdc, int width, int height) noexcept;
std::uint64_t capturePixelRegionDigest(HDC hdc, const RECT& region) noexcept;
PixelBounds findColorBounds(HDC hdc, const RECT& region, COLORREF color) noexcept;
PixelBounds findChangedBounds(HDC hdc, const RECT& region, COLORREF background) noexcept;
bool saveHdcImage(HDC source, int width, int height, const std::filesystem::path& path) noexcept;
bool captureClientImage(
HWND hwnd,
const std::filesystem::path& path,
std::string* captureBackend = nullptr) noexcept;
std::wstring makeUtf16(const std::vector<std::uint32_t>& codePoints, bool& valid);
std::string utf16UnitsJson(const std::wstring& text);
std::string codePointsJson(const std::vector<std::uint32_t>& codePoints);
std::string jsonString(const std::string& value);
std::string hexValue(std::uint64_t value);
void writeBoolean(std::ostream& output, bool value);
void writeRectJson(std::ostream& output, const RECT& rect);
void writePointJson(std::ostream& output, const POINT& point);
void writeSizeJson(std::ostream& output, const SIZE& size);
void writeBoundsJson(std::ostream& output, const PixelBounds& bounds);
void writeGdiStateJson(std::ostream& output, const GdiState& state);
void writeSurfaceFingerprintJson(std::ostream& output, const SurfaceFingerprint& fingerprint);
bool writeRawResult(
const Options& options,
const DpiConfiguration& dpiConfiguration,
const EnvironmentSnapshot& environment,
bool completed,
const std::vector<EvidenceFile>& evidence,
const std::string& resultJson,
std::string& error);
int runWideTextProbe(const Options& options, const DpiConfiguration& dpiConfiguration);
int runDpiCoordinateProbe(const Options& options, const DpiConfiguration& dpiConfiguration);
int runImePositionProbe(const Options& options, const DpiConfiguration& dpiConfiguration);
}
+29
View File
@@ -0,0 +1,29 @@
#include "VisualProbeCommon.h"
#include <iostream>
int main(int argc, char** argv)
{
VisualProbe::Options options;
std::string error;
if (!VisualProbe::parseOptions(argc, argv, options, error))
{
std::cerr << error << "\n"
<< "Usage: EasyX.VisualProbe.exe --mode wide-text|dpi-coordinate|ime-position "
<< "--output <directory> [--dpi-awareness inherit|unaware|system|per-monitor|per-monitor-v2] "
<< "[--ime-name unspecified|MicrosoftPinyin|Sogou] [--auto-close-ms N]\n";
return 2;
}
const VisualProbe::DpiConfiguration dpiConfiguration =
VisualProbe::applyRequestedDpiAwareness(options.requestedDpiAwareness);
if (options.mode == "wide-text")
return VisualProbe::runWideTextProbe(options, dpiConfiguration);
if (options.mode == "dpi-coordinate")
return VisualProbe::runDpiCoordinateProbe(options, dpiConfiguration);
if (options.mode == "ime-position")
return VisualProbe::runImePositionProbe(options, dpiConfiguration);
std::cerr << "Unsupported mode: " << options.mode << '\n';
return 2;
}
+434
View File
@@ -0,0 +1,434 @@
#include "VisualProbeCommon.h"
#include <algorithm>
#include <array>
#include <fstream>
#include <iostream>
#include <sstream>
namespace VisualProbe
{
namespace
{
constexpr COLORREF MainBackground = RGB(22, 34, 56);
constexpr COLORREF WorkingBackground = RGB(64, 28, 46);
constexpr COLORREF EasyXMarker = RGB(30, 210, 235);
constexpr COLORREF WideTextColor = RGB(246, 224, 64);
struct TextTest
{
const char* id;
std::vector<std::uint32_t> codePoints;
};
struct TextApiResult
{
std::string id;
std::vector<std::uint32_t> codePoints;
std::wstring text;
bool validUtf16 = false;
bool extentSuccess = false;
SIZE extent{};
bool textOutSuccess = false;
int drawTextHeight = 0;
PixelBounds textOutBounds{};
PixelBounds drawTextBounds{};
};
struct WideCaseResult
{
std::string id;
std::string surfaceId;
std::string batchState;
SurfaceFingerprint before{};
SurfaceFingerprint afterDraw{};
SurfaceFingerprint afterFlush{};
SurfaceFingerprint afterEnd{};
SurfaceFingerprint afterEasyXFollowUp{};
SurfaceFingerprint nonTargetBefore{};
SurfaceFingerprint nonTargetAfterDraw{};
bool targetChanged = false;
bool nonTargetUnchanged = false;
bool gdiStateRestored = false;
bool flushApplicable = false;
bool endApplicable = false;
bool explicitWorkingComposition = false;
bool wideContentPreservedAfterEasyXFollowUp = false;
bool evidenceComplete = true;
std::vector<TextApiResult> textResults;
};
std::vector<TextTest> fixedTests()
{
return {
{ "ascii", { 0x0053, 0x0074, 0x0065, 0x006C, 0x006C, 0x0061, 0x0072,
0x0058, 0x0020, 0x0030, 0x0043, 0x0020, 0x0050, 0x0072, 0x006F, 0x0062, 0x0065 } },
{ "cjk_bmp", { 0x661F, 0x57A3, 0x7ED8, 0x5236, 0x6D4B, 0x8BD5 } },
{ "supplementary", { 0x1F600 } },
{ "combining_nfd", { 0x0065, 0x0301 } },
{ "mixed", { 0x661F, 0x57A3, 0x0020, 0x0041, 0x1F600, 0x0042,
0x0020, 0x0065, 0x0301 } }
};
}
void checkpoint(const Options& options, const std::string& value) noexcept
{
try
{
std::ofstream output(options.outputDirectory / "progress.log",
std::ios::binary | std::ios::app);
output << GetTickCount64() << ' ' << value << '\n';
output.flush();
}
catch (...)
{
}
}
void clearSurface(IMAGE* image, COLORREF background)
{
SetWorkingImage(image);
setbkcolor(background);
cleardevice();
}
std::string normalizedRelative(const std::filesystem::path& path)
{
std::string value = path.generic_string();
std::replace(value.begin(), value.end(), '\\', '/');
return value;
}
bool addSurfaceEvidence(
const Options& options,
HDC hdc,
const std::string& fileName,
const std::string& purpose,
std::vector<EvidenceFile>& evidence)
{
const std::filesystem::path relative = std::filesystem::path("evidence") / fileName;
const bool saved = saveHdcImage(hdc, CanvasWidth, CanvasHeight,
options.outputDirectory / relative);
if (saved)
evidence.push_back({ normalizedRelative(relative), purpose, "surface_bitblt" });
return saved;
}
bool addClientEvidence(
const Options& options,
HWND hwnd,
const std::string& fileName,
const std::string& purpose,
std::vector<EvidenceFile>& evidence)
{
const std::filesystem::path relative = std::filesystem::path("evidence") / fileName;
Sleep(30);
std::string backend;
const bool saved = captureClientImage(hwnd, options.outputDirectory / relative, &backend);
if (saved)
evidence.push_back({ normalizedRelative(relative), purpose, backend });
return saved;
}
void drawEasyXBaseline()
{
setfillcolor(EasyXMarker);
solidrectangle(20, 20, 170, 58);
settextcolor(EasyXMarker);
setbkmode(TRANSPARENT);
settextstyle(22, 0, "Microsoft YaHei UI");
outtextxy(20, 68, "EasyX baseline");
}
std::vector<TextApiResult> drawWideText(HDC hdc, COLORREF background, bool& restored)
{
std::vector<TextApiResult> results;
const GdiState before = captureGdiState(hdc);
const int saved = SaveDC(hdc);
HFONT font = CreateFontW(-28, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
DEFAULT_PITCH | FF_DONTCARE, L"Microsoft YaHei UI");
HGDIOBJ previousFont = font ? SelectObject(hdc, font) : nullptr;
SetBkMode(hdc, TRANSPARENT);
SetTextColor(hdc, WideTextColor);
const auto tests = fixedTests();
for (std::size_t index = 0; index < tests.size(); ++index)
{
TextApiResult result;
result.id = tests[index].id;
result.codePoints = tests[index].codePoints;
result.text = makeUtf16(result.codePoints, result.validUtf16);
const int y = 120 + static_cast<int>(index) * 105;
result.extentSuccess = GetTextExtentPoint32W(hdc, result.text.data(),
static_cast<int>(result.text.size()), &result.extent) != FALSE;
result.textOutSuccess = TextOutW(hdc, 20, y, result.text.data(),
static_cast<int>(result.text.size())) != FALSE;
RECT drawRect{ 430, y, 930, y + 70 };
result.drawTextHeight = DrawTextW(hdc, result.text.data(),
static_cast<int>(result.text.size()), &drawRect,
DT_LEFT | DT_TOP | DT_SINGLELINE | DT_NOPREFIX);
const RECT textOutRegion{ 15, y - 3, 410, y + 70 };
const RECT drawTextRegion{ 425, y - 3, 940, y + 70 };
result.textOutBounds = findChangedBounds(hdc, textOutRegion, background);
result.drawTextBounds = findChangedBounds(hdc, drawTextRegion, background);
results.push_back(std::move(result));
}
if (previousFont) SelectObject(hdc, previousFont);
if (saved) RestoreDC(hdc, saved);
if (font) DeleteObject(font);
const GdiState after = captureGdiState(hdc);
restored = saved != 0 && sameGdiState(before, after);
return results;
}
WideCaseResult runCase(
const Options& options,
HWND hwnd,
IMAGE& working,
bool targetMain,
bool insideBatch,
std::vector<EvidenceFile>& evidence)
{
WideCaseResult result;
result.surfaceId = targetMain ? "main" : "working";
result.batchState = insideBatch ? "inside" : "outside";
result.id = result.surfaceId + "_" + result.batchState;
result.flushApplicable = insideBatch;
result.endApplicable = insideBatch;
checkpoint(options, result.id + ":begin");
clearSurface(nullptr, MainBackground);
clearSurface(&working, WorkingBackground);
checkpoint(options, result.id + ":surfaces_cleared");
IMAGE* targetImage = targetMain ? nullptr : &working;
SetWorkingImage(nullptr);
HDC mainDc = GetImageHDC(nullptr);
HDC workingDc = GetImageHDC(&working);
HDC targetDc = targetMain ? mainDc : workingDc;
HDC nonTargetDc = targetMain ? workingDc : mainDc;
const COLORREF targetBackground = targetMain ? MainBackground : WorkingBackground;
result.before = captureSurfaceFingerprint(targetDc, CanvasWidth, CanvasHeight);
result.nonTargetBefore = captureSurfaceFingerprint(nonTargetDc, CanvasWidth, CanvasHeight);
checkpoint(options, result.id + ":before_captured");
SetWorkingImage(nullptr);
if (insideBatch) BeginBatchDraw();
SetWorkingImage(targetImage);
drawEasyXBaseline();
result.textResults = drawWideText(targetDc, targetBackground, result.gdiStateRestored);
checkpoint(options, result.id + ":draw_complete");
result.afterDraw = captureSurfaceFingerprint(targetDc, CanvasWidth, CanvasHeight);
result.nonTargetAfterDraw = captureSurfaceFingerprint(nonTargetDc, CanvasWidth, CanvasHeight);
checkpoint(options, result.id + ":after_draw_captured");
result.targetChanged = result.before.pixelDigest != result.afterDraw.pixelDigest;
result.nonTargetUnchanged =
result.nonTargetBefore.pixelDigest == result.nonTargetAfterDraw.pixelDigest;
result.evidenceComplete &= addSurfaceEvidence(options, targetDc,
result.id + "_after_draw_target.png", result.id + " target after draw", evidence);
result.evidenceComplete &= addSurfaceEvidence(options, nonTargetDc,
result.id + "_after_draw_non_target.png", result.id + " non-target after draw", evidence);
result.evidenceComplete &= addClientEvidence(options, hwnd,
result.id + "_after_draw_client.png", result.id + " client after draw", evidence);
checkpoint(options, result.id + ":after_draw_evidence");
if (insideBatch)
{
SetWorkingImage(nullptr);
FlushBatchDraw();
result.afterFlush = captureSurfaceFingerprint(targetDc, CanvasWidth, CanvasHeight);
result.evidenceComplete &= addClientEvidence(options, hwnd,
result.id + "_after_flush_client.png", result.id + " client after flush", evidence);
EndBatchDraw();
checkpoint(options, result.id + ":batch_ended");
result.afterEnd = captureSurfaceFingerprint(targetDc, CanvasWidth, CanvasHeight);
result.evidenceComplete &= addClientEvidence(options, hwnd,
result.id + "_after_end_client.png", result.id + " client after end", evidence);
}
else
{
result.afterFlush = result.afterDraw;
result.afterEnd = result.afterDraw;
}
std::vector<std::uint64_t> protectedRegionDigests;
for (std::size_t index = 0; index < result.textResults.size(); ++index)
{
const int y = 120 + static_cast<int>(index) * 105;
const RECT protectedRegion{ 15, y - 3, 940, y + 70 };
protectedRegionDigests.push_back(
capturePixelRegionDigest(targetDc, protectedRegion));
}
SetWorkingImage(targetImage);
setfillcolor(EasyXMarker);
solidrectangle(760, 20, 900, 55);
result.afterEasyXFollowUp = captureSurfaceFingerprint(
targetDc, CanvasWidth, CanvasHeight);
result.wideContentPreservedAfterEasyXFollowUp = true;
for (std::size_t index = 0; index < result.textResults.size(); ++index)
{
const int y = 120 + static_cast<int>(index) * 105;
const RECT protectedRegion{ 15, y - 3, 940, y + 70 };
result.wideContentPreservedAfterEasyXFollowUp =
result.wideContentPreservedAfterEasyXFollowUp &&
protectedRegionDigests[index] ==
capturePixelRegionDigest(targetDc, protectedRegion);
}
result.evidenceComplete &= addSurfaceEvidence(options, targetDc,
result.id + "_after_easyx_follow_up_target.png",
result.id + " target after EasyX follow-up drawing", evidence);
result.evidenceComplete &= addClientEvidence(options, hwnd,
result.id + "_after_easyx_follow_up_client.png",
result.id + " client after EasyX follow-up drawing", evidence);
checkpoint(options, result.id + ":easyx_follow_up_complete");
if (!targetMain)
{
SetWorkingImage(nullptr);
BeginBatchDraw();
putimage(0, 0, &working);
result.evidenceComplete &= addClientEvidence(options, hwnd,
result.id + "_composition_before_flush.png",
result.id + " explicit putimage before flush", evidence);
FlushBatchDraw();
result.evidenceComplete &= addClientEvidence(options, hwnd,
result.id + "_composition_after_flush.png",
result.id + " explicit putimage after flush", evidence);
EndBatchDraw();
result.evidenceComplete &= addClientEvidence(options, hwnd,
result.id + "_composition_after_end.png",
result.id + " explicit putimage after end", evidence);
result.explicitWorkingComposition = true;
}
SetWorkingImage(nullptr);
checkpoint(options, result.id + ":end");
return result;
}
void writeTextResult(std::ostream& output, const TextApiResult& result)
{
output << "{\"testStringId\":" << jsonString(result.id)
<< ",\"codePoints\":" << codePointsJson(result.codePoints)
<< ",\"utf16Units\":" << utf16UnitsJson(result.text)
<< ",\"isValidUtf16\":";
writeBoolean(output, result.validUtf16);
output << ",\"getTextExtentPoint32W\":";
writeBoolean(output, result.extentSuccess);
output << ",\"logicalExtent\":";
writeSizeJson(output, result.extent);
output << ",\"textOutW\":";
writeBoolean(output, result.textOutSuccess);
output << ",\"drawTextWHeight\":" << result.drawTextHeight
<< ",\"textOutInkBounds\":";
writeBoundsJson(output, result.textOutBounds);
output << ",\"drawTextInkBounds\":";
writeBoundsJson(output, result.drawTextBounds);
output << '}';
}
void writeCaseResult(std::ostream& output, const WideCaseResult& result)
{
output << "{\"testCaseId\":" << jsonString(result.id)
<< ",\"surfaceId\":" << jsonString(result.surfaceId)
<< ",\"batchState\":" << jsonString(result.batchState)
<< ",\"targetChanged\":";
writeBoolean(output, result.targetChanged);
output << ",\"nonTargetUnchanged\":";
writeBoolean(output, result.nonTargetUnchanged);
output << ",\"gdiStateRestored\":";
writeBoolean(output, result.gdiStateRestored);
output << ",\"flushApplicable\":";
writeBoolean(output, result.flushApplicable);
output << ",\"endApplicable\":";
writeBoolean(output, result.endApplicable);
output << ",\"explicitWorkingComposition\":";
writeBoolean(output, result.explicitWorkingComposition);
output << ",\"wideContentPreservedAfterEasyXFollowUp\":";
writeBoolean(output, result.wideContentPreservedAfterEasyXFollowUp);
output << ",\"evidenceComplete\":";
writeBoolean(output, result.evidenceComplete);
output << ",\"observations\":{\"afterDraw\":";
writeSurfaceFingerprintJson(output, result.afterDraw);
output << ",\"afterFlush\":";
writeSurfaceFingerprintJson(output, result.afterFlush);
output << ",\"afterEnd\":";
writeSurfaceFingerprintJson(output, result.afterEnd);
output << ",\"afterEasyXFollowUp\":";
writeSurfaceFingerprintJson(output, result.afterEasyXFollowUp);
output << "},\"before\":";
writeSurfaceFingerprintJson(output, result.before);
output << ",\"nonTargetBefore\":";
writeSurfaceFingerprintJson(output, result.nonTargetBefore);
output << ",\"nonTargetAfterDraw\":";
writeSurfaceFingerprintJson(output, result.nonTargetAfterDraw);
output << ",\"textTests\":[";
for (std::size_t index = 0; index < result.textResults.size(); ++index)
{
if (index) output << ',';
writeTextResult(output, result.textResults[index]);
}
output << "]}";
}
}
int runWideTextProbe(const Options& options, const DpiConfiguration& dpiConfiguration)
{
checkpoint(options, "probe:before_initgraph");
HWND hwnd = initgraph(CanvasWidth, CanvasHeight, EX_SHOWCONSOLE);
if (!hwnd)
{
std::cerr << "initgraph failed\n";
return 3;
}
checkpoint(options, "probe:after_initgraph");
SetWindowTextA(hwnd, "StellarX 0C Wide Text Probe");
ShowWindow(hwnd, SW_SHOWNORMAL);
SetForegroundWindow(hwnd);
Sleep(80);
const EnvironmentSnapshot environment = captureEnvironment(hwnd);
IMAGE working(CanvasWidth, CanvasHeight);
checkpoint(options, "probe:environment_and_working_ready");
std::vector<EvidenceFile> evidence;
std::vector<WideCaseResult> cases;
cases.push_back(runCase(options, hwnd, working, true, false, evidence));
cases.push_back(runCase(options, hwnd, working, true, true, evidence));
cases.push_back(runCase(options, hwnd, working, false, false, evidence));
cases.push_back(runCase(options, hwnd, working, false, true, evidence));
checkpoint(options, "probe:all_cases_complete");
bool completed = true;
for (const auto& item : cases)
{
completed = completed && item.targetChanged && item.nonTargetUnchanged &&
item.gdiStateRestored && item.wideContentPreservedAfterEasyXFollowUp &&
item.evidenceComplete;
for (const auto& text : item.textResults)
{
completed = completed && text.validUtf16 && text.extentSuccess &&
text.textOutSuccess && text.drawTextHeight > 0;
}
}
std::ostringstream result;
result << "{\"caseCount\":" << cases.size() << ",\"cases\":[";
for (std::size_t index = 0; index < cases.size(); ++index)
{
if (index) result << ',';
writeCaseResult(result, cases[index]);
}
result << "]}";
SetWorkingImage(nullptr);
closegraph();
std::string error;
if (!writeRawResult(options, dpiConfiguration, environment, completed,
evidence, result.str(), error))
{
std::cerr << error << '\n';
return 4;
}
return completed ? 0 : 5;
}
}