/** * @file Capture.cpp * @brief 后备缓冲抓帧、区域哈希、PNG 落盘、目录工具。 * * 全程不碰窗口 DC:读的是 `Application::BackBufferSurface()` 给的 top-down 32bpp DIB 段, * 因此窗口即使被隐藏(`SW_HIDE`)也能抓到与真实显示一致的像素。 */ #include #include #include #include #include #include #include #include #include "OffscreenSupport.hpp" namespace jdi { namespace tools { namespace { /** @brief 取 GDI+ 编码器的 CLSID。 */ bool FindEncoderClsid(const wchar_t* mimeType, CLSID& clsid) { UINT count = 0; UINT size = 0; if (Gdiplus::GetImageEncodersSize(&count, &size) != Gdiplus::Ok || size == 0) { return false; } std::vector buffer(size); Gdiplus::ImageCodecInfo* codecs = reinterpret_cast(buffer.data()); if (Gdiplus::GetImageEncoders(count, size, codecs) != Gdiplus::Ok) { return false; } for (UINT index = 0; index < count; ++index) { if (wcscmp(codecs[index].MimeType, mimeType) == 0) { clsid = codecs[index].Clsid; return true; } } return false; } /** @brief 把 ASCII 路径转宽字符串。 */ std::wstring WidenPath(const std::string& path) { const int length = MultiByteToWideChar(CP_ACP, 0, path.c_str(), -1, nullptr, 0); if (length <= 1) { return std::wstring(); } std::wstring wide(static_cast(length - 1), L'\0'); MultiByteToWideChar(CP_ACP, 0, path.c_str(), -1, &wide[0], length); return wide; } /** @brief 夹取区域到帧内,返回 false 表示空区域。 */ bool ClampRect(const Frame& frame, const PixelRect& rect, PixelRect& clamped) { const int left = std::max(0, rect.x); const int top = std::max(0, rect.y); const int right = std::min(frame.width, rect.x + rect.width); const int bottom = std::min(frame.height, rect.y + rect.height); if (right <= left || bottom <= top) { return false; } clamped.x = left; clamped.y = top; clamped.width = right - left; clamped.height = bottom - top; return true; } } // namespace bool CaptureFrame(const Application& app, Frame& frame) { const UiSurface surface = app.BackBufferSurface(); if (!surface.Valid() || surface.bits == nullptr || surface.stride <= 0) { return false; } const int width = static_cast(surface.rect.Width); const int height = static_cast(surface.rect.Height); const std::size_t bytes = static_cast(surface.stride) * static_cast(height); frame.width = width; frame.height = height; frame.stride = surface.stride; frame.pixels.resize(bytes); std::memcpy(frame.pixels.data(), surface.bits, bytes); return true; } std::uint64_t HashRegion(const Frame& frame, const PixelRect& rect) { PixelRect area; if (!frame.Valid() || !ClampRect(frame, rect, area)) { return 0ull; } std::uint64_t hash = 1469598103934665603ull; // FNV-1a 64 位 offset basis for (int row = 0; row < area.height; ++row) { const std::uint8_t* line = frame.Row(area.y + row) + static_cast(area.x) * 4u; for (int column = 0; column < area.width; ++column) { // 只混 BGR:GDI 写 DIB 时不设置 alpha 字节,混进去会让哈希随填充方式漂移。 hash ^= line[column * 4 + 0]; hash *= 1099511628211ull; hash ^= line[column * 4 + 1]; hash *= 1099511628211ull; hash ^= line[column * 4 + 2]; hash *= 1099511628211ull; } } return hash; } int CountInk(const Frame& frame, const PixelRect& rect) { PixelRect area; if (!frame.Valid() || !ClampRect(frame, rect, area)) { return 0; } // 先找到一个"主色"(出现次数最多的颜色 = 背景),再数与之不同的像素。 std::unordered_map histogram; histogram.reserve(static_cast(area.width) * 2u); for (int row = 0; row < area.height; row += 2) { const std::uint8_t* line = frame.Row(area.y + row) + static_cast(area.x) * 4u; for (int column = 0; column < area.width; column += 2) { const std::uint32_t color = static_cast(line[column * 4 + 0]) | (static_cast(line[column * 4 + 1]) << 8) | (static_cast(line[column * 4 + 2]) << 16); ++histogram[color]; } } std::uint32_t background = 0; int best = -1; for (const auto& entry : histogram) { if (entry.second > best) { best = entry.second; background = entry.first; } } int ink = 0; for (int row = 0; row < area.height; ++row) { const std::uint8_t* line = frame.Row(area.y + row) + static_cast(area.x) * 4u; for (int column = 0; column < area.width; ++column) { const std::uint32_t color = static_cast(line[column * 4 + 0]) | (static_cast(line[column * 4 + 1]) << 8) | (static_cast(line[column * 4 + 2]) << 16); if (color != background) { ++ink; } } } return ink; } int CountDiff(const Frame& left, const Frame& right, const PixelRect& rect, const int tolerance) { PixelRect area; if (!left.Valid() || !right.Valid() || left.width != right.width || left.height != right.height || !ClampRect(left, rect, area)) { return -1; } int changed = 0; for (int row = 0; row < area.height; ++row) { const std::uint8_t* a = left.Row(area.y + row) + static_cast(area.x) * 4u; const std::uint8_t* b = right.Row(area.y + row) + static_cast(area.x) * 4u; for (int column = 0; column < area.width; ++column) { const int deltaB = std::abs(static_cast(a[column * 4 + 0]) - b[column * 4 + 0]); const int deltaG = std::abs(static_cast(a[column * 4 + 1]) - b[column * 4 + 1]); const int deltaR = std::abs(static_cast(a[column * 4 + 2]) - b[column * 4 + 2]); if (deltaB > tolerance || deltaG > tolerance || deltaR > tolerance) { ++changed; } } } return changed; } bool SavePng(const Frame& frame, const std::string& path) { if (!frame.Valid()) { return false; } const std::size_t slash = path.find_last_of("/\\"); if (slash != std::string::npos) { EnsureDirectory(path.substr(0, slash)); } Gdiplus::Bitmap bitmap(frame.width, frame.height, frame.stride, PixelFormat32bppRGB, const_cast(frame.pixels.data())); if (bitmap.GetLastStatus() != Gdiplus::Ok) { return false; } CLSID pngClsid = {}; if (!FindEncoderClsid(L"image/png", pngClsid)) { return false; } const std::wstring widePath = WidenPath(path); const Gdiplus::Status status = bitmap.Save(widePath.c_str(), &pngClsid, nullptr); return status == Gdiplus::Ok; } void EnsureDirectory(const std::string& asciiPath) { if (asciiPath.empty()) { return; } const std::wstring wide = WidenPath(asciiPath); if (wide.empty()) { return; } std::wstring prefix; prefix.reserve(wide.size()); for (std::size_t index = 0; index < wide.size(); ++index) { prefix.push_back(wide[index]); const bool boundary = (wide[index] == L'/' || wide[index] == L'\\' || index + 1 == wide.size()); if (boundary) { if (!prefix.empty() && prefix.back() != L'/' && prefix.back() != L'\\') { CreateDirectoryW(prefix.c_str(), nullptr); } } } } void RemoveDirectoryTree(const std::string& asciiPath) { const std::wstring wide = WidenPath(asciiPath); if (wide.empty()) { return; } // 用 SHFileOperation 太重;这里用"先递归删文件再删目录"的最小实现。 WIN32_FIND_DATAW data = {}; HANDLE handle = FindFirstFileW((wide + L"\\*").c_str(), &data); if (handle != INVALID_HANDLE_VALUE) { do { if (wcscmp(data.cFileName, L".") == 0 || wcscmp(data.cFileName, L"..") == 0) { continue; } const std::wstring child = wide + L"\\" + data.cFileName; if ((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { const int length = WideCharToMultiByte(CP_ACP, 0, child.c_str(), -1, nullptr, 0, nullptr, nullptr); std::string narrow(static_cast(std::max(0, length - 1)), '\0'); if (length > 1) { WideCharToMultiByte(CP_ACP, 0, child.c_str(), -1, &narrow[0], length, nullptr, nullptr); } RemoveDirectoryTree(narrow); } else { DeleteFileW(child.c_str()); } } while (FindNextFileW(handle, &data) != FALSE); FindClose(handle); } RemoveDirectoryW(wide.c_str()); } } // namespace tools } // namespace jdi