Skip to content

Commit 38bfdc9

Browse files
committed
Two-pass detection to balance speed and accuracy
1 parent 90da8f9 commit 38bfdc9

File tree

3 files changed

+52
-22
lines changed

3 files changed

+52
-22
lines changed

TextGrabber2/Sources/App.swift

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,15 @@ final class App: NSObject, NSApplicationDelegate {
142142
item.setOn(SMAppService.mainApp.isEnabled)
143143
return item
144144
}()
145+
146+
func statusItemPosition() -> (CGRect, NSScreen?)? {
147+
guard let button = statusItem.button, let window = button.window else {
148+
Logger.log(.error, "Missing button or window to provide positioning info")
149+
return nil
150+
}
151+
152+
return (window.convertToScreen(button.frame), window.screen ?? .main)
153+
}
145154
}
146155

147156
// MARK: - Life Cycle
@@ -220,6 +229,7 @@ private extension App {
220229
return Logger.assertFail("Missing menu to proceed")
221230
}
222231

232+
currentResult = nil
223233
pasteboardChangeCount = NSPasteboard.general.changeCount
224234
clipboardItem.isHidden = NSPasteboard.general.isEmpty
225235
saveImageItem.isEnabled = false
@@ -232,23 +242,36 @@ private extension App {
232242
howToItem.isHidden = true
233243

234244
Task {
235-
let resultData = await Recognizer.detect(image: image)
236-
currentResult = resultData
237-
238-
hintItem.title = resultData.candidates.isEmpty ? Localized.menuTitleHintCapture : Localized.menuTitleHintCopy
239-
howToItem.isHidden = !resultData.candidates.isEmpty
240-
copyAllItem.isHidden = resultData.candidates.count < 2
241-
saveImageItem.isEnabled = true
242-
243-
let separator = NSMenuItem.separator()
244-
menu.insertItem(separator, at: menu.index(of: howToItem) + 1)
245-
menu.removeItems { $0 is ResultItem }
246-
247-
for text in resultData.candidates.reversed() {
248-
let item = ResultItem(title: text)
249-
item.addAction { NSPasteboard.general.string = text }
250-
menu.insertItem(item, at: menu.index(of: separator) + 1)
251-
}
245+
let fastResult = await Recognizer.detect(image: image, level: .fast)
246+
showResult(fastResult, in: menu)
247+
248+
let accurateResult = await Recognizer.detect(image: image, level: .accurate)
249+
showResult(accurateResult, in: menu)
250+
}
251+
}
252+
253+
func showResult(_ resultData: Recognizer.ResultData, in menu: NSMenu) {
254+
guard currentResult != resultData else {
255+
#if DEBUG
256+
Logger.log(.debug, "No change in result data")
257+
#endif
258+
return
259+
}
260+
261+
currentResult = resultData
262+
hintItem.title = resultData.candidates.isEmpty ? Localized.menuTitleHintCapture : Localized.menuTitleHintCopy
263+
howToItem.isHidden = !resultData.candidates.isEmpty
264+
copyAllItem.isHidden = resultData.candidates.count < 2
265+
saveImageItem.isEnabled = true
266+
267+
let separator = NSMenuItem.separator()
268+
menu.insertItem(separator, at: menu.index(of: howToItem) + 1)
269+
menu.removeItems { $0 is ResultItem }
270+
271+
for text in resultData.candidates.reversed() {
272+
let item = ResultItem(title: text)
273+
item.addAction { NSPasteboard.general.string = text }
274+
menu.insertItem(item, at: menu.index(of: separator) + 1)
252275
}
253276
}
254277
}

TextGrabber2/Sources/Extensions/NSWindow+Extension.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,16 @@ extension NSWindow {
2929
}
3030

3131
var preferredRect = originalRect
32-
preferredRect.origin.x += preferredRect.size.width - Constants.preferredWidth
3332
preferredRect.size.width = Constants.preferredWidth
3433

34+
// Ensure the window always appears below the status item, within the screen boundaries
35+
if let (rect, screen) = (NSApp.delegate as? App)?.statusItemPosition() {
36+
preferredRect.origin.x = min(
37+
max(rect.minX - Constants.breathPadding, Constants.breathPadding),
38+
(screen?.frame.width ?? 1e6) - Constants.preferredWidth - Constants.breathPadding
39+
)
40+
}
41+
3542
swizzled_setFrame(preferredRect, display: display, animate: animate)
3643
}
3744
}
@@ -41,5 +48,6 @@ extension NSWindow {
4148
private extension NSWindow {
4249
enum Constants {
4350
static let preferredWidth: Double = 240
51+
static let breathPadding: Double = 8
4452
}
4553
}

TextGrabber2/Sources/Recognizer.swift

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import AppKit
1212
https://developer.apple.com/documentation/vision/recognizing_text_in_images
1313
*/
1414
enum Recognizer {
15-
struct ResultData {
15+
struct ResultData: Equatable {
1616
let candidates: [String]
1717

1818
init(candidates: [String]) {
@@ -37,7 +37,7 @@ enum Recognizer {
3737
}
3838
}
3939

40-
static func detect(image: CGImage) async -> ResultData {
40+
static func detect(image: CGImage, level: VNRequestTextRecognitionLevel) async -> ResultData {
4141
await withCheckedContinuation { continuation in
4242
let request = VNRecognizeTextRequest { request, error in
4343
let candidates = request.results?
@@ -49,8 +49,7 @@ enum Recognizer {
4949
}
5050
}
5151

52-
// Prefer accuracy over speed
53-
request.recognitionLevel = .accurate
52+
request.recognitionLevel = level
5453
request.usesLanguageCorrection = true
5554
request.automaticallyDetectsLanguage = true
5655

0 commit comments

Comments
 (0)