diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..930426e --- /dev/null +++ b/plan.md @@ -0,0 +1,380 @@ +# OCR Screenshot 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 + +``` +ocr-screenshot-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 +├── screenshot-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 +│ ├── orcs-gallery-512.png +│ ├── orcs-gallery-256.png +│ ├── orcs-gallery-128.png +│ └── orcs-gallery-64.png +│ +└── build/ # CMake build output (git-ignored) + └── screenshot-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/screenshot-gallery +``` + +### Install system-wide + +```bash +cd build && sudo make install +screenshot-gallery # from anywhere +``` + +Installation puts: +- Binary → `${CMAKE_INSTALL_BINDIR}` (typically `/usr/local/bin/`) +- Icons → `/share/icons/hicolor/{64x64,128x128,256x256,512x512}/apps/orcs-gallery.png` +- Desktop entry → `/share/applications/screenshot-gallery.desktop` + +--- + +## Database + +### Location + +Default (XDG-compliant): `~/.local/share/screenshot-gallery/screenshot_ocr.db` + +Configurable in Settings dialog; stored in `~/.config/ScreenshotOCRGallery/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/ScreenshotOCRGallery/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` 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 --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 +├───────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ │ │ │ │ │ │ │ │ ← 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/screenshot-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/ScreenshotOCRGallery/settings.ini` + +```ini +[General] +databasePath=/home//.local/share/screenshot-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 | 500 ms inactivity timer before issuing query | +| Thread-safe DB access | Per-thread `QSqlDatabase` connections + `QMutex` guards | +| Missing files | Placeholder thumbnail rendered inline; no crash | + +--- + +## Linux Desktop Integration + +**`screenshot-gallery.desktop`** +```ini +[Desktop Entry] +Type=Application +Name=Screenshot OCR Gallery +Icon=orcs-gallery +Exec=screenshot-gallery +Categories=Graphics;Utility;Viewer; +Keywords=screenshots;ocr;gallery;search;images; +``` + +XDG paths used by the application: +- Config: `QStandardPaths::AppConfigLocation` → `~/.config/ScreenshotOCRGallery/` +- Data: `QStandardPaths::AppLocalDataLocation` → `~/.local/share/screenshot-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.