This commit is contained in:
AuroraWright
2025-04-01 00:21:19 +02:00
parent 2378876500
commit fe188ca14b
6 changed files with 176 additions and 53 deletions

View File

@@ -14,7 +14,7 @@ Additionally:
- Scanning the clipboard takes basically zero system resources on macOS and Windows - Scanning the clipboard takes basically zero system resources on macOS and Windows
- Supports reading images and/or writing text to a websocket with the `-r=websocket` and/or `-w=websocket` parameters (the port is 7331 by default, and is configurable in the config file) - Supports reading images and/or writing text to a websocket with the `-r=websocket` and/or `-w=websocket` parameters (the port is 7331 by default, and is configurable in the config file)
- Supports reading images from a Unix domain socket (`/tmp/owocr.sock`) on macOS and Linux with `-r=unixsocket` - Supports reading images from a Unix domain socket (`/tmp/owocr.sock`) on macOS and Linux with `-r=unixsocket`
- Supports capturing the screen directly, or a portion of the screen or a specific window with `-r=screencapture`. By default it will read from the entire main screen every 3 seconds, but you can change it to screenshot a different screen or a portion of a screen (with a set of screen coordinates `x,y,width,height`) or just a specific window (with the window title). You can also change the delay between screenshots or specify a keyboard combo if you don't want screenshots to be taken periodically. Refer to the config file or to `owocr --help` for more details about the screen capture settings - Supports capturing from the screen directly or from a specific window with `-r=screencapture`. By default it will open a coordinate picker so you can select an area of the screen and then read from it every 3 seconds, but you can change it to screenshot the whole screen, a manual set of coordinates `x,y,width,height` or just a specific window (with the window title). You can also change the delay between screenshots or specify a keyboard combo if you don't want screenshots to be taken periodically. Refer to the config file or to `owocr --help` for more details about the screen capture settings
- You can pause/unpause the image processing by pressing "p" or terminate the script with "t" or "q" inside the terminal window - You can pause/unpause the image processing by pressing "p" or terminate the script with "t" or "q" inside the terminal window
- You can switch between OCR providers pressing their corresponding keyboard key inside the terminal window (refer to the list of keys in the providers list below) - You can switch between OCR providers pressing their corresponding keyboard key inside the terminal window (refer to the list of keys in the providers list below)
- You can start the script paused with the `-p` option or with a specific provider with the `-e` option (refer to `owocr -h` for the list) - You can start the script paused with the `-p` option or with a specific provider with the `-e` option (refer to `owocr -h` for the list)

View File

@@ -25,8 +25,7 @@ class Config:
'notifications': False, 'notifications': False,
'combo_pause': '', 'combo_pause': '',
'combo_engine_switch': '', 'combo_engine_switch': '',
'screen_capture_monitor': 1, 'screen_capture_area': '',
'screen_capture_coords': '',
'screen_capture_delay_secs': 3, 'screen_capture_delay_secs': 3,
'screen_capture_only_active_windows': True, 'screen_capture_only_active_windows': True,
'screen_capture_combo': '' 'screen_capture_combo': ''

View File

@@ -23,8 +23,9 @@ from desktop_notifier import DesktopNotifierSync
import psutil import psutil
import inspect import inspect
from owocr.ocr import * from .ocr import *
from owocr.config import Config from .config import Config
from .screen_coordinate_picker import get_screen_selection
try: try:
import win32gui import win32gui
@@ -279,22 +280,23 @@ def capture_macos_window_screenshot(window_id):
def get_windows_window_handle(window_title): def get_windows_window_handle(window_title):
def callback(hwnd, window_title_part): def callback(hwnd, window_title_part):
if window_title_part in win32gui.GetWindowText(hwnd): window_title = win32gui.GetWindowText(hwnd)
handles.append(hwnd) if window_title_part in window_title:
handles.append((hwnd, window_title))
return True return True
handle = win32gui.FindWindow(None, window_title) handle = win32gui.FindWindow(None, window_title)
if handle: if handle:
return handle return (handle, window_title)
handles = [] handles = []
win32gui.EnumWindows(callback, window_title) win32gui.EnumWindows(callback, window_title)
for handle in handles: for handle in handles:
_, pid = win32process.GetWindowThreadProcessId(handle) _, pid = win32process.GetWindowThreadProcessId(handle[0])
if psutil.Process(pid).name().lower() not in ('cmd.exe', 'powershell.exe', 'windowsterminal.exe'): if psutil.Process(pid).name().lower() not in ('cmd.exe', 'powershell.exe', 'windowsterminal.exe'):
return handle return handle
return 0 return (None, None)
class TextFiltering: class TextFiltering:
@@ -616,8 +618,7 @@ def run(read_from=None,
auto_pause=None, auto_pause=None,
combo_pause=None, combo_pause=None,
combo_engine_switch=None, combo_engine_switch=None,
screen_capture_monitor=None, screen_capture_area=None,
screen_capture_coords=None,
screen_capture_delay_secs=None, screen_capture_delay_secs=None,
screen_capture_only_active_windows=None, screen_capture_only_active_windows=None,
screen_capture_combo=None screen_capture_combo=None
@@ -640,10 +641,9 @@ def run(read_from=None,
:param auto_pause: Automatically pause the program after the specified amount of seconds since the last successful text recognition. Will be ignored when reading with screen capture. 0 to disable. :param auto_pause: Automatically pause the program after the specified amount of seconds since the last successful text recognition. Will be ignored when reading with screen capture. 0 to disable.
:param combo_pause: Specifies a combo to wait on for pausing the program. As an example: "<ctrl>+<shift>+p". The list of keys can be found here: https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key :param combo_pause: Specifies a combo to wait on for pausing the program. As an example: "<ctrl>+<shift>+p". The list of keys can be found here: https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key
:param combo_engine_switch: Specifies a combo to wait on for switching the OCR engine. As an example: "<ctrl>+<shift>+a". To be used with combo_pause. The list of keys can be found here: https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key :param combo_engine_switch: Specifies a combo to wait on for switching the OCR engine. As an example: "<ctrl>+<shift>+a". To be used with combo_pause. The list of keys can be found here: https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key
:param screen_capture_monitor: Specifies monitor to target when reading with screen capture. Will be ignored when screen_capture_coords is a window name. :param screen_capture_area: Specifies area to target when reading with screen capture. Can be either empty (automatic selector), a set of coordinates (x,y,width,height), "screen_N" (captures a whole screen, where N is the screen number starting from 1) or a window name (the first matching window title will be used).
:param screen_capture_coords: Specifies area to target when reading with screen capture. Can be either empty (whole screen), a set of coordinates (x,y,width,height) or a window name (the first matching window title will be used).
:param screen_capture_delay_secs: Specifies the delay (in seconds) between screenshots when reading with screen capture. :param screen_capture_delay_secs: Specifies the delay (in seconds) between screenshots when reading with screen capture.
:param screen_capture_only_active_windows: When reading with screen capture and screen_capture_coords is a window name, specifies whether to only target the window while it's active. :param screen_capture_only_active_windows: When reading with screen capture and screen_capture_area is a window name, specifies whether to only target the window while it's active.
:param screen_capture_combo: When reading with screen capture, specifies a combo to wait on for taking a screenshot instead of using the delay. As an example: "<ctrl>+<shift>+s". The list of keys can be found here: https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key :param screen_capture_combo: When reading with screen capture, specifies a combo to wait on for taking a screenshot instead of using the delay. As an example: "<ctrl>+<shift>+s". The list of keys can be found here: https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key
""" """
@@ -768,8 +768,8 @@ def run(read_from=None,
global screenshot_event global screenshot_event
screenshot_event = threading.Event() screenshot_event = threading.Event()
key_combos[screen_capture_combo] = on_screenshot_combo key_combos[screen_capture_combo] = on_screenshot_combo
if type(screen_capture_coords) == tuple: if type(screen_capture_area) == tuple:
screen_capture_coords = ','.join(map(str, screen_capture_coords)) screen_capture_area = ','.join(map(str, screen_capture_area))
global screencapture_window_active global screencapture_window_active
global screencapture_window_visible global screencapture_window_visible
global sct_params global sct_params
@@ -777,32 +777,53 @@ def run(read_from=None,
screencapture_window_active = True screencapture_window_active = True
screencapture_window_visible = True screencapture_window_visible = True
last_result = ([], engine_index) last_result = ([], engine_index)
if screen_capture_coords == '': if screen_capture_area == '':
screencapture_mode = 0 screencapture_mode = 0
elif len(screen_capture_coords.split(',')) == 4: elif screen_capture_area.startswith('screen_'):
parts = screen_capture_area.split('_')
if len(parts) != 2 or not parts[1].isdigit():
raise ValueError('Invalid screen_capture_area')
screen_capture_monitor = int(parts[1])
screencapture_mode = 1 screencapture_mode = 1
elif len(screen_capture_area.split(',')) == 4:
screencapture_mode = 3
else: else:
screencapture_mode = 2 screencapture_mode = 2
if screencapture_mode != 2: if screencapture_mode != 2:
sct = mss.mss() sct = mss.mss()
mon = sct.monitors
if len(mon) <= screen_capture_monitor:
msg = '"screen_capture_monitor" must be a valid monitor number'
raise ValueError(msg)
if screencapture_mode == 0: if screencapture_mode == 1:
mon = sct.monitors
if len(mon) <= screen_capture_monitor:
raise ValueError('Invalid monitor number in screen_capture_area')
coord_left = mon[screen_capture_monitor]['left'] coord_left = mon[screen_capture_monitor]['left']
coord_top = mon[screen_capture_monitor]['top'] coord_top = mon[screen_capture_monitor]['top']
coord_width = mon[screen_capture_monitor]['width'] coord_width = mon[screen_capture_monitor]['width']
coord_height = mon[screen_capture_monitor]['height'] coord_height = mon[screen_capture_monitor]['height']
elif screencapture_mode == 3:
coord_left, coord_top, coord_width, coord_height = [int(c.strip()) for c in screen_capture_area.split(',')]
else: else:
x, y, coord_width, coord_height = [int(c.strip()) for c in screen_capture_coords.split(',')] logger.opt(ansi=True).info('Launching screen coordinate picker')
coord_left = mon[screen_capture_monitor]['left'] + x screen_selection = get_screen_selection()
coord_top = mon[screen_capture_monitor]['top'] + y if not screen_selection:
raise ValueError('Picker window was closed or an error occurred')
screen_capture_monitor = screen_selection['monitor']
x, y, coord_width, coord_height = screen_selection['coordinates']
if coord_width > 0 and coord_height > 0:
coord_top = screen_capture_monitor['top'] + y
coord_left = screen_capture_monitor['left'] + x
else:
logger.opt(ansi=True).info('Selection is empty, selecting whole screen')
coord_left = screen_capture_monitor['left']
coord_top = screen_capture_monitor['top']
coord_width = screen_capture_monitor['width']
coord_height = screen_capture_monitor['height']
sct_params = {'top': coord_top, 'left': coord_left, 'width': coord_width, 'height': coord_height, 'mon': screen_capture_monitor} sct_params = {'top': coord_top, 'left': coord_left, 'width': coord_width, 'height': coord_height}
logger.opt(ansi=True).info(f'Selected coordinates: {coord_left},{coord_top},{coord_width},{coord_height}')
else: else:
area_invalid_error = '"screen_capture_area" must be empty, "screen_N" where N is a screen number starting from 1, a valid set of coordinates, or a valid window name'
if sys.platform == 'darwin': if sys.platform == 'darwin':
if int(platform.mac_ver()[0].split('.')[0]) < 14: if int(platform.mac_ver()[0].split('.')[0]) < 14:
old_macos_screenshot_api = True old_macos_screenshot_api = True
@@ -815,35 +836,37 @@ def run(read_from=None,
window_list = CGWindowListCopyWindowInfo(kCGWindowListExcludeDesktopElements, kCGNullWindowID) window_list = CGWindowListCopyWindowInfo(kCGWindowListExcludeDesktopElements, kCGNullWindowID)
window_titles = [] window_titles = []
window_ids = [] window_ids = []
window_id = 0 window_index = None
for i, window in enumerate(window_list): for i, window in enumerate(window_list):
window_title = window.get(kCGWindowName, '') window_title = window.get(kCGWindowName, '')
if psutil.Process(window['kCGWindowOwnerPID']).name() not in ('Terminal', 'iTerm2'): if psutil.Process(window['kCGWindowOwnerPID']).name() not in ('Terminal', 'iTerm2'):
window_titles.append(window_title) window_titles.append(window_title)
window_ids.append(window['kCGWindowNumber']) window_ids.append(window['kCGWindowNumber'])
if screen_capture_coords in window_titles: if screen_capture_area in window_titles:
window_id = window_ids[window_titles.index(screen_capture_coords)] window_index = window_titles.index(screen_capture_area)
else: else:
for t in window_titles: for t in window_titles:
if screen_capture_coords in t: if screen_capture_area in t:
window_id = window_ids[window_titles.index(t)] window_index = window_titles.index(t)
break break
if not window_id: if not window_index:
msg = '"screen_capture_coords" must be empty (for the whole screen), a valid set of coordinates, or a valid window name' raise ValueError(area_invalid_error)
raise ValueError(msg)
window_id = window_ids[window_index]
window_title = window_titles[window_index]
if screen_capture_only_active_windows: if screen_capture_only_active_windows:
screencapture_window_active = False screencapture_window_active = False
macos_window_tracker = MacOSWindowTracker(window_id) macos_window_tracker = MacOSWindowTracker(window_id)
macos_window_tracker.start() macos_window_tracker.start()
logger.opt(ansi=True).info(f'Selected window: {window_title}')
elif sys.platform == 'win32': elif sys.platform == 'win32':
window_handle = get_windows_window_handle(screen_capture_coords) window_handle, window_title = get_windows_window_handle(screen_capture_area)
if not window_handle: if not window_handle:
msg = '"screen_capture_coords" must be empty (for the whole screen), a valid set of coordinates, or a valid window name' raise ValueError(area_invalid_error)
raise ValueError(msg)
ctypes.windll.shcore.SetProcessDpiAwareness(1) ctypes.windll.shcore.SetProcessDpiAwareness(1)
@@ -851,21 +874,21 @@ def run(read_from=None,
screencapture_window_active = False screencapture_window_active = False
windows_window_tracker = WindowsWindowTracker(window_handle, screen_capture_only_active_windows) windows_window_tracker = WindowsWindowTracker(window_handle, screen_capture_only_active_windows)
windows_window_tracker.start() windows_window_tracker.start()
logger.opt(ansi=True).info(f'Selected window: {window_title}')
else: else:
sct = mss.mss() sct = mss.mss()
window_title = None window_title = None
window_titles = pywinctl.getAllTitles() window_titles = pywinctl.getAllTitles()
if screen_capture_coords in window_titles: if screen_capture_area in window_titles:
window_title = screen_capture_coords window_title = screen_capture_area
else: else:
for t in window_titles: for t in window_titles:
if screen_capture_coords in t and t != active_window_name: if screen_capture_area in t and t != active_window_name:
window_title = t window_title = t
break break
if not window_title: if not window_title:
msg = '"screen_capture_coords" must be empty (for the whole screen), a valid set of coordinates, or a valid window name' raise ValueError(area_invalid_error)
raise ValueError(msg)
target_window = pywinctl.getWindowsWithTitle(window_title)[0] target_window = pywinctl.getWindowsWithTitle(window_title)[0]
coord_top = target_window.top coord_top = target_window.top
@@ -881,6 +904,7 @@ def run(read_from=None,
target_window.watchdog.start(isAliveCB=on_window_closed, isMinimizedCB=on_window_minimized, resizedCB=on_window_resized, movedCB=on_window_moved) target_window.watchdog.start(isAliveCB=on_window_closed, isMinimizedCB=on_window_minimized, resizedCB=on_window_resized, movedCB=on_window_moved)
sct_params = {'top': coord_top, 'left': coord_left, 'width': coord_width, 'height': coord_height} sct_params = {'top': coord_top, 'left': coord_left, 'width': coord_width, 'height': coord_height}
logger.opt(ansi=True).info(f'Selected window: {window_title}')
filtering = TextFiltering() filtering = TextFiltering()
read_from_readable = 'screen capture' read_from_readable = 'screen capture'

View File

@@ -0,0 +1,100 @@
from multiprocessing import Process, Manager
import mss
from PIL import Image, ImageTk
try:
import tkinter as tk
selector_available = True
except:
selector_available = False
class ScreenSelector:
def __init__(self, result):
self.sct = mss.mss()
self.monitors = self.sct.monitors[1:]
self.root = None
self.result = result
def on_select(self, monitor, coordinates):
self.result['monitor'] = monitor
self.result['coordinates'] = coordinates
self.root.destroy()
def create_window(self, monitor):
screenshot = self.sct.grab(monitor)
img = Image.frombytes('RGB', screenshot.size, screenshot.rgb)
if img.width != monitor['width']:
img = img.resize((monitor['width'], monitor['height']), Image.Resampling.LANCZOS)
window = tk.Toplevel(self.root)
window.geometry(f"{monitor['width']}x{monitor['height']}+{monitor['left']}+{monitor['top']}")
window.overrideredirect(1)
window.attributes('-topmost', 1)
img_tk = ImageTk.PhotoImage(img)
canvas = tk.Canvas(window, cursor='cross', highlightthickness=0)
canvas.pack(fill=tk.BOTH, expand=True)
canvas.image = img_tk
canvas.create_image(0, 0, image=img_tk, anchor=tk.NW)
start_x, start_y, rect = None, None, None
def on_click(event):
nonlocal start_x, start_y, rect
start_x, start_y = event.x, event.y
rect = canvas.create_rectangle(start_x, start_y, start_x, start_y, outline='red')
def on_drag(event):
nonlocal rect, start_x, start_y
if rect:
canvas.coords(rect, start_x, start_y, event.x, event.y)
def on_release(event):
nonlocal start_x, start_y
end_x, end_y = event.x, event.y
x1 = min(start_x, end_x)
y1 = min(start_y, end_y)
x2 = max(start_x, end_x)
y2 = max(start_y, end_y)
self.on_select(monitor, (x1, y1, x2 - x1, y2 - y1))
canvas.bind('<ButtonPress-1>', on_click)
canvas.bind('<B1-Motion>', on_drag)
canvas.bind('<ButtonRelease-1>', on_release)
def start(self):
self.root = tk.Tk()
self.root.withdraw()
for monitor in self.monitors:
self.create_window(monitor)
self.root.mainloop()
self.root.update()
def run_screen_selector(result):
selector = ScreenSelector(result)
selector.start()
def get_screen_selection():
if not selector_available:
raise ValueError('tkinter is not installed, unable to open picker')
with Manager() as manager:
res = manager.dict()
process = Process(target=run_screen_selector, args=(res,))
process.start()
process.join()
if 'monitor' in res and 'coordinates' in res:
return res.copy()
else:
return False

View File

@@ -17,12 +17,12 @@
;combo_pause = <ctrl>+<shift>+p ;combo_pause = <ctrl>+<shift>+p
;note: this specifies a combo to wait on for switching the OCR engine. As an example: <ctrl>+<shift>+a. To be used with combo_pause. The list of keys can be found here: https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key ;note: this specifies a combo to wait on for switching the OCR engine. As an example: <ctrl>+<shift>+a. To be used with combo_pause. The list of keys can be found here: https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key
;combo_engine_switch = <ctrl>+<shift>+a ;combo_engine_switch = <ctrl>+<shift>+a
;screen_capture_monitor = 2 ;note: screen_capture_area can be empty for the coordinate picker, "screen_N" (where N is the screen number starting from 1) for an entire screen, have a manual set of coordinates (x,y,width,height) or a window name (the first matching window title will be used)
;note: screen_capture_coords can be empty (whole screen), have a set of coordinates (x,y,width,height) or a window name (the first matching window title will be used) ;screen_capture_area =
;screen_capture_coords = ;screen_capture_area = screen_1
;screen_capture_coords = 400,200,1500,600 ;screen_capture_area = 400,200,1500,600
;screen_capture_coords = OBS ;screen_capture_area = OBS
;note: if screen_capture_coords is a window name, this can be changed to capture inactive windows too. On Linux, the window must then not be covered by other windows! ;note: if screen_capture_area is a window name, this can be changed to capture inactive windows too. On Linux, the window must then not be covered by other windows!
;screen_capture_only_active_windows = True ;screen_capture_only_active_windows = True
;screen_capture_delay_secs = 3 ;screen_capture_delay_secs = 3
;note: this specifies a combo to wait on for taking a screenshot instead of using the delay. As an example: <ctrl>+<shift>+s. The list of keys can be found here: https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key ;note: this specifies a combo to wait on for taking a screenshot instead of using the delay. As an example: <ctrl>+<shift>+s. The list of keys can be found here: https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "owocr" name = "owocr"
version = "1.10" version = "1.11"
description = "Japanese OCR" description = "Japanese OCR"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"