feat(diagnostic): 完成阶段 0C 视觉与 IME 坐标探测
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user