Release v0.6.37: isolate image capture and OCR, fingerprint history duplicates
This commit is contained in:
+18
-2
@@ -536,7 +536,10 @@ void App::scheduleHistoryOcr(const Settings &config)
|
||||
if (!entry.image || (entry.ocrState != 1 && entry.ocrProfile == profile)) continue;
|
||||
try
|
||||
{
|
||||
auto text = recognizeText(history->load(entry.id), config);
|
||||
auto text = history->recognizeImage(entry.id, config, [this, &profile] {
|
||||
std::lock_guard lock(jobsMutex);
|
||||
return stop || ocrProfile(historyOcrSettings) != profile;
|
||||
});
|
||||
std::lock_guard lock(jobsMutex);
|
||||
if (stop || ocrProfile(historyOcrSettings) != profile) continue;
|
||||
if (history->saveOcr(entry.id, text, config))
|
||||
@@ -1585,6 +1588,17 @@ void App::captureHistory()
|
||||
for (auto &name : config.excluded)
|
||||
if (lower(name) == lower(app))
|
||||
return;
|
||||
if (IsClipboardFormatAvailable(CF_DIB) || IsClipboardFormatAvailable(CF_DIBV5) ||
|
||||
IsClipboardFormatAvailable(CF_BITMAP) ||
|
||||
IsClipboardFormatAvailable(RegisterClipboardFormatW(L"PNG")))
|
||||
{
|
||||
if (history->captureImage(sequence, app, config))
|
||||
{
|
||||
scheduleHistoryOcr(config);
|
||||
PostMessageW(window, HistoryMessage, 0, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
auto clip = readClipboard(window, (size_t)config.maxImageMiB * 1024 * 1024);
|
||||
if (sequence != GetClipboardSequenceNumber())
|
||||
return;
|
||||
@@ -1622,7 +1636,9 @@ void App::recognizeEntry(std::wstring id)
|
||||
try
|
||||
{
|
||||
auto cached = history->ocrText(id, ocrProfile(config));
|
||||
auto text = cached ? *cached : recognizeText(history->load(id), config);
|
||||
auto text = cached ? *cached : history->recognizeImage(id, config, [this] {
|
||||
std::lock_guard lock(jobsMutex); return stop;
|
||||
});
|
||||
if (!cached) history->saveOcr(id, text, config);
|
||||
if (text.empty())
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "screenshot.h"
|
||||
#include "screenshot_notifications.h"
|
||||
#include "storage.h"
|
||||
#include <commctrl.h>
|
||||
#include <shellapi.h>
|
||||
#include <roapi.h>
|
||||
@@ -49,6 +50,14 @@ int WINAPI wWinMain(HINSTANCE instance,HINSTANCE,PWSTR,int)
|
||||
std::wstring token;
|
||||
bool autoOpenEditor=false, writeLog=false;
|
||||
int argc{}; auto argv=CommandLineToArgvW(GetCommandLineW(),&argc);
|
||||
if(argc==3 && (std::wstring(argv[1])==L"--capture-history" || std::wstring(argv[1])==L"--history-ocr")) {
|
||||
bool ocr=std::wstring(argv[1])==L"--history-ocr";
|
||||
std::filesystem::path request=argv[2]; LocalFree(argv);
|
||||
auto initialized=CoInitializeEx(nullptr,COINIT_MULTITHREADED);
|
||||
if(FAILED(initialized)) return 1;
|
||||
int result=ocr ? ks::runHistoryOcr(request) : ks::runImageHistoryCapture(request);
|
||||
CoUninitialize(); return result;
|
||||
}
|
||||
if(argc>1 && std::wstring(argv[argc-1])==L"--write-log") {
|
||||
writeLog=true; --argc;
|
||||
ks::setLoggingEnabled(true);
|
||||
|
||||
+235
-18
@@ -4,6 +4,7 @@
|
||||
#include <algorithm>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <wincrypt.h>
|
||||
|
||||
namespace ks
|
||||
{
|
||||
@@ -374,9 +375,17 @@ Clip decodeClip(const Bytes &b)
|
||||
History::History(const std::filesystem::path &folder) : folder_(folder)
|
||||
{
|
||||
std::filesystem::create_directories(folder_);
|
||||
loadIndex();
|
||||
}
|
||||
void History::loadIndex()
|
||||
{
|
||||
std::vector<Entry> loaded;
|
||||
auto index = folder_ / L"index.dat";
|
||||
if (!std::filesystem::exists(index))
|
||||
{
|
||||
entries_.clear();
|
||||
return;
|
||||
}
|
||||
auto j = parse(unprotect(readFile(index, 16 * 1024 * 1024)));
|
||||
if (!j.is_array() || j.size() > 10000)
|
||||
throw std::runtime_error("Invalid history index");
|
||||
@@ -391,8 +400,88 @@ History::History(const std::filesystem::path &folder) : folder_(folder)
|
||||
e.value("image", false),
|
||||
e.value("files", false), e.value("ocrState", e.value("image", false) ? 1 : 0),
|
||||
str(e, "ocrProfile", L"windows")};
|
||||
if (e.value("fingerprintVersion", 0) == 1) {
|
||||
item.contentHash = e.value("contentHash", std::string{});
|
||||
item.imageHash = e.value("imageHash", std::string{});
|
||||
item.pixelHash = e.value("pixelHash", std::string{});
|
||||
}
|
||||
if (validId(item.id) && std::filesystem::exists(folder_ / (item.id + L".dat")))
|
||||
entries_.push_back(std::move(item));
|
||||
loaded.push_back(std::move(item));
|
||||
}
|
||||
entries_ = std::move(loaded);
|
||||
}
|
||||
bool History::captureImage(DWORD sequence, const std::wstring &app, const Settings &settings)
|
||||
{
|
||||
// Serialize helper index changes with all resident history mutations. Only
|
||||
// metadata is reloaded: GetClipboardData's image allocations die with the child.
|
||||
std::lock_guard lock(mutex_);
|
||||
if (sequence != GetClipboardSequenceNumber()) return false;
|
||||
auto request = std::filesystem::absolute(folder_) / (L"capture-" + newSnippetId() + L".request");
|
||||
struct Cleanup {
|
||||
std::filesystem::path path;
|
||||
~Cleanup() { std::error_code ignored; std::filesystem::remove(path, ignored); }
|
||||
} cleanup{request};
|
||||
Json job{{"schema", 1}, {"sequence", sequence}, {"app", utf8(app)},
|
||||
{"settings", Json::parse(encodeSettings(settings))}};
|
||||
atomicWrite(request, protect(jsonBytes(job)));
|
||||
auto helper = std::filesystem::path(executable()).parent_path() / L"KSwitcher.Screenshot.exe";
|
||||
auto command = L"\"" + helper.wstring() + L"\" --capture-history \"" + 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(utf8(lastError(L"Cannot start image history helper")));
|
||||
CloseHandle(process.hThread);
|
||||
auto wait = WaitForSingleObject(process.hProcess, 30000);
|
||||
DWORD result = ERROR_GEN_FAILURE;
|
||||
if (wait != WAIT_OBJECT_0)
|
||||
{
|
||||
// Never release the index lock while a timed-out child can still write it.
|
||||
TerminateProcess(process.hProcess, ERROR_TIMEOUT);
|
||||
WaitForSingleObject(process.hProcess, INFINITE);
|
||||
}
|
||||
GetExitCodeProcess(process.hProcess, &result);
|
||||
CloseHandle(process.hProcess);
|
||||
loadIndex(); // Also reconcile any completed atomic save before a child failure.
|
||||
if (wait != WAIT_OBJECT_0) throw std::runtime_error("Image history helper timed out");
|
||||
if (result == 2) return false;
|
||||
if (result == 3) throw std::runtime_error("Clipboard exceeds the configured size limit");
|
||||
if (result) throw std::runtime_error("Image history helper failed: " + std::to_string(result));
|
||||
return true;
|
||||
}
|
||||
int runImageHistoryCapture(const std::filesystem::path &request)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!request.is_absolute() || !request.filename().wstring().starts_with(L"capture-") ||
|
||||
request.extension() != L".request") return 1;
|
||||
auto job = parse(unprotect(readFile(request, 4 * 1024 * 1024)));
|
||||
if (job.at("schema") != 1) return 1;
|
||||
auto sequence = job.at("sequence").get<DWORD>();
|
||||
auto settings = decodeSettings(jsonBytes(job.at("settings")), readLayouts());
|
||||
auto valid = [&] {
|
||||
return settings.history && sequence == GetClipboardSequenceNumber() &&
|
||||
!IsClipboardFormatAvailable(RegisterClipboardFormatW(L"ExcludeClipboardContentFromMonitorProcessing")) &&
|
||||
!IsClipboardFormatAvailable(RegisterClipboardFormatW(L"Clipboard Viewer Ignore"));
|
||||
};
|
||||
if (!valid()) return 2;
|
||||
if (!IsClipboardFormatAvailable(CF_DIB) && !IsClipboardFormatAvailable(CF_DIBV5) &&
|
||||
!IsClipboardFormatAvailable(CF_BITMAP) &&
|
||||
!IsClipboardFormatAvailable(RegisterClipboardFormatW(L"PNG"))) return 2;
|
||||
auto clip = readClipboard(nullptr, size_t(settings.maxImageMiB) * 1024 * 1024);
|
||||
if (!valid()) return 2;
|
||||
for (const auto &format : clip.formats)
|
||||
if (format.name == L"CanIncludeInClipboardHistory" && format.data.size() >= 4 &&
|
||||
!format.data[0] && !format.data[1] && !format.data[2] && !format.data[3]) return 2;
|
||||
History history(request.parent_path());
|
||||
if (!valid()) return 2;
|
||||
history.add(clip, str(job, "app"), settings);
|
||||
return 0;
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
return std::string(e.what()) == "Clipboard exceeds the configured size limit" ? 3 : 1;
|
||||
}
|
||||
}
|
||||
void History::saveIndex()
|
||||
@@ -406,7 +495,9 @@ void History::saveIndex()
|
||||
{"at", e.at},
|
||||
{"pinned", e.pinned},
|
||||
{"image", e.image},
|
||||
{"files", e.files}, {"ocrState", e.ocrState}, {"ocrProfile", utf8(e.ocrProfile)}});
|
||||
{"files", e.files}, {"ocrState", e.ocrState}, {"ocrProfile", utf8(e.ocrProfile)},
|
||||
{"fingerprintVersion", 1}, {"contentHash", e.contentHash},
|
||||
{"imageHash", e.imageHash}, {"pixelHash", e.pixelHash}});
|
||||
atomicWrite(folder_ / L"index.dat", protect(jsonBytes(j)));
|
||||
}
|
||||
std::vector<Entry> History::entries() const
|
||||
@@ -439,12 +530,10 @@ void History::prune(const Settings &s)
|
||||
entries_.erase(entries_.begin() + i);
|
||||
}
|
||||
}
|
||||
bool sameClipboardImage(const Clip &a, const Clip &b)
|
||||
static const Format *clipboardImageFormat(const Clip &c)
|
||||
{
|
||||
// OLE DataObject / Ole Private Data change when Snipping Tool republishes
|
||||
// the same capture. They are transport metadata, not image identity.
|
||||
auto image = [](const Clip &c) -> const Format *
|
||||
{
|
||||
if (!c.text().empty())
|
||||
return nullptr;
|
||||
for (const auto &f : c.formats)
|
||||
@@ -458,8 +547,42 @@ bool sameClipboardImage(const Clip &a, const Clip &b)
|
||||
if (f.id == id && !f.data.empty())
|
||||
return &f;
|
||||
return nullptr;
|
||||
};
|
||||
const auto *left = image(a), *right = image(b);
|
||||
}
|
||||
static std::string clipboardHash(const Bytes &bytes)
|
||||
{
|
||||
struct Hash {
|
||||
HCRYPTPROV provider{}; HCRYPTHASH hash{};
|
||||
~Hash() { if (hash) CryptDestroyHash(hash); if (provider) CryptReleaseContext(provider, 0); }
|
||||
} state;
|
||||
if (!CryptAcquireContextW(&state.provider, nullptr, nullptr, PROV_RSA_AES, CRYPT_VERIFYCONTEXT) ||
|
||||
!CryptCreateHash(state.provider, CALG_SHA_256, 0, 0, &state.hash) ||
|
||||
!CryptHashData(state.hash, bytes.data(), static_cast<DWORD>(bytes.size()), 0))
|
||||
throw std::runtime_error("Cannot hash clipboard data");
|
||||
BYTE digest[32]{}; DWORD size = sizeof(digest);
|
||||
if (!CryptGetHashParam(state.hash, HP_HASHVAL, digest, &size, 0) || size != sizeof(digest))
|
||||
throw std::runtime_error("Cannot read clipboard hash");
|
||||
static constexpr char hex[] = "0123456789abcdef";
|
||||
std::string result; result.reserve(64);
|
||||
for (auto b : digest) { result += hex[b >> 4]; result += hex[b & 15]; }
|
||||
return result;
|
||||
}
|
||||
static void imageFingerprints(Entry &entry, const Clip &clip, bool decodePixels = true)
|
||||
{
|
||||
const auto *image = clipboardImageFormat(clip);
|
||||
if (!image) return;
|
||||
if (entry.imageHash.empty())
|
||||
entry.imageHash = std::to_string(image->id) + ":" + utf8(image->name) + ":" + clipboardHash(image->data);
|
||||
if (!decodePixels) return;
|
||||
try {
|
||||
auto pixels = decodeImage(clip);
|
||||
entry.pixelHash = std::to_string(pixels.width) + "x" + std::to_string(pixels.height) + ":" + clipboardHash(pixels.bgra);
|
||||
} catch (const std::exception &) {
|
||||
// Undecodable images can still be compared by their original bytes.
|
||||
}
|
||||
}
|
||||
bool sameClipboardImage(const Clip &a, const Clip &b)
|
||||
{
|
||||
const auto *left = clipboardImageFormat(a), *right = clipboardImageFormat(b);
|
||||
if (!left || !right)
|
||||
return false;
|
||||
if (left->id == right->id && left->name == right->name && left->data == right->data)
|
||||
@@ -483,15 +606,40 @@ void History::add(const Clip &clip, const std::wstring &app, const Settings &s)
|
||||
auto data = encodeClip(clip);
|
||||
if (data.size() > uint64_t(s.maxImageMiB) * 1024 * 1024)
|
||||
return;
|
||||
bool image = false, files = false;
|
||||
for (const auto &f : clip.formats)
|
||||
{
|
||||
image |= f.id == CF_DIB || f.id == CF_DIBV5 || f.name == L"PNG";
|
||||
files |= f.id == CF_HDROP;
|
||||
}
|
||||
std::lock_guard lock(mutex_);
|
||||
// Compare only the newest record; never keep image payloads in the resident index.
|
||||
if (!entries_.empty())
|
||||
Entry fingerprints;
|
||||
fingerprints.contentHash = clipboardHash(data);
|
||||
if (!entries_.empty() && entries_.front().contentHash == fingerprints.contentHash)
|
||||
return;
|
||||
imageFingerprints(fingerprints, clip, false);
|
||||
if (!entries_.empty() && !fingerprints.imageHash.empty() && entries_.front().imageHash == fingerprints.imageHash)
|
||||
return;
|
||||
imageFingerprints(fingerprints, clip);
|
||||
// Different payload kinds cannot be duplicates. Consult the index first so
|
||||
// copying text after a screenshot never loads/decrypts the previous image.
|
||||
if (!entries_.empty() && entries_.front().image == image && entries_.front().files == files)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto previous = load(entries_.front().id);
|
||||
if (encodeClip(previous) == data || sameClipboardImage(previous, clip))
|
||||
auto &previous = entries_.front();
|
||||
bool migrated = previous.contentHash.empty();
|
||||
if (migrated) {
|
||||
auto oldClip = load(previous.id);
|
||||
previous.contentHash = clipboardHash(encodeClip(oldClip));
|
||||
imageFingerprints(previous, oldClip);
|
||||
}
|
||||
if (previous.contentHash == fingerprints.contentHash ||
|
||||
(!fingerprints.imageHash.empty() && previous.imageHash == fingerprints.imageHash) ||
|
||||
(!fingerprints.pixelHash.empty() && previous.pixelHash == fingerprints.pixelHash)) {
|
||||
if (migrated) saveIndex();
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
@@ -506,12 +654,6 @@ void History::add(const Clip &clip, const std::wstring &app, const Settings &s)
|
||||
auto encrypted = protect(data);
|
||||
atomicWrite(folder_ / (id + L".dat"), encrypted);
|
||||
auto text = clip.text();
|
||||
bool image = false, files = false;
|
||||
for (auto &f : clip.formats)
|
||||
{
|
||||
image |= f.id == CF_DIB || f.id == CF_DIBV5 || f.name == L"PNG";
|
||||
files |= f.id == CF_HDROP;
|
||||
}
|
||||
if (text.empty())
|
||||
text = files ? L"[Files]" : image ? L"[Image]" : L"[Clipboard data]";
|
||||
for (auto &c : text)
|
||||
@@ -524,7 +666,8 @@ void History::add(const Clip &clip, const std::wstring &app, const Settings &s)
|
||||
entries_.insert(entries_.begin(),
|
||||
Entry{id, text, app, encrypted.size(),
|
||||
(uint64_t(now.dwHighDateTime) << 32) | now.dwLowDateTime, false, image, files,
|
||||
image ? 1 : 0, ocrProfile(s)});
|
||||
image ? 1 : 0, ocrProfile(s), std::move(fingerprints.contentHash),
|
||||
std::move(fingerprints.imageHash), std::move(fingerprints.pixelHash)});
|
||||
prune(s);
|
||||
saveIndex();
|
||||
}
|
||||
@@ -563,6 +706,80 @@ 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
|
||||
{
|
||||
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 response = request; response += L".result";
|
||||
struct Cleanup {
|
||||
std::filesystem::path request, response;
|
||||
~Cleanup() {
|
||||
std::error_code ignored;
|
||||
std::filesystem::remove(request, ignored);
|
||||
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"\"";
|
||||
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(utf8(lastError(L"Cannot start history OCR helper")));
|
||||
CloseHandle(process.hThread);
|
||||
struct Child {
|
||||
HANDLE handle;
|
||||
~Child() {
|
||||
if (WaitForSingleObject(handle, 0) != WAIT_OBJECT_0) {
|
||||
TerminateProcess(handle, ERROR_CANCELLED);
|
||||
WaitForSingleObject(handle, INFINITE);
|
||||
}
|
||||
CloseHandle(handle);
|
||||
}
|
||||
} child{process.hProcess};
|
||||
auto deadline = GetTickCount64() + 60000;
|
||||
for (;;) {
|
||||
auto wait = WaitForSingleObject(child.handle, 100);
|
||||
if (wait == WAIT_OBJECT_0) break;
|
||||
if (wait != WAIT_TIMEOUT) throw std::runtime_error("Cannot wait for history OCR helper");
|
||||
if (cancelled && cancelled()) throw std::runtime_error("History OCR cancelled");
|
||||
if (GetTickCount64() >= deadline) throw std::runtime_error("History OCR helper timed out");
|
||||
}
|
||||
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)));
|
||||
if (reply.at("schema") != 1 || str(reply, "id") != id)
|
||||
throw std::runtime_error("Invalid history OCR response");
|
||||
return str(reply, "text");
|
||||
}
|
||||
int runHistoryOcr(const std::filesystem::path &request)
|
||||
{
|
||||
try {
|
||||
if (!request.is_absolute() || !request.filename().wstring().starts_with(L"ocr-") ||
|
||||
request.extension() != L".request") return 1;
|
||||
auto job = parse(unprotect(readFile(request, 1024 * 1024)));
|
||||
auto id = str(job, "id");
|
||||
if (job.at("schema") != 1 || !validId(id)) return 1;
|
||||
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";
|
||||
atomicWrite(response, protect(jsonBytes(Json{{"schema", 1}, {"id", utf8(id)}, {"text", text}})));
|
||||
return 0;
|
||||
} catch (...) { return 1; }
|
||||
}
|
||||
bool History::saveOcr(const std::wstring &id, const std::optional<std::wstring> &text, const Settings &settings)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "platform.h"
|
||||
#include <mutex>
|
||||
#include <functional>
|
||||
namespace ks
|
||||
{
|
||||
Settings loadSettings(const std::filesystem::path &, const std::vector<Layout> &);
|
||||
@@ -15,6 +16,7 @@ struct Entry
|
||||
bool pinned{}, image{}, files{};
|
||||
int ocrState{}; // 0: legacy/not applicable, 1: pending, 2: saved, 3: failed
|
||||
std::wstring ocrProfile = L"windows";
|
||||
std::string contentHash, imageHash, pixelHash;
|
||||
};
|
||||
class History
|
||||
{
|
||||
@@ -22,6 +24,7 @@ class History
|
||||
std::vector<Entry> entries_;
|
||||
mutable std::mutex mutex_;
|
||||
void saveIndex();
|
||||
void loadIndex();
|
||||
void prune(const Settings &);
|
||||
|
||||
public:
|
||||
@@ -29,6 +32,9 @@ class History
|
||||
std::vector<Entry> entries() const;
|
||||
Clip load(const std::wstring &id) const;
|
||||
void add(const Clip &, const std::wstring &app, const Settings &);
|
||||
bool captureImage(DWORD sequence, const std::wstring &app, const Settings &);
|
||||
std::wstring recognizeImage(const std::wstring &id, const Settings &,
|
||||
const std::function<bool()> &cancelled = {}) const;
|
||||
bool saveOcr(const std::wstring &id, const std::optional<std::wstring> &, const Settings &);
|
||||
std::optional<std::wstring> ocrText(const std::wstring &id, const std::wstring &profile = L"") const;
|
||||
void remove(const std::wstring &);
|
||||
@@ -39,4 +45,6 @@ class History
|
||||
Bytes encodeClip(const Clip &);
|
||||
Clip decodeClip(const Bytes &);
|
||||
bool sameClipboardImage(const Clip &, const Clip &);
|
||||
int runImageHistoryCapture(const std::filesystem::path &request);
|
||||
int runHistoryOcr(const std::filesystem::path &request);
|
||||
} // namespace ks
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
#include "platform.h"
|
||||
namespace ks
|
||||
{
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.36";
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.37";
|
||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||
|
||||
Reference in New Issue
Block a user