plan.md file
This commit is contained in:
@@ -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<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/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/<user>/.local/share/screenshot-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 | 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.
|
||||||
Reference in New Issue
Block a user