cmd_server.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. # ===============================================
  2. # =============== 命令行-解析和执行 ===============
  3. # ===============================================
  4. import time
  5. import json
  6. import argparse
  7. from threading import Condition
  8. from ..utils.call_func import CallFunc
  9. from ..utils.utils import findImages
  10. from ..event_bus.pubsub_service import PubSubService # 发布/订阅管理器
  11. # 命令执行器
  12. class _Actuator:
  13. def __init__(self):
  14. self.pyDict = {} # python模块字典
  15. self.qmlDict = {} # qml模块字典
  16. self.tagPageConn = None # 页面连接器的引用
  17. # 初始化,并收集信息。传入qml模块字典
  18. def initCollect(self, qmlModuleDict):
  19. qmlModuleDict = qmlModuleDict.toVariant()
  20. self.qmlDict.update(qmlModuleDict)
  21. # 获取页面连接器实例
  22. from ..tag_pages.tag_pages_connector import TagPageConnObj
  23. self.tagPageConn = TagPageConnObj
  24. # ============================== 页面管理 ==============================
  25. # 返回当前 [可创建的页面模板] 和 [已创建的页面] 的信息
  26. def getAllPages(self):
  27. TabViewManager = self.qmlDict["TabViewManager"]
  28. pageList = TabViewManager.getPageList().toVariant()
  29. infoStr = "All opened pages:\npage_index\tkey\ttitle\n"
  30. for index, value in enumerate(pageList):
  31. infoStr += f'{index}\t{value["ctrlKey"]}\t{value["info"]["title"]}\n'
  32. infoList = TabViewManager.getInfoList().toVariant()
  33. infoStr += (
  34. "\nAll page templates that can be opened:\ntemplate_index\tkey\ttitle\n"
  35. )
  36. for index, value in enumerate(infoList):
  37. infoStr += f'{index}\t{value["key"]}\t{value["title"]}\n'
  38. infoStr += "\nUsage of create a page:\n"
  39. infoStr += " Umi-OCR --add_page [template_index]\n"
  40. infoStr += "Usage of delete a page:\n"
  41. infoStr += " Umi-OCR --del_page [page_index]\n"
  42. infoStr += "Usage of query the modules that can be called:\n"
  43. infoStr += " Umi-OCR --all_modules\n"
  44. return infoStr
  45. # 创建页面
  46. def addPage(self, index):
  47. try:
  48. index = int(index)
  49. except ValueError:
  50. return f"[Error] template_index must be integer, not {index}."
  51. TabViewManager = self.qmlDict["TabViewManager"]
  52. infoList = TabViewManager.getInfoList().toVariant()
  53. l = len(infoList) - 1
  54. if index < 0 or index > l:
  55. return f"[Error] template_index {index} out of range (0~{l})."
  56. return self.call("TabViewManager", "qml", "addTabPage", False, -1, index)
  57. # 删除页面
  58. def delPage(self, index):
  59. try:
  60. index = int(index)
  61. except ValueError:
  62. return f"[Error] page_index must be integer, not {index}."
  63. TabViewManager = self.qmlDict["TabViewManager"]
  64. pageList = TabViewManager.getPageList().toVariant()
  65. l = len(pageList) - 1
  66. if index < 0 or index > l:
  67. return f"[Error] page_index {index} out of range (0~{l})."
  68. return self.call("TabViewManager", "qml", "delTabPage", False, index)
  69. # 通过key创建页面
  70. def addPageByKey(self, key):
  71. # 1. 检查截图标签页,如果未创建则创建
  72. module, _ = self.getModuleFromName(key, "qml")
  73. if module == None:
  74. tvm = self.qmlDict["TabViewManager"]
  75. infoList = tvm.getInfoList().toVariant()
  76. f2 = False
  77. for i, v in enumerate(infoList):
  78. if v["key"] == key:
  79. f2 = True
  80. self.addPage(i)
  81. break
  82. if not f2:
  83. return f"[Error] Template {key} not found."
  84. for i in range(10):
  85. time.sleep(0.5)
  86. module, _ = self.getModuleFromName(key, "qml")
  87. if module != None:
  88. break
  89. if module == None:
  90. return f"[Error] Unable to create template {key}."
  91. return "[Success]"
  92. # ============================== 动态模块调用 ==============================
  93. # 返回所有可调用模块
  94. def getModules(self):
  95. pyd, qmld = {}, {}
  96. pages = self.tagPageConn.pages
  97. for p in pages:
  98. if pages[p]["qmlObj"]:
  99. qmld[p] = pages[p]["qmlObj"]
  100. if pages[p]["pyObj"]:
  101. pyd[p] = pages[p]["pyObj"]
  102. pyd.update(self.pyDict)
  103. qmld.update(self.qmlDict)
  104. return {"py": pyd, "qml": qmld}
  105. # 传入(不完整的)模块名,搜索并返回模块实例。type: py / qml
  106. def getModuleFromName(self, moduleName, type_):
  107. d = self.getModules()[type_]
  108. module = None
  109. if moduleName in d:
  110. module = d[moduleName]
  111. else:
  112. for name in d.keys(): # 若输入模块名的前几个字母,也可以匹配
  113. if name.startswith(moduleName):
  114. moduleName = name
  115. module = d[name]
  116. break
  117. return module, moduleName
  118. # 返回所有可调用模块的帮助信息
  119. def getModulesHelp(self):
  120. modules = self.getModules()
  121. help = "\nPython modules: (Usage: Umi-OCR --call_py [module name])\n"
  122. for k in modules["py"].keys():
  123. help += f" {k}\n"
  124. help += "\nQml modules: (Usage: Umi-OCR --call_qml [module name])\n"
  125. for k in modules["qml"].keys():
  126. help += f" {k}\n"
  127. help += f"\nTips: module name can only write the first letters, such as [ScreenshotOCR_1] → [Scr]"
  128. return help
  129. # 返回一个模块的所有函数的帮助信息
  130. def getModuleFuncsHelp(self, moduleName, type_):
  131. module, moduleName = self.getModuleFromName(moduleName, type_)
  132. typeStr = "Python" if type_ == "py" else "qml"
  133. if not module:
  134. return f'[Error] {typeStr} module "{moduleName}" non-existent.'
  135. funcs = [
  136. func
  137. for func in vars(type(module)).keys()
  138. if callable(getattr(module, func))
  139. ]
  140. help = f'All functions in {typeStr} module "{moduleName}":\n'
  141. for f in funcs:
  142. f = str(f)
  143. if not f.startswith("_"):
  144. help += f" {f}\n"
  145. help += f"Usage: Umi-OCR --call_qml {moduleName} --func [function name]\n"
  146. return help
  147. # 调用一个模块函数。type: py / qml , thread: True 同步在子线程 / False 异步在主线程
  148. def call(self, moduleName, type_, funcName, thread, *paras):
  149. module, moduleName = self.getModuleFromName(moduleName, type_)
  150. typeStr = "Python" if type_ == "py" else "qml"
  151. if not module:
  152. return f'[Error] {typeStr} module "{moduleName}" non-existent.'
  153. func = getattr(module, funcName, None)
  154. if not func:
  155. return f'[Error] func "{funcName}" not exist in {typeStr} module "{moduleName}".'
  156. try:
  157. if thread: # 在子线程执行,返回结果
  158. return func(*paras)
  159. else: # 在主线程执行,返回标志文本
  160. CallFunc.now(func, *paras) # 在主线程中调用回调函数
  161. return f'Calling "{funcName}" in main thread.'
  162. except Exception as e:
  163. return f'[Error] calling {typeStr} module "{moduleName}" - "{funcName}" {paras}: {e}'
  164. # ============================== 便捷指令 ==============================
  165. # 控制主窗口
  166. def ctrlWindow(self, show, hide, quit):
  167. if show:
  168. self.call("MainWindow", "qml", "setVisibility", False, True)
  169. return "Umi-OCR show."
  170. elif hide:
  171. self.call("MainWindow", "qml", "setVisibility", False, False)
  172. return "Umi-OCR hide."
  173. elif quit:
  174. self.call("MainWindow", "qml", "quit", False)
  175. return "Umi-OCR quit."
  176. # 快捷OCR:截图/粘贴/路径,并获取返回结果
  177. def quick_ocr(self, ss, clip, paras):
  178. # 1. 检查截图标签页,如果未创建则创建
  179. msg = self.addPageByKey("ScreenshotOCR")
  180. if msg != "[Success]":
  181. return msg
  182. # 2. 订阅事件,监听 <<ScreenshotOcrEnd>>
  183. isOcrEnd = False
  184. resList = []
  185. condition = Condition() # 线程同步器
  186. def onOcrEnd(recentResult):
  187. nonlocal isOcrEnd, resList
  188. isOcrEnd = True
  189. resList = recentResult
  190. with condition: # 释放线程阻塞
  191. condition.notify()
  192. PubSubService.subscribe("<<ScreenshotOcrEnd>>", onOcrEnd)
  193. # 3. 调用截图标签页的函数
  194. if ss: # 截图
  195. if not paras: # 无参数,手动截图
  196. self.call("ScreenshotOCR", "qml", "screenshot", False)
  197. else: # 有参数,自动截图 umi-ocr --screenshot screen=0 rect=0,100,500,200
  198. rect = [0, 0, 0, 0] # 截图矩形框
  199. screen = 0 # 显示器编号
  200. para_args = []
  201. try:
  202. for para in paras: # 空格分隔
  203. para_args.extend(para.split())
  204. for part in para_args:
  205. if part.startswith("rect="):
  206. rect_values = part[len("rect=") :].split(",")
  207. rect_values = [int(v) for v in rect_values]
  208. rect[: len(rect_values)] = rect_values # 补齐rect的值
  209. elif part.startswith("screen="):
  210. screen = int(part[len("screen=") :])
  211. self.call(
  212. "ScreenshotOCR", "qml", "autoScreenshot", False, rect, screen
  213. )
  214. except Exception as e:
  215. return f"[Error] {e}"
  216. elif clip: # 粘贴
  217. self.call("ScreenshotOCR", "qml", "paste", False)
  218. else: # 路径
  219. if not paras:
  220. return "[Error] Paths is empty."
  221. paths = findImages(paras, True) # 递归搜索
  222. if not paths:
  223. return "[Error] No valid path."
  224. self.call("ScreenshotOCR", "qml", "ocrPaths", False, paths)
  225. # 4. 堵塞等待任务完成,注销事件订阅
  226. with condition:
  227. while not isOcrEnd:
  228. condition.wait()
  229. PubSubService.unsubscribe("<<ScreenshotOcrEnd>>", onOcrEnd)
  230. # 5. 处理结果列表,转文本
  231. text = ""
  232. for i, r in enumerate(resList): # 遍历图片
  233. if text and not text.endswith("\n"): # 如果上次结果结尾没有换行,则补换行
  234. text += "\n"
  235. if r["code"] == 100:
  236. for d in r["data"]: # 遍历文本块
  237. text += d["text"] + d["end"]
  238. elif r["code"] != 101 and isinstance(r["data"], str):
  239. text += r["data"]
  240. if not text:
  241. text = "[Message] No text in OCR result."
  242. return text
  243. # 创建二维码
  244. def qrcode_create(self, paras):
  245. if len(paras) < 2:
  246. return (
  247. '[Error] Not enough arguments passed! Must pass "text" "save_image.jpg"'
  248. )
  249. text, path = paras[0], paras[1]
  250. if len(paras) == 3:
  251. w = h = int(paras[2])
  252. elif len(paras) == 4:
  253. w, h = int(paras[2]), int(paras[3])
  254. else:
  255. w = h = 0
  256. try:
  257. from ..mission.mission_qrcode import MissionQRCode
  258. pil = MissionQRCode.createImage(
  259. text,
  260. format="QRCode", # 格式
  261. w=w, # 宽高
  262. h=h,
  263. quiet_zone=-1, # 边缘宽度
  264. ec_level=-1, # 纠错等级
  265. )
  266. if isinstance(pil, str):
  267. return pil
  268. pil.save(path)
  269. return f"Successfully saved to {path}"
  270. except Exception as e:
  271. return f"[Error] {str(e)}"
  272. # 识别二维码
  273. def qrcode_read(self, paras):
  274. if len(paras) < 1:
  275. return '[Error] Not enough arguments passed! Must pass "image_to_recognize.jpg"'
  276. try:
  277. from ..mission.mission_qrcode import MissionQRCode
  278. from PIL import Image
  279. except Exception as e:
  280. return f"[Error] {str(e)}"
  281. resText = ""
  282. paths = findImages(paras, True) # 递归搜索图片
  283. for index, path in enumerate(paths):
  284. if index != 0:
  285. resText += "\n"
  286. try:
  287. pil = Image.open(path)
  288. res = MissionQRCode.addMissionWait({}, [{"pil": pil}])
  289. res = res[0]["result"]
  290. if res["code"] == 100:
  291. t = ""
  292. for i, d in enumerate(res["data"]):
  293. if i != 0:
  294. t += "\n"
  295. t += d["text"]
  296. resText += t
  297. elif res["code"] == 101:
  298. resText += "No code in image."
  299. else:
  300. resText += f"[Error] Code: {res['code']}\nMessage: {res['data']}"
  301. except Exception as e:
  302. resText += f"[Error] {str(e)}"
  303. return resText
  304. CmdActuator = _Actuator()
  305. # 命令解析器
  306. class _Cmd:
  307. def __init__(self):
  308. self._parser = None
  309. def init(self):
  310. if self._parser:
  311. return
  312. self._parser = argparse.ArgumentParser(prog="Umi-OCR")
  313. # 便捷指令
  314. self._parser.add_argument(
  315. "--show", action="store_true", help="Make the app appear in the foreground."
  316. )
  317. self._parser.add_argument(
  318. "--hide", action="store_true", help="Hide app in the background."
  319. )
  320. self._parser.add_argument("--quit", action="store_true", help="Quit app.")
  321. self._parser.add_argument(
  322. "--screenshot",
  323. action="store_true",
  324. help="Screenshot OCR and output the result.",
  325. )
  326. self._parser.add_argument(
  327. "--clipboard",
  328. action="store_true",
  329. help="Clipboard OCR and output the result.",
  330. )
  331. self._parser.add_argument(
  332. "--path",
  333. action="store_true",
  334. help="OCR the image in path and output the result.",
  335. )
  336. self._parser.add_argument(
  337. "--qrcode_create",
  338. action="store_true",
  339. help='Create a QR code from the text. Use --qrcode_create "text" "save_image.jpg"',
  340. )
  341. self._parser.add_argument(
  342. "--qrcode_read",
  343. action="store_true",
  344. help='Read the QR code. Use --qrcode_read "image_to_recognize.jpg"',
  345. )
  346. # 页面管理
  347. self._parser.add_argument(
  348. "--all_pages",
  349. action="store_true",
  350. help="Output all template and page information.",
  351. )
  352. self._parser.add_argument(
  353. "--add_page", type=int, help="usage: Umi-OCR --all_pages"
  354. )
  355. self._parser.add_argument(
  356. "--del_page", type=int, help="usage: Umi-OCR --all_pages"
  357. )
  358. # 函数调用
  359. self._parser.add_argument(
  360. "--all_modules",
  361. action="store_true",
  362. help="Output all module names that can be called.",
  363. )
  364. self._parser.add_argument(
  365. "--call_py", help="Calling a function on a Python module."
  366. )
  367. self._parser.add_argument(
  368. "--call_qml", help="Calling a function on a Qml module."
  369. )
  370. self._parser.add_argument(
  371. "--func", help="The name of the function to be called."
  372. )
  373. self._parser.add_argument(
  374. "--thread",
  375. action="store_true",
  376. help="The function will be called on the child thread and return the result, but it may be unstable or cause QML to crash.",
  377. )
  378. # 输出
  379. self._parser.add_argument(
  380. "--clip",
  381. action="store_true",
  382. help="Copy the results to the clipboard.",
  383. )
  384. self._parser.add_argument(
  385. "--output",
  386. help="The path to the file where results will be saved. (overwrite)",
  387. )
  388. self._parser.add_argument(
  389. "--output_append",
  390. help="The path to the file where results will be saved. (append)",
  391. )
  392. self._parser.add_argument("-->", help='"-->" equivalent to "--output"')
  393. self._parser.add_argument("-->>", help='"-->>" equivalent to "--output_append"')
  394. self._parser.add_argument("paras", nargs="*", help="parameters of [--func].")
  395. # 分析指令,返回指令对象或报错字符串
  396. def parse(self, argv):
  397. self.init()
  398. # 特殊情况
  399. if "-h" in argv or "--help" in argv: # 帮助
  400. return self._parser.format_help()
  401. if len(argv) == 0: # 空指令
  402. CmdActuator.ctrlWindow(True, False, False) # 展示主窗
  403. return self._parser.format_help() # 返回帮助
  404. # 正常解析
  405. try:
  406. return self._parser.parse_args(argv)
  407. except SystemExit as e:
  408. return f"Your argv: {argv}\n[Error]: {e}\nusage: Umi-OCR --help"
  409. except Exception as e:
  410. return f"Your argv: {argv}\n[Error]: {e}\nusage: Umi-OCR --help"
  411. # 执行指令,返回执行结果字符串
  412. def execute(self, argv):
  413. args = self.parse(argv)
  414. if isinstance(args, str):
  415. return args
  416. if args.all_modules:
  417. return CmdActuator.getModulesHelp()
  418. # 便捷指令
  419. if args.show or args.hide or args.quit: # 控制主窗
  420. return CmdActuator.ctrlWindow(args.show, args.hide, args.quit)
  421. if args.screenshot or args.clipboard or args.path: # 快捷识图
  422. return CmdActuator.quick_ocr(args.screenshot, args.clipboard, args.paras)
  423. if args.qrcode_create: # 写二维码
  424. return CmdActuator.qrcode_create(args.paras)
  425. if args.qrcode_read: # 读二维码
  426. return CmdActuator.qrcode_read(args.paras)
  427. # 页面管理
  428. if args.all_pages:
  429. return CmdActuator.getAllPages()
  430. if not args.add_page is None:
  431. return CmdActuator.addPage(args.add_page)
  432. if not args.del_page is None:
  433. return CmdActuator.delPage(args.del_page)
  434. # 动态模块调用
  435. if args.call_py:
  436. if args.func:
  437. return CmdActuator.call(
  438. args.call_py,
  439. "py",
  440. args.func,
  441. args.thread,
  442. *self.format_paras(args.paras),
  443. )
  444. else:
  445. return CmdActuator.getModuleFuncsHelp(args.call_py, "py")
  446. if args.call_qml:
  447. if args.func:
  448. return CmdActuator.call(
  449. args.call_qml,
  450. "qml",
  451. args.func,
  452. args.thread,
  453. *self.format_paras(args.paras),
  454. )
  455. else:
  456. return CmdActuator.getModuleFuncsHelp(args.call_qml, "qml")
  457. # paras 格式化
  458. def format_paras(self, paras):
  459. def convert_param(param):
  460. try:
  461. return int(param)
  462. except ValueError:
  463. pass
  464. try:
  465. return float(param)
  466. except ValueError:
  467. pass
  468. try:
  469. return json.loads(param)
  470. except json.JSONDecodeError:
  471. pass
  472. return param
  473. return [convert_param(p) for p in paras]
  474. CmdServer = _Cmd()