Release v0.6.39: isolate history UI and image clipboard memory

This commit is contained in:
agrynco committed 2026-11-04 19:40:54 +02:00
1 parent fa2f6d2ce4
commit 00c9cb0ee4
22 files changed
+747 -49

No files matched your search

+112 -8
View File
@@ -410,6 +410,87 @@ void History::loadIndex()
}
entries_ = std::move(loaded);
}
void History::reload() { std::lock_guard lock(mutex_); loadIndex(); }
void ClipboardSnapshot::cleanup() noexcept
{
if (request_.empty()) return;
std::error_code ignored;
for (const auto *suffix : {L"", L".snapshot", L".result"})
std::filesystem::remove(std::filesystem::path(request_.wstring() + suffix), ignored);
request_.clear();
}
ClipboardSnapshot::~ClipboardSnapshot() { cleanup(); }
DWORD ClipboardSnapshot::run(bool restore, DWORD sequence)
{
atomicWrite(request_, protect(jsonBytes(Json{{"schema", 1}, {"restore", restore}, {"sequence", sequence}})));
auto helper = std::filesystem::path(executable()).parent_path() / L"KSwitcher.Screenshot.exe";
auto command = L"\"" + helper.wstring() + L"\" --clipboard-snapshot \"" + request_.wstring() + L"\"";
STARTUPINFOW startup{sizeof(startup)};
startup.dwFlags = STARTF_USESHOWWINDOW; startup.wShowWindow = SW_HIDE;
PROCESS_INFORMATION process{};
if (!CreateProcessW(helper.c_str(), command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW,
nullptr, helper.parent_path().c_str(), &startup, &process))
throw std::runtime_error("Cannot start clipboard snapshot helper");
CloseHandle(process.hThread);
auto wait = WaitForSingleObject(process.hProcess, 10000);
DWORD result = ERROR_GEN_FAILURE;
if (wait != WAIT_OBJECT_0)
{
TerminateProcess(process.hProcess, ERROR_TIMEOUT);
WaitForSingleObject(process.hProcess, INFINITE);
}
GetExitCodeProcess(process.hProcess, &result);
CloseHandle(process.hProcess);
if (wait != WAIT_OBJECT_0 || result) throw std::runtime_error("Clipboard snapshot helper failed");
return parse(unprotect(readFile(request_.wstring() + L".result", 4096))).at("sequence").get<DWORD>();
}
void ClipboardSnapshot::capture(HWND window, DWORD sequence)
{
cleanup();
clip_ = {};
if (!IsClipboardFormatAvailable(CF_DIB) && !IsClipboardFormatAvailable(CF_DIBV5) &&
!IsClipboardFormatAvailable(CF_BITMAP) && !IsClipboardFormatAvailable(RegisterClipboardFormatW(L"PNG")))
{
clip_ = readClipboard(window, 64 * 1024 * 1024, ClipboardRead::WithoutImages, sequence);
return;
}
request_ = std::filesystem::temp_directory_path() / (L"ks-clipboard-" + newSnippetId() + L".request");
try { run(false, sequence); }
catch (...) { cleanup(); throw; }
}
DWORD ClipboardSnapshot::restore(HWND window, DWORD sequence)
{
return request_.empty() ? writeClipboardGuarded(window, clip_, sequence) : run(true, sequence);
}
int runClipboardSnapshot(const std::filesystem::path &request)
{
try
{
if (!request.is_absolute() || !request.filename().wstring().starts_with(L"ks-clipboard-") ||
request.extension() != L".request") return 1;
auto job = parse(unprotect(readFile(request, 4096)));
if (job.at("schema") != 1) return 1;
auto sequence = job.at("sequence").get<DWORD>();
const auto snapshot = request.wstring() + L".snapshot";
if (job.at("restore").get<bool>())
{
auto clip = decodeClip(unprotect(readFile(snapshot, 70 * 1024 * 1024)));
HWND window = CreateWindowExW(0, L"STATIC", L"", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, nullptr, nullptr);
if (!window) return 1;
struct WindowCleanup { HWND window; ~WindowCleanup() { DestroyWindow(window); } } cleanup{window};
sequence = writeClipboardGuarded(window, clip, sequence);
}
else
{
auto clip = readClipboard(nullptr, 64 * 1024 * 1024, ClipboardRead::All, sequence);
if (sequence != GetClipboardSequenceNumber()) return 2;
atomicWrite(snapshot, protect(encodeClip(clip)));
}
atomicWrite(request.wstring() + L".result", protect(jsonBytes(Json{{"sequence", sequence}})));
return 0;
}
catch (...) { return 1; }
}
bool History::captureImage(DWORD sequence, const std::wstring &app, const Settings &settings)
{
// Serialize helper index changes with all resident history mutations. Only
@@ -706,12 +787,12 @@ void History::clear()
std::filesystem::remove(folder_ / (e.id + L".ocr"), ec);
}
}
std::wstring History::recognizeImage(const std::wstring &id, const Settings &settings,
const std::function<bool()> &cancelled) const
static Bytes historyImageJob(const std::filesystem::path &folder, const std::wstring &id,
const Json &job, const std::function<bool()> &cancelled)
{
if (!validId(id)) throw std::runtime_error("Invalid history ID");
if (cancelled && cancelled()) throw std::runtime_error("History OCR cancelled");
auto request = std::filesystem::absolute(folder_) / (L"ocr-" + newSnippetId() + L".request");
auto request = std::filesystem::absolute(folder) / (L"ocr-" + newSnippetId() + L".request");
auto response = request; response += L".result";
struct Cleanup {
std::filesystem::path request, response;
@@ -721,9 +802,6 @@ std::wstring History::recognizeImage(const std::wstring &id, const Settings &set
std::filesystem::remove(response, ignored);
}
} cleanup{request, response};
// Send only OCR options and the entry ID. The resident never loads image bytes.
Json job{{"schema", 1}, {"id", utf8(id)}, {"tesseract", settings.useTesseract},
{"path", utf8(settings.tesseractPath)}, {"languages", utf8(settings.tesseractLanguages)}};
atomicWrite(request, protect(jsonBytes(job)));
auto helper = std::filesystem::path(executable()).parent_path() / L"KSwitcher.Screenshot.exe";
auto command = L"\"" + helper.wstring() + L"\" --history-ocr \"" + request.wstring() + L"\"";
@@ -755,11 +833,28 @@ std::wstring History::recognizeImage(const std::wstring &id, const Settings &set
DWORD result{};
if (!GetExitCodeProcess(child.handle, &result) || result)
throw std::runtime_error("History OCR helper failed: " + std::to_string(result));
auto reply = parse(unprotect(readFile(response, 8 * 1024 * 1024)));
return unprotect(readFile(response, 8 * 1024 * 1024));
}
std::wstring History::recognizeImage(const std::wstring &id, const Settings &settings,
const std::function<bool()> &cancelled) const
{
Json job{{"schema", 1}, {"id", utf8(id)}, {"tesseract", settings.useTesseract},
{"path", utf8(settings.tesseractPath)}, {"languages", utf8(settings.tesseractLanguages)}};
auto reply = parse(historyImageJob(folder_, id, job, cancelled));
if (reply.at("schema") != 1 || str(reply, "id") != id)
throw std::runtime_error("Invalid history OCR response");
return str(reply, "text");
}
Pixels History::thumbnail(const std::wstring &id, const std::function<bool()> &cancelled) const
{
auto bytes = historyImageJob(folder_, id, Json{{"schema", 1}, {"id", utf8(id)}, {"thumbnail", true}}, cancelled);
size_t at = 0;
auto width = get32(bytes, at), height = get32(bytes, at);
if (!width || !height || width > 208 || height > 128 || bytes.size() - at != width * height * 4)
throw std::runtime_error("Invalid history thumbnail response");
bytes.erase(bytes.begin(), bytes.begin() + at);
return Pixels{width, height, std::move(bytes)};
}
int runHistoryOcr(const std::filesystem::path &request)
{
try {
@@ -768,11 +863,20 @@ int runHistoryOcr(const std::filesystem::path &request)
auto job = parse(unprotect(readFile(request, 1024 * 1024)));
auto id = str(job, "id");
if (job.at("schema") != 1 || !validId(id)) return 1;
auto clip = decodeClip(unprotect(readFile(request.parent_path() / (id + L".dat"), 80 * 1024 * 1024)));
if (job.value("thumbnail", false)) {
auto pixels = decodeImage(clip, 208, 128);
if (!pixels.width || !pixels.height) return 1;
Bytes reply;
put32(reply, pixels.width); put32(reply, pixels.height);
reply.insert(reply.end(), pixels.bgra.begin(), pixels.bgra.end());
atomicWrite(request.wstring() + L".result", protect(reply));
return 0;
}
Settings settings;
settings.useTesseract = job.at("tesseract").get<bool>();
settings.tesseractPath = str(job, "path");
settings.tesseractLanguages = str(job, "languages");
auto clip = decodeClip(unprotect(readFile(request.parent_path() / (id + L".dat"), 80 * 1024 * 1024)));
auto text = utf8(recognizeText(clip, settings));
if (text.size() > 1024 * 1024) return 1;
auto response = request; response += L".result";