Skip to content

Commit 46e15f1

Browse files
authored
Merge pull request #13 from RhetTbull/feature-services-menu-5
Added Services menu action for detecting text in image files #5
2 parents 50d1b22 + c7d2cb0 commit 46e15f1

File tree

2 files changed

+74
-7
lines changed

2 files changed

+74
-7
lines changed

setup.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@
3333
"you will also need to grant Textinator full disk access in "
3434
"System Preferences > Security & Privacy > Privacy > Full Disk Access.",
3535
"NSAppleEventsUsageDescription": "Textinator needs permission to send AppleScript events to add itself to Login Items.",
36+
"NSServices": [
37+
{
38+
"NSMenuItem": {"default": "Detect text with Textinator"},
39+
"NSMessage": "detectTextInImage",
40+
"NSPortName": "Textinator",
41+
"NSUserData": "detectTextInImage",
42+
"NSRequiredContext": {"NSTextContent": "FilePath"},
43+
"NSSendTypes": ["NSPasteboardTypeURL"],
44+
"NSSendFileTypes": ["public.image"],
45+
},
46+
],
3647
},
3748
}
3849

src/textinator.py

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,35 @@
77
import datetime
88
import plistlib
99

10+
import objc
1011
import Quartz
1112
import rumps
13+
from AppKit import NSPasteboardTypeFileURL
1214
from Foundation import (
15+
NSURL,
1316
NSLog,
1417
NSMetadataQuery,
1518
NSMetadataQueryDidFinishGatheringNotification,
1619
NSMetadataQueryDidStartGatheringNotification,
1720
NSMetadataQueryDidUpdateNotification,
1821
NSMetadataQueryGatheringProgressNotification,
1922
NSNotificationCenter,
23+
NSObject,
2024
NSPredicate,
25+
NSString,
26+
NSUTF8StringEncoding,
2127
)
2228

2329
from loginitems import add_login_item, list_login_items, remove_login_item
2430
from macvision import (
2531
ciimage_from_file,
2632
detect_qrcodes_in_ciimage,
27-
detect_qrcodes_in_file,
2833
detect_text_in_ciimage,
29-
detect_text_in_file,
3034
get_supported_vision_languages,
3135
)
32-
from pasteboard import Pasteboard, TIFF
36+
from pasteboard import TIFF, Pasteboard
3337
from utils import get_app_path, verify_desktop_access
38+
import typing as t
3439

3540
__version__ = "0.9.0"
3641

@@ -142,6 +147,11 @@ def __init__(self, *args, **kwargs):
142147

143148
self.log(__file__)
144149

150+
from AppKit import NSApplication
151+
152+
self.service_provider = ServiceProvider.alloc().initWithApp_(self)
153+
NSApplication.sharedApplication().setServicesProvider_(self.service_provider)
154+
145155
# This will be used by clipboard_watcher() to detect changes to the pasteboard
146156
# (which everyone but Apple calls the clipboard)
147157
self.pasteboard = Pasteboard()
@@ -241,9 +251,6 @@ def on_toggle(self, sender):
241251
def on_clear_clipboard(self, sender):
242252
"""Clear the clipboard"""
243253
self.pasteboard.clear()
244-
# ZZZ replace pyperclip with a self.clipboard_clear, self.clipboard_copy, self.clipboard_paste
245-
# or Clipboard class with those methods
246-
# that auto update the pasteboard_count
247254

248255
def on_confidence(self, sender):
249256
"""Change confidence threshold."""
@@ -434,7 +441,7 @@ def process_image(self, image: Quartz.CIImage) -> str:
434441
clipboard_text = (
435442
self.pasteboard.paste() if self.pasteboard.has_text() else ""
436443
)
437-
clipboard_text = f"{clipboard_text}\n{text}"
444+
clipboard_text = f"{clipboard_text}\n{text}" if clipboard_text else text
438445
else:
439446
clipboard_text = text
440447

@@ -503,5 +510,54 @@ def process_clipboard_image(self):
503510
self.log("failed to get image data from pasteboard")
504511

505512

513+
def serviceSelector(fn):
514+
# this is the signature of service selectors
515+
return objc.selector(fn, signature=b"v@:@@o^@")
516+
517+
518+
def ErrorValue(e):
519+
"""Handler for errors returned by the service."""
520+
NSLog(f"{APP_NAME} {__version__} error: {e}")
521+
return e
522+
523+
524+
class ServiceProvider(NSObject):
525+
"""Service provider class to handle messages from the Services menu
526+
527+
Initialize with ServiceProvider.alloc().initWithApp_(app)
528+
"""
529+
530+
app: t.Optional[Textinator] = None
531+
532+
def initWithApp_(self, app: Textinator):
533+
self = objc.super(ServiceProvider, self).init()
534+
self.app = app
535+
return self
536+
537+
@serviceSelector
538+
def detectTextInImage_userData_error_(self, pasteboard, userdata, error) -> None:
539+
"""Detect text in an image on the clipboard."""
540+
self.app.log("detectTextInImage_userData_error_ called via Services menu")
541+
542+
if self.app._paused:
543+
self.app.log("skipping text detection because app is paused")
544+
545+
try:
546+
for item in pasteboard.pasteboardItems():
547+
pb_url_data = item.dataForType_(NSPasteboardTypeFileURL)
548+
pb_url = NSURL.URLWithString_(
549+
NSString.alloc().initWithData_encoding_(
550+
pb_url_data, NSUTF8StringEncoding
551+
)
552+
)
553+
self.app.log(f"processing file from Services menu: {pb_url.path()}")
554+
image = Quartz.CIImage.imageWithContentsOfURL_(pb_url)
555+
self.app.process_image(image)
556+
except Exception as e:
557+
return ErrorValue(e)
558+
559+
return None
560+
561+
506562
if __name__ == "__main__":
507563
Textinator(name=APP_NAME, quit_button=None).run()

0 commit comments

Comments
 (0)