用Swift写一个MAC上的截图OCR工具

  1. 1. 想法来源
  2. 2. 可行性调查
  3. 3. 开始编写
  4. 4. 总结

想法来源

工作中,很多人喜欢把截图添附到Confluence里面,而这些图片里面的文字,一旦被需要,却很难像文本一样简单选择复制出来。

于是自然而然想到,要是有一个截图工具,能够对截取的图中的文字进行识别就好了。

这样就产生了一个需求: 编写一个能够在MacOS上运行的截图并识别的工具。

对这个需求再具体话一点,可以得到以下几个关键要素。

  1. 能运行在我现在的OS 10.15.4 上

  2. 这个工具不需要界面,只需要显示在右上角status bar中。

  3. 这个工具由两部分组成,截图,以及对截图的内容进行文字识别,最后识别的内容保存到剪贴板中。

可行性调查

首先截图,我们可以用mac自带的截图工具。

在swift代码中,新建一个进程然后调用截图工具。

1
2
3
4
5
let task = Process()
task.launchPath = "/usr/sbin/screencapture"
task.arguments = ["-i", "-r", "-c"]
task.launch()
task.waitUntilExit()

使用option -c 可以使截图直接保存到剪贴板中,之后我们的程序可以直接访问剪贴板获取图片。

接着如何实现OCR?

这里自然而然查到可以用apple的Vision Framework。

稍微改编一下这里的例子,就行了。

当然,最关键的,得看一下剪贴板如何调用。

MacOS中,剪贴板是所有程序公用的,编程的时候,可以通过NSPasteboard来访问。

1
2
3
4
5
6
7
//获取剪贴板
let pasteboard = NSPasteboard.general
// 获取剪贴板中的图片
if let readData = pasteboard.data(forType: NSPasteboard.PasteboardType.tiff),
let cbImage = CIImage(data: readData) {
// 执行识别文字
}

那如何把识别出来的问题重新放回剪贴板呢?

1
2
pasteboard.clearContents()
pasteboard.setString('text I want to copy to pasteboard', forType: .string)

好了,关键点都梳理清楚了。

还有一些并不关键的,放到下一节说。

开始编写

首先打开xcode,创建一个mac os 的app。

由于我们需要的是一个status bar only的程序,我们并不需要任何窗口,仅需要一个下拉菜单,于是可以参考
这里
来建一个项目的雏形。

AppDelegate.swift中创建一个菜单。

1
var statusItem: NSStatusItem?

然后override awakeFromNib

1
2
3
4
5
6
override func awakeFromNib() {
super.awakeFromNib()

statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem?.button?.title = "ScreenOCR"
}

在storyboard上
删除不需要的菜单项目。

创建一个

1
@IBOutlet weak var menu: NSMenu?

通过IBOutlet把menu关联到storyboard上的菜单中。

然后删除windowView,这样就没有了讨厌的窗口。不过我们会发现dock上还是会显示app的图标,所以我们在Info.plist中还得添加一行。
在末尾点击加号,然后选择Application is agent (UIElement), 然后在value的地方选择Yes。这样就不会有图标了。

接下来就是把我们上面整理好的逻辑添加上去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import Cocoa
import Vision
import CoreImage

@main
class AppDelegate: NSObject, NSApplicationDelegate {

var statusItem: NSStatusItem?
@IBOutlet weak var menu: NSMenu?
let pasteboard = NSPasteboard.general

func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
}

func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}

override func awakeFromNib() {
super.awakeFromNib()

//我们在这里向菜单里添加一个新的项目。当用户点击这个或者使用快捷键,就自动调用takeScreenShot这个function。
menu?.insertItem(withTitle: "Take screenshot", action: #selector(takeScreenShot), keyEquivalent: "4", at: 0)
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem?.button?.title = "OCR"
if let menu = menu {
statusItem?.menu = menu
}

}

@objc func takeScreenShot() {
let task = Process()
task.launchPath = "/usr/sbin/screencapture"
task.arguments = ["-i", "-r", "-c"]
task.launch()
task.waitUntilExit()
if let readData = pasteboard.data(forType: NSPasteboard.PasteboardType.tiff),
let cbImage = CIImage(data: readData)
{
let requestHandler = VNImageRequestHandler(ciImage: cbImage)
let request = VNRecognizeTextRequest(completionHandler: recognizeTextHandler)
do {
// Perform the text-recognition request.
try requestHandler.perform([request])
} catch {
print("Unable to perform the requests: \(error).")
}
}


}

func recognizeTextHandler(request: VNRequest, error: Error?) {
guard let observations =
request.results as? [VNRecognizedTextObservation] else {
return
}
let recognizedStrings = observations.compactMap { observation in
// Return the string of the top VNRecognizedText instance.
return observation.topCandidates(1).first?.string
}

// Process the recognized strings.
pasteboard.clearContents()
pasteboard.setString(recognizedStrings[0], forType: .string)
}
}

总结

能够从解决问题出发进行针对性的学习,还是学习的最大动力。否则看了也忘了,学了也没用。
有了基础的知识能力之后,遇到需要解决的问题,往往需要的只是组合,以及稍微再学习一点额外的东西。
继续加油吧。

如果你觉得本文对你有帮助,请给我点赞助。