#include "VisualProbeCommon.h" #include #include #include #include #include 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 codePoints; }; struct TextApiResult { std::string id; std::vector 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 textResults; }; std::vector 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& 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& 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 drawWideText(HDC hdc, COLORREF background, bool& restored) { std::vector 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(index) * 105; result.extentSuccess = GetTextExtentPoint32W(hdc, result.text.data(), static_cast(result.text.size()), &result.extent) != FALSE; result.textOutSuccess = TextOutW(hdc, 20, y, result.text.data(), static_cast(result.text.size())) != FALSE; RECT drawRect{ 430, y, 930, y + 70 }; result.drawTextHeight = DrawTextW(hdc, result.text.data(), static_cast(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& 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 protectedRegionDigests; for (std::size_t index = 0; index < result.textResults.size(); ++index) { const int y = 120 + static_cast(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(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 evidence; std::vector 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; } }