Files
ocr-screenshot-gallery/plan.md
T
yzinchuk 55454d7ef6 rebrand to orc-gallery and optimise search/thumbnail responsiveness
Rebrand:
- Rename project, binary, desktop file, and icons from screenshot-gallery/orcs-gallery to orc-gallery
- Update display name to "ORC Gallery" throughout (window title, .desktop, README, plan.md)
- Config dir changed from ScreenshotOCRGallery to OrcGallery

Performance:
- cancelSearch() is now non-blocking; new searches start immediately instead of waiting for the previous thread to drain
- Remove double-emit from QFutureWatcher::finished handler; results emitted once directly from background thread
- SQLite WAL mode + NORMAL sync + 20 MB page cache + memory temp store + 256 MB mmap applied on init and per-thread connections
- Typing inactivity timer reduced from 500 ms to 150 ms
- COUNT(*) OVER() window function folds total-count into the main SELECT, eliminating a separate COUNT round-trip per search
- ImageThumbnail::paintEvent draws the filename overlay; removes 3 child QObjects (QFrame + QLabel + QHBoxLayout) per thumbnail
- loadThumbnailAsync posts QImage load+scale to QThreadPool; QPixmap::fromImage called on main thread via Qt::QueuedConnection invokeMethod; QPointer guards against use-after-free on gallery clear

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 03:53:07 -04:00

390 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 500 ms typing-inactivity timer (`m_typingTimer`) — search only fires after the user stops typing
- 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)
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)` | Paginated full-path list, no search filter |
| `getImageCount()` | Total row count (cached) |
| `searchImages(text, offset, limit)` | FTS5 or LIKE search, runs in background thread via `QtConcurrent::run` |
| `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 |
Caching:
- `m_searchCache`: `QMap<QString, CachedResult>` keyed by query string, 5-minute TTL
- `m_allImagesCache`: paginated all-images results
- `m_imageCountCache`: 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 <path> --screenshots-dir <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
├───────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ │ │ │ │ │ │ │ │ ← 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
- ~280560 px: 2 columns
- ~560840 px: 3 columns
- ~8401120 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/<user>/.local/share/orc-gallery/screenshot_ocr.db
screenshotsDir=/home/<user>/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 |
---
## 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_currentSearchText == searchText`.
- `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<ImageThumbnail>` guard prevents use-after-free if the gallery clears before the load completes.