435 lines
16 KiB
C++
435 lines
16 KiB
C++
#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;
|
|
}
|
|
}
|