EU-Utility/plugins/game_reader_test/plugin.py

1193 lines
44 KiB
Python

"""
EU-Utility - Game Reader Test Plugin
Debug and test tool for OCR and game reading functionality.
Tests screen capture, OCR accuracy, and text extraction.
"""
import re
from pathlib import Path
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QTextEdit,
QLabel, QPushButton, QComboBox, QCheckBox,
QSpinBox, QGroupBox, QSplitter, QFrame,
QTabWidget, QLineEdit, QProgressBar,
QFileDialog, QMessageBox, QTableWidget,
QTableWidgetItem, QHeaderView
)
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal
from PyQt6.QtGui import QPixmap, QImage, QColor
from plugins.base_plugin import BasePlugin
class OCRTestThread(QThread):
"""Background thread for OCR testing."""
result_ready = pyqtSignal(dict)
progress_update = pyqtSignal(int, str)
def __init__(self, region=None, backend='auto'):
super().__init__()
self.region = region
self.backend = backend
def run(self):
"""Run OCR test."""
import time
results = {
'success': False,
'text': '',
'backend_used': '',
'processing_time': 0,
'error': None
}
try:
start_time = time.time()
self.progress_update.emit(10, "Capturing screen...")
# Capture screen
from PIL import Image, ImageGrab
if self.region:
screenshot = ImageGrab.grab(bbox=self.region)
else:
screenshot = ImageGrab.grab()
self.progress_update.emit(30, "Running OCR...")
# Try OCR backends
text = ""
backend_used = "none"
if self.backend in ('auto', 'easyocr'):
try:
import easyocr
import numpy as np
self.progress_update.emit(50, "Loading EasyOCR...")
reader = easyocr.Reader(['en'], gpu=False, verbose=False)
self.progress_update.emit(70, "Processing with EasyOCR...")
# Convert PIL Image to numpy array
screenshot_np = np.array(screenshot)
ocr_result = reader.readtext(
screenshot_np,
detail=0,
paragraph=True
)
text = '\n'.join(ocr_result)
backend_used = "easyocr"
except Exception as e:
if self.backend == 'easyocr':
raise e
if not text and self.backend in ('auto', 'tesseract'):
try:
import pytesseract
self.progress_update.emit(70, "Processing with Tesseract...")
text = pytesseract.image_to_string(screenshot)
backend_used = "tesseract"
except Exception as e:
if self.backend == 'tesseract':
raise e
if not text and self.backend in ('auto', 'paddle'):
try:
from paddleocr import PaddleOCR
import numpy as np
self.progress_update.emit(70, "Processing with PaddleOCR...")
# Try with show_log, fall back without for compatibility
try:
ocr = PaddleOCR(use_angle_cls=True, lang='en', show_log=False)
except TypeError:
ocr = PaddleOCR(use_angle_cls=True, lang='en')
# Convert PIL to numpy
screenshot_np = np.array(screenshot)
result = ocr.ocr(screenshot_np, cls=True)
if result and result[0]:
texts = [line[1][0] for line in result[0]]
text = '\n'.join(texts)
backend_used = "paddleocr"
except Exception as e:
if self.backend == 'paddle':
raise e
processing_time = time.time() - start_time
results.update({
'success': True,
'text': text or "No text detected",
'backend_used': backend_used,
'processing_time': processing_time
})
self.progress_update.emit(100, "Complete!")
except Exception as e:
results['error'] = str(e)
self.progress_update.emit(100, f"Error: {e}")
self.result_ready.emit(results)
class GameReaderTestPlugin(BasePlugin):
"""Test and debug tool for game reading/OCR functionality."""
name = "Game Reader Test"
version = "1.0.0"
author = "EU-Utility"
description = "Debug tool for testing OCR and screen reading"
# Dependencies for OCR functionality
dependencies = {
'pip': ['pillow', 'numpy'],
'optional': {
'easyocr': 'Best OCR accuracy, auto-downloads models',
'pytesseract': 'Alternative OCR engine',
'paddleocr': 'Advanced OCR with layout detection'
}
}
def __init__(self, overlay_window, config):
super().__init__(overlay_window, config)
self.test_history = []
self.max_history = 50
self.ocr_thread = None
def initialize(self):
"""Initialize plugin."""
self.log_info("Game Reader Test initialized")
def get_ui(self):
"""Create plugin UI."""
widget = QWidget()
layout = QVBoxLayout(widget)
layout.setSpacing(12)
# Header
header = QLabel("📷 Game Reader Test & Debug Tool")
header.setStyleSheet("font-size: 16px; font-weight: bold; color: #ff8c42;")
layout.addWidget(header)
# Create tabs
tabs = QTabWidget()
# Tab 1: Quick Test
tabs.addTab(self._create_quick_test_tab(), "Quick Test")
# Tab 2: File Test (NEW)
tabs.addTab(self._create_file_test_tab(), "File Test")
# Tab 3: Region Test
tabs.addTab(self._create_region_test_tab(), "Region Test")
# Tab 3: History
tabs.addTab(self._create_history_tab(), "History")
# Tab 5: Skills Parser (NEW)
tabs.addTab(self._create_skills_parser_tab(), "Skills Parser")
# Tab 6: Calibration
tabs.addTab(self._create_calibration_tab(), "Calibration")
layout.addWidget(tabs, 1)
# Status bar
status_frame = QFrame()
status_frame.setStyleSheet("background-color: #1a1f2e; border-radius: 6px; padding: 8px;")
status_layout = QHBoxLayout(status_frame)
self.status_label = QLabel("Ready - Select a tab to begin testing")
self.status_label.setStyleSheet("color: #4ecdc4;")
status_layout.addWidget(self.status_label)
layout.addWidget(status_frame)
return widget
def _create_quick_test_tab(self):
"""Create quick test tab."""
tab = QWidget()
layout = QVBoxLayout(tab)
# Info
info = QLabel("Quick OCR Test - Captures full screen and extracts text")
info.setStyleSheet("color: #888;")
layout.addWidget(info)
# Backend selection
backend_layout = QHBoxLayout()
backend_layout.addWidget(QLabel("OCR Backend:"))
self.backend_combo = QComboBox()
self.backend_combo.addItems(["Auto (try all)", "EasyOCR", "Tesseract", "PaddleOCR"])
backend_layout.addWidget(self.backend_combo)
backend_layout.addStretch()
layout.addLayout(backend_layout)
# Progress bar
self.progress_bar = QProgressBar()
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(0)
self.progress_bar.setStyleSheet("""
QProgressBar {
border: 1px solid #333;
border-radius: 4px;
text-align: center;
}
QProgressBar::chunk {
background-color: #ff8c42;
}
""")
layout.addWidget(self.progress_bar)
# Test button
self.test_btn = QPushButton("▶ Run OCR Test")
self.test_btn.setStyleSheet("""
QPushButton {
background-color: #ff8c42;
color: #141f23;
font-weight: bold;
padding: 12px;
font-size: 14px;
}
QPushButton:hover {
background-color: #ffa05c;
}
""")
self.test_btn.clicked.connect(self._run_quick_test)
layout.addWidget(self.test_btn)
# Results
results_group = QGroupBox("OCR Results")
results_layout = QVBoxLayout(results_group)
self.results_text = QTextEdit()
self.results_text.setReadOnly(True)
self.results_text.setPlaceholderText("OCR results will appear here...")
self.results_text.setStyleSheet("""
QTextEdit {
background-color: #0d1117;
color: #c9d1d9;
font-family: Consolas, monospace;
font-size: 12px;
}
""")
results_layout.addWidget(self.results_text)
# Stats
self.stats_label = QLabel("Backend: - | Time: - | Status: Waiting")
self.stats_label.setStyleSheet("color: #888;")
results_layout.addWidget(self.stats_label)
layout.addWidget(results_group)
# Save buttons
btn_layout = QHBoxLayout()
save_text_btn = QPushButton("💾 Save Text")
save_text_btn.clicked.connect(self._save_text)
btn_layout.addWidget(save_text_btn)
copy_btn = QPushButton("📋 Copy to Clipboard")
copy_btn.clicked.connect(self._copy_to_clipboard)
btn_layout.addWidget(copy_btn)
clear_btn = QPushButton("🗑 Clear")
clear_btn.clicked.connect(self._clear_results)
btn_layout.addWidget(clear_btn)
btn_layout.addStretch()
layout.addLayout(btn_layout)
layout.addStretch()
return tab
def _create_file_test_tab(self):
"""Create file-based OCR test tab for testing with saved screenshots."""
tab = QWidget()
layout = QVBoxLayout(tab)
# Info
info = QLabel("Test OCR on an image file (PNG, JPG, BMP)")
info.setStyleSheet("color: #888;")
layout.addWidget(info)
# File selection
file_layout = QHBoxLayout()
self.file_path_label = QLabel("No file selected")
self.file_path_label.setStyleSheet("color: #aaa; padding: 8px; background-color: #1a1f2e; border-radius: 4px;")
file_layout.addWidget(self.file_path_label, 1)
browse_btn = QPushButton("Browse...")
browse_btn.setStyleSheet("""
QPushButton {
background-color: #4ecdc4;
color: #141f23;
font-weight: bold;
padding: 8px 16px;
}
""")
browse_btn.clicked.connect(self._browse_image_file)
file_layout.addWidget(browse_btn)
layout.addLayout(file_layout)
# Backend selection
backend_layout = QHBoxLayout()
backend_layout.addWidget(QLabel("OCR Backend:"))
self.file_backend_combo = QComboBox()
self.file_backend_combo.addItems(["Auto (try all)", "EasyOCR", "Tesseract", "PaddleOCR"])
backend_layout.addWidget(self.file_backend_combo)
backend_layout.addStretch()
layout.addLayout(backend_layout)
# Test button
file_test_btn = QPushButton("▶ Run OCR on File")
file_test_btn.setStyleSheet("""
QPushButton {
background-color: #ff8c42;
color: #141f23;
font-weight: bold;
padding: 12px;
font-size: 14px;
}
""")
file_test_btn.clicked.connect(self._run_file_test)
layout.addWidget(file_test_btn)
# Results
results_group = QGroupBox("OCR Results")
results_layout = QVBoxLayout(results_group)
self.file_results_text = QTextEdit()
self.file_results_text.setReadOnly(True)
self.file_results_text.setPlaceholderText("OCR results will appear here...")
self.file_results_text.setStyleSheet("""
QTextEdit {
background-color: #0d1117;
color: #c9d1d9;
font-family: Consolas, monospace;
font-size: 12px;
}
""")
results_layout.addWidget(self.file_results_text)
# Stats
self.file_stats_label = QLabel("File: - | Backend: - | Status: Waiting")
self.file_stats_label.setStyleSheet("color: #888;")
results_layout.addWidget(self.file_stats_label)
layout.addWidget(results_group, 1)
layout.addStretch()
return tab
def _create_region_test_tab(self):
"""Create region test tab."""
tab = QWidget()
layout = QVBoxLayout(tab)
info = QLabel("Test OCR on specific screen region")
info.setStyleSheet("color: #888;")
layout.addWidget(info)
# Region input
region_group = QGroupBox("Screen Region (pixels)")
region_layout = QHBoxLayout(region_group)
self.x_input = QSpinBox()
self.x_input.setRange(0, 10000)
self.x_input.setValue(100)
region_layout.addWidget(QLabel("X:"))
region_layout.addWidget(self.x_input)
self.y_input = QSpinBox()
self.y_input.setRange(0, 10000)
self.y_input.setValue(100)
region_layout.addWidget(QLabel("Y:"))
region_layout.addWidget(self.y_input)
self.w_input = QSpinBox()
self.w_input.setRange(100, 10000)
self.w_input.setValue(400)
region_layout.addWidget(QLabel("Width:"))
region_layout.addWidget(self.w_input)
self.h_input = QSpinBox()
self.h_input.setRange(100, 10000)
self.h_input.setValue(300)
region_layout.addWidget(QLabel("Height:"))
region_layout.addWidget(self.h_input)
layout.addWidget(region_group)
# Presets
preset_layout = QHBoxLayout()
preset_layout.addWidget(QLabel("Quick Presets:"))
presets = [
("Chat Window", 10, 800, 600, 200),
("Skills Window", 100, 100, 500, 400),
("Inventory", 1200, 200, 600, 500),
("Mission Tracker", 1600, 100, 300, 600),
]
for name, x, y, w, h in presets:
btn = QPushButton(name)
btn.clicked.connect(lambda checked, px=x, py=y, pw=w, ph=h: self._set_region(px, py, pw, ph))
preset_layout.addWidget(btn)
preset_layout.addStretch()
layout.addLayout(preset_layout)
# Test button
region_test_btn = QPushButton("▶ Test Region OCR")
region_test_btn.setStyleSheet("""
QPushButton {
background-color: #4ecdc4;
color: #141f23;
font-weight: bold;
padding: 10px;
}
""")
region_test_btn.clicked.connect(self._run_region_test)
layout.addWidget(region_test_btn)
# Results
self.region_results = QTextEdit()
self.region_results.setReadOnly(True)
self.region_results.setPlaceholderText("Region OCR results will appear here...")
self.region_results.setStyleSheet("""
QTextEdit {
background-color: #0d1117;
color: #c9d1d9;
font-family: Consolas, monospace;
}
""")
layout.addWidget(self.region_results)
layout.addStretch()
return tab
def _create_history_tab(self):
"""Create history tab."""
tab = QWidget()
layout = QVBoxLayout(tab)
info = QLabel("History of OCR tests")
info.setStyleSheet("color: #888;")
layout.addWidget(info)
self.history_text = QTextEdit()
self.history_text.setReadOnly(True)
self.history_text.setStyleSheet("""
QTextEdit {
background-color: #0d1117;
color: #c9d1d9;
}
""")
layout.addWidget(self.history_text)
# Controls
btn_layout = QHBoxLayout()
refresh_btn = QPushButton("🔄 Refresh")
refresh_btn.clicked.connect(self._update_history)
btn_layout.addWidget(refresh_btn)
clear_hist_btn = QPushButton("🗑 Clear History")
clear_hist_btn.clicked.connect(self._clear_history)
btn_layout.addWidget(clear_hist_btn)
btn_layout.addStretch()
layout.addLayout(btn_layout)
return tab
def _create_skills_parser_tab(self):
"""Create skills parser tab for testing skill window OCR."""
tab = QWidget()
layout = QVBoxLayout(tab)
info = QLabel("📊 Skills Window Parser - Extract skills from the EU Skills window")
info.setStyleSheet("color: #888;")
layout.addWidget(info)
# Instructions
instructions = QLabel(
"1. Open your Skills window in Entropia Universe\n"
"2. Click 'Capture Skills Window' below\n"
"3. View parsed skills in the table"
)
instructions.setStyleSheet("color: #666; font-size: 11px;")
layout.addWidget(instructions)
# Capture button
capture_btn = QPushButton("📷 Capture Skills Window")
capture_btn.setStyleSheet("""
QPushButton {
background-color: #ff8c42;
color: #141f23;
font-weight: bold;
padding: 12px;
font-size: 14px;
}
""")
capture_btn.clicked.connect(self._capture_and_parse_skills)
layout.addWidget(capture_btn)
# Results table
from PyQt6.QtWidgets import QTableWidget, QTableWidgetItem, QHeaderView
self.skills_table = QTableWidget()
self.skills_table.setColumnCount(3)
self.skills_table.setHorizontalHeaderLabels(["Skill Name", "Rank", "Points"])
self.skills_table.horizontalHeader().setStretchLastSection(True)
self.skills_table.setStyleSheet("""
QTableWidget {
background-color: #0d1117;
border: 1px solid #333;
}
QTableWidget::item {
padding: 6px;
color: #c9d1d9;
}
QHeaderView::section {
background-color: #1a1f2e;
color: #ff8c42;
padding: 8px;
font-weight: bold;
}
""")
layout.addWidget(self.skills_table, 1)
# Stats label
self.skills_stats_label = QLabel("No skills captured yet")
self.skills_stats_label.setStyleSheet("color: #888;")
layout.addWidget(self.skills_stats_label)
# Raw text view
raw_group = QGroupBox("Raw OCR Text (for debugging)")
raw_layout = QVBoxLayout(raw_group)
self.skills_raw_text = QTextEdit()
self.skills_raw_text.setReadOnly(True)
self.skills_raw_text.setMaximumHeight(150)
self.skills_raw_text.setStyleSheet("""
QTextEdit {
background-color: #0d1117;
color: #666;
font-family: Consolas, monospace;
font-size: 10px;
}
""")
raw_layout.addWidget(self.skills_raw_text)
layout.addWidget(raw_group)
layout.addStretch()
return tab
def _capture_and_parse_skills(self):
"""Capture screen and parse skills."""
from PyQt6.QtCore import Qt
self.skills_stats_label.setText("Capturing...")
self.skills_stats_label.setStyleSheet("color: #4ecdc4;")
# Run in thread to not block UI
from threading import Thread
def capture_and_parse():
try:
from PIL import ImageGrab
import re
from datetime import datetime
# Capture screen
screenshot = ImageGrab.grab()
# Run OCR
text = ""
try:
import easyocr
reader = easyocr.Reader(['en'], gpu=False, verbose=False)
import numpy as np
result = reader.readtext(np.array(screenshot), detail=0, paragraph=False)
text = '\n'.join(result)
except Exception as e:
# Fallback to raw OCR
text = str(e)
# Parse skills
skills = self._parse_skills_from_text(text)
# Update UI
from PyQt6.QtCore import QMetaObject, Qt, Q_ARG
QMetaObject.invokeMethod(
self.skills_raw_text,
"setPlainText",
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, text)
)
# Update table
QMetaObject.invokeMethod(
self, "_update_skills_table",
Qt.ConnectionType.QueuedConnection,
Q_ARG(object, skills)
)
# Update stats
stats_text = f"Found {len(skills)} skills"
QMetaObject.invokeMethod(
self.skills_stats_label,
"setText",
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, stats_text)
)
QMetaObject.invokeMethod(
self.skills_stats_label,
"setStyleSheet",
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, "color: #4ecdc4;")
)
except Exception as e:
from PyQt6.QtCore import QMetaObject, Qt, Q_ARG
QMetaObject.invokeMethod(
self.skills_stats_label,
"setText",
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, f"Error: {str(e)}")
)
QMetaObject.invokeMethod(
self.skills_stats_label,
"setStyleSheet",
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, "color: #ff6b6b;")
)
thread = Thread(target=capture_and_parse)
thread.daemon = True
thread.start()
def _parse_skills_from_text(self, text):
"""Parse skills from OCR text."""
skills = {}
# Ranks in Entropia Universe
RANKS = [
'Newbie', 'Inept', 'Beginner', 'Amateur', 'Average',
'Skilled', 'Expert', 'Professional', 'Master', 'Grand Master',
'Champion', 'Legendary', 'Guru', 'Astonishing', 'Remarkable',
'Outstanding', 'Marvelous', 'Prodigious', 'Amazing', 'Incredible', 'Awesome'
]
rank_pattern = '|'.join(RANKS)
# Clean text
text = text.replace('SKILLS', '').replace('ALL CATEGORIES', '')
text = text.replace('SKILL NAME', '').replace('RANK', '').replace('POINTS', '')
lines = text.split('\n')
for line in lines:
line = line.strip()
if not line or len(line) < 10:
continue
# Pattern: SkillName Rank Points
match = re.search(
rf'([A-Za-z][A-Za-z\s]{{2,50}}?)\s+({rank_pattern})\s+(\d{{1,6}})(?:\s|$)',
line, re.IGNORECASE
)
if match:
skill_name = match.group(1).strip()
rank = match.group(2)
points = int(match.group(3))
if points > 0 and skill_name:
skills[skill_name] = {'rank': rank, 'points': points}
return skills
def _update_skills_table(self, skills):
"""Update the skills table with parsed data."""
self.skills_table.setRowCount(len(skills))
for i, (skill_name, data) in enumerate(sorted(skills.items())):
self.skills_table.setItem(i, 0, QTableWidgetItem(skill_name))
self.skills_table.setItem(i, 1, QTableWidgetItem(data['rank']))
self.skills_table.setItem(i, 2, QTableWidgetItem(str(data['points'])))
self.skills_table.resizeColumnsToContents()
def _create_calibration_tab(self):
"""Create calibration tab."""
tab = QWidget()
layout = QVBoxLayout(tab)
info = QLabel("Calibration tools for optimizing OCR accuracy")
info.setStyleSheet("color: #888;")
layout.addWidget(info)
# DPI awareness
dpi_group = QGroupBox("Display Settings")
dpi_layout = QVBoxLayout(dpi_group)
self.dpi_label = QLabel("Detecting display DPI...")
dpi_layout.addWidget(self.dpi_label)
detect_dpi_btn = QPushButton("Detect Display Settings")
detect_dpi_btn.clicked.connect(self._detect_display_settings)
dpi_layout.addWidget(detect_dpi_btn)
layout.addWidget(dpi_group)
# Backend status
backend_group = QGroupBox("OCR Backend Status")
backend_layout = QVBoxLayout(backend_group)
self.backend_status = QTextEdit()
self.backend_status.setReadOnly(True)
self.backend_status.setMaximumHeight(200)
self._check_backends()
backend_layout.addWidget(self.backend_status)
# Install buttons
install_layout = QHBoxLayout()
install_easyocr_btn = QPushButton("📦 Install EasyOCR")
install_easyocr_btn.setStyleSheet("""
QPushButton {
background-color: #4ecdc4;
color: #141f23;
font-weight: bold;
padding: 8px;
}
""")
install_easyocr_btn.clicked.connect(self._install_easyocr)
install_layout.addWidget(install_easyocr_btn)
install_tesseract_btn = QPushButton("📦 Install Tesseract Package")
install_tesseract_btn.setStyleSheet("""
QPushButton {
background-color: #ff8c42;
color: #141f23;
font-weight: bold;
padding: 8px;
}
""")
install_tesseract_btn.clicked.connect(self._install_pytesseract)
install_layout.addWidget(install_tesseract_btn)
detect_tesseract_btn = QPushButton("🔍 Auto-Detect Tesseract")
detect_tesseract_btn.setStyleSheet("""
QPushButton {
background-color: #a0aec0;
color: #141f23;
font-weight: bold;
padding: 8px;
}
""")
detect_tesseract_btn.clicked.connect(self._auto_detect_tesseract)
install_layout.addWidget(detect_tesseract_btn)
install_paddle_btn = QPushButton("📦 Install PaddleOCR")
install_paddle_btn.setStyleSheet("""
QPushButton {
background-color: #4a5568;
color: white;
font-weight: bold;
padding: 8px;
}
""")
install_paddle_btn.clicked.connect(self._install_paddleocr)
install_layout.addWidget(install_paddle_btn)
backend_layout.addLayout(install_layout)
layout.addWidget(backend_group)
# Tips
tips_group = QGroupBox("Tips for Best Results")
tips_layout = QVBoxLayout(tips_group)
tips_text = QLabel("""
• Make sure text is clearly visible and not blurry
• Use region selection to focus on specific text areas
• Higher resolution screens work better for OCR
• Close other windows to reduce background noise
• Ensure good contrast between text and background
""")
tips_text.setWordWrap(True)
tips_text.setStyleSheet("color: #aaa;")
tips_layout.addWidget(tips_text)
layout.addWidget(tips_group)
layout.addStretch()
return tab
def _set_region(self, x, y, w, h):
"""Set region values."""
self.x_input.setValue(x)
self.y_input.setValue(y)
self.w_input.setValue(w)
self.h_input.setValue(h)
def _browse_image_file(self):
"""Browse for an image file to test OCR on."""
file_path, _ = QFileDialog.getOpenFileName(
None,
"Select Image File",
"",
"Images (*.png *.jpg *.jpeg *.bmp *.tiff);;All Files (*)"
)
if file_path:
self.selected_file_path = file_path
self.file_path_label.setText(Path(file_path).name)
self.file_path_label.setStyleSheet("color: #4ecdc4; padding: 8px; background-color: #1a1f2e; border-radius: 4px;")
def _run_file_test(self):
"""Run OCR on the selected image file."""
if not hasattr(self, 'selected_file_path') or not self.selected_file_path:
QMessageBox.warning(None, "No File", "Please select an image file first!")
return
backend_map = {
0: 'auto',
1: 'easyocr',
2: 'tesseract',
3: 'paddle'
}
backend = backend_map.get(self.file_backend_combo.currentIndex(), 'auto')
self.file_results_text.setPlainText("Processing...")
self.file_stats_label.setText("Processing...")
# Run OCR in a thread
from threading import Thread
def process_file():
try:
from PIL import Image
import time
start_time = time.time()
# Load image
image = Image.open(self.selected_file_path)
# Try OCR backends
text = ""
backend_used = "none"
if backend in ('auto', 'easyocr'):
try:
import easyocr
import numpy as np
reader = easyocr.Reader(['en'], gpu=False, verbose=False)
# Convert PIL Image to numpy array for EasyOCR
image_np = np.array(image)
ocr_result = reader.readtext(
image_np,
detail=0,
paragraph=True
)
text = '\n'.join(ocr_result)
backend_used = "easyocr"
except Exception as e:
if backend == 'easyocr':
raise e
if not text and backend in ('auto', 'tesseract'):
try:
import pytesseract
text = pytesseract.image_to_string(image)
backend_used = "tesseract"
except Exception as e:
if backend == 'tesseract':
error_msg = str(e)
if "tesseract is not installed" in error_msg.lower() or "not in your path" in error_msg.lower():
raise Exception(
"Tesseract is not installed.\n\n"
"To use Tesseract OCR:\n"
"1. Download from: https://github.com/UB-Mannheim/tesseract/wiki\n"
"2. Install to C:\\Program Files\\Tesseract-OCR\\\n"
"3. Add to PATH or restart EU-Utility\n\n"
"Alternatively, use EasyOCR (auto-installs): pip install easyocr"
)
raise e
if not text and backend in ('auto', 'paddle'):
try:
from paddleocr import PaddleOCR
# Try without show_log argument for compatibility
try:
ocr = PaddleOCR(use_angle_cls=True, lang='en', show_log=False)
except TypeError:
# Older version without show_log
ocr = PaddleOCR(use_angle_cls=True, lang='en')
# Convert PIL to numpy for PaddleOCR
import numpy as np
image_np = np.array(image)
result = ocr.ocr(image_np, cls=True)
if result and result[0]:
texts = [line[1][0] for line in result[0]]
text = '\n'.join(texts)
backend_used = "paddleocr"
except Exception as e:
if backend == 'paddle':
raise e
processing_time = time.time() - start_time
# Update UI (thread-safe via Qt signals would be better, but this works for simple case)
from PyQt6.QtCore import QMetaObject, Qt, Q_ARG
QMetaObject.invokeMethod(
self.file_results_text,
"setPlainText",
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, text if text else "No text detected")
)
QMetaObject.invokeMethod(
self.file_stats_label,
"setText",
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, f"File: {Path(self.selected_file_path).name} | Backend: {backend_used} | Time: {processing_time:.2f}s")
)
except Exception as e:
from PyQt6.QtCore import QMetaObject, Qt, Q_ARG
QMetaObject.invokeMethod(
self.file_results_text,
"setPlainText",
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, f"Error: {str(e)}")
)
thread = Thread(target=process_file)
thread.daemon = True
thread.start()
def _run_quick_test(self):
"""Run quick OCR test."""
if self.ocr_thread and self.ocr_thread.isRunning():
return
backend_map = {
0: 'auto',
1: 'easyocr',
2: 'tesseract',
3: 'paddle'
}
backend = backend_map.get(self.backend_combo.currentIndex(), 'auto')
self.test_btn.setEnabled(False)
self.test_btn.setText("⏳ Running...")
self.progress_bar.setValue(0)
self.ocr_thread = OCRTestThread(backend=backend)
self.ocr_thread.progress_update.connect(self._update_progress)
self.ocr_thread.result_ready.connect(self._on_ocr_complete)
self.ocr_thread.start()
def _update_progress(self, value, message):
"""Update progress bar."""
self.progress_bar.setValue(value)
self.status_label.setText(message)
def _on_ocr_complete(self, results):
"""Handle OCR completion."""
self.test_btn.setEnabled(True)
self.test_btn.setText("▶ Run OCR Test")
if results['success']:
self.results_text.setPlainText(results['text'])
self.stats_label.setText(
f"Backend: {results['backend_used']} | "
f"Time: {results['processing_time']:.2f}s | "
f"Status: ✅ Success"
)
# Add to history
self._add_to_history(results)
else:
self.results_text.setPlainText(f"Error: {results.get('error', 'Unknown error')}")
self.stats_label.setText(f"Backend: {results.get('backend_used', '-')} | Status: ❌ Failed")
def _run_region_test(self):
"""Run region OCR test."""
region = (
self.x_input.value(),
self.y_input.value(),
self.w_input.value(),
self.h_input.value()
)
self.region_results.setPlainText("Running OCR on region...")
# Use api's ocr_capture if available
try:
result = self.ocr_capture(region=region)
self.region_results.setPlainText(result.get('text', 'No text detected'))
except Exception as e:
self.region_results.setPlainText(f"Error: {e}")
def _save_text(self):
"""Save OCR text to file."""
text = self.results_text.toPlainText()
if not text:
QMessageBox.warning(None, "Save Error", "No text to save!")
return
file_path, _ = QFileDialog.getSaveFileName(
None, "Save OCR Text", "ocr_result.txt", "Text Files (*.txt)"
)
if file_path:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(text)
self.status_label.setText(f"Saved to {file_path}")
def _copy_to_clipboard(self):
"""Copy text to clipboard."""
text = self.results_text.toPlainText()
if text:
self.copy_to_clipboard(text)
self.status_label.setText("Copied to clipboard!")
def _clear_results(self):
"""Clear results."""
self.results_text.clear()
self.stats_label.setText("Backend: - | Time: - | Status: Waiting")
self.progress_bar.setValue(0)
def _add_to_history(self, results):
"""Add result to history."""
from datetime import datetime
entry = {
'time': datetime.now().strftime("%H:%M:%S"),
'backend': results['backend_used'],
'time_taken': results['processing_time'],
'text_preview': results['text'][:100] + "..." if len(results['text']) > 100 else results['text']
}
self.test_history.insert(0, entry)
if len(self.test_history) > self.max_history:
self.test_history = self.test_history[:self.max_history]
self._update_history()
def _update_history(self):
"""Update history display."""
lines = []
for entry in self.test_history:
lines.append(f"[{entry['time']}] {entry['backend']} ({entry['time_taken']:.2f}s)")
lines.append(f" {entry['text_preview']}")
lines.append("")
self.history_text.setPlainText('\n'.join(lines) if lines else "No history yet")
def _clear_history(self):
"""Clear history."""
self.test_history.clear()
self._update_history()
def _detect_display_settings(self):
"""Detect display settings."""
try:
from PyQt6.QtWidgets import QApplication
from PyQt6.QtGui import QScreen
app = QApplication.instance()
if app:
screens = app.screens()
info = []
for i, screen in enumerate(screens):
geo = screen.geometry()
dpi = screen.logicalDotsPerInch()
info.append(f"Screen {i+1}: {geo.width()}x{geo.height()} @ {dpi:.0f} DPI")
self.dpi_label.setText('\n'.join(info))
except Exception as e:
self.dpi_label.setText(f"Error: {e}")
def _install_easyocr(self):
"""Install EasyOCR backend."""
from core.ocr_backend_manager import get_ocr_backend_manager
manager = get_ocr_backend_manager()
success, message = manager.install_backend('easyocr')
if success:
self.notify_success("Installation Complete", message)
else:
self.notify_error("Installation Failed", message)
self._check_backends()
def _install_pytesseract(self):
"""Install pytesseract Python package."""
from core.ocr_backend_manager import get_ocr_backend_manager
manager = get_ocr_backend_manager()
success, message = manager.install_backend('tesseract')
if success:
self.notify_success("Installation Complete", message + "\n\nNote: You also need to install the Tesseract binary from:\nhttps://github.com/UB-Mannheim/tesseract/wiki")
else:
self.notify_error("Installation Failed", message)
self._check_backends()
def _install_paddleocr(self):
"""Install PaddleOCR backend."""
from core.ocr_backend_manager import get_ocr_backend_manager
manager = get_ocr_backend_manager()
success, message = manager.install_backend('paddleocr')
if success:
self.notify_success("Installation Complete", message)
else:
self.notify_error("Installation Failed", message)
self._check_backends()
def _auto_detect_tesseract(self):
"""Auto-detect Tesseract from registry/paths."""
from core.ocr_backend_manager import get_ocr_backend_manager
manager = get_ocr_backend_manager()
if manager.auto_configure_tesseract():
self.notify_success("Tesseract Found", f"Auto-configured Tesseract at:\n{manager.backends['tesseract']['path']}")
else:
self.notify_warning("Not Found", "Could not find Tesseract installation.\n\nPlease install from:\nhttps://github.com/UB-Mannheim/tesseract/wiki")
self._check_backends()
def _check_backends(self):
"""Check OCR backend availability with install buttons."""
from core.ocr_backend_manager import get_ocr_backend_manager
manager = get_ocr_backend_manager()
statuses = []
# EasyOCR
easyocr_status = manager.get_backend_status('easyocr')
if easyocr_status['available']:
statuses.append("✅ EasyOCR - Available (recommended)")
else:
statuses.append("❌ EasyOCR - Not installed\n Click 'Install EasyOCR' button below")
# Tesseract
tesseract_status = manager.get_backend_status('tesseract')
if tesseract_status['available']:
path_info = f" at {tesseract_status['path']}" if tesseract_status['path'] else ""
statuses.append(f"✅ Tesseract - Available{path_info}")
elif tesseract_status['installed']:
statuses.append("⚠️ Tesseract - Python package installed but binary not found\n Click 'Auto-Detect Tesseract' or install binary from:\n https://github.com/UB-Mannheim/tesseract/wiki")
else:
statuses.append("❌ Tesseract - Not installed\n Click 'Install Tesseract Package' then install binary")
# PaddleOCR
paddle_status = manager.get_backend_status('paddleocr')
if paddle_status['available']:
statuses.append("✅ PaddleOCR - Available")
else:
statuses.append("❌ PaddleOCR - Not installed\n Click 'Install PaddleOCR' button below")
statuses.append("\n💡 Recommendation: EasyOCR is easiest (auto-downloads models)")
self.backend_status.setPlainText('\n'.join(statuses))
def shutdown(self):
"""Clean up."""
if self.ocr_thread and self.ocr_thread.isRunning():
self.ocr_thread.wait(1000)
super().shutdown()