#include "ocr.h" #include "app.h" #include 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 { BITMAPINFO info{}; info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); info.bmiHeader.biWidth = 800; info.bmiHeader.biHeight = -150; info.bmiHeader.biPlanes = 1; info.bmiHeader.biBitCount = 32; void *data{}; HBITMAP bitmap = CreateDIBSection(nullptr, &info, DIB_RGB_COLORS, &data, nullptr, 0); if (!bitmap) throw std::runtime_error("Cannot create OCR fixture"); HDC dc = CreateCompatibleDC(nullptr); auto old = SelectObject(dc, bitmap); RECT rect{0, 0, 800, 150}; FillRect(dc, &rect, (HBRUSH)GetStockObject(WHITE_BRUSH)); HFONT font = CreateFontW(-54, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET, 0, 0, ANTIALIASED_QUALITY, 0, L"Arial"); auto oldFont = SelectObject(dc, font); SetTextColor(dc, RGB(0, 0, 0)); SetBkColor(dc, RGB(255, 255, 255)); 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); SelectObject(dc, old); DeleteObject(font); DeleteObject(bitmap); DeleteDC(dc); ks::Clip clip{{ks::imageDib(pixels)}}; auto decoded = ks::decodeImage(clip); if (decoded.width != 800 || decoded.height != 150) throw std::runtime_error("WIC decode size mismatch"); 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"); 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; } catch (const std::exception &e) { std::cerr << e.what() << '\n'; CoUninitialize(); return 1; } }