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>
16 KiB
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)
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
mkdir -p build && cd build
cmake ..
make -j$(nproc)
Run
./build/orc-gallery
Install system-wide
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
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 imageocr_text— raw Tesseract output; this is what gets searchedfile_size,created_date,ocr_date— metadata/audit fields
FTS5 Virtual Table (auto-created if SQLite FTS5 is available)
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
DatabaseManagerandImageGallery - 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.dbfilescreenshotsDir— directory scanned by OCR scriptimagePreloadCount— 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
mousePressEventopens file in default viewer (QDesktopServices::openUrl)- Filename label overlaid at bottom with semi-transparent dark background
- Cursor changes to
Qt::PointingHandCursoron hover
ImageGallery:
QScrollAreawrapping aQWidgetwithQGridLayout- 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 resultssetSearchResults(paths)— accepts async search output, resets paginationshowLoadingIndicator()/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 TTLm_allImagesCache: paginated all-images resultsm_imageCountCache: row count integer- Cache eviction when
m_searchCacheexceeds 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(opensQFileDialog::getSaveFileName) - Screenshots directory —
QLineEdit+QPushButton(opensQFileDialog::getExistingDirectory) - Image preload count —
QLineEditwithQIntValidator(1, 100)
On Accept:
- Creates missing directories (prompts user)
- Auto-appends
/screenshot_ocr.dbif 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
QPlainTextEditlog (errors highlighted red) - Indeterminate
QProgressBarwhile running - Cancel → sends
SIGTERM; force-kills after 3 s if process doesn't exit - Close button disabled until process finishes
- Emits
updateFinished()on success →MainWindowreloads 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.
python3 OCR-scripts/ocr_screenshots.py \
--db ~/.local/share/orc-gallery/screenshot_ocr.db \
--screenshots-dir ~/Screenshots
- Handles
SIGTERMgracefully (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 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
[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
- User types in search bar.
- Each keystroke resets the 500 ms
m_typingTimer. - Timer fires →
MainWindow::performSearch()called. DatabaseManager::searchImages()dispatched viaQtConcurrent::run().- Inside the thread: check
m_searchCache; if miss, build FTS5 or LIKE query and execute. - Result (list of
full_pathstrings) returned to main thread via signal. ImageGallery::setSearchResults()resets pagination, populates grid with first page.- Status bar updated: "Found N results for 'query'".
- User clicks "Load More" →
getAllImages(offset, limit)or subsequent search page fetched. - 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
[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
.uifiles — all widgets constructed programmatically in C++. CMAKE_AUTOMOC ONandCMAKE_AUTORCC ONare set;CMAKE_AUTOUIC OFF.- Qt resource system (
resources.qrc) embeds icons at compile time; no runtime asset paths needed. QSettingsusesIniFormatexplicitly so the file is human-readable.- The
UpdateDatabaseDialogis instantiated fresh each time (not reused) to avoid stale process state. ImageGallerydoes not use aQAbstractItemModel; it buildsImageThumbnailwidgets directly and manages them in a flat list (m_thumbnails).- Database connections must not be shared across threads — each
QtConcurrenttask opens its own named connection and closes it on exit. cancelSearch()is non-blocking: it setsm_searchCancelledand returns immediately. The background thread checks the flag at multiple points and exits early. The destructor sets the flag then callswaitForFinished()for clean shutdown.- The watcher's
finishedsignal is not used for emitting results (was causing double-emit). Results are emitted directly fromperformSearchInBackgroundonce and only ifm_currentSearchText == searchText. ImageThumbnail::paintEventdraws the filename overlay; no child widgets needed. Them_displayNamefield is pre-truncated in the constructor.- Async thumbnail loading:
loadThumbnailAsyncposts toQThreadPool::globalInstance().QImageis loaded and.scaled()off the main thread;QPixmap::fromImage()is called inside aQt::QueuedConnectioninvokeMethod lambda. AQPointer<ImageThumbnail>guard prevents use-after-free if the gallery clears before the load completes.