Release v0.6.39: isolate history UI and image clipboard memory
This commit is contained in:
1 parent
fa2f6d2ce4
commit
00c9cb0ee4
22 files changed
+747
-49
No files matched your search
+112
-8
@@ -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";
|
||||
|
||||
Reference in new issue
Block a user