Release v0.6.8: add background image OCR and optional Tesseract
This commit is contained in:
+76
-3
@@ -44,6 +44,13 @@ bool validId(const std::wstring &id)
|
||||
} // namespace
|
||||
void validateSettings(const Settings &s)
|
||||
{
|
||||
if (s.tesseractPath.size() > 32700 || s.tesseractPath.find_first_of(L"\r\n\"") != s.tesseractPath.npos)
|
||||
throw std::runtime_error("Invalid Tesseract executable path");
|
||||
if (s.tesseractLanguages.empty() || s.tesseractLanguages.size() > 128 ||
|
||||
s.tesseractLanguages.front() == L'+' || s.tesseractLanguages.back() == L'+' ||
|
||||
s.tesseractLanguages.find(L"++") != s.tesseractLanguages.npos ||
|
||||
s.tesseractLanguages.find_first_not_of(L"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_+") != s.tesseractLanguages.npos)
|
||||
throw std::runtime_error("Use Tesseract language codes separated by +, for example ukr+eng+rus");
|
||||
std::vector<Hotkey> used;
|
||||
std::vector<std::wstring> keys{s.toggle, s.casing, s.translit, s.undo, s.clipboard, s.screenshot};
|
||||
keys.insert(keys.end(), s.direct.begin(), s.direct.end());
|
||||
@@ -125,6 +132,9 @@ Settings decodeSettings(const Bytes &bytes, const std::vector<Layout> &layouts)
|
||||
s.switchLayout = j.value("switchSystemLayout", true);
|
||||
s.writeLog = j.value("writeLog", true);
|
||||
s.history = j.value("historyEnabled", true);
|
||||
s.useTesseract = j.value("useTesseract", false);
|
||||
s.tesseractPath = str(j, "tesseractPath");
|
||||
s.tesseractLanguages = str(j, "tesseractLanguages", s.tesseractLanguages);
|
||||
s.autostart = j.value("startWithSystem", false);
|
||||
s.elevatedAutostart = j.value("autostartAsAdministrator", false);
|
||||
s.awake = j.value("preventScreenLock", false);
|
||||
@@ -196,6 +206,9 @@ Bytes encodeSettings(const Settings &s)
|
||||
{"writeLog", s.writeLog},
|
||||
{"autostartAsAdministrator", s.elevatedAutostart},
|
||||
{"historyEnabled", s.history},
|
||||
{"useTesseract", s.useTesseract},
|
||||
{"tesseractPath", utf8(s.tesseractPath)},
|
||||
{"tesseractLanguages", utf8(s.tesseractLanguages)},
|
||||
{"startWithSystem", s.autostart},
|
||||
{"preventScreenLock", s.awake},
|
||||
{"maxItems", s.maxItems},
|
||||
@@ -297,7 +310,8 @@ History::History(const std::filesystem::path &folder) : folder_(folder)
|
||||
e.value("at", uint64_t{}),
|
||||
e.value("pinned", false),
|
||||
e.value("image", false),
|
||||
e.value("files", false)};
|
||||
e.value("files", false), e.value("ocrState", e.value("image", false) ? 1 : 0),
|
||||
str(e, "ocrProfile", L"windows")};
|
||||
if (validId(item.id) && std::filesystem::exists(folder_ / (item.id + L".dat")))
|
||||
entries_.push_back(std::move(item));
|
||||
}
|
||||
@@ -313,7 +327,7 @@ void History::saveIndex()
|
||||
{"at", e.at},
|
||||
{"pinned", e.pinned},
|
||||
{"image", e.image},
|
||||
{"files", e.files}});
|
||||
{"files", e.files}, {"ocrState", e.ocrState}, {"ocrProfile", utf8(e.ocrProfile)}});
|
||||
atomicWrite(folder_ / L"index.dat", protect(jsonBytes(j)));
|
||||
}
|
||||
std::vector<Entry> History::entries() const
|
||||
@@ -342,6 +356,7 @@ void History::prune(const Settings &s)
|
||||
total -= entries_[i].bytes;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(folder_ / (entries_[i].id + L".dat"), ec);
|
||||
std::filesystem::remove(folder_ / (entries_[i].id + L".ocr"), ec);
|
||||
entries_.erase(entries_.begin() + i);
|
||||
}
|
||||
}
|
||||
@@ -429,7 +444,8 @@ void History::add(const Clip &clip, const std::wstring &app, const Settings &s)
|
||||
GetSystemTimeAsFileTime(&now);
|
||||
entries_.insert(entries_.begin(),
|
||||
Entry{id, text, app, encrypted.size(),
|
||||
(uint64_t(now.dwHighDateTime) << 32) | now.dwLowDateTime, false, image, files});
|
||||
(uint64_t(now.dwHighDateTime) << 32) | now.dwLowDateTime, false, image, files,
|
||||
image ? 1 : 0, ocrProfile(s)});
|
||||
prune(s);
|
||||
saveIndex();
|
||||
}
|
||||
@@ -445,6 +461,7 @@ void History::remove(const std::wstring &id)
|
||||
saveIndex();
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(folder_ / (id + L".dat"), ec);
|
||||
std::filesystem::remove(folder_ / (id + L".ocr"), ec);
|
||||
}
|
||||
void History::pin(const std::wstring &id)
|
||||
{
|
||||
@@ -464,8 +481,49 @@ void History::clear()
|
||||
{
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(folder_ / (e.id + L".dat"), ec);
|
||||
std::filesystem::remove(folder_ / (e.id + L".ocr"), ec);
|
||||
}
|
||||
}
|
||||
bool History::saveOcr(const std::wstring &id, const std::optional<std::wstring> &text, const Settings &settings)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
auto it = std::find_if(entries_.begin(), entries_.end(), [&](auto &e) { return e.id == id; });
|
||||
// A late OCR completion must never recreate a deleted/pruned image.
|
||||
auto profile = ocrProfile(settings);
|
||||
if (it == entries_.end() || !it->image || (it->ocrState == 2 && it->ocrProfile == profile)) return false;
|
||||
if (text)
|
||||
{
|
||||
auto utf = utf8(*text);
|
||||
if (utf.size() > 1024 * 1024) throw std::runtime_error("OCR text exceeds storage limit");
|
||||
// A version byte also makes empty recognition results encryptable by DPAPI.
|
||||
Bytes payload{1};
|
||||
payload.insert(payload.end(), utf.begin(), utf.end());
|
||||
auto encrypted = protect(payload);
|
||||
auto path = folder_ / (id + L".ocr");
|
||||
std::error_code ec;
|
||||
auto previousBytes = std::filesystem::file_size(path, ec);
|
||||
if (ec) previousBytes = 0;
|
||||
atomicWrite(path, encrypted);
|
||||
it->bytes = it->bytes - std::min(it->bytes, uint64_t(previousBytes)) + encrypted.size();
|
||||
it->ocrState = 2;
|
||||
}
|
||||
else it->ocrState = 3;
|
||||
it->ocrProfile = profile;
|
||||
prune(settings);
|
||||
saveIndex();
|
||||
return true;
|
||||
}
|
||||
std::optional<std::wstring> History::ocrText(const std::wstring &id, const std::wstring &profile) const
|
||||
{
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
auto it = std::find_if(entries_.begin(), entries_.end(), [&](auto &e) { return e.id == id; });
|
||||
if (it == entries_.end() || it->ocrState != 2 || (!profile.empty() && it->ocrProfile != profile)) return std::nullopt;
|
||||
}
|
||||
auto bytes = unprotect(readFile(folder_ / (id + L".ocr"), 2 * 1024 * 1024));
|
||||
if (bytes.empty() || bytes.front() != 1) throw std::runtime_error("Invalid OCR text format");
|
||||
return wide(std::string(bytes.begin() + 1, bytes.end()));
|
||||
}
|
||||
std::vector<Entry> History::search(const std::wstring &query) const
|
||||
{
|
||||
auto items = entries();
|
||||
@@ -477,6 +535,21 @@ std::vector<Entry> History::search(const std::wstring &query) const
|
||||
{
|
||||
if (lower(e.preview + L" " + e.app).find(q) != q.npos)
|
||||
return false;
|
||||
if (e.ocrState == 2)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto text = ocrText(e.id);
|
||||
if (text && lower(*text).find(q) != q.npos) return false;
|
||||
}
|
||||
catch (...) {} // Missing/corrupt OCR must not hide the original clip.
|
||||
}
|
||||
// Short previews contain the complete text (or a placeholder for
|
||||
// textless clips). Do not decrypt image payloads just to rule them out.
|
||||
// Newlines/tabs are flattened in previews, so those queries still
|
||||
// need the original text. Exactly 240 characters may be truncated.
|
||||
if (e.preview.size() < 240 && q.find_first_of(L"\r\n\t") == q.npos)
|
||||
return true;
|
||||
try
|
||||
{
|
||||
return lower(load(e.id).text()).find(q) == q.npos;
|
||||
|
||||
Reference in New Issue
Block a user