# ORC Gallery — Project Reference A Qt6 desktop application for Linux that OCRs screenshots and provides a searchable, paginated gallery interface. Users run a Python OCR script to populate a SQLite database, then browse and search all recognized text visually. --- ## Directory Structure ``` orc-gallery/ ├── CMakeLists.txt # CMake build configuration (Qt6) ├── build.sh # One-shot build automation script ├── README.md # User-facing documentation ├── resources.qrc # Qt resource file bundling icons ├── orc-gallery.desktop # Linux XDG desktop entry ├── .gitignore │ ├── src/ # C++ application source │ ├── main.cpp │ ├── mainwindow.h / mainwindow.cpp │ ├── imagegallery.h / imagegallery.cpp │ ├── databasemanager.h / databasemanager.cpp │ ├── settingsdialog.h / settingsdialog.cpp │ └── updatedatabasedialog.h / updatedatabasedialog.cpp │ ├── OCR-scripts/ # Python OCR utilities + shell helpers │ ├── ocr_screenshots.py # Main OCR runner │ ├── ocr_rofi.sh # Rofi launcher integration │ ├── direct_rofi_ocr.sh │ ├── simple_rofi_ocr.sh │ ├── rofi_ocr_search.sh │ ├── ocr_search_rofi.py │ ├── search_ocr.py │ └── screenshot_ocr.db # Example/seed database │ ├── icons/ # Application icon assets │ ├── orc-gallery-512.png │ ├── orc-gallery-256.png │ ├── orc-gallery-128.png │ └── orc-gallery-64.png │ └── build/ # CMake build output (git-ignored) └── orc-gallery # Compiled executable ``` --- ## Technology Stack | Layer | Technology | |---|---| | GUI framework | Qt 6 (Core, Gui, Widgets, Sql, Concurrent) | | Build system | CMake 3.16+, C++17 | | Database | SQLite 3 via Qt6::Sql | | Full-text search | SQLite FTS5 with Porter stemmer | | OCR engine | Tesseract (invoked via Python subprocess) | | OCR scripting | Python 3 | | Linux integration | XDG Base Directory Specification | | Launcher integration | Rofi (optional, via shell scripts) | Qt modules used: `Qt6::Core`, `Qt6::Gui`, `Qt6::Widgets`, `Qt6::Sql`, `Qt6::Concurrent` Qt features relied upon: MOC (signals/slots), QSettings (INI persistence), QProcess (subprocess management), QtConcurrent (thread pool), QScrollArea, QGridLayout, QSqlDatabase. --- ## Building ### Automated (recommended) ```bash chmod +x build.sh ./build.sh ``` The script checks for cmake and Qt6 packages, creates the build directory, runs cmake, then `make -j$(nproc)`. ### Manual ```bash mkdir -p build && cd build cmake .. make -j$(nproc) ``` ### Run ```bash ./build/orc-gallery ``` ### Install system-wide ```bash cd build && sudo make install orc-gallery # from anywhere ``` Installation puts: - Binary → `${CMAKE_INSTALL_BINDIR}` (typically `/usr/local/bin/`) - Icons → `/share/icons/hicolor/{64x64,128x128,256x256,512x512}/apps/orc-gallery.png` - Desktop entry → `/share/applications/orc-gallery.desktop` --- ## Database ### Location Default (XDG-compliant): `~/.local/share/orc-gallery/screenshot_ocr.db` Configurable in Settings dialog; stored in `~/.config/OrcGallery/settings.ini`. ### Schema ```sql CREATE TABLE ocr_results ( id INTEGER PRIMARY KEY AUTOINCREMENT, filename TEXT UNIQUE, full_path TEXT, ocr_text TEXT, file_size INTEGER, created_date TEXT, ocr_date TEXT ); ``` - `filename` — bare filename, uniqueness-constrained (prevents duplicate OCR runs) - `full_path` — absolute path used to load the thumbnail image - `ocr_text` — raw Tesseract output; this is what gets searched - `file_size`, `created_date`, `ocr_date` — metadata/audit fields ### FTS5 Virtual Table (auto-created if SQLite FTS5 is available) ```sql CREATE VIRTUAL TABLE ocr_fts USING fts5( ocr_text, content='ocr_results', content_rowid='id', tokenize='porter unicode61' ); ``` Three triggers keep `ocr_fts` in sync with `ocr_results` (INSERT, UPDATE, DELETE). The Porter stemmer enables morphological matching (e.g. "running" matches "run"). Unicode61 tokenizer handles non-ASCII text. When FTS5 is unavailable, the app falls back to a `LIKE '%...%'` query on a standard index on `ocr_text`. --- ## Application Architecture ### Source Files #### `main.cpp` Entry point. Constructs `QApplication`, sets app metadata and icon, instantiates `MainWindow`, calls `app.exec()`. #### `mainwindow.h/.cpp` Central controller. Inherits `QMainWindow`. Responsibilities: - Menu bar: File → Open, Settings, Update Database, Quit - Search bar (`QLineEdit`) with a 150 ms typing-inactivity timer (`m_typingTimer`) — search only fires after the user stops typing - Sort/filter toolbar row: sort combo (`QComboBox`) + From/To date pickers (`QDateEdit`) + Clear button - Animated "Searching…" status bar indicator (`m_searchingAnimationTimer`) - Instantiates and owns `DatabaseManager` and `ImageGallery` - Reads/writes settings via `QSettings` (INI at `~/.config/OrcGallery/settings.ini`) - On search complete: updates status bar with result count - On clear search: reloads all images (first page) Sort options: Newest First (default), Oldest First, Name A-Z, Name Z-A. Date filter: "Any" (special value = minimum date, treated as no-filter) to any specific date. `QSignalBlocker` used in `handleClearDates` to reset both date edits atomically without double-refresh. Settings keys: - `databasePath` — path to `.db` file - `screenshotsDir` — directory scanned by OCR script - `imagePreloadCount` — initial gallery page size (default: 20) #### `imagegallery.h/.cpp` Custom `QWidget` displaying thumbnails in a responsive grid. **`ImageThumbnail`** (inner `QLabel` subclass): - Fixed 256×256 px display - `mousePressEvent` opens file in default viewer (`QDesktopServices::openUrl`) - Filename label overlaid at bottom with semi-transparent dark background - Cursor changes to `Qt::PointingHandCursor` on hover **`ImageGallery`**: - `QScrollArea` wrapping a `QWidget` with `QGridLayout` - Column count recalculated on resize: `columns = max(1, availableWidth / (256 + spacing))` - Pagination: default 20 images per page; "Load More Images (X of Y)" button for explicit load - `loadImages(offset, limit)` — populates grid from cached results - `setSearchResults(paths)` — accepts async search output, resets pagination - `showLoadingIndicator()` / `hideLoadingIndicator()` — spinners during async ops - Scroll area horizontal scrollbar always off; vertical scrollbar auto - 300 ms periodic resize check to handle dynamic reflows #### `databasemanager.h/.cpp` All SQLite operations. Thread-safe via `QMutex`. Key public API: | Method | Description | |---|---| | `initialize(path)` | Opens/validates an existing database | | `createDatabase(path)` | Creates schema from scratch | | `getAllImages(offset, limit, sort, from, to)` | Paginated list with optional sort order and date range | | `getImageCount()` | Total row count (unfiltered) | | `searchImages(text, offset, limit, sort, from, to)` | FTS5 or LIKE search, runs async; `SortOrder` enum + `QDate` range | | `cancelSearch()` | Sets cancellation flag; background thread checks it | | `initializeFTS()` | Creates virtual table + triggers if FTS5 available | | `prepareFTSQuery(text)` | Converts user text to FTS5 query syntax | `SortOrder` enum: `DateNewest` (default), `DateOldest`, `NameAsc`, `NameDesc`. Static helpers (internal): `sortClause(sort)` → SQL ORDER BY fragment; `dateWhereFragment(from, to)` → SQL WHERE fragment (empty string when dates are invalid/unset); `bindDateParams(query, from, to)` → binds `:from`/`:to`; `makeSearchKey(text, sort, from, to)` → composite string cache key. Caching: - `m_searchCache`: `QMap, SearchCacheItem>>` — outer key is `makeSearchKey(...)` encoding text+sort+date, inner key is `(offset, limit)`, 5-minute TTL - `m_allImagesCache`: `QMap>` — key encodes sort+date+offset+limit - `m_cachedImageCount`: row count integer - Cache eviction when `m_searchCache` exceeds 50 entries - All cache access under `m_cacheMutex` Threading: each background thread gets its own `QSqlDatabase` connection (Qt requirement). Connections named by `QThread::currentThread()` pointer. #### `settingsdialog.h/.cpp` Modal `QDialog` for user configuration. Fields: - Database file path — `QLineEdit` + `QPushButton` (opens `QFileDialog::getSaveFileName`) - Screenshots directory — `QLineEdit` + `QPushButton` (opens `QFileDialog::getExistingDirectory`) - Image preload count — `QLineEdit` with `QIntValidator(1, 100)` On Accept: - Creates missing directories (prompts user) - Auto-appends `/screenshot_ocr.db` if a directory path is given for the DB field - Saves to `QSettings` #### `updatedatabasedialog.h/.cpp` Non-modal `QDialog` that manages the OCR subprocess. Behavior: - Launches `python3 OCR-scripts/ocr_screenshots.py --db --screenshots-dir ` - Streams stdout/stderr into a `QPlainTextEdit` log (errors highlighted red) - Indeterminate `QProgressBar` while running - Cancel → sends `SIGTERM`; force-kills after 3 s if process doesn't exit - Close button disabled until process finishes - Emits `updateFinished()` on success → `MainWindow` reloads gallery --- ## GUI Layout ``` ┌───────────────────────────────────────────────────────────┐ │ File │ ← Menu bar ├───────────────────────────────────────────────────────────┤ │ [ Search OCR text... ] [×] │ ← Search bar ├───────────────────────────────────────────────────────────┤ │ Sort: [Newest First v] From: [ Any ] To: [ Any ] │ ← Filter bar │ [Clear Dates] │ ├───────────────────────────────────────────────────────────┤ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ │ │ │ │ │ │ │ │ ← Thumbnails │ │ image │ │ image │ │ image │ │ image │ │ 256×256 px │ │ │ │ │ │ │ │ │ │ dynamic grid │ │ name.png │ │ name.png │ │ name.png │ │ name.png │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ … │ │ [ Load More Images (20 of 157) ] │ ← Pagination │ │ ├───────────────────────────────────────────────────────────┤ │ Ready Showing 20 of 157 │ ← Status bar └───────────────────────────────────────────────────────────┘ ``` Grid columns reflow automatically: - < ~280 px wide: 1 column - ~280–560 px: 2 columns - ~560–840 px: 3 columns - ~840–1120 px: 4 columns - 1120 px+: 5+ columns No horizontal scrollbar ever appears. --- ## OCR Scripts ### `OCR-scripts/ocr_screenshots.py` Main OCR runner. Scans a screenshots directory, runs Tesseract on each image not already in the database, and writes results to SQLite. ```bash python3 OCR-scripts/ocr_screenshots.py \ --db ~/.local/share/orc-gallery/screenshot_ocr.db \ --screenshots-dir ~/Screenshots ``` - Handles `SIGTERM` gracefully (used by the Update Database dialog cancel button) - Skips files already present in `ocr_results.filename` - Outputs progress to stdout and errors to stderr ### Rofi Integration Scripts These scripts hook OCR-screenshot workflows into the [Rofi](https://github.com/davatorium/rofi) application launcher for keyboard-driven use: | Script | Purpose | |---|---| | `ocr_rofi.sh` | Take a screenshot, OCR it immediately, copy text to clipboard | | `direct_rofi_ocr.sh` | Same, direct variant | | `simple_rofi_ocr.sh` | Minimal version | | `rofi_ocr_search.sh` | Launch Rofi with searchable OCR text list | | `ocr_search_rofi.py` | Python backend for Rofi search | | `search_ocr.py` | Standalone CLI search against the database | --- ## Settings & Configuration **Settings file:** `~/.config/OrcGallery/settings.ini` ```ini [General] databasePath=/home//.local/share/orc-gallery/screenshot_ocr.db screenshotsDir=/home//Screenshots imagePreloadCount=20 ``` All three keys have fallbacks defined in `MainWindow::loadSettings()` and `SettingsDialog`. --- ## Search Flow 1. User types in search bar. 2. Each keystroke resets the 500 ms `m_typingTimer`. 3. Timer fires → `MainWindow::performSearch()` called. 4. `DatabaseManager::searchImages()` dispatched via `QtConcurrent::run()`. 5. Inside the thread: check `m_searchCache`; if miss, build FTS5 or LIKE query and execute. 6. Result (list of `full_path` strings) returned to main thread via signal. 7. `ImageGallery::setSearchResults()` resets pagination, populates grid with first page. 8. Status bar updated: "Found N results for 'query'". 9. User clicks "Load More" → `getAllImages(offset, limit)` or subsequent search page fetched. 10. Clearing the search bar → `searchImages("")` → gallery reverts to full unfiltered list. --- ## Performance Design | Concern | Solution | |---|---| | Full-text search speed | FTS5 virtual table (≈100× faster than LIKE on large datasets) | | UI responsiveness | All DB queries on background threads via `QtConcurrent` | | Repeated searches | 5-minute LRU cache in `DatabaseManager` | | Large galleries | Explicit pagination (default 20, user-controlled via Load More) | | Column reflow cost | 300 ms debounce on resize events | | Typing lag | 150 ms inactivity timer before issuing query | | Thread-safe DB access | Per-thread `QSqlDatabase` connections + `QMutex` guards | | Missing files | Placeholder thumbnail rendered inline; no crash | | Cancel stall | Non-blocking cancel: flag set immediately, old thread drains in background | | COUNT round-trip | `COUNT(*) OVER()` window function folds count into the main SELECT | | SQLite I/O | WAL journal + 20 MB page cache + memory-mapped I/O via PRAGMA on init | | Thumbnail load stall | `QImage` loaded/scaled on `QThreadPool`, marshalled to main thread via `invokeMethod` | | Overlay widget cost | Filename drawn in `ImageThumbnail::paintEvent` — no child QFrame/QLabel/QHBoxLayout | | Disk thumbnail cache | Scaled PNGs cached under `~/.cache/orc-gallery/thumbs/` keyed by MD5(path+size); avoids repeated disk I/O and scaling on gallery reload | --- ## Linux Desktop Integration **`orc-gallery.desktop`** ```ini [Desktop Entry] Type=Application Name=ORC Gallery Icon=orc-gallery Exec=orc-gallery Categories=Graphics;Utility;Viewer; Keywords=screenshots;ocr;gallery;search;images; ``` XDG paths used by the application: - Config: `QStandardPaths::AppConfigLocation` → `~/.config/OrcGallery/` - Data: `QStandardPaths::AppLocalDataLocation` → `~/.local/share/orc-gallery/` --- ## Development Notes - No `.ui` files — all widgets constructed programmatically in C++. - `CMAKE_AUTOMOC ON` and `CMAKE_AUTORCC ON` are set; `CMAKE_AUTOUIC OFF`. - Qt resource system (`resources.qrc`) embeds icons at compile time; no runtime asset paths needed. - `QSettings` uses `IniFormat` explicitly so the file is human-readable. - The `UpdateDatabaseDialog` is instantiated fresh each time (not reused) to avoid stale process state. - `ImageGallery` does not use a `QAbstractItemModel`; it builds `ImageThumbnail` widgets directly and manages them in a flat list (`m_thumbnails`). - Database connections must not be shared across threads — each `QtConcurrent` task opens its own named connection and closes it on exit. - `cancelSearch()` is non-blocking: it sets `m_searchCancelled` and returns immediately. The background thread checks the flag at multiple points and exits early. The destructor sets the flag then calls `waitForFinished()` for clean shutdown. - The watcher's `finished` signal is not used for emitting results (was causing double-emit). Results are emitted directly from `performSearchInBackground` once and only if `m_currentSearchKey == makeSearchKey(searchText, sort, from, to)`. - `ImageThumbnail::paintEvent` draws the filename overlay; no child widgets needed. The `m_displayName` field is pre-truncated in the constructor. - Async thumbnail loading: `loadThumbnailAsync` posts to `QThreadPool::globalInstance()`. `QImage` is loaded and `.scaled()` off the main thread; `QPixmap::fromImage()` is called inside a `Qt::QueuedConnection` invokeMethod lambda. A `QPointer` guard prevents use-after-free if the gallery clears before the load completes. - Thumbnail disk cache stored at `QStandardPaths::CacheLocation + "/thumbs/"`. Cache key = `QCryptographicHash::Md5(path + "|" + fileSize)` as hex string + `.png`. Avoids reloading/rescaling images that haven't changed. - Sort/date state is held in `ImageGallery` (`m_sortOrder`, `m_fromDate`, `m_toDate`). `MainWindow` pushes state to the gallery via `setSortOrder()`/`setDateFilter()` before triggering any refresh. - Date filter uses `QDateEdit::setSpecialValueText(" Any ")` with `minimumDate()` as the sentinel; the gallery receives `QDate()` (invalid) when the date edit is at minimum, which `dateWhereFragment()` treats as "no filter".