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

16 KiB
Raw Blame History

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

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 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)

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.

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 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

  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

[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.