Release v0.6.8: add background image OCR and optional Tesseract
This commit is contained in:
@@ -570,6 +570,34 @@ int main(int argc, char **argv) {
|
||||
ks::showHistory(app,false);
|
||||
auto history=app.historyWindow;
|
||||
require(history&&!IsWindowVisible(history),"history layout tested without changing focus");
|
||||
{
|
||||
app.history=std::make_unique<ks::History>(temp/L"history-search");
|
||||
app.history->add(ks::textClip(L"згенеруй приклад"),L"test",app.settings);
|
||||
app.history->add(ks::textClip(L"unrelated entry"),L"test",app.settings);
|
||||
// This App has no general worker: a search on that queue can never finish.
|
||||
auto originalFocus=GetFocus();
|
||||
auto pumpUntil=[&](int count) {
|
||||
auto deadline=GetTickCount64()+3000;
|
||||
do {
|
||||
MSG message{};
|
||||
while(PeekMessageW(&message,nullptr,0,0,PM_REMOVE)) {
|
||||
TranslateMessage(&message); DispatchMessageW(&message);
|
||||
}
|
||||
if(SendDlgItemMessageW(history,102,LB_GETCOUNT,0,0)==count) return true;
|
||||
Sleep(5);
|
||||
} while(GetTickCount64()<deadline);
|
||||
return false;
|
||||
};
|
||||
ks::refreshHistory(app);
|
||||
require(pumpUntil(2),"history loads independently of general worker queue");
|
||||
SetDlgItemTextW(history,101,L"згенеруй");
|
||||
require(pumpUntil(1),"typing filters history via debounce without focus change");
|
||||
SetDlgItemTextW(history,101,L"missing query");
|
||||
require(pumpUntil(0),"unmatched search clears history list");
|
||||
SetDlgItemTextW(history,101,L"");
|
||||
require(pumpUntil(2),"clearing search restores history");
|
||||
require(GetFocus()==originalFocus,"history search preserves focus");
|
||||
}
|
||||
auto childRect=[&](int id) { RECT r{}; GetWindowRect(GetDlgItem(history,id),&r); MapWindowPoints(nullptr,history,(POINT*)&r,2); return r; };
|
||||
RECT historyFrame{}; GetWindowRect(history,&historyFrame);
|
||||
auto listBefore=childRect(102),searchBefore=childRect(101),buttonBefore=childRect(110);
|
||||
|
||||
+46
-4
@@ -1,7 +1,14 @@
|
||||
#include "ocr.h"
|
||||
#include "app.h"
|
||||
#include <iostream>
|
||||
int main()
|
||||
namespace ks {
|
||||
struct InputRegressionAccess {
|
||||
static void scheduleOcr(App &app) { app.scheduleHistoryOcr(app.settings); }
|
||||
};
|
||||
}
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const bool tesseract = argc == 2 && std::string(argv[1]) == "--tesseract";
|
||||
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
try
|
||||
{
|
||||
@@ -24,7 +31,8 @@ int main()
|
||||
auto oldFont = SelectObject(dc, font);
|
||||
SetTextColor(dc, RGB(0, 0, 0));
|
||||
SetBkColor(dc, RGB(255, 255, 255));
|
||||
TextOutW(dc, 20, 40, L"HELLO KSWITCHER", 15);
|
||||
std::wstring fixture = tesseract ? L"Привіт Україно HELLO" : L"HELLO KSWITCHER";
|
||||
TextOutW(dc, 20, 40, fixture.c_str(), (int)fixture.size());
|
||||
GdiFlush();
|
||||
ks::Pixels pixels{800, 150, ks::Bytes((BYTE *)data, (BYTE *)data + 800 * 150 * 4)};
|
||||
SelectObject(dc, oldFont);
|
||||
@@ -36,11 +44,45 @@ int main()
|
||||
auto decoded = ks::decodeImage(clip);
|
||||
if (decoded.width != 800 || decoded.height != 150)
|
||||
throw std::runtime_error("WIC decode size mismatch");
|
||||
auto text = ks::recognizeText(clip);
|
||||
ks::Settings config;
|
||||
config.useTesseract = tesseract;
|
||||
auto text = ks::recognizeText(clip, config);
|
||||
std::cout << "OCR: " << ks::utf8(text) << '\n';
|
||||
if (ks::upper(text).find(L"HELLO") == std::wstring::npos)
|
||||
throw std::runtime_error("OCR fixture was not recognized");
|
||||
std::cout << "PASS native Windows OCR, DIB decoding and async result retrieval\n";
|
||||
if (tesseract && ks::lower(text).find(L"привіт") == std::wstring::npos)
|
||||
throw std::runtime_error("Ukrainian OCR fixture was not recognized");
|
||||
auto temp = std::filesystem::temp_directory_path() / (L"KSwitcher-auto-ocr-" + std::to_wstring(GetCurrentProcessId()));
|
||||
auto sequence = GetClipboardSequenceNumber();
|
||||
{
|
||||
ks::App app(GetModuleHandleW(nullptr), true, temp);
|
||||
app.settings.history = true;
|
||||
app.history->add(clip, L"OCR fixture", app.settings);
|
||||
auto id = app.history->entries().front().id;
|
||||
if (tesseract)
|
||||
{
|
||||
app.history->saveOcr(id, L"old Windows result", app.settings);
|
||||
app.settings.useTesseract = true;
|
||||
}
|
||||
ks::InputRegressionAccess::scheduleOcr(app);
|
||||
auto deadline = GetTickCount64() + 35000;
|
||||
while ((app.history->entries().front().ocrState == 1 ||
|
||||
app.history->entries().front().ocrProfile != ks::ocrProfile(app.settings)) &&
|
||||
GetTickCount64() < deadline) Sleep(20);
|
||||
if (app.history->search(L"HELLO").size() != 1)
|
||||
throw std::runtime_error("Background OCR did not make the image searchable");
|
||||
ks::History restored(temp / L"history");
|
||||
if (!restored.ocrText(id) || restored.search(L"HELLO").size() != 1)
|
||||
throw std::runtime_error("Background OCR did not persist its result");
|
||||
if (tesseract && (restored.search(L"привіт").size() != 1 || !restored.search(L"old Windows result").empty()))
|
||||
throw std::runtime_error("Engine change did not replace cached Windows OCR with Ukrainian text");
|
||||
if (GetClipboardSequenceNumber() != sequence)
|
||||
throw std::runtime_error("Background OCR changed the clipboard");
|
||||
std::cout << "PASS automatic background OCR, persisted search and unchanged clipboard\n";
|
||||
}
|
||||
std::filesystem::remove_all(temp);
|
||||
std::cout << (tesseract ? "PASS optional Tesseract, mixed Ukrainian/English and cache migration\n" :
|
||||
"PASS native Windows OCR, DIB decoding and async result retrieval\n");
|
||||
CoUninitialize();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ int main()
|
||||
"editor is an independent Alt+Tab application window");
|
||||
require(GetDlgItem(window,50)!=nullptr && c.statusHeight>0,"status bar is created");
|
||||
require(c.pencilCursor!=nullptr,"pencil drawing cursor loads");
|
||||
require(c.freehand && !c.pointer && ks::toolCursor(c)==c.pencilCursor,
|
||||
"new screenshot editor starts with the pencil tool");
|
||||
for(int size:{32,48,64,96}) {
|
||||
auto cursor=(HCURSOR)LoadImageW(app.instance,MAKEINTRESOURCEW(IDC_SCREENSHOT_PENCIL),IMAGE_CURSOR,size,size,0);
|
||||
ICONINFO info{};
|
||||
|
||||
@@ -468,6 +468,75 @@ int main(int argc, char **argv)
|
||||
history.clear();
|
||||
require(history.entries().empty(), "clear history");
|
||||
}
|
||||
{
|
||||
ks::History history(temp / L"search");
|
||||
history.add(ks::textClip(std::wstring(260, L'x') + L" hidden needle"), L"Test", settings);
|
||||
history.add(ks::textClip(L"first\nsecond\tthird"), L"Test", settings);
|
||||
require(history.search(L"hidden needle").size() == 1, "search beyond truncated preview");
|
||||
require(history.search(L"first\nsecond").size() == 1, "search retains original newlines");
|
||||
require(history.search(L"second\tthird").size() == 1, "search retains original tabs");
|
||||
require(history.search(L"absent").empty(), "short previews reject unmatched queries");
|
||||
require(history.search(L"FIRST").size() == 1, "short preview search ignores case");
|
||||
}
|
||||
{
|
||||
auto folder = temp / L"ocr-history";
|
||||
ks::History history(folder);
|
||||
ks::Clip image{{ks::imageDib(ks::Pixels{2, 2, ks::Bytes(16, 255)})}};
|
||||
history.add(image, L"OCR test", settings);
|
||||
auto id = history.entries().front().id;
|
||||
require(history.entries().front().ocrState == 1, "new images await background OCR");
|
||||
require(ks::History(folder).entries().front().ocrState == 1, "pending OCR survives restart");
|
||||
auto original = ks::encodeClip(history.load(id));
|
||||
auto beforeBytes = history.entries().front().bytes;
|
||||
require(history.saveOcr(id, L"Розпізнаний текст HELLO", settings), "save image OCR");
|
||||
require(history.entries().front().bytes > beforeBytes, "OCR storage counts against history size");
|
||||
require(ks::encodeClip(history.load(id)) == original, "OCR preserves original image formats");
|
||||
require(!history.saveOcr(id, L"replacement", settings), "completed OCR is not overwritten");
|
||||
ks::History restored(folder);
|
||||
require(restored.search(L"розпізнаний").size() == 1, "persisted Ukrainian OCR is searchable");
|
||||
require(restored.ocrText(id) == L"Розпізнаний текст HELLO", "OCR text survives restart");
|
||||
auto raw = ks::readFile(folder / (id + L".ocr"), 2 * 1024 * 1024);
|
||||
require(std::string(raw.begin(), raw.end()).find("HELLO") == std::string::npos, "OCR sidecar is encrypted");
|
||||
restored.remove(id);
|
||||
require(!std::filesystem::exists(folder / (id + L".ocr")), "deleting image removes OCR sidecar");
|
||||
require(!restored.saveOcr(id, L"late result", settings), "late OCR cannot resurrect deleted image");
|
||||
restored.add(image, L"OCR test", settings);
|
||||
id = restored.entries().front().id;
|
||||
restored.saveOcr(id, L"", settings);
|
||||
require(restored.ocrText(id).has_value() && restored.ocrText(id)->empty(), "empty OCR is a completed result");
|
||||
restored.clear();
|
||||
require(!std::filesystem::exists(folder / (id + L".ocr")), "clear removes OCR sidecars");
|
||||
restored.add(image, L"OCR test", settings);
|
||||
id = restored.entries().front().id;
|
||||
restored.saveOcr(id, std::nullopt, settings);
|
||||
require(ks::History(folder).entries().front().ocrState == 3, "failed OCR is not retried on every search or restart");
|
||||
restored.saveOcr(id, L"manual retry", settings);
|
||||
require(restored.search(L"manual retry").size() == 1, "explicit retry can recover failed OCR");
|
||||
restored.add(ks::textClip(L"newer"), L"test", settings);
|
||||
auto limited = settings; limited.maxItems = 1;
|
||||
restored.add(ks::textClip(L"newest"), L"test", limited);
|
||||
require(!std::filesystem::exists(folder / (id + L".ocr")), "pruning image removes OCR sidecar");
|
||||
}
|
||||
{
|
||||
ks::Settings options;
|
||||
require(!options.useTesseract, "Tesseract is opt-in by default");
|
||||
options.useTesseract = true;
|
||||
options.tesseractPath = L"C:\\Program Files\\Tesseract-OCR\\tesseract.exe";
|
||||
options.tesseractLanguages = L"ukr+eng+rus";
|
||||
auto restored = ks::decodeSettings(ks::encodeSettings(options), {});
|
||||
require(restored.useTesseract && restored.tesseractPath == options.tesseractPath &&
|
||||
restored.tesseractLanguages == options.tesseractLanguages, "OCR settings roundtrip");
|
||||
options.tesseractLanguages = L"ukr+../eng";
|
||||
rejects([&] { ks::validateSettings(options); }, "OCR language paths rejected");
|
||||
options.tesseractLanguages = L"ukr++eng";
|
||||
rejects([&] { ks::validateSettings(options); }, "empty OCR language rejected");
|
||||
options.tesseractLanguages = L"ukr+eng";
|
||||
options.tesseractPath = L"C:\\missing-tesseract\\tesseract.exe";
|
||||
rejects([&] { ks::validateTesseract(options); }, "missing optional Tesseract reports clear failure");
|
||||
options.useTesseract = false;
|
||||
ks::validateTesseract(options);
|
||||
require(true, "Windows OCR works without optional installation");
|
||||
}
|
||||
// Test-owned directory only; never touch the user's clipboard or stored history.
|
||||
std::filesystem::remove_all(temp);
|
||||
CoUninitialize();
|
||||
|
||||
Reference in New Issue
Block a user