# 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
- ~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/<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.
