commit 8dc83df34aae95024d096edbfa86056b67cd1600 Author: wehub-resource-sync Date: Mon Jul 13 12:07:56 2026 +0800 chore: import upstream snapshot with attribution diff --git a/.github/ISSUE_TEMPLATE/01_bug.yaml b/.github/ISSUE_TEMPLATE/01_bug.yaml new file mode 100644 index 0000000..4f8c664 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_bug.yaml @@ -0,0 +1,60 @@ +name: Bug 错误 +description: Report bugs 报告错误 +body: +- type: markdown + attributes: + value: | + Please first search the [Issues](https://github.com/hiroi-sora/Umi-OCR/issues) to make sure that no one else has already raised a similar issue. If there is an existing issue, you can reply under that Issue to provide more details. The more the developer knows, the easier it is to fix the problem. + + 请先浏览或搜索 [Issues](https://github.com/hiroi-sora/Umi-OCR/issues) ,确保没有其他人曾经提出过类似问题。如果已有类似问题,你可以在该Issue下回复,提供更多细节。开发者知道得越多,修复问题越容易。 + + Please ensure that you have read the [FAQ](https://github.com/hiroi-sora/Umi-OCR/issues/447) before raising a new issue. + + 请确保在提问之前已经阅读过 [常见问题解答](https://github.com/hiroi-sora/Umi-OCR/issues/447)。 +- type: checkboxes + attributes: + label: Issues + options: + - label: I have browsed through the Issues. 我已浏览过Issues,确定没有重复提问。 + required: true +- type: input + attributes: + label: Umi-OCR version 程序版本 + placeholder: 2.1.0 + validations: + required: true +- type: input + attributes: + label: Windows version 系统版本 + placeholder: win10 + validations: + required: true +- type: dropdown + attributes: + label: OCR plugins Used 使用的OCR插件 + description: If the bug is related to OCR, please select the plugins you were using. 如果Bug与OCR相关,请选择您使用的插件。可多选。 + multiple: true + options: + - PaddleOCR + - RapidOCR + - TesseractOCR + - Pix2Text + - ChineseOCR + - Other 其他插件/自开发插件 + validations: + required: false +- type: textarea + attributes: + label: Reproduction steps 复现步骤 + description: Describe the reproduction steps precisely if possible. 尽可能详细描述错误产生的细节。 + validations: + required: true +- type: textarea + attributes: + label: Problem screenshots or related files (optional) 问题截图或相关文件(可选) + description: + If there are pop-up errors, interface anomalies, or obviously incorrect results, please provide complete screenshots. + If a specific image or document can reliably trigger a bug, please upload that file. + + 如果存在弹窗报错、界面异常、结果明显错误等问题,请提供完整截图。 + 如果特定的图片或文档能稳定触发Bug,请上传该文件。 diff --git a/.github/ISSUE_TEMPLATE/02_request.yaml b/.github/ISSUE_TEMPLATE/02_request.yaml new file mode 100644 index 0000000..5fd9995 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02_request.yaml @@ -0,0 +1,33 @@ +name: Feature Request 功能请求 +description: Create the request for a new feature 提出一个增加新功能的请求 +body: +- type: markdown + attributes: + value: | + Please first browse the [Issues](https://github.com/hiroi-sora/Umi-OCR/issues) to ensure that no one else has previously raised a similar request. If there is a similar request, you can post a reply under the Issue; the number of replies may affect the priority decision of development. The developer has limited time and energy, and may not be able to fulfill all requests. Your understanding is appreciated. + + When proposing a new feature, please clearly state the applicable scenarios, operation methods, and the desired effects. + + --- + + 请先浏览 [Issues](https://github.com/hiroi-sora/Umi-OCR/issues) ,确保没有其他人曾经提出过类似请求。如果已有类似的请求,你可以在Issue下回帖;回帖人数可能影响开发的优先级决策。开发者时间精力有限,不一定能实现所有的请求,敬请谅解。 + + 提出新功能时,请明确说明适用场景、操作方式、希望达到的效果。 +- type: checkboxes + attributes: + label: Issues + options: + - label: I have browsed through the Issues. 我已浏览过Issues,确定没有重复的建议。 + required: true +- type: textarea + attributes: + label: Expected behavior 预期的功能 + validations: + required: true +- type: textarea + attributes: + label: Approximate reference (optional) 近似的参考(可选) + description: + If another software already has a similar feature, you can provide a description or screenshots of that software for reference. + + 如果别的软件已有近似功能,可以提供该软件的描述或截图,以供参考。 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..90ffd33 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Discussions 讨论 + url: https://github.com/hiroi-sora/Umi-OCR/discussions + about: | + If you encounter issues with secondary development, wish to implement niche features, have suggestions for the future development of the project, or want to share your usage experiences, tips, or insights about the project, please post them here. + + 二次开发遇到问题,想要实现小众功能,对项目未来发展的建议,使用经验、技巧或者对项目的见解,请在这里发表。 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5cf79ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +开发工具 +命名规范.md +Temp +build +PyStand_for_UmiOCR/build +.vscode +*.exe +*.txt + +# 发布 +release +release.py + +# 开发工具 +!dev-tools/i18n/release +!dev-tools/i18n/*.exe +dev-tools/7z +dev-tools/*.zip + +# 排除一级目录 +UmiOCR-data/* +!UmiOCR-data/main.py +!UmiOCR-data/version.py +!UmiOCR-data/Help 帮助.txt +# 允许二级目录 +!UmiOCR-data/**/ +# 排除运行环境目录 +UmiOCR-data/site-packages* +UmiOCR-data/runtime +UmiOCR-data/lib +UmiOCR-data/plugins/*/ +UmiOCR-data/temp +UmiOCR-data/temp_doc +# 允许运行环境压缩包 +!UmiOCR-data/*.7z +!UmiOCR-data/*.zip +# 排除缓存 +**__pycache__** +UmiOCR-data/i18n/_ \ No newline at end of file diff --git a/CHANGE_LOG.md b/CHANGE_LOG.md new file mode 100644 index 0000000..dc2b45d --- /dev/null +++ b/CHANGE_LOG.md @@ -0,0 +1,334 @@ +# 分支使用说明 + +点击后续列表的版本号链接,可前往对应备份分支页面。 + +部分备份分支含有体积较大的二进制库,会让你花费长时间下载。因此,建议只下载你需要用的分支。 + +方法1:将所需的分支,fork到你自己的账号下,然后clone你自己仓库。 + +方法2:使用以下命令手动clone指定分支: + +``` +git clone --single-branch --branch [分支名] https://github.com/hiroi-sora/Umi-OCR.git +``` + +方法3:在本仓库手动下载指定分支的zip源码包。 + +`[分支名]` 可以是 `main` 、`release/2.0.0` 等,详见下方列表。 + +`main`、`dev` 等分支,可能含有开发中的不稳定的新功能。如果用于研究学习或二次开发,建议选择 `release` 开头的分支。 + +# 更新日志 CHANGE LOG + +### v2.1.5 `2025.3.26` + +分支名:`main` + +- 新增:日志机制。在命令行中启动 Umi-OCR 可查看实时日志。指定级别以上(默认为ERROR)的日志被保存到 `Umi-OCR/UmiOCR-data/logs` 目录中,保存级别可以在全局设置标签页中更改。 +- 新增:大部分标签页能手动切换左右/上下双栏模式。 (#789) +- 新增:Esc键隐藏主窗口。 (#652) +- 新增:调整二维码生成相关参数后,自动刷新二维码生成。 (#690) +- 新增:命令行指令 `--reload` ,用于重新加载配置文件。 👉 [文档](https://github.com/hiroi-sora/Umi-OCR/blob/main/docs/README_CLI.md#软件操控指令) +- 修复:文档识别提取PDF自带的文本内容时,未考虑页面旋转的影响。 (#785) +- 修复:文档识别生成单层PDF时,未写入原PDF自带的文本内容。 +- 修复:OCR结果展示列表的一些显示Bug和鼠标划选Bug。 +- 修复:调整标签页顺序或删除标签页后,未及时保存顺序信息。 +- 修复:HTTP接口 `/api/doc/download` 参数 `ignore_blank` 的错误。 +- 修复:Linux版本截图时,系统任务栏推移顶层窗口,导致截图位置偏移。 (#778) +- 修复:Linux版本截图后,主窗口的位置与操作前不一致。 +- 优化:图片/文档的异步加载机制。现在可以流畅地加载含有数万个子文件的文件夹,且能预览加载进度。 (#710) +- Windows 版本更新第三方依赖库:`PyMuPDF 1.24.11` ,`fontTools 4.56.0` ,`Pillow 10.4.0` ,`psutil 10.4.0` ,`pynput 1.8.0` ,`zxing-cpp 2.3.0` +- 新增UI语言:俄语 `Русский` ,译者:[Вячеслав Анатольевич Малышев](https://hosted.weblate.org/user/1969)、[Muhammadyusuf Kurbonov](https://hosted.weblate.org/user/muhammadyusuf.kurbonov2002)。泰米尔语 `தமிழ்` ,译者:[தமிழ்நேரம்](https://hosted.weblate.org/user/TamilNeram/)。 + + +### [v2.1.4](https://github.com/hiroi-sora/Umi-OCR/tree/release/2.1.4) `2024.8.31` + +分支名:`release/2.1.4` + +- 修复:引擎原始输出项的"text"为空时,导致文本分析越界的错误。 (#625) +- 新增UI语言:葡萄牙语 `Português` 。译者:[@杨鹏](https://hosted.weblate.org/user/ypf) +- **PaddleOCR 插件相关**: + - 修复:布尔类型选项不生效的问题。 + - 修复: `繁体中文` 配置文件错误的问题。 + - 优化:调整默认线程/内存限制,默认内存占用不超过系统总内存的一半。 + - 优化:调整识别语言库结构,精简不必要的文件。 +- **Linux 版本相关**: + - 修复:[Docker 部署](https://github.com/hiroi-sora/Umi-OCR_runtime_linux/blob/main/README-docker.md) 在部分旧系统中报错 `'code': 803` 的问题。(#621) + - 优化:`glibc` 依赖降级至 `2.31` ,兼容 Debian-11 、 Ubuntu-20 等发行版。 + +### [v2.1.3](https://github.com/hiroi-sora/Umi-OCR/tree/release/2.1.3) `2024.7.23` + +分支名:`release/2.1.3` + +- 新兼容 Linux 平台。 +- 支持:[Docker 部署](https://github.com/hiroi-sora/Umi-OCR_runtime_linux/blob/main/README-docker.md) 。 +- 新增:HTTP 文档识别接口。👉 [文档](https://github.com/hiroi-sora/Umi-OCR/blob/main/docs/http/api_doc.md#/api/doc) +- 修复:文档中宽高为0的异常图片会导致任务卡住。 +- 优化:排版解析的 **单栏-单行** 方案,对于间隔较大的两个相邻文本块,会添加空格作为间隔符。 + +### [v2.1.2](https://github.com/hiroi-sora/Umi-OCR/tree/release/2.1.2) `2024.6.5` + +分支名:`release/2.1.2` + +- 新增:批量任务(图片、文档)支持 **暂停任务** 。只要不退出软件,在待机/休眠后可以恢复任务。 (#469) +- 新增:批量文档任务,支持保存为 **单层纯文本PDF** 。 +- 新增:HTTP OCR接口 允许设置 **忽略区域** 参数。 (#525) +- 新增:HTTP 二维码接口 允许设置 **图像预处理** 参数。 (#512) +- 新增:命令行`--screenshot`允许指定屏幕范围,自动进行截图。 (#452) +- 修复:文档内容提取和写入时的坐标旋转、比例适配问题。 +- 修复:截图预览 & 记录面板中,跨区块复制文本时,尾块的复制字数不正确。 +- 优化:批量文档标签页,拖入大量文件、提交大批任务时的性能和流畅度。 (#476) +- UI:优化一些布局,美化一些视觉效果。 + +### [v2.1.1](https://github.com/hiroi-sora/Umi-OCR/tree/release/2.1.1) `2024.3.29` + +分支名:`release/2.1.1` + +- 新增:批量文档识别允许指定忽略区域的页数范围。 +- 新增:命令行`--path`允许传入多个图片或文件夹。 +- 新增:新的命令行、HTTP接口:识别/生成二维码图像。 +- 新增:命令行新指令 `--output`, `--output_append`, `--clip` 。 +- 新增:清空全部识别记录的快捷键。 (#420) +- 新增:截图预览 删除图片显示的选项和快捷键。 (#427) +- 修复:Esc取消截图的消息传递错误。 (#402) +- 修复:文档`整页强制OCR`模式下,提高小尺寸页面的渲染精度。 +- 修复:Windows7及低端CPU无法进行文档识别的Bug。 +- 优化:开始菜单/开机自启快捷方式,默认放置于用户目录下,避免请求管理员权限。 +- 优化:保存双层PDF,在没有新文本写入时的处理逻辑。 (#416) +- 优化:PDF解析时,提取原有文本行的逻辑。 +- 优化:双栏面板会记忆左右折叠状态。 +- UI:优化一些提示,修复一些渲染问题,美化一些视觉效果。 + +### [v2.1.0](https://github.com/hiroi-sora/Umi-OCR/tree/release/2.1.0) `2024.2.29` + +分支名:`release/2.1.0` + +- 新增:批量文档识别功能。支持 `pdf, epub, mobi` 等格式。 +- 新增:允许隐藏托盘图标。 (#338) +- 新增:重复上一次截图区域的快捷键。 (#357) +- 新增:用本地图片浏览器打开图片的快捷按钮。 (#335) +- 更新:更强大的排版解析器。 +- 修复:避免系统环境变量`QMLSCENE_DEVICE`的影响。 (#270) + +### [v2.0.2](https://github.com/hiroi-sora/Umi-OCR/tree/release/2.0.2) `2024.1.15` + +分支名:`release/2.0.2` + +- 更新:全局设置可调节 图片文字叠加层`开启/关闭`默认显示状态。 (#264) +- 优化:输出为`txt 单独文件`时,将应用`指定路径`参数。 (#269) +- 优化:`段落合并-多行-代码段` 去除结尾多余换行符。 (#292) +- 优化:渲染器不兼容时,减少渲染层级错误的影响。 (#259) +- 修复:清理图片缓存前进行检查,避免空图错误。 (#279) +- 修复:记录面板中,光标无法移到第1个字符前面。 (#264) +- 修复:系统语言非简体中文时,软件启动异常。 (#274) (#306) +- 修复:csv输出的字符编码兼容性问题。 (#284) +- 修复:`段落合并参数不存在` 的误报。 +- 修复:组件`DefaultTips`不生效。 + +### [v2.0.1](https://github.com/hiroi-sora/Umi-OCR/tree/release/2.0.1) `2023.12.8` + +分支名:`release/2.0.1` + +- 更新:重新设计了OCR HTTP接口,允许省略参数,允许指定段落合并。 +- 更新:命令行增加指令`-->`和`-->>`,将结果输出到文件。 +- 优化:调整图像数据的内部编码,减少英文空格丢失的几率。 +- 优化:调整部分UI文本和布局。 +- 优化:csv默认保存为ansi编码,以兼容Office Excel。 (#237) +- 修复:开启“禁用美化效果”后,外部通知弹窗无法关闭。 (#234) +- 修复:别的程序通过命令行调用Umi-OCR时,无法获取stdout输出。 + +### [v2.0.0](https://github.com/hiroi-sora/Umi-OCR/tree/release/2.0.0) `2023.11.19` + +分支名:`release/2.0.0` + +- 优化:插件UI翻译机制。 +- 优化:下拉框UI。 +- 修复:段落合并-自然段合并的bug。 + +### v2.0.0 dev `2023.11.14` + +- 新增:生成二维码功能。 +- 新增:所有图片预览窗口(如截图、二维码页),允许保存图片到指定路径。 +- 优化:二维码解析库改用性能更好、功能更丰富的zxingcpp。 ([v2 #47](https://github.com/hiroi-sora/Umi-OCR_v2/issues/47)) (感谢:@Byxs20) +- 优化:截图预览面板中,文本框的位置更准确。 +- 修复:图片预览窗口,无法复制本地图片的Bug。 +- 修复:tbpu合并自然段时,垂直距离不准确的Bug。 +- 修复:HTTP API 的跨域问题。 [v2 #52](https://github.com/hiroi-sora/Umi-OCR_v2/issues/52) +- 修复:HTTP API 传base64的大小限制问题。 [v2 #49](https://github.com/hiroi-sora/Umi-OCR_v2/issues/49) +- 修复:其它少量报错。 +- 翻译:人工校对`繁体中文`和`英语`。 (贡献:@QZGao) + +### v2.0.0 dev `2023.11.5` + +- 新增:记忆窗口位置。 [v2 #44](https://github.com/hiroi-sora/Umi-OCR_v2/issues/44) +- 新增:批量识图页增加图片预览窗口,单击图片条目打开。 [v2 #2](https://github.com/hiroi-sora/Umi-OCR_v2/issues/2) +- 新增:检查软件是否有权限读写配置文件。 [v2 #30](https://github.com/hiroi-sora/Umi-OCR_v2/issues/30) +- 新增:报错弹窗提供一键复制及打开issues的功能。 +- 新增:全局设置页添加左侧目录栏。 +- 新增:插件的多国语言UI机制。 +- 优化:截图预览面板中,文本框的位置更准确。 +- 优化:调整部分UI布置。 +- 修复:扫码模块添加导入异常检查。 [v2 #33](https://github.com/hiroi-sora/Umi-OCR_v2/issues/33) +- 修复:补充扫码页的拖入图片功能。 [v2 #43](https://github.com/hiroi-sora/Umi-OCR_v2/issues/43) +- 修复:输出到单独文件txt时,文件名去除原后缀。 [v2 #36](https://github.com/hiroi-sora/Umi-OCR_v2/issues/36) +- 修复:一些小Bug。 + +### v2.0.0 dev `2023.10.25` +- 新增:命令行支持传入图片路径。 [v2 #28](https://github.com/hiroi-sora/Umi-OCR_v2/issues/28) +- 新增:HTTP接口支持Base64传输图片。 [v2 #28](https://github.com/hiroi-sora/Umi-OCR_v2/issues/28) +- 新增:忽略区域功能。 +- 新增:二维码识别页。支持识别多种格式的二维码、条形码。 ([Umi-OCR #95](https://github.com/hiroi-sora/Umi-OCR/issues/95)) +- 新增:提供备选启动器`UmiOCR-data/RUN_GUI.bat`,供`Umi-OCR.exe`不兼容时使用。 [v2 #21](https://github.com/hiroi-sora/Umi-OCR_v2/issues/21) +- 优化:图片预览窗口,支持用`Tab`切换显示/隐藏文本。 +- 优化:记录面板,每条记录顶部添加复制按钮。 [v2 #32](https://github.com/hiroi-sora/Umi-OCR_v2/issues/32) +- 优化:记录面板,拖拽过程中允许指针移出文本框区域。 [v2 #32](https://github.com/hiroi-sora/Umi-OCR_v2/issues/32) +- 优化:重新设计截图缓存机制,避免Image组件销毁时的内存泄露。 +- 优化:标签页应用动态解析机制,小幅提高加载速度。 +- 优化:运行环境转为64位包。(计划不再提供对32位的兼容) +- 修正:配置项中布尔值解析不正确的问题。 [v2 #30](https://github.com/hiroi-sora/Umi-OCR_v2/issues/30) +- 修正:拖入非图片文件可能导致卡顿几秒的问题。 +- 修正:PaddleOCR插件的兼容性问题。 ([Umi-OCR #209](https://github.com/hiroi-sora/Umi-OCR/issues/209)) + +### v2.0.0 dev `2023.10.18` +- 新增:截图前自动隐藏窗口。 [v2 #26](https://github.com/hiroi-sora/Umi-OCR_v2/issues/26) +- 新增:更改字体功能。 [v2 #25](https://github.com/hiroi-sora/Umi-OCR_v2/issues/25) +- 新增:可爱的加载动画。 +- 新增:截图预览面板 支持显示结果文本、划选文本。 +- 新增:截图预览面板 支持将图片复制到剪贴板。 +- 新增:结果记录面板 支持跨文本框划选文本。 [v2 #18](https://github.com/hiroi-sora/Umi-OCR_v2/issues/18) +- 新增:结果记录面板 支持删除一条或多条记录。 [v2 #10](https://github.com/hiroi-sora/Umi-OCR_v2/issues/10) +- 新增:支持用Esc或右键中断截图。 +- 优化:更改插件目录结构和导入机制。 +- 修正:文件重复导致无法添加开机自启。 [v2 #27](https://github.com/hiroi-sora/Umi-OCR_v2/issues/27) + + +### v2.0.0 dev `2023.10.10` +- 新功能:第一次启动软件时,根据系统情况,选择最恰当的渲染器。解决截图闪烁问题。 [v2 #7](https://github.com/hiroi-sora/Umi-OCR_v2/issues/7) +- 新功能:初步实现插件机制,切换引擎等组件更加便捷。 +- 新功能:支持调整界面比例(文字大小)。 +- 优化:调整截图页UI,提高屏占比。优化标签栏阴影。 [v2 #8](https://github.com/hiroi-sora/Umi-OCR_v2/issues/8) +- 优化:双击通知弹窗可打开主窗口。 [v2 #10](https://github.com/hiroi-sora/Umi-OCR_v2/issues/10) +- 优化:截图完成后,如果主窗口在前台,则不弹出成功提示。 [v2 #10](https://github.com/hiroi-sora/Umi-OCR_v2/issues/10) +- 优化:禁用美化效果时,外部弹窗将不会渲染阴影区域。 [v2 #14](https://github.com/hiroi-sora/Umi-OCR_v2/issues/14) +- 优化:Paddle引擎也支持win7系统了。 + +### v2.0.0 dev `2023.9.25` + +### v2.0.0 dev `2023.9.8` +- 支持多种界面语言(实验性) + +### v2.0.0 dev `2023.9.7` + +### v2.0.0 dev `2023.8.9` +- 截图OCR +- 兼容高分辨率屏幕和多屏幕系统 +- 更准确、智能的段落合并 + +### v2.0.0 dev `2023.7.26` +- 批量OCR +- 现代化UI风格 +- 自定义标签页系统 +- 主题切换:明亮/深色 + +--- + +### [v1.3.7](https://github.com/hiroi-sora/Umi-OCR/tree/release/1.3.7) `2023.10.10` +- Paddle引擎兼容Win7 x64 。 + +### v1.3.6 `2023.9.26` +- 新功能:更强大的段落合并方案-`单行/多行自然段/多行代码段`。支持自动判断中/英文段落,采取对应的合并规则。 +- 移除一些过时的段落合并方案。 +- 功能调整:`截图联动` 划分为独立的功能,不受常规截图OCR影响。 + +### [v1.3.5](https://github.com/hiroi-sora/Umi-OCR/tree/release/1.3.5) `2023.6.20` + +- 新功能:复制识别结果后,可发送指定按键,以便联动唤起翻译器等工具。 +- 新功能:命令行增加切换识别语言的指令。 +- 修Bug:低配置机器上有概率误报`OCR init timeout: 5s` 。[#154](https://github.com/hiroi-sora/Umi-OCR/issues/154) , [#156](https://github.com/hiroi-sora/Umi-OCR/issues/156)。 +- 调整:默认停止任务30秒后释放一次内存。 + +### [v1.3.4](https://github.com/hiroi-sora/Umi-OCR/tree/release/1.3.4) `2023.4.26` + +- 新功能:截图预览窗口。 +- 新功能:可用方向键微调截图框位置。 +- 修Bug:拖入图片时有几率卡退主窗口 [issue #126](https://github.com/hiroi-sora/Umi-OCR/issues/126) 。 +- 优化了一些处理流程。 + +### [v1.3.3](https://github.com/hiroi-sora/Umi-OCR/tree/release/1.3.3) `2023.3.19` + +- 新功能:命令行模式。 +- 新功能:识图完成的通知悬浮窗。 +- 新功能:自动清理引擎内存。 +- 修复了一些BUG,优化了一些UI表现。 + +### [v1.3.2](https://github.com/hiroi-sora/Umi-OCR/tree/release/1.3.2) `2022.12.1` + +- 新功能:创建开机启动项时,可选`不显示主窗口`。 +- 新功能:OCR结果输出到每个图片同名的单独txt文件。 +- 新功能:增加独立的设置语言窗口,可在多处点开,便于切换语言。 +- 新功能:合并段落添加`合并自然段-西文模式`,可在英文段落换行时补充空格。 +- 新功能:快捷识图可选`自动清空面板`,只显示本次识别结果,且隐藏时间信息。 +- 修复了一些BUG。 + +### [v1.3.1](https://github.com/hiroi-sora/Umi-OCR/tree/release/1.3.1) `2022.11.4` + +- 修Bug:快捷键模块重写,引入pynput库,舍弃keyboard库,解决几率失效、录制不正确等Bug。 +- 新功能:添加开机自启,桌面快捷方式,开始菜单快捷方式。 +- 新功能:多开软件时提示。 +- 新功能:截图时隐藏窗口。 +- 调整UI:使用频率极低的设置项设为隐藏的高级选项。 +- 优化:检查引擎组件是否存在。 +- 优化:`横排-合并多行-自然段` 优化逻辑,支持0~2全角空格首行缩进。 + +### [v1.3.0](https://github.com/hiroi-sora/Umi-OCR/tree/release/1.3.0) `2022.9.29` +- 新功能:框选截屏。 +- 新功能:系统托盘图标。 +- 新功能:引擎进程常驻。 +- 新功能:文本块后处理模块。 +- 新功能:自定义主输出栏字体。 +- 新功能:设置窗口弹出模式(保持置顶)。 +- 调整UI:自适应Win风格组件。 +- 修正了Bug:系统语言兼容性问题 [issue #16](https://github.com/hiroi-sora/Umi-OCR/issues/16) 。 +- 修正了Bug:微信图片粘贴问题 [issue #22](https://github.com/hiroi-sora/Umi-OCR/issues/22) 。 +- 更新PaddleOCR-json模块至`v1.2.1`,提供剪贴板支持。快捷识图通过剪贴板中转,无需再保存临时文件到硬盘。 + +### [v1.2.6](https://github.com/hiroi-sora/Umi-OCR/tree/release/1.2.6) `2022.9.1` + +- 更新PaddleOCR-json模块至`v1.2.0`,提高识别速度、准确度。 +- 调整UI:更方便地用下拉框切换识别语言。 +- 调整UI:可以从主窗口任意位置/任意选项卡拖入图片。 +- 修正了Bug:提高程序健壮性,增加启动子进程时的更多异常处理情况。 +- 修正了Bug:彻底解决了对边缘过窄的图片,识别结果不准确的问题 [issue #7](https://github.com/hiroi-sora/Umi-OCR/issues/7) 。 +- 优化适配PP-OCRv3模型,彻底解决了v3版模型比v2慢、不准的问题 [issue #4](https://github.com/hiroi-sora/Umi-OCR/issues/4#issuecomment-1141735773) 。 + +### v1.2.5 `2022.7.22` +- 新功能:计划任务。识图完成后执行自动关机等任务。 +- 新功能:可选拖入文件夹时递归导入子文件夹中所有图片。 +- 调整UI:添加一些配置文件的快捷入口。 + +### v1.2.4 `2022.6.4` +- 新功能:可选识别剪贴板图片后自动复制识别的文本。 +- 补充功能:快捷键调用剪贴板识图时,若程序窗口被最小化,则恢复前台状态并挪到最前位置。 + +### v1.2.3 `2022.5.31` +- 新功能:读取剪贴板图片。配置全局快捷键调用该功能。 + +### v1.2.2 `2022.4.30` +- 新功能:可选任务完成后自动打开输出文件或目录。 + +### v1.2.1 `2022.4.16` +- 更新PaddleOCR-json模块至`v1.1.1`,修正了可能得到错误包围盒的漏洞。 + +### v1.2.0 `2022.4.8` +- 可选生成图文链接.md文件,作为索引使用有更佳的观感。 +- 修改设置面板的样式,改为滚动面板以容纳更多设置选项。 +- 用户修改配置项后可自动保存。 + +### v1.1.1 `2022.3.30` +- 修正了Bug:退出忽略区域窗口时,OCR子进程未关闭。 + +### v1.1.0 `2022.3.30` +- 新功能:忽略区域窗口以虚线框 展示识别出的文字块。 + +### v1.0.0 `2022.3.28` +- “梦开始的地方” \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..61b28ff --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 hiroi-sora + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec03688 --- /dev/null +++ b/README.md @@ -0,0 +1,409 @@ +

+ + 中文 + + + + English + + + + 日本語 + +

+ +

+ + Umi-OCR + +

+ +

Umi-OCR 文字识别工具

+ +

+ + Umi-OCR + + + LICENSE + + + forks + + + stars + + + forks + + + 翻译状态 + +

+ +
+

+ + 使用说明 + + + + 下载地址 + + + + 更新日志 + + + + 提交Bug + +

+
+
+ +
+ 免费,开源,可批量的离线OCR软件
+ 适用于 Windows7 x64 、Linux x64 +

+ +- **免费**:本项目所有代码开源,完全免费。 +- **方便**:解压即用,离线运行,无需网络。 +- **高效**:自带高效率的离线OCR引擎,内置多种语言识别库。 +- **灵活**:支持命令行、HTTP接口等外部调用方式。 +- **功能**:截图OCR / 批量OCR / PDF识别 / 二维码 / 公式识别 + +

1-标题-1.png

+ +![1-标题-2.png](https://tupian.li/images/2023/11/19/6559909fdeeba.png) + +## 目录 + +- [截图识别](#截图OCR) + - [排版解析](#文本后处理) - 识别不同排版,按正确顺序输出文字 +- [批量识别](#批量OCR) + - [忽略区域](#忽略区域) - 排除截图水印处的文字 +- [二维码](#二维码) 支持扫码或生成二维码图片 +- [文档识别](#文档识别) 从PDF扫描件中提取文本,或转为双层可搜索PDF +- [全局设置](#全局设置) +- [命令行调用](docs/README_CLI.md) +- [HTTP接口](docs/http/README.md) +- [构建项目(Windows、Linux)](#构建项目) + +## 使用源码 + +开发者请务必阅读 [构建项目](#构建项目) 。 + +## 下载发行版 + +以下发布链接均长期维护,提供稳定版本的下载。 + +- **蓝奏云** https://hiroi-sora.lanzoul.com/s/umi-ocr (国内推荐,免注册/无限速) +- **GitHub** https://github.com/hiroi-sora/Umi-OCR/releases/latest +- **Source Forge** https://sourceforge.net/projects/umi-ocr + + +
+•  Scoop Installer(点击展开) + +[Scoop](https://scoop.sh/) 是一款Windows下的命令行安装程序,可方便地管理多个应用。您可以先安装 Scoop ,再使用以下指令安装 `Umi-OCR` : + +- 添加 `extras` 桶: +``` +scoop bucket add extras +``` + +- (可选1)安装 Umi-OCR(自带 `Rapid-OCR` 引擎,兼容性好): +``` +scoop install extras/umi-ocr +``` + +- (可选2)安装 Umi-OCR(自带 `Paddle-OCR` 引擎,速度稍快): +``` +scoop install extras/umi-ocr-paddle +``` + +- 不要同时安装二者,快捷方式可能会被覆盖。但您可以额外导入 [插件](https://github.com/hiroi-sora/Umi-OCR_plugins) ,随时切换不同OCR引擎。 + +
+
+ +## 开始使用 + +软件发布包下载为 `.7z` 压缩包或 `.7z.exe` 自解压包。自解压包可在没有安装压缩软件的电脑上,解压文件。 + +本软件无需安装。解压后,点击 `Umi-OCR.exe` 即可启动程序。 + +遇到任何问题,请提 [Issue](https://github.com/hiroi-sora/Umi-OCR/issues) ,我会尽可能帮助你。 + +## 界面语言 + +Umi-OCR 支持的界面多国语言。在第一次打开软件时,将会按照你的电脑的系统设置,自动切换语言。 + +如果需要手动切换语言,请参考下图,`全局设置`→`语言/Language` 。 + +

1-标题-1.png

+ +## 标签页 + +Umi-OCR v2 由一系列灵活好用的**标签页**组成。您可按照自己的喜好,打开需要的标签页。 + +标签栏左上角可以切换**窗口置顶**。右上角能够**锁定标签页**,以防止日常使用中误触关闭标签页。 + +### 截图OCR + +

2-截图-1.png

+ +**截图OCR**:打开这一页后,就可以用快捷键唤起截图,识别图中的文字。 +- 左侧的图片预览栏,可直接用鼠标划选复制。 +- 右侧的识别记录栏,可以编辑文字,允许划选多个记录复制。 +- 也支持在别处复制图片,粘贴到Umi-OCR进行识别。 +- 关于 [公式识别](https://github.com/hiroi-sora/Umi-OCR/issues/254) 功能 + +#### 文本后处理 + +

2-截图-2.png

+ +关于 **OCR文本后处理 - 排版解析方案**: 可以整理OCR结果的排版和顺序,使文本更适合阅读和使用。预设方案: +- `多栏-按自然段换行`:适合大部分情景,自动识别多栏布局,按自然段规则进行换行。 +- `多栏-总是换行`:每段语句都进行换行。 +- `多栏-无换行`:强制将所有语句合并到同一行。 +- `单栏-按自然段换行`/`总是换行`/`无换行`:与上述类似,不过 不区分多栏布局。 +- `单栏-保留缩进`:适用于解析代码截图,保留行首缩进和行中空格。 +- `不做处理`:OCR引擎的原始输出,默认每段语句都进行换行。 + +上述方案,均能自动处理横排和竖排(从右到左)的排版。(竖排文字还需要OCR引擎本身支持) + +--- + +### 批量OCR + +

3-批量-1.png

+ +**批量OCR**:这一页用于批量导入本地图片进行识别。 +- 支持格式:`jpg, jpe, jpeg, jfif, png, webp, bmp, tif, tiff`。 +- 保存识别结果的支持格式:`txt, jsonl, md, csv(Excel)`。 +- 与截图OCR一样,支持`文本后处理`功能,整理OCR文本的排版和顺序。 +- 没有数量上限,可一次性导入几百张图片进行任务。 +- 支持任务完成后自动关机/待机。 +- 如果要识别像素超大的长图或大图,请调整:**页面的设置→文字识别→限制图像边长→【调高数值】**。 +- 拥有特殊功能 `忽略区域` 。 + +#### 忽略区域 + +

3-批量-2.png

+ +关于 **OCR文本后处理 - 忽略区域**: 批量OCR中的一种特殊功能,适用于排除图片中的不想要的文字。 +- 在批量识别页的右栏设置中可进入忽略区域编辑器。 +- 如上方样例,图片顶部和右下角存在多个水印 / LOGO。如果批量识别这类图片,水印会对识别结果造成干扰。 +- 按住右键,绘制多个矩形框。这些区域内的文字将在任务中被忽略。 +- 请尽量将矩形框画得大一些,完全包裹住水印所有可能出现的位置。 +- 注意,只有处于忽略区域框内部的整个文本块(而不是单个字符)会被忽略。如下图所示,黄色边框的深色矩形是一个忽略区域。那么只有`key_mouse`才会被忽略。`pubsub_connector.py`、`pubsub_service.py` 这两个文本块得以保留。 +

忽略区域范围示例.png

+ +--- + +### 文档识别 + +

+ +**文档识别**: +- 支持格式:`pdf, xps, epub, mobi, fb2, cbz`。 +- 对扫描件进行OCR,或提取原有文本。可输出为 **双层可搜索PDF** 。 +- 支持设定 **忽略区域** ,可用于排除页眉页脚的文字。 +- 可设置任务完成后 **自动关机/休眠** 。 + +--- + +### 二维码 + +

4-二维码-1.png

+ +**扫码**: +- 截图/粘贴/拖入本地图片,读取其中的二维码、条形码。 +- 支持一图多码。 +- 支持19种协议,如下: + +`Aztec`,`Codabar`,`Code128`,`Code39`,`Code93`,`DataBar`,`DataBarExpanded`,`DataMatrix`,`EAN13`,`EAN8`,`ITF`,`LinearCodes`,`MatrixCodes`,`MaxiCode`,`MicroQRCode`,`PDF417`,`QRCode`,`UPCA`,`UPCE` + +

4-二维码-2.png

+ +**生成码**: +- 输入文本,生成二维码图片。 +- 支持19种协议和**纠错等级**等参数。 + +--- + +### 全局设置 + +

5-全局设置-1.png

+ +**全局设置**:在这里可以调整软件的全局参数。常用功能如下: +- 一键添加快捷方式或设置开机自启。 +- 更改界面**语言**。Umi支持繁中、英语、日语等语言。 +- 切换界面**主题**。Umi拥有多个亮/暗主题。 +- 调整界面**文字的大小**和**字体**。 +- 切换OCR插件。 +- **渲染器**:软件界面默认支持显卡加速渲染。如果在你的机器上出现截屏闪烁、UI错位的情况,请调整`界面和外观` → `渲染器` ,尝试切换到不同渲染方案,或关闭硬件加速。 + +## 调用接口: + +- [命令行手册](docs/README_CLI.md) +- [HTTP接口手册](docs/http/README.md) + +--- + +## 关于项目结构 + +### 各仓库: + +- [主仓库](https://github.com/hiroi-sora/Umi-OCR) 👈 +- [插件库](https://github.com/hiroi-sora/Umi-OCR_plugins) +- [Windows 运行库](https://github.com/hiroi-sora/Umi-OCR_runtime_windows) +- [Linux 运行库](https://github.com/hiroi-sora/Umi-OCR_runtime_linux) + +### 工程结构: + +`**` 后缀表示本仓库(`主仓库`)包含的内容。 + +``` +Umi-OCR +├─ Umi-OCR.exe +├─ umi-ocr.sh +└─ UmiOCR-data + ├─ main.py ** + ├─ version.py ** + ├─ qt_res ** + │ └─ 项目qt资源,包括图标和qml源码 + ├─ py_src ** + │ └─ 项目python源码 + ├─ plugins + │ └─ 插件 + └─ i18n ** + └─ 翻译文件 +``` + +支持的离线OCR引擎: + +- [PaddleOCR-json](https://github.com/hiroi-sora/PaddleOCR-json) +- [RapidOCR-json](https://github.com/hiroi-sora/RapidOCR-json) + +运行环境框架: + +- [PyStand](https://github.com/skywind3000/PyStand) 定制版 + +## 构建项目 + +请跳转下述仓库,完成对应平台的开发/运行环境部署。 + +- [Windows](https://github.com/hiroi-sora/Umi-OCR_runtime_windows) +- [Linux](https://github.com/hiroi-sora/Umi-OCR_runtime_linux) + +--- + +## 软件本地化翻译: + +本项目使用 Weblate 平台进行UI界面的本地化翻译协作。我们欢迎任何译者参与翻译工作,您可进入此链接 [Weblate: Umi-OCR](https://hosted.weblate.org/engage/umi-ocr/) ,在线校对、补充现有语言,或添加新语言。 + +感谢以下译者,为 Umi-OCR 贡献了本地化翻译工作: + +| 译者 | 贡献语言 | +| ------------------------------------------------------------------------------------ | ------------------------- | +| [bob](https://hosted.weblate.org/user/q021) | English, 繁體中文, 日本語 | +| [Qingzheng Gao](https://github.com/QZGao) | English, 繁體中文 | +| [Weng, Chia-Ling](https://hosted.weblate.org/user/ChiaLingWeng) | English, 繁體中文 | +| [linzow](https://hosted.weblate.org/user/linzow) | English, 繁體中文 | +| [Marcos i](https://hosted.weblate.org/user/ultramarkorj9) | English, Português | +| [Eric Guo](https://hosted.weblate.org/user/qwedc001) | English | +| [steven0081](https://hosted.weblate.org/user/steven0081) | English | +| [Brandon Cagle](https://hosted.weblate.org/user/random4t4x14) | English | +| [plum7x](https://hosted.weblate.org/user/plum7x) | 繁體中文 | +| [hugoalh](https://hosted.weblate.org/user/hugoalh) | 繁體中文 | +| [Anarkiisto](https://hosted.weblate.org/user/Anarkiisto) | 繁體中文 | +| [ドコモ光](https://hosted.weblate.org/user/umren190402) | 日本語 | +| [杨鹏](https://hosted.weblate.org/user/ypf) | Português | +| [Вячеслав Анатольевич Малышев](https://hosted.weblate.org/user/1969) | Русский | +| [Muhammadyusuf Kurbonov](https://hosted.weblate.org/user/muhammadyusuf.kurbonov2002) | Русский | +| [தமிழ்நேரம்](https://hosted.weblate.org/user/TamilNeram/) | தமிழ் | + +如果有信息错误或人员缺漏,请在 [这个讨论](https://github.com/hiroi-sora/Umi-OCR/discussions/449) 中回复。 + +--- + +## 赞助 + +Umi-OCR 项目主要由作者 [hiroi-sora](https://github.com/hiroi-sora) 用业余时间在开发和维护。如果您喜欢这款软件,欢迎赞助。 + +- 国内用户可通过 [爱发电](https://afdian.com/a/hiroi-sora) 赞助作者。 + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=hiroi-sora/Umi-OCR&type=Date)](https://star-history.com/#hiroi-sora/Umi-OCR&Date) + +## [更新日志](CHANGE_LOG.md) + +## 开发计划 + +
+已完成的工作 + +- 标签页框架。 +- OCR API控制器。 +- OCR 任务控制器。 +- 主题管理器,支持切换浅色/深色主题主题。 +- 实现 **批量OCR**。 +- 实现 **截图OCR**。 +- 快捷键机制。 +- 系统托盘菜单。 +- 文本块后处理(排版优化)。 +- 引擎内存清理。 +- 软件界面多国语言。 +- 命令行模式。 +- Win7兼容。 +- Excel(csv)输出格式。 +- `Esc`中断截图操作 +- 外置主题文件 +- 字体切换 +- 加载动画 +- 忽略区域。 +- 二维码识别。 +- 批量识别页面的图片预览窗口。 +- PDF识别。 +- 调用本地图片浏览器打开图片。 [#335](https://github.com/hiroi-sora/Umi-OCR/issues/335) +- 重复上一次截图。 [#357](https://github.com/hiroi-sora/Umi-OCR/issues/357) +- 修Bug:文档识别在Windows7系统的兼容性问题。 +- HTTP/命令行接口添加二维码识别/生成功能。 (#423) +- 二维码接口的文档。 +- Linux 平台移植。 +- HTTP 文档识别接口。 + +
+ + + +##### 远期计划 + +
+展开 + +这些是预想中的功能,在开发初期已预留好接口,将在远期慢慢实现。 + +但开发途中受限于实际情况,可能更改功能设计、新增及取消功能。 + +- [ ] 重构底层插件机制。 +- [ ] 在线 OCR API 插件。 +- [ ] 独立的数学公式识别插件。 +- [ ] “数学公式”标签页,提供独立的数学公式识别/Latex渲染。 +- [ ] 检查更新机制。 +- [ ] 排版解析之外的文本后处理模块(如保留数字、半全角字符转换、文本纠错)。 +- [ ] 关键接口函数添加事件触发方式。 + +- 基于GPU的离线OCR。 +- 图片翻译 +- 离线翻译。 +- 固定区域识别。 +- 识别表格图片,输出为Excel。 +- 历史记录系统。 +- 兼容 MacOS / Ubuntu 等平台。 + +
diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..1bda0c1 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`hiroi-sora/Umi-OCR` +- 原始仓库:https://github.com/hiroi-sora/Umi-OCR +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/README_en.md b/README_en.md new file mode 100644 index 0000000..22f5c08 --- /dev/null +++ b/README_en.md @@ -0,0 +1,277 @@ +

+ + 中文 + + + + English + + + + 日本語 + +

+ +

+ + Umi-OCR + +

+ +

Umi-OCR

+ +

+ + Umi-OCR + + + LICENSE + + + forks + + + stars + + + forks + + + 翻译状态 + +

+ +
+

+ + Usage + + + + Download Releases + + + + Changelog + + + + Issue a problem + +

+
+
+ +
+ Free, Open-source, Batch Offline OCR Software
+ Compatible with Windows7 x64 and above +

+ +- **Free**: All the code of this project is open-source and completely free. +- **Convenient**: Unzip and use, run offline, no need for network. +- **Efficient**: Comes with a highly efficient offline OCR engine. As long as the computer performance is sufficient, it can be faster than online OCR services. +- **Flexible**: Supports customizable interface, and supports multiple calling methods such as command-line and HTTP API. + +

1-标题-1.png

+ +![1-标题-2.png](https://tupian.li/images/2023/11/19/6559909fdeeba.png) + +## Using the Source Code: + +Developers should read [Building the Project](#Build-the-Project) before proceeding. + +## Download Releases: + +- **GitHub** https://github.com/hiroi-sora/Umi-OCR/releases/latest +- **Source Forge** https://sourceforge.net/projects/umi-ocr +- **Lanzou** https://hiroi-sora.lanzoul.com/s/umi-ocr + +
+•  Scoop Installer (Click to expand) + +[Scoop](https://scoop.sh/) is a command-line installer for Windows that facilitates the management of multiple applications. You can install Scoop first and then use the following commands to install `Umi-OCR`: + +- Add the `extras` bucket: +``` +scoop bucket add extras +``` + +- (Optional 1) Install Umi-OCR (comes with the `Rapid-OCR` engine, better compatibility): +``` +scoop install extras/umi-ocr +``` + +- (Optional 2) Install Umi-OCR (comes with the `Paddle-OCR` engine, slightly faster): +``` +scoop install extras/umi-ocr-paddle +``` + +- Do not install both, as the shortcuts may be overwritten. However, you can import additional [plugins](https://github.com/hiroi-sora/Umi-OCR_plugins) to switch between different OCR engines at any time. + +
+ +## Getting Started + +The software release package is available in `.7z` compressed format or as a self-extracting `.7z.exe` package. The self-extracting package can be used to extract files on a computer without compression software installed. + +This software does not require installation. After extraction, simply click on `Umi-OCR.exe` to start the program. + +If you encounter any problems, please submit an [Issue](https://github.com/hiroi-sora/Umi-OCR/issues) and I will do my best to assist you. + +## Interface Language + +Umi-OCR supports multiple languages for its interface. When you open the software for the first time, it will automatically switch to the language based on your computer's system settings. + +If you need to manually switch languages, please refer to the following figure, `全局设置`→`语言/Language` . + +

1-标题-1.png

+ +## Tabbed Interface + +Umi-OCR v2 is composed of a series of flexible and easy-to-use **tabbed interfaces**. You can open the required tabbed interface according to your preferences. + +The top left corner of the tab bar can be used to switch **window always on top**. The top right corner can be used to **lock the tabbed interface** to prevent accidental closure during daily use. + +### Screenshot OCR + +

2-截图-1.png

+ +**Screenshot OCR**: After opening this page, you can use a keyboard shortcut to capture a screenshot and recognize the text in the image. + +- The left-side image preview panel allows you to select and copy text with your mouse. +- The right-side recognition record panel allows you to edit text and select and copy multiple records. +- It also supports copying images from elsewhere and pasting them into Umi-OCR for recognition. + +#### Paragraph Merge + +

2-截图-2.png

+ +About **OCR Text Post-Processing - Paragraph Merge**: This feature can organize the layout and order of OCR results to make the text more suitable for reading and use. The preset schemes are: + +- **Single line**: Merge text on the same line, suitable for most scenarios. +- **Multiple lines - natural paragraphs**: Intelligently recognize and merge text belonging to the same paragraph, suitable for most scenarios, as shown in the figure above. +- **Multiple lines - code block**: Try to restore the original indentation and spacing of the text. Suitable for recognizing code snippets or scenes that require retaining spaces. +- **Vertical layout**: Suitable for vertical layout. Needs to be used in conjunction with a model library that also supports vertical layout recognition. + +--- + +### Batch OCR + +

3-批量-1.png

+ +**Batch OCR**: This page supports batch importing local images for recognition. + +- The recognized content can be saved in various formats such as txt/jsonl/md/csv(Excel). +- Supports `text post-processing` technology, which can recognize text belonging to the same natural paragraph and merge it. It also supports multiple processing schemes such as code blocks and vertical text. +- There is no limit on the number of images that can be imported for processing at one time, and the software can automatically shut down or sleep after completing the task. + +#### Ignore Regions + +

3-批量-2.png

+ +About **OCR Text Post-Processing - Ignore Regions**: This is a special function in batch OCR that is used to exclude unwanted text in images. + +- The ignore region editor can be accessed in the right column of the batch recognition page settings. +- As shown in the example above, there are multiple watermarks/LOGOs at the top and bottom right corner of the image. If these images are recognized in batches, the watermarks will interfere with the recognition results. +- Hold down the right mouse button to draw multiple rectangular boxes. The text inside these areas will be ignored during the task. +- Please try to draw the rectangular boxes larger, completely wrapping all possible positions of the watermark. + +--- + +### Batch Documents OCR + +

+ +--- + +### QR Code + +

4-二维码-1.png

+ +**Scan Code**: + +- You can capture screenshots, paste, or drag local images to read QR codes and barcodes. +- Supports multiple codes in one image. +- Supports 19 protocols, as follows: + +`Aztec`,`Codabar`,`Code128`,`Code39`,`Code93`,`DataBar`,`DataBarExpanded`,`DataMatrix`,`EAN13`,`EAN8`,`ITF`,`LinearCodes`,`MatrixCodes`,`MaxiCode`,`MicroQRCode`,`PDF417`,`QRCode`,`UPCA`,`UPCE`, + +

4-二维码-2.png

+ +**Generate Code**: + +- Enter text to generate a QR code image. +- Supports 19 protocols and parameters such as **error correction level**. + +--- + +### Global Settings + +

5-全局设置-1.png

+ +**Global Settings**: Here you can adjust the global parameters of the software. Common features include: + +- One-click to add shortcuts or set auto-startup. +- Change the interface **language**. Umi supports traditional Chinese, English, Japanese, and other languages. +- Switch interface **themes**. Umi has multiple light/dark themes. +- Adjust the **font size** and **font** of the interface text. +- Switch OCR plugins. +- **Renderer**: The software interface defaults to support GPU-accelerated rendering. If you encounter screen flickering or UI misalignment on your machine, please adjust `Interface and Appearance` → `Renderer`, try switching to different rendering schemes, or turn off hardware acceleration. + +--- + +## API Usage: + +- [Command-line manual](docs/README_CLI.md) +- [HTTP API manual](docs/http/README.md) + +## About Project Structure + +### Repositories: + +- [Main Repository](https://github.com/hiroi-sora/Umi-OCR) 👈 +- [Plugin Repository](https://github.com/hiroi-sora/Umi-OCR_plugins) +- [Windows Runtime Library](https://github.com/hiroi-sora/Umi-OCR_runtime_windows) +- [Linux Runtime Library](https://github.com/hiroi-sora/Umi-OCR_runtime_linux) + +## Build the Project + +- [Windows](https://github.com/hiroi-sora/Umi-OCR_runtime_windows) +- [Linux](https://github.com/hiroi-sora/Umi-OCR_runtime_linux) + +--- + +## Software Localization: + +This project uses the Weblate platform for collaborative localization of the UI interface. We welcome translators to participate in the translation work. Please visit [Weblate: Umi-OCR](https://hosted.weblate.org/engage/umi-ocr/) to proofread and supplement existing languages, or add new languages online. + +We extend our gratitude to the following translators for contributing to the localization of Umi-OCR: + +| Translator | Contributed Languages | +| ------------------------------------------------------------------------------------ | ------------------------- | +| [bob](https://hosted.weblate.org/user/q021) | English, 繁體中文, 日本語 | +| [Qingzheng Gao](https://github.com/QZGao) | English, 繁體中文 | +| [Weng, Chia-Ling](https://hosted.weblate.org/user/ChiaLingWeng) | English, 繁體中文 | +| [linzow](https://hosted.weblate.org/user/linzow) | English, 繁體中文 | +| [Marcos i](https://hosted.weblate.org/user/ultramarkorj9) | English, Português | +| [Eric Guo](https://hosted.weblate.org/user/qwedc001) | English | +| [steven0081](https://hosted.weblate.org/user/steven0081) | English | +| [Brandon Cagle](https://hosted.weblate.org/user/random4t4x14) | English | +| [plum7x](https://hosted.weblate.org/user/plum7x) | 繁體中文 | +| [hugoalh](https://hosted.weblate.org/user/hugoalh) | 繁體中文 | +| [Anarkiisto](https://hosted.weblate.org/user/Anarkiisto) | 繁體中文 | +| [ドコモ光](https://hosted.weblate.org/user/umren190402) | 日本語 | +| [杨鹏](https://hosted.weblate.org/user/ypf) | Português | +| [Вячеслав Анатольевич Малышев](https://hosted.weblate.org/user/1969) | Русский | +| [Muhammadyusuf Kurbonov](https://hosted.weblate.org/user/muhammadyusuf.kurbonov2002) | Русский | +| [தமிழ்நேரம்](https://hosted.weblate.org/user/TamilNeram/) | தமிழ் | + +If you find any errors in the information or missing contributors, please reply in [this discussion](https://github.com/hiroi-sora/Umi-OCR/discussions/449). + +--- + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=hiroi-sora/Umi-OCR&type=Date)](https://star-history.com/#hiroi-sora/Umi-OCR&Date) + +## [CHANGE LOG](CHANGE_LOG.md) \ No newline at end of file diff --git a/README_ja.md b/README_ja.md new file mode 100644 index 0000000..6cce801 --- /dev/null +++ b/README_ja.md @@ -0,0 +1,256 @@ +

+ + 中文 + + + + English + + + + 日本語 + +

+ +

+ + Umi-OCR + +

+ +

Umi-OCR

+ +

+ + Umi-OCR + + + LICENSE + + + ダウンロード + + + スター + + + フォーク + + + 翻译状态 + +

+ +
+ 無料、オープンソース、大量文字が一括認識できるのソフトウェア
+ Windows7 x64以上と互換性があります +

+ +- **無料**: このプロジェクトのすべてのコードはオープンソースで完全に無料です。 +- **便利**: 解凍して使用し、オフラインで実行し、ネットワークは必要ありません。 +- **効率的**: 高効率のオフライン OCR エンジンが付属しています。コンピュータのパフォーマンスが十分であれば、オンライン OCR サービスよりも速くなることがあります。 +- **柔軟**: カスタマイズ可能なインターフェースをサポートし、コマンドラインや HTTP API など、複数の呼び出し方法をサポートします。 + +

1-タイトル-1.png

+ +![1-タイトル-2.png](https://tupian.li/images/2023/11/19/6559909fdeeba.png) + +## ソースコードの使用: + +開発者は続ける前に、[プロジェクトのビルド](#プロジェクトのビルド)を読んでください + +## リリースのダウンロード: + +- **GitHub** https://github.com/hiroi-sora/Umi-OCR/releases/latest +- **Source Forge** https://sourceforge.net/projects/umi-ocr +- **Lanzou (蓝奏云)** https://hiroi-sora.lanzoul.com/s/umi-ocr + +
+• Scoop インストーラー + +[Scoop](https://scoop.sh/) はWindowsで動作するコマンドラインインストーラーで、複数のアプリケーションを簡単に管理できます。まず Scoop をインストールし、次に以下のコマンドを使用して `Umi-OCR` をインストールしてください: + +- `extras` バケットを追加: +``` +scoop bucket add extras +``` + +- (オプション1)Umi-OCR をインストール( `Rapid-OCR` エンジン付属、互換性が高い): +``` +scoop install extras/umi-ocr +``` + +- (オプション2)Umi-OCR をインストール( `Paddle-OCR` エンジン付属、やや高速): +``` +scoop install extras/umi-ocr-paddle +``` + +- 両方を同時にインストールしないでください。ショートカットが上書きされる可能性があります。しかし、[プラグイン](https://github.com/hiroi-sora/Umi-OCR_plugins) を追加することで、異なるOCRエンジンをいつでも切り替えることができます。 + +
+ +## はじめに + +ソフトウェアリリースパッケージは、`.7z`圧縮形式または自己解凍`.7z.exe`パッケージで利用可能です。自己解凍パッケージは、圧縮ソフトウェアがインストールされていないコンピューターでファイルを抽出するために使用できます。 + +このソフトウェアはインストールを必要としません。抽出後、`Umi-OCR.exe`をクリックしてプログラムを開始します。 + +問題が発生した場合は、[Issue](https://github.com/hiroi-sora/Umi-OCR/issues)を提出してください。最善を尽くしてサポートします。 + +## インターフェース言語 + +Umi-OCR は、インターフェースの複数の言語をサポートしています。ソフトウェアを初めて開くと、コンピューターのシステム設定に基づいて自動的に言語が切り替わります。 + +言語を手動で切り替える必要がある場合は、以下の図を参照してください。`全局设置`→`语言/Language`。 + +

1-タイトル-1.png

+ +## タブインターフェース + +Umi-OCR v2 は、一連の柔軟で使いやすい**タブインターフェース**で構成されています。好みに応じて必要なタブインターフェースを開くことができます。 + +タブバーの左上隅を使用して**ウィンドウ常に最前面**を切り替えることができます。右上隅は、日常使用中の偶発的な閉鎖を防ぐために**タブインターフェースをロック**するために使用できます。 + +### スクリーンショット OCR + +

2-スクリーンショット-1.png

+ +**スクリーンショット OCR**: このページを開いた後、キーボードショートカットを使用してスクリーンショットをキャプチャし、画像内のテキストを認識することができます。 + +- 左側の画像プレビューパネルを使用して、マウスでテキストを選択してコピーすることができます。 +- 右側の認識レコードパネルを使用して、テキストを編集し、複数のレコードを選択してコピーすることができます。 +- 他の場所から画像をコピーして Umi-OCR に貼り付け、認識することもサポートしています。 + +#### 段落マージ + +

2-スクリーンショット-2.png

+ +**OCR テキスト後処理 - 段落マージ**について: この機能は、OCR 結果のレイアウトと順序を整理し、テキストを読みやすく使用しやすくすることができます。プリセットスキームは以下の通りです。 + +- **単一行**: 同じ行上のテキストをマージします。ほとんどのシナリオに適しています。 +- **複数行 - 自然な段落**: 同じ段落に属するテキストを知的に認識してマージします。ほとんどのシナリオに適しており、上記の図に示されています。 +- **複数行 - コードブロック**: テキストの元のインデントとスペーシングを復元しようとします。コードスニペットやスペースを保持する必要があるシーンを認識するのに適しています。 +- **縦書きレイアウト**: 縦書きレイアウトに適しています。縦書き認識もサポートするモデルライブラリと併用する必要があります。 + +--- + +### バッチ OCR + +

3-バッチ-1.png

+ +**バッチ OCR**: このページでは、ローカル画像をバッチでインポートして認識することがサポートされています。 + +- 認識されたコンテンツは、txt/jsonl/md/csv(Excel)などのさまざまな形式で保存できます。 +- `テキスト後処理`技術をサポートし、同じ自然な段落に属するテキストを認識してマージすることができます。また、コードブロックや縦書きテキストなど、複数の処理スキームもサポートしています。 +- 一度に処理できる画像の数に制限はなく、タスクを完了した後、ソフトウェアは自動的にシャットダウンまたはスリープすることができます。 + +#### 無視域 + +

3-バッチ-2.png

+ +**OCR テキスト後処理 - 無視域**について: これは、バッチ OCR の特別な機能で、画像内の望ましくないテキストを除外するために使用されます。 + +- 無視領域エディタは、バッチ認識ページ設定の右側の列でアクセスできます。 +- 上記の例のように、画像の上部と右下隅に複数の透かし/LOGO があります。これらの画像がバッチで認識される場合、透かしは認識結果に干渉します。 +- 右マウスボタンを押し続けて、複数の長方形ボックスを描画します。これらのエリア内のテキストは、タスク中に無視されます。 +- 長方形ボックスをできるだけ大きく描画し、透かしのすべての可能な位置を完全に包むようにしてください。 + +--- + +### 大量文章のocr + +

+ +--- + +### QR コード + +

4-QRコード-1.png

+ +**スキャンコード**: + +- スクリーンショットをキャプチャしたり、貼り付けたり、ローカル画像をドラッグして、QR コードやバーコードを読み取ることができます。 +- 1 つの画像で複数のコードをサポートします。 +- 以下の 19 のプロトコルをサポートします: + +`Aztec`,`Codabar`,`Code128`,`Code39`,`Code93`,`DataBar`,`DataBarExpanded`,`DataMatrix`,`EAN13`,`EAN8`,`ITF`,`LinearCodes`,`MatrixCodes`,`MaxiCode`,`MicroQRCode`,`PDF417`,`QRCode`,`UPCA`,`UPCE`, + +

4-QRコード-2.png

+ +**コード生成**: + +- テキストを入力して QR コード画像を生成します。 +- **誤り訂正レベル**などのパラメータを含む 19 のプロトコルをサポートします。 + +--- + +### グローバル設定 + +

5-グローバル設定-1.png

+ +**グローバル設定**: ここでは、ソフトウェアのグローバルパラメータを調整できます。一般的な機能には、以下が含まれます: + +- ショートカットを一度に追加するか、自動起動を設定します。 +- インターフェースの**言語**を変更します。Umi は繁体字中国語、英語、日本語などの言語をサポートしています。 +- インターフェースの**テーマ**を切り替えます。Umi には複数のライト/ダークテーマがあります。 +- インターフェーステキストの**フォントサイズ**と**フォント**を調整します。 +- OCR プラグインを切り替えます。 +- **レンダラー**: ソフトウェアインターフェースは、デフォルトで GPU 加速レンダリングをサポートしています。画面のちらつきや UI の位置ずれが発生する場合は、`インターフェースと外観` → `レンダラー`に移動して、異なるレンダリングスキームに切り替えるか、ハードウェアアクセラレーションをオフにしてみてください。 + +--- + +## API の使用: + +- [コマンドラインマニュアル](docs/README_CLI.md) +- [HTTP API マニュアル](docs/http/README.md) + +## プロジェクト構造について + +### リポジトリ: + +- [Main Repository](https://github.com/hiroi-sora/Umi-OCR) 👈 +- [Plugin Repository](https://github.com/hiroi-sora/Umi-OCR_plugins) +- [Windows Runtime Library](https://github.com/hiroi-sora/Umi-OCR_runtime_windows) +- [Linux Runtime Library](https://github.com/hiroi-sora/Umi-OCR_runtime_linux) + +## プロジェクトのビルド + +- [Windows](https://github.com/hiroi-sora/Umi-OCR_runtime_windows) +- [Linux](https://github.com/hiroi-sora/Umi-OCR_runtime_linux) + +--- + +## ソフトウェアのローカライゼーション: + +本プロジェクトはUIのローカライズ作業にWeblateプラットフォームを利用しています。翻訳者による協力を歓迎します。[Weblate: Umi-OCR](https://hosted.weblate.org/engage/umi-ocr/) にアクセスし、既存言語の校正・補完や新規言語の追加が可能です。 + +以下に、Umi-OCRのローカライゼーションに貢献いただいた翻訳者の皆様を感謝とともに掲載します: + +| 翻訳者 | 対応言語 | +| ------------------------------------------------------------------------------------ | ------------------------- | +| [bob](https://hosted.weblate.org/user/q021) | English, 繁體中文, 日本語 | +| [Qingzheng Gao](https://github.com/QZGao) | English, 繁體中文 | +| [Weng, Chia-Ling](https://hosted.weblate.org/user/ChiaLingWeng) | English, 繁體中文 | +| [linzow](https://hosted.weblate.org/user/linzow) | English, 繁體中文 | +| [Marcos i](https://hosted.weblate.org/user/ultramarkorj9) | English, Português | +| [Eric Guo](https://hosted.weblate.org/user/qwedc001) | English | +| [steven0081](https://hosted.weblate.org/user/steven0081) | English | +| [Brandon Cagle](https://hosted.weblate.org/user/random4t4x14) | English | +| [plum7x](https://hosted.weblate.org/user/plum7x) | 繁體中文 | +| [hugoalh](https://hosted.weblate.org/user/hugoalh) | 繁體中文 | +| [Anarkiisto](https://hosted.weblate.org/user/Anarkiisto) | 繁體中文 | +| [ドコモ光](https://hosted.weblate.org/user/umren190402) | 日本語 | +| [杨鹏](https://hosted.weblate.org/user/ypf) | Português | +| [Вячеслав Анатольевич Малышев](https://hosted.weblate.org/user/1969) | Русский | +| [Muhammadyusuf Kurbonov](https://hosted.weblate.org/user/muhammadyusuf.kurbonov2002) | Русский | +| [தமிழ்நேரம்](https://hosted.weblate.org/user/TamilNeram/) | தமிழ் | + +情報の誤りや記載漏れがありましたら、[こちらのディスカッション](https://github.com/hiroi-sora/Umi-OCR/discussions/449) までご連絡ください。 + +--- + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=hiroi-sora/Umi-OCR&type=Date)](https://star-history.com/#hiroi-sora/Umi-OCR&Date) + +## [変更ログ](CHANGE_LOG.md) diff --git a/Umi-OCR_Rapid_v2.1.5.7z b/Umi-OCR_Rapid_v2.1.5.7z new file mode 100644 index 0000000..f000d2e Binary files /dev/null and b/Umi-OCR_Rapid_v2.1.5.7z differ diff --git a/UmiOCR-data/Help 帮助.txt b/UmiOCR-data/Help 帮助.txt new file mode 100644 index 0000000..4e2134b --- /dev/null +++ b/UmiOCR-data/Help 帮助.txt @@ -0,0 +1,112 @@ +错误排查手册 + +详细内容请见: https://github.com/hiroi-sora/Umi-OCR/issues/447 + +================================ + +# 弹窗报错 Cannot find Py_Main() in …… + +解决方法1: +下载安装VC运行库,重启系统,测试Umi-OCR能否正常运行。 +https://aka.ms/vs/17/release/vc_redist.x64.exe + +解决方法2: +使用这个bat文件代替 Umi-OCR.exe 来启动软件。双击它即可。 +UmiOCR-data/RUN_GUI.bat +注意1:通过bat启动时,部分功能受限,将无法使用命令行指令和创建快捷方式(开始菜单、开机自启等)。如有需要,请参考后面【手动放置快捷方式】条目。 +注意2:不要移动bat的位置。 + +================================ + +# 手动放置快捷方式 + +如果因为某些原因,软件自身无法创建快捷方式;请手动创建并将快捷方式放置到如下目录: + +开始菜单目录: +C:\ProgramData\Microsoft\Windows\Start Menu +开机自启目录: +C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup + +================================ + +# 弹窗报错 Failed to create OpenGL context …… + +1. 下载运行库: +https://github.com/hiroi-sora/Umi-OCR_v2/files/13167436/opengl32sw_64.zip +2. 解压,将其中的dll文件放置于: +UmiOCR-data/site-packages/PySide2/ + +================================ + +# 弹窗报错 Umi-OCR.exe 已停止工作 + +此问题常见于:软件已经在win10使用过,然后原封不动复制到win7上,导致配置冲突。 +删除下述配置文件即可: +UmiOCR-data/.pre_settings + +================================ + +# 应用程序无法正常启动(0xc0000142) +# 或者:`[Error] OCR init fail.l. Argd: {'enable_mkldnn': True...` + +此问题大概率是:CPU不支持AVX指令集。 + +请换用 PaddleOCR 之外的插件或软件版本。比如: +方法1:更换使用 [Umi-OCR_Rapid](https://github.com/hiroi-sora/Umi-OCR/releases) 。 +方法2:额外导入 [RapleOCR插件](https://github.com/hiroi-sora/Umi-OCR_plugins/releases) 。 + +================================ + +# 弹出黑框控制台窗口,【错误】Umi-OCR Error 找不到指定的程序 OSError + +可能的发生条件: +- win7早期版本,缺少系统补丁(尤其是`KB2533623`) +- 其他原因,导致 Python 3.8 无法在该系统中运行。 + +修复方法: + +推荐方法:使用系统检查更新(`控制面板 → 系统和安全 → Windows Update → 检查更新`),安装所有更新。 + +另一种方法:单独下载安装 **KB2533623补丁** ,重启系统即可。不过,微软官方渠道已经关闭了该补丁的下载链接,您可能只能从第三方网站下载补丁,请务必注意安全。 + +================================ + +# 保存PDF时 “已停止工作” + +文档识别,能识别出文字,能保存为txt等文件。但如果选择保存为`双层可搜索PDF`,则最后导出文件时弹窗报错: + +``` +Umi-OCR.exe 已停止工作 + +查看问题详细信息: + +问题事件名称:BEX64 +故障模块名称:ucrtbase.DLL +故障模块版本:10.0.10240.16390 +异常偏移:00000000064399 +异常代码:c0000417 +``` + +可能的发生条件:win7早期版本,缺少系统补丁(尤其是 `KB4534310`、`KB3042058`) + +修复方法: + +注意!由于 `KB4534310` 依赖前置补丁,因此 **不能** 单独下载安装该补丁。 + +您必须使用系统检查更新(`控制面板 → 系统和安全 → Windows Update → 检查更新`),安装所有缺失的更新,直到下面这个项目安装完毕: + +2020-01 适用于基于 x64 的系统的 Windows 7 月度安全质量汇总 (KB4534310) + +================================ + +# 丢失`api-ms-win-crt-runtime-l1-1-0.dll` + +打开软件时,弹出错误弹窗: + +Umi-OCR.exe-系统错误 +无法启动此程序,因为计算机中丢失 api-ms-win-crt-runtime-l1-1-0.dll 。尝试重新安装该程序以解决此问题。 + +可能的发生条件:缺少VC运行库。 + +# 修复方法:安装VC运行库: +https://aka.ms/vs/17/release/vc_redist.x64.exe diff --git a/UmiOCR-data/i18n/en_US.qm b/UmiOCR-data/i18n/en_US.qm new file mode 100644 index 0000000..aa55a07 Binary files /dev/null and b/UmiOCR-data/i18n/en_US.qm differ diff --git a/UmiOCR-data/i18n/ja_JP.qm b/UmiOCR-data/i18n/ja_JP.qm new file mode 100644 index 0000000..9d281e6 Binary files /dev/null and b/UmiOCR-data/i18n/ja_JP.qm differ diff --git a/UmiOCR-data/i18n/pt.qm b/UmiOCR-data/i18n/pt.qm new file mode 100644 index 0000000..a5d95b2 Binary files /dev/null and b/UmiOCR-data/i18n/pt.qm differ diff --git a/UmiOCR-data/i18n/ru_RU.qm b/UmiOCR-data/i18n/ru_RU.qm new file mode 100644 index 0000000..403f73d Binary files /dev/null and b/UmiOCR-data/i18n/ru_RU.qm differ diff --git a/UmiOCR-data/i18n/ta.qm b/UmiOCR-data/i18n/ta.qm new file mode 100644 index 0000000..8afd11c Binary files /dev/null and b/UmiOCR-data/i18n/ta.qm differ diff --git a/UmiOCR-data/i18n/zh_TW.qm b/UmiOCR-data/i18n/zh_TW.qm new file mode 100644 index 0000000..99d9c82 Binary files /dev/null and b/UmiOCR-data/i18n/zh_TW.qm differ diff --git a/UmiOCR-data/main.py b/UmiOCR-data/main.py new file mode 100644 index 0000000..f21bd05 --- /dev/null +++ b/UmiOCR-data/main.py @@ -0,0 +1,156 @@ +# Umi-OCR +# OCR software, free and offline. 开源、免费的离线OCR软件。 +# Website - https://github.com/hiroi-sora/Umi-OCR +# Author - hiroi-sora +# +# You are free to use, modify, and distribute Umi-OCR, but it must include +# the original author's copyright statement and the following license statement. +# 您可以免费地使用、修改和分发 Umi-OCR ,但必须包含原始作者的版权声明和下列许可声明。 +""" +Copyright (c) 2023 hiroi-sora + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + + +""" +====================================================== +========== Umi-OCR Windows 运行环境初始化入口 ========== +====================================================== + +说明: +本文件负责 Windows + PyStand 运行环境的初始化,主要涉及: +- 创建底层弹窗接口 os.MessageBox +- 重定向标准输入输出流 +- 指定工作目录为 "/UmiOCR-data" +- 添加Python库搜索目录 "site-packages" +- 添加PySide2插件搜索目录 "PySide2/plugins" + +环境初始化后,调用正式入口 py_src/run.py 启动软件。 + +耗时分析: +runtime/python.exe -X importtime main.py +""" + + +import os +import sys +import site +import traceback +import subprocess + + +def MessageBox(msg, type_="error"): + # 软件中如遇到错误,会优先用QT弹窗来警示。 + # 但一些异常可能触发太早或导致QT事件循环崩溃。 + # 因此 os.MessageBox() 是用于报告错误的最后防线。 + info = "Umi-OCR Message" + if type_ == "error": + info = "【错误】 Umi-OCR Error" + elif type_ == "warning": + info = "【警告】 Umi-OCR Warning" + try: + # 通过 ctypes 发起弹窗 + import ctypes + + ctypes.windll.user32.MessageBoxW(None, str(msg), str(info), 0) + except Exception: + # 部分系统,连ctypes也用不了。转为在新的控制台窗口中打印信息。 + msg_cmd = ( + msg.replace("^", "^^") + .replace("&", "^&") + .replace("<", "^<") + .replace(">", "^>") + .replace("|", "^|") + .replace("\n\n", "___") + .replace("\n", "___") + ) + subprocess.Popen(["start", "cmd", "/k", f"echo {info}: {msg_cmd}"], shell=True) + return 0 + + +os.MessageBox = MessageBox + + +def initRuntimeEnvironment(): + """初始化运行环境""" + + # 尝试获取控制台的输出对象 + try: + fd = os.open("CONOUT$", os.O_RDWR | os.O_BINARY) + fp = os.fdopen(fd, "w", encoding="utf-8") + except Exception as e: + fp = open(os.devnull, "w", encoding="utf-8") + # 输出流不存在时,重定向到控制台 + if not sys.stdout: + sys.stdout = fp + if not sys.stderr: + sys.stderr = fp + # def except_hook(cls, exception, traceback): + # sys.__excepthook__(cls, exception, traceback) + # sys.excepthook = except_hook + + # 初始化工作目录和Python搜索路径 + script = os.path.abspath(__file__) # 启动脚本.py的路径 + cwd = os.path.dirname(script) # 工作目录 + os.chdir(cwd) # 重新设定工作目录(不在最顶层,而在 UmiOCR-data 文件夹下) + for n in [".", "site-packages"]: # 将模块目录添加到 Python 搜索路径中 + path = os.path.abspath(os.path.join(cwd, n)) + if os.path.exists(path): + site.addsitedir(path) + # 初始化Qt搜索路径为相对路径,避免上层目录存在中文编码 + from PySide2.QtCore import QCoreApplication + + QCoreApplication.addLibraryPath("./site-packages/PySide2/plugins") + + +if __name__ == "__main__": + try: + initRuntimeEnvironment() # 初始化运行环境 + except Exception: + err = traceback.format_exc() + from py_src.imports.umi_log import logger, Logs_Dir + + logger.critical( + "Failed to initialize running environment!", + exc_info=True, + stack_info=True, + ) + msg = f"Failed to initialize running environment!\n\n{err}\n\nSave the log file to: {Logs_Dir}" + MessageBox(msg) + sys.exit(0) + try: + # 获取 pystand.exe 记录的程序入口环境变量 + app_path = os.environ.get("PYSTAND", "") + # 启动正式入口 + from py_src.run import main + + main(app_path=app_path, engineAddImportPath="./site-packages/PySide2/qml") + except Exception: + err = traceback.format_exc() + from py_src.imports.umi_log import logger, Logs_Dir + + logger.critical( + "Failed to startup main program!", + exc_info=True, + stack_info=True, + ) + msg = f"Failed to startup main program!\n\n{err}\n\nSave the log file to: {Logs_Dir}" + MessageBox(msg) + sys.exit(0) diff --git a/UmiOCR-data/py_src/event_bus/key_mouse/key_mouse_connector.py b/UmiOCR-data/py_src/event_bus/key_mouse/key_mouse_connector.py new file mode 100644 index 0000000..3283046 --- /dev/null +++ b/UmiOCR-data/py_src/event_bus/key_mouse/key_mouse_connector.py @@ -0,0 +1,26 @@ +# ======================================================== +# =============== 键盘/鼠标连接器,在qml调用 =============== +# ======================================================== + +from PySide2.QtCore import QObject, Slot + +from .keyboard import HotkeyCtrl + + +class KeyMouseConnector(QObject): + # ========================= 【热键】 ========================= + + # 注册热键 + @Slot(str, str, int, result=str) + def addHotkey(self, keysName, title, press): + return HotkeyCtrl.addHotkey(keysName, title, press) + + # 取消热键 + @Slot(str, str, int) + def delHotkey(self, keysName, title, press): + return HotkeyCtrl.delHotkey(keysName, title, press) + + # 开始录制热键 + @Slot(str, str, result=str) + def readHotkey(self, runningTitle, finishTitle): + return HotkeyCtrl.readHotkey(runningTitle, finishTitle) diff --git a/UmiOCR-data/py_src/event_bus/key_mouse/keyboard.py b/UmiOCR-data/py_src/event_bus/key_mouse/keyboard.py new file mode 100644 index 0000000..4062be8 --- /dev/null +++ b/UmiOCR-data/py_src/event_bus/key_mouse/keyboard.py @@ -0,0 +1,198 @@ +from pynput import keyboard +from PySide2.QtCore import QMutex +from time import time + +from umi_log import logger +from ...platform import Platform +from ..pubsub_service import PubSubService + + +# 按键转换器 +class _KeyTranslator: + # 回调函数的 KeyCode 类型 转为键名字符串 + @staticmethod + def key2name(key): + return Platform.getKeyName(key) + + # 键名字符串 转为KeyCode + @staticmethod + def name2key(char): + if hasattr(keyboard.Key, char): # 控制键返回Code + return getattr(keyboard.Key, char).value + else: # 非控制键返回自己 + return char + + # 多个键名的字符串 转为集合 + @staticmethod + def names2set(char): + return set(char.split("+")) + + # 集合转键名 + def set2names(keys): + keys = keys.copy() + # 优先级键名 + highPriority = ("win", "cmd", "shift", "ctrl", "alt") + names = "" + # 添加高优先级键 + for h in highPriority: + if h in keys: + if names != "": + names += "+" + names += h + keys.discard(h) + # 添加剩下的键 + for k in keys: + if names != "": + names += "+" + names += k + return names + + +# 热键控制器类 +class _HotkeyController: + # ========================= 【接口】 ========================= + + # 添加一组快捷键,对应触发事件为title。 press 为0时按下触发,1抬起触发 + def addHotkey(self, keysName, title, press=0): + self._start() + if press != 0 and press != 1: + return f"[Error] press只能为0按下或1抬起,不能为 {press} 。" + keySet = _KeyTranslator.names2set(keysName) + self._hotkeyMutex.lock() + kl = self._hotkeyList[press] + for k in kl: # 检测重复 + if k["keySet"] == keySet: # 键集合相同 + self._hotkeyMutex.unlock() + msg = "[Success] 注册事件相同的重复快捷键。" + if k["title"] != title: # 事件标题不同 + msg = f'[Warning] Registering same hotkey. The existing event for {keysName} is {k["title"]}, new event is {title} .' + return msg + # 加入列表 + kl.append({"keySet": keySet, "title": title}) + self._hotkeyMutex.unlock() + return "[Success]" + + # 移除一组快捷键,传入键名或事件之一 + def delHotkey(self, keysName="", title="", press=0): + if press != 0 and press != 1: + logger.error(f"press只能为0按下或1抬起,不能为 {press} 。") + return + keySet = _KeyTranslator.names2set(keysName) + self._hotkeyMutex.lock() + kListOld = self._hotkeyList[press] # 旧列表 + kListNew = [] # 新列表 + for k in kListOld: + # 忽略键集合相同或标题相同 + if (keysName and k["keySet"] == keySet) or (title and title == k["title"]): + pass + # 其余键写入新列表 + else: + kListNew.append(k) + self._hotkeyList[press] = kListNew + self._hotkeyMutex.unlock() + + # 开始录制快捷键。过程发送事件为runningTitle,完毕发送事件为finishTitle + def readHotkey( + self, runningTitle="<>", finishTitle="<>" + ): + self._start() + if self._status == 1: + return ( + "[Warning] Recording is running. 当前快捷键录制已在进行,不能同时录制!" + ) + self._status = 1 + self._readRunningTitle = runningTitle + self._readFinishTitle = finishTitle + return "[Success]" + + # ========================= 【实现】 ========================= + + def __init__(self): + self._listener = None # 监听器 + # 热键列表,[0]存放按下触发,[1]存放抬起触发 + # 每个元素为:{"keySet":按键集合, "title":事件标题} + self._hotkeyList = [[], []] + self._hotkeyMutex = QMutex() # 热键列表的锁 + self._status = 0 # 状态,0正常,1录制中 + self._pressSet = set() # 当前已按下的按键集合 + self._strict = True # 键集合相等的判定,T为严格,F为宽松 + self._ttl = 30 # 长按键超时忽略时间,秒 + self._ttlDict = {} # 存放当前已按下按键的超时时间 + self._readRunningTitle = "" + self._readFinishTitle = "" + # 注册默认监听的热键 + self.addHotkey("esc", "<>") + + # 第一次注册热键时,启动监听 + def _start(self): + if not self._listener: + self._listener = keyboard.Listener( + on_press=self._onPress, on_release=self._onRelease + ) + self._listener.start() + + # 按键按下的回调 + def _onPress(self, key): + keyName = _KeyTranslator.key2name(key) + # 禁止重复触发 + if keyName in self._pressSet: + return + self._checkTTL() # 检查超时 + self._pressSet.add(keyName) # 加入集合 + self._ttlDict[keyName] = time() + self._ttl # 记录超时时间 + if self._status == 0: # 正常运行 + self._checkKeyEvent(0, keyName) # 检查按下模式的快捷键 + elif self._status == 1: # 录制中 + self._readRunning() + + # 按键抬起的回调 + def _onRelease(self, key): + keyName = _KeyTranslator.key2name(key) + if not keyName in self._pressSet: + return + self._checkTTL() # 检查超时 + if self._status == 0: # 正常运行 + self._checkKeyEvent(1, keyName) # 检查抬起模式的快捷键 + elif self._status == 1: # 录制结束 + self._readFinish() + if keyName in self._pressSet: + self._pressSet.discard(keyName) # 从集合中删除 + del self._ttlDict[keyName] # 删除超时时间 + + # 检查并触发按键事件 + def _checkKeyEvent(self, press, nowKey): + # 对比每组按键集合。一致触发,则发送事件 + if self._strict: # 严格模式,要求完全一致 + for k in self._hotkeyList[press]: + if k["keySet"] == self._pressSet: + PubSubService.publish(k["title"]) + else: # 宽松模式,只要求当前组合中包含指定按键,且当前按下的按键在指定按键中 + for k in self._hotkeyList[press]: + if k["keySet"] <= self._pressSet and nowKey in k["keySet"]: + PubSubService.publish(k["title"]) + + # 更新录制 + def _readRunning(self): + names = _KeyTranslator.set2names(self._pressSet) + PubSubService.publish(self._readRunningTitle, names) + + # 录制结束 + def _readFinish(self): + self._status = 0 + if "esc" in self._pressSet: # 含esc,则为退出 + PubSubService.publish(self._readFinishTitle, "") + else: + names = _KeyTranslator.set2names(self._pressSet) + PubSubService.publish(self._readFinishTitle, names) + + # 检查已按键的超时时间。若超时,则删除该键 + def _checkTTL(self): + nowTime = time() + for k in self._pressSet.copy(): + if nowTime >= self._ttlDict[k]: + logger.debug(f"忽略超时按键 {k}") + del self._ttlDict[k] + self._pressSet.discard(k) + + +HotkeyCtrl = _HotkeyController() diff --git a/UmiOCR-data/py_src/event_bus/pubsub_connector.py b/UmiOCR-data/py_src/event_bus/pubsub_connector.py new file mode 100644 index 0000000..9bae2d2 --- /dev/null +++ b/UmiOCR-data/py_src/event_bus/pubsub_connector.py @@ -0,0 +1,57 @@ +# ======================================================== +# =============== 发布/订阅连接器,在qml调用 =============== +# ======================================================== + +from PySide2.QtCore import QObject, Slot + +from .pubsub_service import PubSubService +from umi_log import logger + + +class PubSubConnector(QObject): + def __init__(self, *args): + super().__init__(*args) + self._funcDict = {} # 缓存历史已订阅的函数 + + # 订阅事件。传入 标题,函数所在Item,函数名 + @Slot(str, "QVariant", str) + def subscribe(self, title, item, funcName): + func = getattr(item, funcName, None) + if not func: + logger.error(f"qml订阅事件失败!未在 {item} 中找到函数 {funcName} 。") + return + PubSubService.subscribe(title, func) + fKey = title + funcName + self._funcDict[fKey] = func + + # 订阅事件,可额外传入组 + @Slot(str, "QVariant", str, str) + def subscribeGroup(self, title, item, funcName, groupName): + func = getattr(item, funcName, None) + if not func: + logger.error(f"qml订阅事件失败!未在 {item} 中找到函数 {funcName} 。") + return + PubSubService.subscribeGroup(title, func, groupName) + fKey = title + funcName + self._funcDict[fKey] = func + + # 取消订阅事件,由于getattr的不稳定性,因此从历史记录中取函数引用,而不是重新查询。 + @Slot(str, "QVariant", str) + def unsubscribe(self, title, item, funcName): + fKey = title + funcName + if fKey not in self._funcDict: + logger.error(f"qml订阅事件失败!fKey {fKey} 未在 _funcDict 中。") + return + func = self._funcDict[fKey] + del self._funcDict[fKey] + PubSubService.unsubscribe(title, func) + + # 取消订阅整个组的事件 + @Slot(str) + def unsubscribeGroup(self, groupName): + PubSubService.unsubscribeGroup(groupName) + + # 发布事件,在qml扩展此函数。传入 args 为参数列表。 + @Slot(str, list) + def publish_(self, title, args): + PubSubService.publish(title, *args) diff --git a/UmiOCR-data/py_src/event_bus/pubsub_service.py b/UmiOCR-data/py_src/event_bus/pubsub_service.py new file mode 100644 index 0000000..1d03cba --- /dev/null +++ b/UmiOCR-data/py_src/event_bus/pubsub_service.py @@ -0,0 +1,105 @@ +# ============================================ +# =============== 发布/订阅模式 =============== +# ============================================ + +from PySide2.QtCore import QObject, Slot, Signal, QMutex, QThread, QCoreApplication + +from umi_log import logger + + +# 发布/订阅 服务类 +class _PubSubServiceClass: + def __init__(self): + # 事件字典,元素为 回调函数列表 + self._eventDict = {} + self._eventDictMutex = QMutex() # 事件字典的锁 + # 组字典,元素为 组列表。可以成组取消订阅,方便管理。 + self._groupDict = {} + self._groupDictMutex = QMutex() # 组字典的锁 + # 信号 + self._eventSignal = self._EventSignal() + self._eventSignal.signal.connect(self._publish) + + # ========================= 【接口】 ========================= + + # 订阅事件 + def subscribe(self, title, func): + if not callable(func): + logger.error(f"订阅事件失败!传入 {func} 不是可调用对象。") + return + self._eventDictMutex.lock() # 上锁 + if title not in self._eventDict: + self._eventDict[title] = [func] + else: + self._eventDict[title].append(func) + self._eventDictMutex.unlock() # 解锁 + + # 订阅事件,可额外传入组名,以便管理。 + def subscribeGroup(self, title, func, groupName): + self._groupDictMutex.lock() # 上锁 + if groupName not in self._groupDict: + self._groupDict[groupName] = [(title, func)] + else: + self._groupDict[groupName].append((title, func)) + self._groupDictMutex.unlock() # 解锁 + self.subscribe(title, func) + + # 取消订阅事件 + def unsubscribe(self, title, func): + if not callable(func): + logger.error(f"取消订阅事件失败!传入 {func} 不是可调用对象。") + return + # 将回调函数从 对应标题的事件列表中 移除 + self._eventDictMutex.lock() # 上锁 + if title in self._eventDict: + l = self._eventDict[title] + if func in l: + l.remove(func) + self._eventDictMutex.unlock() # 解锁 + + # 取消订阅某个组的所有事件 + def unsubscribeGroup(self, groupName): + self._groupDictMutex.lock() # 上锁 + if groupName in self._groupDict: + l = self._groupDict[groupName] + for i in l: + self.unsubscribe(i[0], i[1]) + self._groupDict[groupName] = [] + self._groupDictMutex.unlock() # 解锁 + + # 发布事件 + def publish(self, title, *args): + # 在主线程调用 + if QThread.currentThread() == QCoreApplication.instance().thread(): + self._publish(title, args) + # 在子线程调用 + else: + self._eventSignal.signal.emit(title, args) + + # ========================= 【实现】 ========================= + + # 发布事件的实现(主线程) + @Slot(str, "QVariant") + def _publish(self, title, args): + event_funcs = [] + self._eventDictMutex.lock() # 上锁 + if title in self._eventDict: + event_funcs = self._eventDict[title].copy() # 拷贝一份 + self._eventDictMutex.unlock() # 解锁 + for func in event_funcs: + try: + func(*args) + except Exception: + logger.error( + f"发送事件异常。 title: {title}, args: {args}, func: {func}", + exc_info=True, + stack_info=True, + ) + + # 信号类 + class _EventSignal(QObject): + signal = Signal(str, "QVariant") + + +# 发布/订阅 服务单例 +PubSubService = _PubSubServiceClass() diff --git a/UmiOCR-data/py_src/image_controller/ImageQt.py b/UmiOCR-data/py_src/image_controller/ImageQt.py new file mode 100644 index 0000000..4bf7ca3 --- /dev/null +++ b/UmiOCR-data/py_src/image_controller/ImageQt.py @@ -0,0 +1,187 @@ +# +# The Python Imaging Library. +# $Id$ +# +# a simple Qt image interface. +# +# history: +# 2006-06-03 fl: created +# 2006-06-04 fl: inherit from QImage instead of wrapping it +# 2006-06-05 fl: removed toimage helper; move string support to ImageQt +# 2013-11-13 fl: add support for Qt5 (aurelien.ballier@cyclonit.com) +# +# Copyright (c) 2006 by Secret Labs AB +# Copyright (c) 2006 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +import sys +from io import BytesIO + +from PIL import Image + +from PySide2.QtCore import QBuffer, QIODevice +from PySide2.QtGui import QImage, QPixmap, qRgba + +qt_is_installed = True +qt_version = None + + +def rgb(r, g, b, a=255): + """(Internal) Turns an RGB color into a Qt compatible color integer.""" + # use qRgb to pack the colors, and then turn the resulting long + # into a negative integer with the same bitpattern. + return qRgba(r, g, b, a) & 0xFFFFFFFF + + +def fromqimage(im): + """ + :param im: QImage or PIL ImageQt object + """ + buffer = QBuffer() + qt_openmode = QIODevice + buffer.open(qt_openmode.ReadWrite) + # preserve alpha channel with png + # otherwise ppm is more friendly with Image.open + if im.hasAlphaChannel(): + im.save(buffer, "png") + else: + im.save(buffer, "ppm") + + b = BytesIO() + b.write(buffer.data()) + buffer.close() + b.seek(0) + + return Image.open(b) + + +def fromqpixmap(im): + return fromqimage(im) + # buffer = QBuffer() + # buffer.open(QIODevice.ReadWrite) + # # im.save(buffer) + # # What if png doesn't support some image features like animation? + # im.save(buffer, 'ppm') + # bytes_io = BytesIO() + # bytes_io.write(buffer.data()) + # buffer.close() + # bytes_io.seek(0) + # return Image.open(bytes_io) + + +def align8to32(bytes, width, mode): + """ + converts each scanline of data from 8 bit to 32 bit aligned + """ + + bits_per_pixel = {"1": 1, "L": 8, "P": 8, "I;16": 16}[mode] + + # calculate bytes per line and the extra padding if needed + bits_per_line = bits_per_pixel * width + full_bytes_per_line, remaining_bits_per_line = divmod(bits_per_line, 8) + bytes_per_line = full_bytes_per_line + (1 if remaining_bits_per_line else 0) + + extra_padding = -bytes_per_line % 4 + + # already 32 bit aligned by luck + if not extra_padding: + return bytes + + new_data = [] + for i in range(len(bytes) // bytes_per_line): + new_data.append( + bytes[i * bytes_per_line : (i + 1) * bytes_per_line] + + b"\x00" * extra_padding + ) + + return b"".join(new_data) + + +def _toqclass_helper(im): + data = None + colortable = None + exclusive_fp = False + + # handle filename, if given instead of image name + if hasattr(im, "toUtf8"): + # FIXME - is this really the best way to do this? + im = str(im.toUtf8(), "utf-8") + + qt_format = QImage + if im.mode == "1": + format = qt_format.Format_Mono + elif im.mode == "L": + format = qt_format.Format_Indexed8 + colortable = [] + for i in range(256): + colortable.append(rgb(i, i, i)) + elif im.mode == "P": + format = qt_format.Format_Indexed8 + colortable = [] + palette = im.getpalette() + for i in range(0, len(palette), 3): + colortable.append(rgb(*palette[i : i + 3])) + elif im.mode == "RGB": + # Populate the 4th channel with 255 + im = im.convert("RGBA") + + data = im.tobytes("raw", "BGRA") + format = qt_format.Format_RGB32 + elif im.mode == "RGBA": + data = im.tobytes("raw", "BGRA") + format = qt_format.Format_ARGB32 + elif im.mode == "I;16" and hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+ + im = im.point(lambda i: i * 256) + + format = qt_format.Format_Grayscale16 + else: + if exclusive_fp: + im.close() + msg = f"unsupported image mode {repr(im.mode)}" + raise ValueError(msg) + + size = im.size + __data = data or align8to32(im.tobytes(), size[0], im.mode) + if exclusive_fp: + im.close() + return {"data": __data, "size": size, "format": format, "colortable": colortable} + + +class ImageQt(QImage): + def __init__(self, im): + """ + An PIL image wrapper for Qt. This is a subclass of PyQt's QImage + class. + + :param im: A PIL Image object, or a file name (given either as + Python string or a PyQt string object). + """ + im_data = _toqclass_helper(im) + # must keep a reference, or Qt will crash! + # All QImage constructors that take data operate on an existing + # buffer, so this buffer has to hang on for the life of the image. + # Fixes https://github.com/python-pillow/Pillow/issues/1370 + self._data = im_data["data"] + super().__init__( + self._data, + im_data["size"][0], + im_data["size"][1], + im_data["format"], + ) + if im_data["colortable"]: + self.setColorTable(im_data["colortable"]) + + +def toqimage(im): + return ImageQt(im) + + +def toqpixmap(im): + # # This doesn't work. For now using a dumb approach. + # im_data = _toqclass_helper(im) + # result = QPixmap(im_data["size"][0], im_data["size"][1]) + # result.loadFromData(im_data["data"]) + qimage = toqimage(im) + return QPixmap.fromImage(qimage) diff --git a/UmiOCR-data/py_src/image_controller/image_connector.py b/UmiOCR-data/py_src/image_controller/image_connector.py new file mode 100644 index 0000000..48ab535 --- /dev/null +++ b/UmiOCR-data/py_src/image_controller/image_connector.py @@ -0,0 +1,40 @@ +# ============================================= +# =============== 图片处理连接器 =============== +# ============================================= + +from .image_provider import copyImage, saveImage, openImage +from .screenshot_controller import ScreenshotController + +from PySide2.QtCore import QObject, Slot, Signal + + +class ImageConnector(QObject): + # 对所有屏幕截图。传入延时时间。返回截图列表 + @Slot(float, result="QVariant") + def getScreenshot(self, wait): + return ScreenshotController.getScreenshot(wait) + + # 对一张图片做裁切。传入原图imgID和裁切参数,返回裁切后的imgID或[Error] + @Slot(str, int, int, int, int, result=str) + def getClipImgID(self, imgID, x, y, w, h): + return ScreenshotController.getClipImgID(imgID, x, y, w, h) + + # 获取当前剪贴板的内容,返回 {"type":"", "": ""} + @Slot(result="QVariant") + def getPaste(self): + return ScreenshotController.getPaste() + + # 将图片写入剪贴板 + @Slot(str, result=str) + def copyImage(self, path): + return copyImage(path) + + # 用系统默认应用打开图片 + @Slot(str, result=str) + def openImage(self, path): + return openImage(path) + + # 将图片保存到本地 + @Slot(str, str, result=str) + def saveImage(self, fromPath, toPath): + return saveImage(fromPath, toPath) diff --git a/UmiOCR-data/py_src/image_controller/image_provider.py b/UmiOCR-data/py_src/image_controller/image_provider.py new file mode 100644 index 0000000..8937bbe --- /dev/null +++ b/UmiOCR-data/py_src/image_controller/image_provider.py @@ -0,0 +1,234 @@ +# ========================================================== +# =============== Python向Qml传输 Pixmap 图像 =============== +# ========================================================== + +import os +from uuid import uuid4 # 唯一ID +from urllib.parse import unquote +from PySide2.QtCore import Qt, QByteArray, QBuffer +from PySide2.QtGui import QPixmap, QImage, QPainter, QClipboard +from PySide2.QtQuick import QQuickImageProvider + +from umi_log import logger +from . import ImageQt +from ..platform import Platform + +Clipboard = QClipboard() # 剪贴板 + + +# Pixmap型图片提供器 +class PixmapProviderClass(QQuickImageProvider): + def __init__(self): + super().__init__(QQuickImageProvider.Pixmap) + self.pixmapDict = {} # 缓存所有pixmap的字典 + self.compDict = {} # 缓存所有组件的字典 + # 空图占位符 + self._noneImg = None + + # 向qml返回图片,imgID不存在时返回警告图 + def requestPixmap(self, path, size=None, resSize=None): + if "/" in path: + compID, imgID = path.split("/", 1) + self._delCompCache(compID, imgID) # 先清缓存 + if imgID in self.pixmapDict: + self.compDict[compID] = imgID # 记录缓存 + return self.pixmapDict[imgID] + else: # 清空一个组件的缓存 + self._delCompCache(path) + return self._getNoneImg() # 返回占位符 + + # 添加一个Pixmap图片到提供器,返回imgID + def addPixmap(self, pixmap): + imgID = str(uuid4()) + self.pixmapDict[imgID] = pixmap + return imgID + + # 向py返回图片,相当于requestPixmap,但imgID不存在时返回None + def getPixmap(self, imgID): + return self.pixmapDict.get(imgID, None) + + # 向py返回PIL对象 + def getPilImage(self, imgID): + im = self.getPixmap(imgID) + if not im: + return None + try: + return ImageQt.fromqimage(im) + except Exception: + logger.error("QPixmap 转 PIL 失败。", exc_info=True, stack_info=True) + return None + + # py将PIL对象写回pixmapDict。主要是记录预处理的图像 + # imgID可以已存在,也可以新添加 + def setPilImage(self, img, imgID=""): + try: + pixmap = ImageQt.toqpixmap(img) + except Exception as e: + logger.error("PIL 转 QPixmap 失败。", exc_info=True, stack_info=True) + return f"[Error] PIL 转 QPixmap 失败:{e}" + if not imgID: + imgID = str(uuid4()) + self.pixmapDict[imgID] = pixmap + return imgID + + # 从pixmapDict缓存中删除一个或一批图片 + # 一般无需手动调用此函数!缓存会自动管理、清除。 + def delPixmap(self, imgIDs): + if isinstance(imgIDs, str): + imgIDs = [imgIDs] + for i in imgIDs: + if i in self.pixmapDict: + del self.pixmapDict[i] + logger.debug(f"删除图片缓存,剩余:{len(self.pixmapDict)}") + + # 将 QPixmap 或 QImage 转换为字节 + @staticmethod + def toBytes(image): + if isinstance(image, QPixmap): + image = image.toImage() + elif not isinstance(image, QImage): + raise ValueError( + f"[Error] Only QImage or QPixmap can toBytes(), no {str(type(image))}." + ) + byteArray = QByteArray() # 创建一个字节数组 + buffer = QBuffer(byteArray) # 创建一个缓冲区 + buffer.open(QBuffer.WriteOnly) + image.save(buffer, "PNG") # 将 QImage 保存为字节数组 + buffer.close() + bytesData = byteArray.data() # 获取字节数组的内容 + return bytesData + + # 清空一个组件的缓存。imgID可选该组件下一次更新的图片ID。 + def _delCompCache(self, compID, imgID=""): + if compID in self.compDict: + last = self.compDict[compID] + if imgID and imgID == last: + logger.warning(f"图片组件异常清理: {compID} {imgID}") + return # 如果下一次更新的ID等于当前ID,则为异常,不进行清理 + if last in self.pixmapDict: + del self.pixmapDict[last] + del self.compDict[compID] + + # 返回空图占位符 + def _getNoneImg(self): + if self._noneImg: + return self._noneImg + pixmap = QPixmap(1, 100) + pixmap.fill(Qt.blue) + painter = QPainter(pixmap) # 绘制警告条纹 + painter.setPen(Qt.red) + painter.drawLine(0, 0, 0, 5) + painter.drawLine(0, 95, 0, 100) + self._noneImg = pixmap + return self._noneImg + + +# 图片提供器 单例 +PixmapProvider = PixmapProviderClass() + + +# 读入一张图片,返回该图片 +# type: pixmap / qimage / error +def _imread(path): + path = unquote(path) # 做一次URL解码 + if path.startswith("image://pixmapprovider/"): + path = path[23:] + if "/" in path: + compID, imgID = path.split("/", 1) + if imgID in PixmapProvider.pixmapDict: + return {"type": "pixmap", "data": PixmapProvider.pixmapDict[imgID]} + else: + return {"type": "error", "data": f"[Warning] ID not in pixmapDict: {path}"} + elif path.startswith("file:///"): + path = path[8:] + if os.path.exists(path): + try: + image = QImage(path) + return {"type": "qimage", "data": image, "path": path} + except Exception as e: + return { + "type": "error", + "data": f"[Error] QImage cannot read path: {path}", + } + else: + return {"type": "error", "data": f"[Warning] Path {path} not exists."} + elif path in PixmapProvider.pixmapDict: + return {"type": "pixmap", "data": PixmapProvider.pixmapDict[path]} + elif os.path.exists(path): + try: + image = QImage(path) + return {"type": "qimage", "data": image, "path": path} + except Exception as e: + return {"type": "error", "data": f"[Error] QImage cannot read path: {path}"} + return {"type": "error", "data": f"[Warning] Unknow: {path}"} + + +# 复制一张图片到剪贴板 +def copyImage(path): + im = _imread(path) + typ, data = im["type"], im["data"] + if typ == "error": + return data + try: + if typ == "pixmap": + Clipboard.setPixmap(data) + elif typ == "qimage": + Clipboard.setImage(data) + return "[Success]" + except Exception as e: + return f"[Error] can't copy: {e}\n{path}" + + +# 用系统默认应用打开图片 +def openImage(path): + im = _imread(path) + typ, data = im["type"], im["data"] + if typ == "error": + return data + # 若原本为本地图片,则直接打开 + if "path" in im: + path = im["path"] + # 若为内存数据,则创建缓存文件 + else: + path = "umi_temp_image.png" + try: + if typ == "pixmap": + data = data.toImage() + data.save(path) + logger.debug(f"用系统默认应用打开图片时,缓存临时图片到 {path}") + except Exception as e: + logger.error( + f"用系统默认应用打开图片时,无法缓存临时图片到 {path}", + exc_info=True, + stack_info=True, + ) + return f"[Error] can't save to temp file: {e}\n{path}" + # 打开文件 + try: + Platform.startfile(path) + return "[Success]" + except Exception as e: + logger.error( + f"无法用系统默认应用打开图片 {path}", + exc_info=True, + stack_info=True, + ) + return f"[Error] can't open image: {e}\n{path}" + + +# 保存一张图片 +def saveImage(fromPath, toPath): + if toPath.startswith("file:///"): + toPath = toPath[8:] + im = _imread(fromPath) + typ, data = im["type"], im["data"] + if typ == "error": + return data + try: + if typ == "pixmap": + data.save(toPath) + elif typ == "qimage": + data.save(toPath) + return f"[Success] {toPath}" + except Exception as e: + return f"[Error] can't save: {e}\n{fromPath}\n{toPath}" diff --git a/UmiOCR-data/py_src/image_controller/screenshot_controller.py b/UmiOCR-data/py_src/image_controller/screenshot_controller.py new file mode 100644 index 0000000..66573af --- /dev/null +++ b/UmiOCR-data/py_src/image_controller/screenshot_controller.py @@ -0,0 +1,104 @@ +# ======================================== +# =============== 截图控制 =============== +# ======================================== + +from ..image_controller.image_provider import PixmapProvider # 图片提供器 +from ..utils.file_finder import findFiles + +import time +from PySide2.QtGui import QGuiApplication, QClipboard, QImage, QPixmap # 截图 剪贴板 + +Clipboard = QClipboard() # 剪贴板 + + +class _ScreenshotControllerClass: + + def getScreenshot(self, wait=0): + """ + 延时wait秒后,获取所有屏幕的截图。返回列表(不为空),每项为:\n + { + "imgID": 图片ID 或 报错信息 "[Error]开头" , + "screenName": 显示器名称 , + "width": 截图宽度 , + "height": 截图高度 , + } + """ + if wait > 0: + time.sleep(wait) + try: + grabList = [] + screensList = QGuiApplication.screens() + for screen in screensList: + name = screen.name() + # 获取截图 + pixmap = screen.grabWindow(0) + width = pixmap.width() + height = pixmap.height() + # 检查截图失败 + if width <= 0 or height <= 0: + imgID = f"[Error] width={width}, height={height}" + # 检查有效,存入提供器,获取imgID + else: + imgID = PixmapProvider.addPixmap(pixmap) + grabList.append( + { + "imgID": imgID, + "screenName": name, + "width": width, + "height": height, + } + ) + if not grabList: # 获取到的截图列表为空 + return [{"imgID": f"[Error] grabList is empty."}] + return grabList + except Exception as e: + return [{"imgID": f"[Error] Screenshot: {e}"}] + + # 对一张图片做裁切。传入原图imgID和裁切参数,返回裁切后的imgID或[Error] + def getClipImgID(self, imgID, x, y, w, h): + try: + pixmap = PixmapProvider.getPixmap(imgID) + if not pixmap: + return f'[Error] Screenshot: Key "{imgID}" does not exist in the PixmapProvider dict.' + if x < 0 or y < 0 or w <= 0 or h <= 0: + return f"[Error] Screenshot: x/y/w/h value error. {x}/{y}/{w}/{h}" + pixmap = pixmap.copy(x, y, w, h) # 进行裁切 + clipID = PixmapProvider.addPixmap(pixmap) # 存入提供器,获取imgID + return clipID + except Exception as e: + return f"[Error] Screenshot: {e}" + + # 获取当前剪贴板的内容 + # type: imgID paths text + def getPaste(self): + # 获取剪贴板数据 + mimeData = Clipboard.mimeData() + res = {"type": ""} # 结果字典 + # 检查剪贴板的内容,若是图片,则提取它并扔给OCR + if mimeData.hasImage(): + image = Clipboard.image() + pixmap = QPixmap.fromImage(image) + pasteID = PixmapProvider.addPixmap(pixmap) # 存入提供器 + res = {"type": "imgID", "imgID": pasteID} + # 若为URL + elif mimeData.hasUrls(): + urlList = mimeData.urls() + paths = [] + for url in urlList: # 遍历URL列表,提取其中的文件 + if url.isLocalFile(): + p = url.toLocalFile() + paths.append(p) + paths = findFiles(paths, "image", False) # 过滤,保留图片的路径 + if len(paths) == 0: # 没有有效图片 + res = {"type": "error", "error": "[Warning] No image in clipboard."} + else: # 将有效图片地址传入OCR,返回地址列表 + res = {"type": "paths", "paths": paths} + elif mimeData.hasText(): + text = mimeData.text() + res = {"type": "text", "text": text} + else: + res = {"type": "error", "error": "[Warning] Unknow mimeData in clipboard."} + return res # 返回结果字典 + + +ScreenshotController = _ScreenshotControllerClass() diff --git a/UmiOCR-data/py_src/imports/call_func.py b/UmiOCR-data/py_src/imports/call_func.py new file mode 100644 index 0000000..3fefbab --- /dev/null +++ b/UmiOCR-data/py_src/imports/call_func.py @@ -0,0 +1,75 @@ +# ================================================================== +# =============== 为插件提供接口,在主线程中调用指定函数 =============== +# ================================================================== + +""" +from call_func import CallFunc + +def func(): + print("延时3秒调用!") + +id = CallFunc.delay(func, 3) # 延时调用 +# CallFunc.delayStop(id) # 取消延时调用 +""" + +from PySide2.QtCore import QObject, Slot, Signal, QTimer, QMutex +from uuid import uuid4 # 唯一ID + + +class _CallFunc(QObject): + def __init__(self): + super().__init__() + # 信号 在主线程中调用函数 + self._callFuncSignal = self.cSignal() + self._callFuncSignal.signal.connect(self._cFunc) + # 计时器停止字典 + self._timerStopDict = {} + self._timerLock = QMutex() + + # ========================= 【接口】 ========================= + + # 立刻:在主线程中调用python函数 + def now(self, func, *args): + self._callFuncSignal.signal.emit((func, args)) + + # 延时:在主线程中调用python函数。返回计时器ID + def delay(self, func, time, *args): + timerID = str(uuid4()) + + def go(): + timer = QTimer(self) + timer.setSingleShot(True) # 单次运行 + timer.timeout.connect(lambda: self._timerFunc(timerID, func, args)) + timer.start(time * 1000) + + self.now(go) + return timerID + + # 取消已启用的延时 + def delayStop(self, timerID): + self._timerLock.lock() + self._timerStopDict[timerID] = True # 记录停止 + self._timerLock.unlock() + + # ================================================== + # 计时器调用的函数 + def _timerFunc(self, timerID, func, args): + self._timerLock.lock() + if timerID in self._timerStopDict: + del self._timerStopDict[timerID] + self._timerLock.unlock() + return + self._timerLock.unlock() + func(*args) + + # 异步调用的槽函数 + @Slot("QVariant") + def _cFunc(self, args): + args[0](*args[1]) + + # 信号类 + class cSignal(QObject): + signal = Signal("QVariant") + + +CallFunc = _CallFunc() diff --git a/UmiOCR-data/py_src/imports/plugin_i18n.py b/UmiOCR-data/py_src/imports/plugin_i18n.py new file mode 100644 index 0000000..b2e02e0 --- /dev/null +++ b/UmiOCR-data/py_src/imports/plugin_i18n.py @@ -0,0 +1,55 @@ +# ========================================================= +# =============== 为插件提供简易的i18n翻译API =============== +# ========================================================= + +import os +import csv + +from umi_log import logger + +LangCode = "" + + +# 设置语言 +def setLangCode(langCode): + global LangCode + LangCode = langCode + + +class Translator: + # 载入翻译。每个模块调用tr前,必须载入翻译。 + # file: 与翻译文件同目录的任意文件的路径 + # name: 翻译文件名 + def __init__(self, file="", name=""): + self._trDict = {} + if not file or not name: + return + path = os.path.dirname(os.path.abspath(file)) + "/" + name + try: + if os.path.exists(path): + with open(path, "r", encoding="utf-8") as file: + csv_reader = csv.reader(file) + headers = next(csv_reader) + index = -1 + for i, h in enumerate(headers): # 搜索语言 + if h == LangCode: + index = i + if index == -1: # 找不到对应语言 + if LangCode.startswith("zh_"): # 中文 + index = 0 + else: + index = 1 # 找不到对应语言,默认英文 + for row in csv_reader: + self._trDict[row[0]] = row[index] + except Exception: + logger.error( + f"加载插件翻译失败。 file: {file}, name: {name}", + exc_info=True, + stack_info=True, + ) + + # 进行翻译。输入原文,返回译文 + def __call__(self, original): + if original in self._trDict: + return self._trDict[original] + return original diff --git a/UmiOCR-data/py_src/imports/umi_about.py b/UmiOCR-data/py_src/imports/umi_about.py new file mode 100644 index 0000000..8c901f2 --- /dev/null +++ b/UmiOCR-data/py_src/imports/umi_about.py @@ -0,0 +1,113 @@ +# Umi-OCR +# OCR software, free and offline. 开源、免费的离线OCR软件。 +# Website - https://github.com/hiroi-sora/Umi-OCR +# Author - hiroi-sora +# +# You are free to use, modify, and distribute Umi-OCR, but it must include +# the original author's copyright statement and the following license statement. +# 您可以免费地使用、修改和分发 Umi-OCR ,但必须包含原始作者的版权声明和下列许可声明。 +""" +Copyright (c) 2023 hiroi-sora + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + + +""" +========================================================= +=============== 为全局提供 Umi-OCR 关于信息 =============== +========================================================= + +在任意python文件中访问: +from umi_about import UmiAbout +UmiAbout["fullname"] + +在任意qml文件中访问: +UmiAbout.fullname +""" + +import os +import sys +import psutil +import platform +from json import load + +from umi_log import logger + +UmiAbout = None + + +# 初始化时调用一次 +def init(app_path=""): + """ + `app_path`: 程序入口文件 路径\n + """ + global UmiAbout + # 读取配置 + try: + with open("about.json", "r", encoding="utf-8") as file: + u = load(file) + except Exception as e: + os.MessageBox(f"[Error] 无法读取配置 about.json 。\n{e}") + return False + # 完整版本号 + v = f'{u["version"]["major"]}.{u["version"]["minor"]}.{u["version"]["patch"]}' + if u["version"]["prerelease"]: + v += f'-{u["version"]["prerelease"]}.{u["version"]["prereleaseNumber"]}' + u["version"]["string"] = v + # 全名 + u["fullname"] = f'{u["name"]} v{v}' + # 程序运行信息 + if app_path: + app_path = os.path.abspath(app_path) + app_home = os.path.dirname(app_path) + app_dir = os.path.dirname(app_path) + else: + app_path = "" + app_home = "" + app_dir = "" + logger.warning("未能获取程序入口路径。") + # 简短的平台代号 + _plat = sys.platform + if _plat.startswith("win32"): + _system = "win32" + elif _plat.startswith("linux"): + _system = "linux" + elif _plat.startswith("darwin"): + _system = "darwin" + elif _plat and isinstance(_plat, str): + _system = _plat + else: + _system = "unknow" + u["app"] = { + # 程序入口 + "path": app_path, + "home": app_home, + "dir": app_dir, + # 运行平台 + "system": _system, # 简略系统版本,如 'win32' + "platform": platform.platform(), # 详细系统版本,如 'Windows-10-10.0.22631-SP0' + "python": platform.python_version(), + # 硬件信息,用于debug + "cpu": f"{platform.processor()} | {psutil.cpu_count(logical=False)}C{psutil.cpu_count(logical=True)}T", + "ram": f"{(psutil.virtual_memory().total / 1073741824):.1f} GB", + } + + UmiAbout = u + return True diff --git a/UmiOCR-data/py_src/imports/umi_log.py b/UmiOCR-data/py_src/imports/umi_log.py new file mode 100644 index 0000000..cc03746 --- /dev/null +++ b/UmiOCR-data/py_src/imports/umi_log.py @@ -0,0 +1,270 @@ +""" +日志模块 + +Python: ========================= + +from umi_log import logger + +logger.debug("调试信息") +logger.info("普通信息") +logger.warning("警告信息") +logger.error("错误信息")) +logger.critical("严重错误信息") + +# exc_info 只能在 except 块中开启 +logger.error("错误信息", exc_info=True, stack_info=True) +# 覆盖 LogRecord 的属性 +logger.debug("信息", extra={"cover": {"filename": "test.txt", "lineno": 999}} + +Qml: ========================= + +console.log("调试信息") +console.info("普通信息") +console.warn("警告信息") +console.error("错误信息") +console.trace() // 堆栈信息,级别debug,含函数名、文件名、行号 +""" + +import os +import sys +import json +import logging +from datetime import datetime +from logging.handlers import RotatingFileHandler +from logging import LogRecord + +# 保存的日志级别,可在UI修改 +Save_Log_Level: int = logging.ERROR + +# 日志保存目录 +Logs_Dir = "./logs" +Logs_Dir = os.path.abspath(Logs_Dir) + +# 日志级别,对应的int值由小到大 +_Log_Levels = { + "DEBUG": logging.DEBUG, + "INFO": logging.INFO, + "WARNING": logging.WARNING, + "ERROR": logging.ERROR, + "CRITICAL": logging.CRITICAL, + "NONE": logging.CRITICAL + 10, # 最大,表示不记录日志 +} + + +# 忽略日志列表,屏蔽QT框架自发产生的一些日志。(目前仅对 get_qt_message_handler 生效) +Log_Ignore_List = [ + "Retrying to obtain clipboard.", # https://bugreports.qt.io/browse/QTBUG-97930 + "Unable to obtain clipboard.", +] + + +# 覆盖过滤器 +class _CoverFilter(logging.Filter): + def filter(self, record: LogRecord): + try: + # 提取自定义信息,覆盖给 record + cover = record.__dict__.get("cover", {}) + for k, v in cover.items(): + if hasattr(record, k): + setattr(record, k, v) + return True + except Exception: + logger.error("日志过滤错误", exc_info=True, stack_info=True) + return False + + +# 简化日志级别符号的格式化器 +class _LevelFormatter(logging.Formatter): + # 定义日志级别和对应符号的映射 + LEVEL_SYMBOLS = { + "DEBUG": " ", + "INFO": "√", + "WARNING": "?", + "ERROR": "×", + "CRITICAL": "×××", + } + + def format(self, record): + # 获取符号,如果没有定义则使用默认级别名称 + levelname = record.levelname + record.levelsymbol = self.LEVEL_SYMBOLS.get(levelname, levelname) + return super().format(record) + + +# json 日志文件处理器 +class _JsonRotatingFileHandler(RotatingFileHandler): + # 日志信息转字典 + def _record_to_dict(self, record: LogRecord): + # 时间戳格式化 + dt_object = datetime.fromtimestamp(record.created) + formatted_time = dt_object.strftime("%Y-%m-%d %H:%M:%S.%f") + # 构造消息字典 + log_dict = { + # 时间 + "time": formatted_time, + # 日志级别 ( DEBUG, INFO, WARNING, ERROR, CRITICAL ) + "level": record.levelname, + # 日志消息 + "message": record.getMessage(), + # ===== + # 代码所在文件 + "filename": record.filename, + # 代码行号 + "lineno": record.lineno, + # 模块名 + "module": record.module, + # 函数名 + "funcName": record.funcName, + # 异常信息,需在 except 块中开启 exc_info=True + "exc_text": record.exc_text, + # 堆栈信息,需开启 stack_info=True + "stack_info": record.stack_info, + # ===== + # 线程标识符 + "thread": record.thread, + # 线程名称 + "threadName": record.threadName, + # 进程标识符 + "process": record.process, + # 进程名称 + "processName": record.processName, + # 日志记录器的名称 + "name": record.name, + } + return log_dict + + # 发送日志 + def emit(self, record: LogRecord): + # 跳过忽略等级 + if record.levelno < Save_Log_Level: + return + # 检查文件大小并进行轮转 + if self.shouldRollover(record): + self.doRollover() + # 日志信息转字典 + try: + log_dict = self._record_to_dict(record) + except Exception: + self.handleError(record) + # 输出到日志文件 + try: + with open(self.baseFilename, "a", encoding=self.encoding) as f: + json.dump(log_dict, f, ensure_ascii=False) + f.write("\n") + except Exception: + self.handleError(record) + # TODO: 输出到UI界面 + + +# 日志记录器 管理类 +class _LogManager: + @staticmethod # 控制台处理器 + def _get_console_handler(): + # 显式规定输出到 stderr ,避免干涉命令行使用 + console_handler = logging.StreamHandler(sys.stderr) + console_handler.setLevel(logging.DEBUG) + fmt = "%(asctime)s %(levelsymbol)s %(funcName)s | %(message)s" + formatter = _LevelFormatter(fmt, datefmt="%H:%M:%S") # 使用自定义格式化器 + console_handler.setFormatter(formatter) + return console_handler + + @staticmethod # json处理器,输出到本地文件及UI + def _get_json_handler(): + # 确保日志目录存在 + if not os.path.exists(Logs_Dir): + os.makedirs(Logs_Dir) + # 获取当前日期 + current_date = datetime.now().strftime("%Y-%m-%d") + # 构造日志文件路径 + log_file = os.path.join(Logs_Dir, f"log_{current_date}.jsonl.txt") + # 创建json处理器 + json_handler = _JsonRotatingFileHandler( + log_file, + mode="a", # 追加写入 + maxBytes=10485760, # 单个文件最大:10MB + backupCount=3, # 文件备份数量 + encoding="utf-8", # 文件编码 + delay=True, # 延迟创建文件 + ) + json_handler.setLevel(logging.DEBUG) + return json_handler + + @staticmethod + def create_logger(name): + """创建并返回一个新的日志记录器。""" + logger = logging.getLogger(name) + logger.addFilter(_CoverFilter()) # 添加覆盖过滤器 + logger.setLevel(logging.DEBUG) + logger.addHandler(_LogManager._get_console_handler()) + logger.addHandler(_LogManager._get_json_handler()) + return logger + + +# 全局单例日志记录器 +logger = _LogManager.create_logger("Umi-OCR") + + +# 获取 QT 日志重定向器 +def get_qt_message_handler(): + # 确保在初次调用时才导入QT模块 + from PySide2.QtCore import QtMsgType, QMessageLogContext + + # 处理 QT (QML) 抛出的日志 + def qt_message_handler(mode: QtMsgType, context: QMessageLogContext, msg: str): + try: + if msg in Log_Ignore_List: + return + # 提取信息 + filepath = getattr(context, "file", "?") + if filepath: + filename = os.path.basename(filepath) + else: + filepath = filename = "?" + funcName = getattr(context, "function", "?") + if not funcName: # 匿名函数 + funcName = r"()=>{}" + # 覆盖字典 + extra = { + "cover": { + "category": getattr(context, "category", "?"), + "filename": filename, + "funcName": funcName, + "lineno": getattr(context, "line", "?"), + "version": getattr(context, "version", "?"), + "module": "qml", + } + } + if mode == QtMsgType.QtDebugMsg: + logger.debug(msg, extra=extra) + elif mode == QtMsgType.QtInfoMsg: + logger.info(msg, extra=extra) + elif mode == QtMsgType.QtWarningMsg: + logger.warning(msg, extra=extra) + elif mode == QtMsgType.QtCriticalMsg: + logger.error(msg, extra=extra) + elif mode == QtMsgType.QtFatalMsg: + logger.critical(msg, extra=extra) + except Exception: + logger.warning( + "qt_message_handler error", + exc_info=True, + stack_info=True, + ) + + return qt_message_handler + + +# 更改保存的日志级别,成功返回T +def change_save_log_level(levelname): + global Save_Log_Level + if levelname in _Log_Levels.keys(): + Save_Log_Level = _Log_Levels[levelname] + logger.info(f"设置保存日志级别: {levelname}") + return True + logger.error(f"设置保存日志级别 {levelname} 失败。") + return False + + +# 打开日志保存目录 +def open_logs_dir(): + os.startfile(Logs_Dir) diff --git a/UmiOCR-data/py_src/mission/doc_preview_connector.py b/UmiOCR-data/py_src/mission/doc_preview_connector.py new file mode 100644 index 0000000..18e2a74 --- /dev/null +++ b/UmiOCR-data/py_src/mission/doc_preview_connector.py @@ -0,0 +1,114 @@ +# =============================================== +# =============== 文档预览 - 连接器 =============== +# =============================================== + +from PySide2.QtCore import QObject, Slot, Signal +from PySide2.QtGui import QPixmap, QImage +import fitz # PyMuPDF + +from umi_log import logger +from .simple_mission import SimpleMission +from ..image_controller.image_provider import PixmapProvider +from ..utils.call_func import CallFunc +from .mission_doc import MissionDOC + +MinSize = 0 # 最小渲染分辨率 + + +# 文档预览连接器 +class DocPreviewConnector(QObject): + previewImg = Signal(str) # imgID + previewOcr = Signal("QVariant") # [path, page, res] + # 注:信号中含多个变量可能导致崩溃? + + def __init__(self, *args): + super().__init__(*args) + self._previewMission = SimpleMission(self._previewTask) # 简单任务对象 + self._previewDoc = None # 当前预览的对象 + self._previewPath = "" + self._zooms = {} # 缓存页面缩放系数 + + # 预览PDF画面 + @Slot(str, int, str) + def preview(self, path, page, password): + page -= 1 + self._previewMission.addMissionList([(path, page, password)]) + + def _previewTask(self, msn): + path, page, password = msn + if path == self._previewPath: # 已经加载了 + doc = self._previewDoc + else: # 新加载 + try: + doc = fitz.open(path) + if doc.is_encrypted and not doc.authenticate(password): + msg = "[Warning] is_encrypted" # 固定指令,不能改 + self.previewImg.emit(msg) + return + except Exception as e: + msg = f"[Error] 打开文档失败:{path} {e}" + self.previewImg.emit(msg) + return + self._previewDoc = doc + self._previewPath = path + self._zooms = {} + page_count = doc.page_count + if page < 0 or page > page_count: + logger.error(f"页数 {page} 超出范围 0-{page_count} 。") + return + # 检查页面边长,如果低于阈值,则增加放大系数,以提高渲染清晰度 + rect = doc[page].rect + w, h = abs(rect[2] - rect[0]), abs(rect[3] - rect[1]) + m = min(w, h) + if m < MinSize: + zoom = MinSize / max(m, 1) + matrix = fitz.Matrix(zoom, zoom) + self._zooms[page] = zoom + else: + matrix = fitz.Identity + self._zooms[page] = 1 + p = doc[page].get_pixmap(matrix=matrix) + # 通过 QImage fromImage 转换 + # 必须先使用变量提取出图像 https://github.com/pymupdf/PyMuPDF/issues/1210 + samples = p.samples + # 必须传入 pix.stride ,否则部分格式的图像会导致崩溃 + qimage = QImage(samples, p.width, p.height, p.stride, QImage.Format_RGB888) + qpixmap = QPixmap.fromImage(qimage) + imgID = PixmapProvider.addPixmap(qpixmap) + self.previewImg.emit(imgID) + + # 预览一页OCR内容 + @Slot(str, int, str, "QVariant") + def ocr(self, path, page, password, argd): + argd = argd.toVariant() # qml对象转python字典 + # if "tbpu.ignoreArea" in argd: # 删除忽略区域参数 + # del argd["tbpu.ignoreArea"] + + def _onGet(msnInfo, page_, res): + # 缩放文本位置 + if page_ in self._zooms and res["code"] == 100: + z = self._zooms[page_] + for r in res["data"]: + for bi in range(4): + r["box"][bi][0] = r["box"][bi][0] * z + r["box"][bi][1] = r["box"][bi][1] * z + page_ += 1 + self.previewOcr.emit([path, page_, res]) + + def _onEnd(msnInfo, msg): + if not msg.startswith("[Success]"): + res = {"code": 103, "data": msg} + self.previewOcr.emit([path, -1, res]) + + msnInfo = { + "argd": argd, + "onGet": _onGet, + "onEnd": _onEnd, + } + MissionDOC.addMission(msnInfo, path, (page, page), password=password) + + # 清空缓存 + @Slot() + def clear(self): + self._previewDoc = None + self._previewPath = "" diff --git a/UmiOCR-data/py_src/mission/mission.py b/UmiOCR-data/py_src/mission/mission.py new file mode 100644 index 0000000..fb1c4f1 --- /dev/null +++ b/UmiOCR-data/py_src/mission/mission.py @@ -0,0 +1,302 @@ +# ============================================== +# =============== 任务管理器 基类 =============== +# ============================================== + + +from PySide2.QtCore import QMutex, QRunnable +from threading import Condition +from uuid import uuid4 # 唯一ID +import time + +from umi_log import logger +from ..utils.thread_pool import threadRun # 异步执行函数 + + +class Mission: + def __init__(self): + self._msnInfoDict = {} # 任务信息的字典 + self._msnListDict = {} # 任务队列的字典 + self._msnPausedDict = {} # 已暂停的任务队列 + self._msnMutex = QMutex() # 任务队列的锁 + self._task = None # 异步任务对象 + self._taskMutex = QMutex() # 任务对象的锁 + # 任务队列调度方式 + # 1111 : 轮询调度,轮流取每个队列的第1个任务 + # 1234 : 顺序调度,将首个队列所有任务处理完,再进入下一个队列 + self._schedulingMode = "1111" + + # ========================= 【调用接口】 ========================= + + """ + 添加任务队列的格式 + mission = { + "onStart": 任务队列开始回调函数 , (msnInfo) + "onReady": 一项任务准备开始 , (msnInfo, msn) + "onGet": 一项任务获取结果 , (msnInfo, msn, res) + "onEnd": 任务队列结束 , (msnInfo, msg) // msg可选前缀: [Success] [Warning] [Error] + } + MissionOCR.addMissionList(mission, paths) + """ + + # 【异步】添加一条任务队列。成功返回任务ID,失败返回 startswith("[Error]") + # msnInfo: { 回调函数 "onStart", "onReady", "onGet", "onEnd"} + # msnList: [ 任务1, 任务2 ] + def addMissionList(self, msnInfo, msnList): + if len(msnList) < 1: + return "[Error] no valid mission in msnList!" + msnID = str(uuid4()) + # 检查并补充回调函数 + # 队列开始,单个任务准备开始,单任务取得结果,队列结束 + cbKeys = ["onStart", "onReady", "onGet", "onEnd"] + for k in cbKeys: + if k not in msnInfo or not callable(msnInfo[k]): + msnInfo[k] = lambda *e: None + # 任务状态state: waiting 等待开始, running 进行中, stop 要求停止 + msnInfo["state"] = "waiting" + msnInfo["msnID"] = msnID + # 添加到任务队列 + self._msnMutex.lock() # 上锁 + self._msnInfoDict[msnID] = msnInfo # 添加任务信息 + self._msnListDict[msnID] = msnList # 添加任务队列 + self._msnMutex.unlock() # 解锁 + # 启动任务 + self._startMsns() + # 返回任务id + return msnID + + # 停止一些任务队列 + def stopMissionList(self, msnIDs): + if not isinstance(msnIDs, list): + msnIDs = [msnIDs] + self._msnMutex.lock() # 上锁 + for msnID in msnIDs: + # 将暂停中的任务恢复 + if msnID in self._msnPausedDict: + info, list_ = self._msnPausedDict[msnID] + self._msnInfoDict[msnID] = info + self._msnListDict[msnID] = list_ + # 将进行中的任务置为停止状态 + if msnID in self._msnListDict: + self._msnInfoDict[msnID]["state"] = "stop" # 设为停止状态 + self._msnMutex.unlock() # 解锁 + self._startMsns() # 拉起工作线程,使已暂停的任务可以正常结束 + + # 停止全部任务 + def stopAllMissions(self): + self._msnMutex.lock() # 上锁 + # 将暂停中的任务恢复 + for msnID in self._msnPausedDict: + info, list_ = self._msnPausedDict[msnID] + self._msnInfoDict[msnID] = info + self._msnListDict[msnID] = list_ + # 将进行中的任务置为停止状态 + for msnID in self._msnListDict: + self._msnInfoDict[msnID]["state"] = "stop" + self._msnMutex.unlock() # 解锁 + self._startMsns() + + # 暂停一些任务队列 + def pauseMissionList(self, msnIDs): + if not isinstance(msnIDs, list): + msnIDs = [msnIDs] + self._msnMutex.lock() # 上锁 + for msnID in msnIDs: + if msnID in self._msnListDict: + msn = (self._msnInfoDict[msnID], self._msnListDict[msnID]) + self._msnPausedDict[msnID] = msn + del self._msnInfoDict[msnID] + del self._msnListDict[msnID] + self._msnMutex.unlock() # 解锁 + logger.debug(f"任务暂停: {msnID}") + + # 恢复一些任务队列的运行 + def resumeMissionList(self, msnIDs): + if not isinstance(msnIDs, list): + msnIDs = [msnIDs] + self._msnMutex.lock() # 上锁 + for msnID in msnIDs: + if msnID in self._msnPausedDict: + info, list_ = self._msnPausedDict[msnID] + self._msnInfoDict[msnID] = info + self._msnListDict[msnID] = list_ + del self._msnPausedDict[msnID] + self._msnMutex.unlock() # 解锁 + self._startMsns() # 拉起工作线程 + logger.debug(f"任务恢复: {msnID}") + + # 获取每一条任务队列长度 + def getMissionListsLength(self): + lenDict = {} + self._msnMutex.lock() + for k in self._msnListDict: + lenDict[str(k)] = len(self._msnListDict[k]) + self._msnMutex.unlock() + return lenDict + + # 【同步】添加一个任务或队列,等待完成,返回任务结果列表。[i]["result"]为结果 + def addMissionWait(self, argd, msnList): + if not isinstance(msnList, list): + msnList = [msnList] + resList = msnList[:] # 浅拷贝出一条结果列表 + nowIndex = 0 # 当前处理的任务 + msnLen = len(msnList) + condition = Condition() # 线程同步器 + endMsg = "" # 任务结束的消息 + + def _onGet(msnInfo, msn, res): + nonlocal nowIndex + resList[nowIndex]["result"] = res + nowIndex += 1 + + def _onEnd(msnInfo, msg): + nonlocal endMsg + endMsg = msg + with condition: # 释放线程阻塞 + condition.notify() + + def _pass(*x): + pass + + msnInfo = { + "onStart": _pass, + "onReady": _pass, + "onGet": _onGet, + "onEnd": _onEnd, + "argd": argd, + } + msnID = self.addMissionList(msnInfo, msnList) + if msnID.startswith("[Error]"): # 添加任务失败 + endMsg = msnID + else: # 添加成功,线程阻塞,直到任务完成。 + with condition: + condition.wait() + # 补充未完成的任务 + for i in range(nowIndex, msnLen): + if "result" not in resList[i]: + resList[i]["result"] = {"code": 803, "data": f"任务提前结束。{endMsg}"} + return resList + + # ========================= 【主线程 方法】 ========================= + + def _startMsns(self): # 启动异步任务,执行所有任务列表 + # 若当前异步任务对象为空,则创建工作线程 + self._taskMutex.lock() # 上锁 + if self._task == None: + self._task = threadRun(self._taskRun) + self._taskMutex.unlock() # 解锁 + + # ========================= 【子线程 方法】 ========================= + + def _taskRun(self): # 异步执行任务字典的流程 + dictIndex = 0 # 当前取任务字典中的第几个任务队列 + # 循环,直到任务队列的列表为空 + while True: + # 1. 检查api和任务字典是否为空 + self._msnMutex.lock() # 锁1 上锁 + dl = len(self._msnInfoDict) # 任务字典长度 + if dl == 0: # 任务字典已空 + self._msnMutex.unlock() # 锁1 解锁 + break + + # 2. 任务调度,取一个任务 + if self._schedulingMode == "1111": # 轮询 + dictIndex = (dictIndex + 1) % dl + elif self._schedulingMode == "1234": # 顺序 + dictIndex = 0 # 始终为首个队列 + dictKey = tuple(self._msnInfoDict.keys())[dictIndex] + msnInfo = self._msnInfoDict[dictKey] + msnList = self._msnListDict[dictKey] + self._msnMutex.unlock() # 锁1 解锁 + + # 3. 检查任务是否要求停止 + if msnInfo["state"] == "stop": + self._msnDictDel(dictKey) + msnInfo["onEnd"](msnInfo, "[Warning] Task stop.") + continue + + # 4. 前处理,检查、更新参数 + preFlag = self.msnPreTask(msnInfo) + if preFlag == "continue": # 跳过本次 + logger.debug(f"任务跳过: {dictKey}") + continue + elif preFlag.startswith("[Error]"): # 异常,结束该队列 + msnInfo["onEnd"](msnInfo, preFlag) + self._msnDictDel(dictKey) + dictIndex -= 1 # 字典下标回退1位,下次执行正确的下一项 + continue + + # 5. 首次任务 + if msnInfo["state"] == "waiting": + msnInfo["state"] = "running" + msnInfo["onStart"](msnInfo) + + # 6. 执行任务,并记录时间 + msn = msnList[0] + msnInfo["onReady"](msnInfo, msn) + t1 = time.time() + res = self.msnTask(msnInfo, msn) + t2 = time.time() + if isinstance(res, dict): # 补充耗时和时间戳 + res["time"] = t2 - t1 + res["timestamp"] = t2 + + # 7. 再次检查任务是否要求停止,或者已暂停 + self._msnMutex.lock() # 锁2 上锁 + if msnInfo["state"] == "stop": + self._msnDictDel(dictKey) + self._msnMutex.unlock() # 锁2 解锁 + msnInfo["onEnd"](msnInfo, "[Warning] Task stop.") + continue + if dictKey not in self._msnInfoDict: + self._msnMutex.unlock() # 锁2 解锁 + continue + + # 8. 不停止,则上报该任务 + msnList.pop(0) # 弹出该任务 + self._msnMutex.unlock() # 锁2 解锁 + # 回调。注意:回调函数执行时间长时,可能用户再次提交了任务暂停,需要后续继续判断。 + msnInfo["onGet"](msnInfo, msn, res) + + # 9. 这条任务队列完成 + if len(msnList) == 0: + msnInfo["onEnd"](msnInfo, "[Success]") + self._msnMutex.lock() # 锁3 上锁 + self._msnDictDel(dictKey) + self._msnMutex.unlock() # 锁3 解锁 + dictIndex -= 1 # 字典下标回退1位,下次执行正确的下一项 + + # 完成 + self._taskFinish() + + def _msnDictDel(self, dictKey): # 停止一组任务队列 + # 正常 删除任务队列项 + if dictKey in self._msnInfoDict: + del self._msnInfoDict[dictKey] + del self._msnListDict[dictKey] + # 如果该任务在暂停中,则移除暂停队列中的项 + if dictKey in self._msnPausedDict: + del self._msnPausedDict[dictKey] + logger.debug(f"移除暂停任务: {dictKey}") + + def _taskFinish(self): # 任务结束 + self._taskMutex.lock() # 上锁 + self._task = None + self._taskMutex.unlock() # 解锁 + + # ========================= 【继承重载】 ========================= + + def msnPreTask(self, msnInfo): # 任务前处理,用于更新api和参数。 + """返回值可选: + "" :空字符串表示正常继续。 + "continue" :跳过本次任务 + "[Error] xxxx" :终止这条任务队列,返回异常信息 + """ + # return "[Error] No overloaded msnPreTask. \n【异常】未重载msnPreTask。" + return "" + + def msnTask(self, msnInfo, msn): # 执行任务msn,返回结果字典。 + logger.debug("mission 未重载 msnTask") + return {"error": "[Error] No overloaded msnTask. \n【异常】未重载msnTask。"} + + def getStatus(self): # 返回当前状态 + return "Mission 基类 返回空状态" diff --git a/UmiOCR-data/py_src/mission/mission_connector.py b/UmiOCR-data/py_src/mission/mission_connector.py new file mode 100644 index 0000000..9e30c16 --- /dev/null +++ b/UmiOCR-data/py_src/mission/mission_connector.py @@ -0,0 +1,32 @@ +# ===================================================================== +# =============== qml与【后端Python任务控制器们】的连接器 =============== +# ===================================================================== + +from PySide2.QtCore import QObject, Slot, Signal + +# 本模块内定义的任务控制器单例 +from .mission_ocr import MissionOCR +from umi_log import logger + +# 控制器字典 +MsnObjDict = {"ocr": MissionOCR} + + +# 任务连接器类(不限制单例) +class MissionConnector(QObject): + # qml访问某个Python任务控制器的方法(同步) + # qml调用ctrlKey的方法funcName,入参为列表(对应参数顺序),返回值为可变类型。 + @Slot(str, str, list, result="QVariant") + def callPy(self, msnKey, funcName, args=()): + if msnKey not in MsnObjDict: + logger.error(f"qml访问不存在的任务控制器 {msnKey} 。") + return None + # 获取方法的引用 + msnObj = MsnObjDict[msnKey] + method = getattr(msnObj, funcName, None) + # 查询失败 + if not callable(method): + logger.error(f"qml调用了 {msnKey} 的不存在的py方法 {funcName} 。") + return None + # 调用方法,参数不对的话让系统抛出错误 + return method(*args) diff --git a/UmiOCR-data/py_src/mission/mission_doc.py b/UmiOCR-data/py_src/mission/mission_doc.py new file mode 100644 index 0000000..fecd99d --- /dev/null +++ b/UmiOCR-data/py_src/mission/mission_doc.py @@ -0,0 +1,372 @@ +# =============================================== +# =============== 文档 - 任务管理器 =============== +# =============================================== + +# API所有页数page 均为1开始 + +import fitz # PyMuPDF +import time +import math +from PIL import Image +from io import BytesIO + +from umi_log import logger +from .mission import Mission +from .mission_ocr import MissionOCR +from ..ocr.tbpu import getParser +from ..ocr.tbpu import IgnoreArea +from ..ocr.tbpu.parser_tools.paragraph_parse import word_separator # 上下句间隔符 + +MinSize = 1080 # 最小渲染分辨率 + +# 合法文件后缀 +DocSuf = [ + ".pdf", + ".xps", + ".epub", + ".mobi", + ".fb2", + ".cbz", +] + + +class FitzOpen: + def __init__(self, path): + self._path = path + self._doc = None + + def __enter__(self): + self._doc = fitz.open(self._path) + return self._doc + + def __exit__(self, exc_type, exc_val, exc_tb): + self._doc.close() + + +# https://pymupdf.readthedocs.io/en/latest/matrix.html#matrix +# 从变换矩阵中提取角度值,返回0~359整数 +def transform_to_rotation(matrix): + # [1, 0, 0, 1, 0, 0] -> [cos(deg), sin(deg), -sin(deg), cos(deg), 0, 0]. + a, b, c, d, _, _ = matrix + # 处理缩放和反射 + scale = math.sqrt(a**2 + b**2) + if scale < 1e-6: + return 0 + # 归一化以消除缩放影响 + cos_theta = a / scale + sin_theta = b / scale + # 检查反射 + determinant = a * d - b * c + if determinant < 0: + # 反射情况,调整角度计算 + cos_theta = -cos_theta + theta_rad = math.atan2(sin_theta, cos_theta) + theta_deg = math.degrees(theta_rad) + rounded_deg = round(theta_deg) % 360 + return rounded_deg + + +class _MissionDocClass(Mission): + def __init__(self): + super().__init__() + self._schedulingMode = "1234" # 调度方式:顺序 + self._minInterval = 0.1 # msnTask最短调用间隔 + self._lastCallTime = 0 # 上一次调用时间 + + # 添加一个文档任务 + # msnInfo: { 回调函数"onXX", 参数"argd":{"tbpu.xx", "ocr.xx"} } + # msnPath: 单个文档路径 + # pageRange: 页数范围。可选: None 全部页 , [1,3] 页面范围(含开头结束)。 + # pageList: 指定多个页数。可选: [] 使用pageRange设置 , [1,2,3] 指定页数 + # password: 密码(非必填) + def addMission(self, msnInfo, msnPath, pageRange=None, pageList=[], password=""): + # =============== 加载文档,获取文档操作对象 =============== + try: + doc = fitz.open(msnPath) + except Exception as e: + return f"[Error] fitz.open error: {msnPath} {e}" + if doc.is_encrypted and not doc.authenticate(password): + if password: + msg = f"[Error] Incorrect password. 文档已加密,密码错误。 [{password}]" + else: + msg = "[Error] Doc encrypted. 文档已加密,请提供密码。" + return msg + msnInfo["doc"] = doc + msnInfo["path"] = msnPath + # =============== 拦截 onEnd =============== + msnInfo["sourceOnEnd"] = msnInfo["onEnd"] if "onEnd" in msnInfo else None + msnInfo["onEnd"] = self._preOnEnd + # =============== pageRange 页面范围 =============== + page_count = doc.page_count + if len(pageList) == 0: + if isinstance(pageRange, (tuple, list)) and len(pageRange) == 2: + a, b = pageRange[0], pageRange[1] + if a < 0: + a += page_count + 1 + if b < 0: + b += page_count + 1 + if a < 1: + return f"[Error] pageRange {pageRange} 范围起始不能小于1" + if b > page_count: + return f"[Error] pageRange {pageRange} 范围结束不能大于页数 {doc.page_count}" + if a > b: + return f"[Error] pageRange {pageRange} 范围错误" + pageList = list(range(a - 1, b)) + else: + pageList = list(range(0, page_count)) + # 检查页数列表合法性 + if len(pageList) == 0: + return "[Error] 页数列表为空" + for p in pageList: + if not isinstance(p, int): + return "[Error] 页数列表内容非整数" + if not 0 <= p < page_count: + return f"[Error] 页数列表超出 1~{page_count} 范围" + msnInfo["pageList"] = pageList + # =============== tbpu文本块后处理 msnInfo["tbpu"] =============== + argd = msnInfo["argd"] # 参数 + msnInfo["tbpu"] = [] + msnInfo["ignoreArea"] = {} + # 忽略区域 + if "tbpu.ignoreArea" in argd: + iArea = argd["tbpu.ignoreArea"] + if isinstance(iArea, list) and len(iArea) > 0: + msnInfo["ignoreArea"]["obj"] = IgnoreArea(iArea) + # 范围,负数转为倒数第x页 + igStart = argd.get("tbpu.ignoreRangeStart", 1) + igEnd = argd.get("tbpu.ignoreRangeEnd", page_count) + if igStart < 0: + igStart += page_count + 1 + if igEnd < 0: + igEnd += page_count + 1 + msnInfo["ignoreArea"]["start"] = igStart - 1 # -1是将起始1页转为起始0页 + msnInfo["ignoreArea"]["end"] = igEnd - 1 + logger.debug(f"忽略区域范围: {igStart} ~ {igEnd} 。") + # 获取排版解析器对象 + if "tbpu.parser" in argd: + msnInfo["tbpu"].append(getParser(argd["tbpu.parser"])) + return self.addMissionList(msnInfo, pageList) + + def msnTask(self, msnInfo, pno): # 执行msn。pno为当前页数 + doc = msnInfo["doc"] # 文档对象 + page = doc[pno] # 页面对象 + argd = msnInfo["argd"] # 参数 + extractionMode = argd["doc.extractionMode"] # OCR内容模式 + """ mixed - 混合OCR/拷贝文本 + fullPage - 整页强制OCR + imageOnly - 仅OCR图片 + textOnly - 仅拷贝原有文本 """ + errMsg = "" # 本次任务流程的异常信息 + + # =============== 提取图片和原文本 =============== + imgs = [] # 待OCR的图片列表 + tbs = [] # text box 文本块列表 + page_rotation = page.rotation # 获取页面的旋转角度 + if extractionMode == "fullPage": # 模式:整页强制OCR + # 检查页面边长,如果低于阈值,则增加放大系数,以提高渲染清晰度 + rect = page.rect + w, h = abs(rect[2] - rect[0]), abs(rect[3] - rect[1]) + m = min(w, h) + if m < MinSize: + zoom = MinSize / max(m, 1) + matrix = fitz.Matrix(zoom, zoom) + else: + zoom = 1 + matrix = fitz.Identity + p = page.get_pixmap(matrix=matrix) + bytes = p.tobytes("png") + scale = 1 / zoom + imgs.append( + {"bytes": bytes, "xy": (0, 0), "scale_w": scale, "scale_h": scale} + ) + else: + # 获取元素 https://pymupdf.readthedocs.io/en/latest/_images/img-textpage.png + # https://pymupdf.readthedocs.io/en/latest/textpage.html#structure-of-dictionary-outputs + # 确保越界图像能被采集 https://github.com/pymupdf/PyMuPDF/issues/3171 + p = page.get_text("dict", clip=fitz.INFINITE_RECT()) + for t in p["blocks"]: # 遍历区块(段落) + # ========== 获取图片 ========== + if t["type"] == 1 and ( + extractionMode == "imageOnly" or extractionMode == "mixed" + ): + # 提取图片相对旋转角,加上页面旋转角,得到图片绝对旋转角 + transform = t["transform"] + img_rotation = transform_to_rotation(transform) + abs_rotation = round(page_rotation+img_rotation) % 360 + img_bytes = t["image"] # 图片字节 + bbox = t["bbox"] # 图片包围盒 + # 图片视觉大小、原始大小、缩放比例 + w1, h1 = bbox[2] - bbox[0], bbox[3] - bbox[1] + w2, h2 = t["width"], t["height"] + # 特殊情况:图片宽高为0 + if w2 <= 0 or h2 <= 0: + continue + # 单独计算宽高的缩放比例 + scale_w = w1 / w2 + scale_h = h1 / h2 + # 如果图片有绝对旋转,则逆向旋转图片字节 + if page_rotation != 0 or img_rotation != 0: + logger.debug(f"P{pno}-{len(imgs)} 旋转:页面{page_rotation}°,图片{img_rotation}°,绝对{abs_rotation}°") + if abs_rotation != 0: + try: + with Image.open(BytesIO(img_bytes)) as pimg: + # 记录原图格式 + format = pimg.format + if not format: + format = "PNG" + # PDF的旋转是顺时针,需要逆时针旋转图片 + pimg = pimg.rotate(-abs_rotation, expand=True) + # 将旋转后的图片转回bytes + buffered = BytesIO() + pimg.save(buffered, format=format) + img_bytes = buffered.getvalue() + except Exception: + logger.error( + "旋转文档图片异常。", exc_info=True, stack_info=True + ) + # 记录图片 + imgs.append( + { + "bytes": img_bytes, + "xy": (bbox[0], bbox[1]), + "scale_w": scale_w, + "scale_h": scale_h, + } + ) + # ========== 获取文本块 ========== + elif t["type"] == 0 and ( + extractionMode == "textOnly" or extractionMode == "mixed" + ): + l = len(t["lines"]) - 1 + for index, line in enumerate(t["lines"]): # 遍历每一行 + # 拼接该行所有子文本块的内容 + text = "" + for span in line["spans"]: + text += span["text"] + # 提取其他信息,组装为OCR文本块格式 + if text: + # 获取该行的的包围盒 + b = line["bbox"] + if page_rotation == 0: # 页面没有旋转,直接提取 + box = [ + [b[0], b[1]], + [b[2], b[1]], + [b[2], b[3]], + [b[0], b[3]], + ] + else: # 页面有旋转,默认文本行无相对旋转,则反向消除文本的绝对旋转 + # https://pymupdf.readthedocs.io/en/latest/page.html#Page.derotation_matrix + rotation_matrix = page.rotation_matrix + b01 = fitz.Point(b[0], b[1]) * rotation_matrix + b23 = fitz.Point(b[2], b[3]) * rotation_matrix + x0 = min(b01.x, b23.x) + x1 = max(b01.x, b23.x) + y0 = min(b01.y, b23.y) + y1 = max(b01.y, b23.y) + box = [ + [x0, y0], + [x1, y0], + [x1, y1], + [x0, y1], + ] + # 组装文本块 + tb = { + "box": box, + "text": text, + "score": 1, + "end": "\n" if index == l else "", # 结尾符 + "from": "text", # 来源:直接提取文本 + } + tbs.append(tb) + # 补充结尾符 + for i1 in range(len(tbs) - 1): + if tbs[i1]["end"]: # 跳过已有结尾符的 + continue + i2 = i1 + 1 + sep = word_separator(tbs[i1]["text"][-1], tbs[i2]["text"][0]) + tbs[i1]["end"] = sep + + # =============== 调用OCR,将 imgs 的内容提取出来放入 tbs =============== + if imgs: + # 提取 "ocr." 开头的参数,组装OCR参数字典 + ocrArgd = {} + for k in argd: + if k.startswith("ocr."): + ocrArgd[k] = argd[k] + # 调用OCR,堵塞等待任务完成 + ocrList = MissionOCR.addMissionWait(ocrArgd, imgs) + # 整理OCR结果 + for o in ocrList: + res = o["result"] + if res["code"] == 100: + x, y = o["xy"] + scale_w = o["scale_w"] + scale_h = o["scale_h"] + for r in res["data"]: + # 将所有文本块的坐标,从图片相对坐标系,转为页面绝对坐标系 + for bi in range(4): + r["box"][bi][0] = r["box"][bi][0] * scale_w + x + r["box"][bi][1] = r["box"][bi][1] * scale_h + y + r["from"] = "ocr" # 来源:OCR + tbs.append(r) + elif res["code"] != 101: + errMsg += f'[Error] OCR code:{res["code"]} msg:{res["data"]}\n' + + # =============== tbpu文本块后处理 =============== + # 忽略区域 + if msnInfo["ignoreArea"] and tbs: + # 检查范围 + igStart = msnInfo["ignoreArea"]["start"] + igEnd = msnInfo["ignoreArea"]["end"] + if pno >= igStart and pno <= igEnd: + tbs = msnInfo["ignoreArea"]["obj"].run(tbs) + # 其他tbpu + if msnInfo["tbpu"] and tbs: + for tbpu in msnInfo["tbpu"]: + tbs = tbpu.run(tbs) + + # =============== 组装结果字典 resDict =============== + if errMsg: + logger.error(f"文档识别异常。P{pno}, errMsg: {errMsg}") + errMsg = f"[Error] Doc P{pno}\n" + errMsg + + if tbs: # 有文本 + resDict = {"code": 100, "data": tbs} + elif errMsg: # 无文本,有异常 + resDict = {"code": 102, "data": errMsg} + else: # 无文本,无异常 + resDict = {"code": 101, "data": ""} + + # ===== 仅提取文本时任务速度过快,频繁回调会导致UI卡死,因此故意引入延迟 ===== + currentTime = time.time() + elapsedTime = currentTime - self._lastCallTime + # 如果与上一次调用的时间差小于最短间隔,则睡至满足最短间隔 + if elapsedTime < self._minInterval: + t = self._minInterval - elapsedTime + time.sleep(t) + self._lastCallTime = currentTime + return resDict + + # 获取一个文档的信息,如页数 + def getDocInfo(self, path): + try: + with FitzOpen(path) as doc: + info = { + "path": path, + "page_count": doc.page_count, + "is_encrypted": doc.is_encrypted, + } + return info + except Exception as e: + return {"path": path, "error": e} + + # 结束前的处理 + def _preOnEnd(self, msnInfo, msg): + # 先关闭文档对象,再触发原本的 onEnd ,防止新文档保存到原路径时的冲突 + msnInfo["doc"].close() + if msnInfo["sourceOnEnd"]: + msnInfo["sourceOnEnd"](msnInfo, msg) + + +# 全局 DOC 任务管理器 +MissionDOC = _MissionDocClass() diff --git a/UmiOCR-data/py_src/mission/mission_ocr.py b/UmiOCR-data/py_src/mission/mission_ocr.py new file mode 100644 index 0000000..7241794 --- /dev/null +++ b/UmiOCR-data/py_src/mission/mission_ocr.py @@ -0,0 +1,166 @@ +# =============================================== +# =============== OCR - 任务管理器 =============== +# =============================================== + +""" +一种任务管理器为全局单例,不同标签页要执行同一种任务,要访问对应的任务管理器。 +任务管理器中有一个引擎API实例,所有任务均使用该API。 +标签页可以向任务管理器提交一组任务队列,其中包含了每一项任务的信息,及总体的参数和回调。 +""" + +import os + +from umi_log import logger +from .mission import Mission +from ..ocr.tbpu import getParser, IgnoreArea +from ..ocr.api import getApiOcr, getLocalOptions +from ..utils.utils import argdIntConvert + +# 合法文件后缀 +ImageSuf = [ + ".jpg", + ".jpe", + ".jpeg", + ".jfif", + ".png", + ".webp", + ".bmp", + ".tif", + ".tiff", +] + + +class __MissionOcrClass(Mission): + def __init__(self): + super().__init__() + self._apiKey = "" # 当前api类型 + self._api = None # 当前引擎api对象 + + # ========================= 【重载】 ========================= + + # msnInfo: { 回调函数"onXX", 参数"argd":{"tbpu.xx", "ocr.xx"} } + # msnList: [ { "path", "bytes", "base64" } ] + def addMissionList(self, msnInfo, msnList): # 添加任务列表 + # 实例化 tbpu 文本后处理模块 + msnInfo["tbpu"] = [] + argd = msnInfo["argd"] + # 忽略区域 + if "tbpu.ignoreArea" in argd: + iArea = argd["tbpu.ignoreArea"] + if isinstance(iArea, list) and len(iArea) > 0: + msnInfo["tbpu"].append(IgnoreArea(iArea)) + # 获取排版解析器对象 + if "tbpu.parser" in argd: + msnInfo["tbpu"].append(getParser(argd["tbpu.parser"])) + # 检查任务合法性 + for i in range(len(msnList) - 1, -1, -1): + if "path" in msnList[i]: + p = msnList[i]["path"] + if os.path.splitext(p)[-1].lower() not in ImageSuf: + logger.warning(f"添加OCR任务时,第{i}项的路径path不是图片:{p}") + del msnList[i] + elif "bytes" not in msnList[i] and "base64" not in msnList[i]: + logger.warning(f"添加OCR任务时,第{i}项不含 path、bytes、base64") + del msnList[i] + return super().addMissionList(msnInfo, msnList) + + def msnPreTask(self, msnInfo): # 用于更新api和参数 + # 检查API对象 + if not self._api: + return "[Error] MissionOCR: API object is None." + # 检查参数更新 + startInfo = self._dictShortKey(msnInfo["argd"]) + # 恢复int类型 + argdIntConvert(startInfo) + msg = self._api.start(startInfo) + if msg.startswith("[Error]"): + logger.error(f"OCR引擎启动失败: {msg}") + return msg # 更新失败,结束该队列 + else: + return "" # 更新成功 TODO: continue + + def msnTask(self, msnInfo, msn): # 执行msn + if "path" in msn: + res = self._api.runPath(msn["path"]) + res["path"] = msn["path"] # 结果字典中补充参数 + elif "bytes" in msn: + res = self._api.runBytes(msn["bytes"]) + elif "base64" in msn: + res = self._api.runBase64(msn["base64"]) + else: + res = { + "code": 901, + "data": f"[Error] Unknown task type.\n【异常】未知的任务类型。\n{str(msn)[:100]}", + } + # 任务成功时的后处理 + if res["code"] == 100: + # 计算平均置信度 + score, num = 0, 0 + for r in res["data"]: + score += r["score"] + num += 1 + if num > 0: + score /= num + res["score"] = score + # 执行 tbpu + if msnInfo["tbpu"]: + for tbpu in msnInfo["tbpu"]: + res["data"] = tbpu.run(res["data"]) + # 如果忽略区域等处理将所有文本删除,则结束tbpu + if not res["data"]: + res["code"] = 101 + res["data"] = "" + break + return res + + # ========================= 【qml接口】 ========================= + + def getStatus(self): # 返回当前状态 + return { + "apiKey": self._apiKey, + "missionListsLength": self.getMissionListsLength(), + } + + def setApi(self, apiKey, info): # 设置api + # 成功返回 [Success] ,失败返回 [Error] 开头的字符串 + self._apiKey = apiKey + info = self._dictShortKey(info) + # 如果api对象已启动,则先停止 + if self._api: + self._api.stop() + # 获取新api对象 + res = getApiOcr(apiKey, info) + # 失败 + if isinstance(res, str): + self._apiKey = "" + self._api = None + return res + # 成功 + else: + self._api = res + return "[Success]" + + # 将字典中配置项的长key转为短key + # 如: ocr.win32_PaddleOCR-json.path → path + def _dictShortKey(self, d): + newD = {} + key1 = "ocr." + key2 = key1 + self._apiKey + "." + for k in d: + if k.startswith(key2): + newD[k[len(key2) :]] = d[k] + elif k.startswith(key1): + newD[k[len(key1) :]] = d[k] + return newD + + # ========================= 【qml接口】 ========================= + + def getLocalOptions(self): + if self._apiKey: + return getLocalOptions(self._apiKey) + else: + return {} + + +# 全局 OCR任务管理器 +MissionOCR = __MissionOcrClass() diff --git a/UmiOCR-data/py_src/mission/mission_qrcode.py b/UmiOCR-data/py_src/mission/mission_qrcode.py new file mode 100644 index 0000000..5381e22 --- /dev/null +++ b/UmiOCR-data/py_src/mission/mission_qrcode.py @@ -0,0 +1,248 @@ +# ================================================= +# =============== 二维码 - 任务管理器 =============== +# ================================================= + +import base64 +from PIL import Image, ImageEnhance, ImageFilter +from io import BytesIO +from .mission import Mission + +try: + import zxingcpp +except Exception as e: + zxingcpp = None + zxingcppErr = str(e) + + +class _MissionQRCodeClass(Mission): + + def createImage(self, text, format="QRCode", w=0, h=0, quiet_zone=-1, ec_level=-1): + """ + 生成二维码图片 + format: "Aztec","Codabar","Code128","Code39","Code93","DataBar","DataBarExpanded","DataMatrix","EAN13","EAN8","ITF","LinearCodes","MatrixCodes","MaxiCode","MicroQRCode","PDF417","QRCode","UPCA","UPCE", + quiet_zone: 四周的空闲区域 + ec_level:纠错等级,-1 - 自动, 1- L-7% , 0 - M-15%, 3 - Q-25%, 2 - H-30% + 纠错仅用于Aztec、PDF417和QRCode + 返回:成功返回PIL对象,失败返回错误原因字符串 + """ + # 转整数 + w, h = round(w), round(h) + quiet_zone, ec_level = round(quiet_zone), round(ec_level) + # 生成格式对象 + bFormat = getattr(zxingcpp.BarcodeFormat, format, None) + if not bFormat: + return f"[Error] format {format} not in zxingcpp.BarcodeFormat!" + try: + bit = zxingcpp.write_barcode(bFormat, text, w, h, quiet_zone, ec_level) + except Exception as e: + return f"[Error] [{format}] {e}" + try: + img = Image.fromarray(bit, "L") + except Exception as e: + return f"[Error] Image.fromarray: {e}" + return img + + # msnInfo: { 回调函数"onXX" , 参数"argd":{ "preprocessing.xx" } } + # msnList: [ { "path", "pil", "base64" } ] + # def addMissionList(self, msnInfo, msnList): # 添加任务列表 + # return super().addMissionList(msnInfo, msnList) + def msnTask(self, msnInfo, msn): # 执行msn + # 导入库失败 + if not zxingcpp: + return { + "code": 901, + "data": f"【Error】无法导入二维码解析器 zxingcpp 。\n[Error] Unable to import zxingcpp.\n{zxingcppErr}", + } + # 读入图片 + try: + if "pil" in msn: + img = msn["pil"] + elif "path" in msn: + imgPath = msn["path"] + img = Image.open(imgPath) + elif "base64" in msn: + imgBase64 = msn["base64"] + img = Image.open(BytesIO(base64.b64decode(imgBase64))) + except Exception as e: + return { + "code": 202, + "data": f"【Error】图片读取失败。\n[Error] Image reading failed.\n {e}", + } + # 预处理 + if "argd" in msnInfo: + try: + img = self._preprocessing(img, msnInfo["argd"]) + except Exception as e: + return { + "code": 203, + "data": f"【Error】图片预处理失败。\n[Error] Image preprocessing failed.\n {e}", + } + # 二维码解析 + try: + codes = zxingcpp.read_barcodes(img) + except Exception as e: + return { + "code": 204, + "data": f"【Error】zxingcpp 二维码解析失败。\n[Error] zxingcpp read_barcodes failed.\n {e}", + } + # 结果解析 + try: + return self._zxingcpp2dict(codes) + except Exception as e: + return { + "code": 205, + "data": f"【Error】zxingcpp 结果解析失败。\n[Error] zxingcpp resule to dict failed.\n {e}", + } + + # 解析 zxingcpp 库的返回结果,转为字典。此函数允许发生异常。 + # 失败返回值: {"code":错误码, "data": "错误信息字符串"} + # 成功返回值: {"code":100, "data": [每个码的数据] } + # data: {"box":[包围盒], "score":1, "text": "文本"} + def _zxingcpp2dict(self, codes): + # 图中无码 + if not codes: + return { + "code": 101, + "data": "QR code not found in the image.", + } + # 处理结果冲每一个二维码 + data = [] + for c in codes: + if not c.valid: # 码无效 + continue + d = {} + # 方向 + d["orientation"] = c.orientation + # 位置 + d["box"] = [ + [c.position.top_left.x, c.position.top_left.y], + [c.position.top_right.x, c.position.top_right.y], + [c.position.bottom_right.x, c.position.bottom_right.y], + [c.position.bottom_left.x, c.position.bottom_left.y], + ] + d["score"] = 1 # 置信度,兼容OCR格式,无意义 + d["end"] = "\n" # 间隔符,兼容OCR格式,无意义 + # 格式 + d["format"] = c.format.name + # 内容为文本类型 + if c.content_type.name == "Text": + d["text"] = c.text + # 内容为其它格式 + # TODO: 现在是通用的处理方法,将二进制内容转为纯文本或base64 + # 或许对于某些格式的码,有更好的转文本方式 + else: + text = f"type: {c.content_type.name}\ndata: " + try: + # 尝试将 bytes 转换为纯文本字符串 + t = c.bytes.decode("utf-8") + text += t + except UnicodeDecodeError: + # 如果无法直接转换为纯文本,则使用 Base64 编码输出结果 + t = base64.b64encode(c.bytes) + text += "[Base64]\n" + t.decode("utf-8") + d["text"] = text + data.append(d) + if data: + return { + "code": 100, + "data": data, + } + else: + l = len(codes) + return { + "code": 102, + "data": f"【Error】{l}组二维码解码失败。\nFailed to decode {l} sets of QR codes.", + } + + # 图像预处理 + def _preprocessing(self, img, argd): + """ + 对图像进行预处理,包括中值滤波、锐度增强、对比度增强以及可选的灰度转换和二值化。 + + img: PIL.Image对象,待处理的图像。 + argd: 参数字典,包含以下可选键值对: + - "preprocessing.median_filter_size": 中值滤波器的大小,应为1~9的奇数。默认不进行。 + - "preprocessing.sharpness_factor": 锐度增强因子,应为0.1~10。默认不调整锐度。 + - "preprocessing.contrast_factor": 对比度增强因子,应为0.1~10。大于1增强对比度,小于1但大于0减少对比度,1保持原样。默认不调整对比度。 + - "preprocessing.grayscale": 布尔值,指定是否将图像转换为灰度图像。True为转换,False为不转换。默认为False。 + - "preprocessing.threshold": 二值化阈值,用于灰度图像的二值化处理。应为0到255之间的整数。只有当"preprocessing.grayscale"为True时,此参数才生效。默认不进行二值化处理。 + + 返回: + 处理后的PIL.Image对象。 + """ + + # 中值滤波 + s = round(argd.get("preprocessing.median_filter_size", -100)) + if s > 0 and s % 2 == 1: + img = img.filter(ImageFilter.MedianFilter(size=s)) + # 锐度增强 + f = round(argd.get("preprocessing.sharpness_factor", -100)) + if f > 0: + img = ImageEnhance.Sharpness(img).enhance(f) + # 对比度增强 + c = argd.get("preprocessing.contrast_factor", -100) + if c > 0: + img = ImageEnhance.Contrast(img).enhance(c) + # 转为灰度 & 二值化 + if argd.get("preprocessing.grayscale", False): + img = img.convert("L") + t = round(argd.get("preprocessing.threshold", -100)) + if t > -1: + img = img.point(lambda p: 255 if p > t else 0) + return img + + +MissionQRCode = _MissionQRCodeClass() + + +""" +zxingcpp 返回值类型 +# 字节 +bytes : b'testtesttesttest' +# 内容类型 +content_type : ContentType.Text + name : Text + value : 0 + Binary : ContentType.Binary + GS1 : ContentType.GS1 + ISO15434 : ContentType.ISO15434 + Mixed : ContentType.Mixed + Text : ContentType.Text + UnknownECI : ContentType.UnknownECI +ec_level : M +# 二维码格式 +format : BarcodeFormat.QRCode + name : QRCode + value : 8192 + Aztec : BarcodeFormat.Aztec + Codabar : BarcodeFormat.Codabar + Code128 : BarcodeFormat.Code128 + Code39 : BarcodeFormat.Code39 + Code93 : BarcodeFormat.Code93 + DataBar : BarcodeFormat.DataBar + DataBarExpanded : BarcodeFormat.DataBarExpanded + DataMatrix : BarcodeFormat.DataMatrix + EAN13 : BarcodeFormat.EAN13 + EAN8 : BarcodeFormat.EAN8 + ITF : BarcodeFormat.ITF + LinearCodes : BarcodeFormat.LinearCodes + MatrixCodes : BarcodeFormat.MatrixCodes + MaxiCode : BarcodeFormat.MaxiCode + MicroQRCode : BarcodeFormat.MicroQRCode + NONE : BarcodeFormat.NONE + PDF417 : BarcodeFormat.PDF417 + QRCode : BarcodeFormat.QRCode + UPCA : BarcodeFormat.UPCA + UPCE : BarcodeFormat.UPCE +# 方向 +orientation : 0 +# 位置 +position : 16x16 116x16 116x116 16x116 + bottom_left : + bottom_right : + top_left : + top_right : +symbology_identifier : ]Q1 +text : testtesttesttest +valid : True +""" diff --git a/UmiOCR-data/py_src/mission/mission_queue.py b/UmiOCR-data/py_src/mission/mission_queue.py new file mode 100644 index 0000000..fcdd3ec --- /dev/null +++ b/UmiOCR-data/py_src/mission/mission_queue.py @@ -0,0 +1,99 @@ +# 任务队列类 + +from uuid import uuid4 +from typing import Callable, Any + +from umi_log import logger + + +class MissionQueue: + def __init__( + self, + msnList: list, # 任务内容列表,每项为一个任务元素 + configs: dict = {}, # 任务控制参数 + # 回调函数 + # 整个队列 即将启动。回调参数(self) + onStart: Callable[["MissionQueue"], None] = None, + # 单个任务 准备进行。回调参数(self, 任务元素) + onReady: Callable[["MissionQueue", Any], None] = None, + # 单个任务 完成。回调参数(self, 任务元素, 任务结果) + onGet: Callable[["MissionQueue"], None] = None, + # 整个队列 已结束。回调参数(self, 结果标志) + # 结果标志:"[Success]...", "[Warning]...", "[Error]..." + onEnd: Callable[["MissionQueue", str], None] = None, + ): + self.msnList = msnList + self.configs = configs + self.id = str(uuid4()) # 任务ID + self.len = len(msnList) # 队列总长度 + self.index = 0 # 当前任务下标 + self._onStart = onStart if onStart else self._pass + self._onReady = onReady if onReady else self._pass + self._onGet = onGet if onGet else self._pass + self._onEnd = onEnd if onEnd else self._pass + # 工作状态: "waiting" "running" + self.state_work = "waiting" + # 控制状态: "" "pause" "stop" + self.state_ctrl = "" + + # ==================== mission调用 ==================== + + # 获取剩余任务长度 + def remainingLen(self): + return self.len - self.index + + # 判空 【1】←在一轮任务循环中的调用顺序 + def empty(self): + return self.index >= self.len + + # 获取一个任务元素 【2】 + def getMsn(self): + return self.msnList[self.index] + + # 完成一个任务元素 【5】 + def popMsn(self): + self.index += 1 + + # 恢复进行 + def resume(self): + if self.state_ctrl == "pause": + self.state_ctrl = "" + + # 暂停 + def pause(self): + self.state_ctrl = "pause" + + # 停止(废弃) + def stop(self): + self.state_ctrl = "stop" + + # ==================== 回调函数 ==================== + + def onStart(self): + try: + self._onStart(self) + except Exception: + logger.error("任务 onStart 回调异常。", exc_info=True, stack_info=True) + + def onReady(self): # 【3】 + try: + self._onReady(self) + except Exception: + logger.error("任务 onReady 回调异常。", exc_info=True, stack_info=True) + + def onGet(self, res): # 【4】 + try: + self._onGet(self, res) + except Exception: + logger.error("任务 onGet 回调异常。", exc_info=True, stack_info=True) + + def onEnd(self, flag): + # 结果标志:"[Success]...", "[Warning]...", "[Error]..." + try: + self._onEnd(self, flag) + except Exception: + logger.error("任务 onEnd 回调异常。", exc_info=True, stack_info=True) + + @staticmethod + def _pass(*e): # 空函数 + pass diff --git a/UmiOCR-data/py_src/mission/simple_mission.py b/UmiOCR-data/py_src/mission/simple_mission.py new file mode 100644 index 0000000..07f70b7 --- /dev/null +++ b/UmiOCR-data/py_src/mission/simple_mission.py @@ -0,0 +1,56 @@ +# ============================================= +# =============== 简单任务管理器 =============== +# ============================================= + + +from PySide2.QtCore import QMutex +from ..utils.thread_pool import threadRun # 异步执行函数 + + +class SimpleMission: + def __init__(self, msnTask): + self._msnTask = msnTask # 任务函数 + self._msnList = [] # 任务队列 + self._msnMutex = QMutex() # 任务队列的锁 + self._task = None # 异步任务对象 + self._taskMutex = QMutex() # 任务对象的锁 + + def addMissionList(self, msnList): # 添加一条任务队列,返回任务ID + if len(msnList) < 1: + return "[Error] len(msnList) < 1 !" + # 添加到任务队列 + self._msnMutex.lock() # 上锁 + self._msnList += msnList + self._msnMutex.unlock() # 解锁 + # 启动任务 + self._startMsns() + return "[Success]" + + def _startMsns(self): # 启动异步任务,执行所有任务列表 + # 若当前异步任务对象为空,则创建工作线程 + self._taskMutex.lock() # 上锁 + if self._task == None: + self._task = threadRun(self._taskRun) + self._taskMutex.unlock() # 解锁 + + # ========================= 【子线程 方法】 ========================= + + def _taskRun(self): # 异步执行任务字典的流程 + # 循环,直到任务队列的列表为空 + while True: + self._msnMutex.lock() # 上锁 + dl = len(self._msnList) # 任务字典长度 + if dl == 0: # 任务队列已空 + self._msnMutex.unlock() # 解锁 + break + msn = self._msnList.pop(0) # 取一个任务 + self._msnMutex.unlock() # 解锁 + if callable(self._msnTask): + self._msnTask(msn) + # 完成 + self._taskFinish() + + def _taskFinish(self): # 任务结束 + self._taskMutex.lock() # 上锁 + self._task = None + self._taskMutex.unlock() # 解锁 diff --git a/UmiOCR-data/py_src/ocr/api/__init__.py b/UmiOCR-data/py_src/ocr/api/__init__.py new file mode 100644 index 0000000..c97028c --- /dev/null +++ b/UmiOCR-data/py_src/ocr/api/__init__.py @@ -0,0 +1,42 @@ +# =============================================== +# =============== OCR 插件接口管理 =============== +# =============================================== + +from umi_log import logger + +ApiDict = {} +AllDict = {} + + +# TODO: 静态插件 +# 由插件控制器调用,初始化OCR插件的接口。传入动态插件 +def initOcrPlugins(plugins): + global ApiDict, AllDict + for p in plugins: + ApiDict[p] = plugins[p]["api_class"] + AllDict[p] = plugins[p] + + +# 生成一个ocr api实例,成功返回对象,失败返回 [Error] 开头的字符串 +def getApiOcr(apiKey, argd): + # 检测argd,恢复int类型 + for k in argd: + n = argd[k] + if isinstance(n, float): + rounded = round(n) + if abs(n - rounded) <= 1e-7: + argd[k] = rounded + if apiKey in ApiDict: + try: + return ApiDict[apiKey](argd) # 实例化后返回 + except Exception as e: + logger.error(f"生成api实例{apiKey}失败。", exc_info=True, stack_info=True) + return f"[Error] Failed to generate API instance {apiKey}: {e}" + return f'[Error] "{apiKey}" not in ApiDict.' + + +# 返回一个API的局部配置字典 +def getLocalOptions(apiKey): + if apiKey in AllDict: + return AllDict[apiKey]["local_options"] + return {} diff --git a/UmiOCR-data/py_src/ocr/output/__init__.py b/UmiOCR-data/py_src/ocr/output/__init__.py new file mode 100644 index 0000000..9541adf --- /dev/null +++ b/UmiOCR-data/py_src/ocr/output/__init__.py @@ -0,0 +1,30 @@ +from .output_txt import OutputTxt +from .output_txt_plain import OutputTxtPlain +from .output_txt_individual import OutputTxtIndividual +from .output_md import OutputMD +from .output_jsonl import OutputJsonl +from .output_csv import OutputCsv +from .output_pdf_layered import OutputPdfLayered +from .output_pdf_one_layer import OutputPdfOneLayer + +"""纯文本输出器。初始化传入参数字典: + outputArgd = { + "outputDir": "", # 输出路径 + "outputDirType": "", # 输出目录类型,"source" 为原文件目录,"specify"为指定目录 + "outputFileName": "", # 输出文件名(前缀) + "startDatetime": "", # 开始日期字符串(标准格式) + "ignoreBlank": True/False, # 忽略空白文件 + } +""" +Output = { + # 纯文本输出器 + "txt": OutputTxt, + "txtPlain": OutputTxtPlain, + "txtIndividual": OutputTxtIndividual, + "md": OutputMD, + "jsonl": OutputJsonl, + "csv": OutputCsv, + # PDF输出器,需要额外的参数 "originPath" 原始文件路径 + "pdfLayered": OutputPdfLayered, + "pdfOneLayer": OutputPdfOneLayer, +} diff --git a/UmiOCR-data/py_src/ocr/output/output.py b/UmiOCR-data/py_src/ocr/output/output.py new file mode 100644 index 0000000..bb361dd --- /dev/null +++ b/UmiOCR-data/py_src/ocr/output/output.py @@ -0,0 +1,32 @@ +# OCR输出器的基类。按指定的格式,将传入的文本输出到指定地方。 + +from .tools import getDataText +from ...platform import Platform +import os + + +class Output: + def __init__(self, argd): + self.dir = argd["outputDir"] # 输出路径(文件夹) + self.fileName = argd["outputFileName"] # 文件名 + self.outputPath = f"{self.dir}/{self.fileName}.txt" # 输出路径 + self.ignoreBlank = argd["ignoreBlank"] # 忽略空白文件 + + def print(self, res): # 输出图片信息 + if not res["code"] == 100 and self.ignoreBlank: + return # 忽略空白图片 + textOut = f"图片路径:{res['path']}\n代码:{res['code']}\n" + if res["code"] == 100: + textOut += getDataText(res["data"]) # 获取拼接结果 + elif res["code"] == 101: + textOut += "无文字" + else: + textOut += f"错误原因:{res['data']}" + print(textOut) + + def openOutputFile(self): # 打开输出文件 + if self.outputPath and os.path.exists(self.outputPath): + Platform.startfile(self.outputPath) + + def onEnd(self): # 结束输出。 + pass diff --git a/UmiOCR-data/py_src/ocr/output/output_csv.py b/UmiOCR-data/py_src/ocr/output/output_csv.py new file mode 100644 index 0000000..3bda719 --- /dev/null +++ b/UmiOCR-data/py_src/ocr/output/output_csv.py @@ -0,0 +1,70 @@ +# 输出到csv表格文件 + +import csv + +from umi_log import logger +from .output import Output +from .tools import getDataText + + +class OutputCsv(Output): + def __init__(self, argd): + self.encodings = [ # 保存编码优先级 + "ansi", # Windows系统本地编码。在linux和macos下会抛出异常 + "ascii", # 纯英 + "gbk", # 简中 + "big5", # 繁中 + "shift_jis", # 日文 + "euc-kr", # 韩文 + "utf-8", + ] + self.dir = argd["outputDir"] # 输出路径(文件夹) + self.fileName = argd["outputFileName"] # 文件名 + self.outputPath = f"{self.dir}/{self.fileName}.csv" # 输出路径 + self.ignoreBlank = argd["ignoreBlank"] # 忽略空白文件 + self.writeLists = [] # 输出内容列表 + self.writeText = "" # 输出内容字符串 + try: # 覆盖创建临时文件 + with open(self.outputPath, "w", encoding="utf-8") as f: + pass + except Exception as e: + raise Exception(f"Failed to create csv file. {e}\n创建csv文件失败。") + + def print(self, res): # 输出图片结果 + if not res["code"] == 100 and self.ignoreBlank: + return # 忽略空白图片 + name = res["fileName"] + path = res["path"] + if res["code"] == 100: + textOut = getDataText(res["data"]) # 获取拼接结果 + elif res["code"] == 101: + textOut = "" + else: + textOut = f'[Error] OCR failed. Code: {res["code"]}, Msg: {res["data"]} .\n' + self.writeLists.append([name, textOut, path]) + self.writeText += textOut + + def onEnd(self): # 结束时保存。 + # 顺序测试编码优先级列表,获取保存编码 + encoding = "utf-8" + for e in self.encodings: + try: + self.writeText.encode(e) + encoding = e + break + # except UnicodeEncodeError: + except Exception: + pass + logger.info(f"csv encoding: {encoding}") + # 创建文件、输出 + headers = ["Name", "OCR", "Path"] # 表头 + try: + with open( + self.outputPath, "w", encoding=encoding, newline="" + ) as f: # 覆盖创建文件 + writer = csv.writer(f) + writer.writerow(headers) # 写入CSV表头 + for writeList in self.writeLists: + writer.writerow(writeList) # 写入CSV内容 + except Exception as e: + raise Exception(f"Failed to write csv file. {e}\n写入csv文件失败。") diff --git a/UmiOCR-data/py_src/ocr/output/output_jsonl.py b/UmiOCR-data/py_src/ocr/output/output_jsonl.py new file mode 100644 index 0000000..6fc6487 --- /dev/null +++ b/UmiOCR-data/py_src/ocr/output/output_jsonl.py @@ -0,0 +1,24 @@ +# 输出到jsonl文件 + +from .output import Output + +import json + + +class OutputJsonl(Output): + def __init__(self, argd): + self.dir = argd["outputDir"] # 输出路径(文件夹) + self.fileName = argd["outputFileName"] # 文件名 + self.outputPath = f"{self.dir}/{self.fileName}.jsonl" # 输出路径 + self.ignoreBlank = argd["ignoreBlank"] # 忽略空白文件 + # 创建输出文件 + try: + with open(self.outputPath, "w", encoding="utf-8") as f: # 覆盖创建文件 + pass + except Exception as e: + raise Exception(f"Failed to create jsonl file. {e}\n创建jsonl文件失败。") + + def print(self, res): # 输出图片结果 + # 不忽略空白条目 + with open(self.outputPath, "a", encoding="utf-8") as f: # 追加写入本地文件 + f.write(json.dumps(res, ensure_ascii=False) + "\n") diff --git a/UmiOCR-data/py_src/ocr/output/output_md.py b/UmiOCR-data/py_src/ocr/output/output_md.py new file mode 100644 index 0000000..ff05d16 --- /dev/null +++ b/UmiOCR-data/py_src/ocr/output/output_md.py @@ -0,0 +1,46 @@ +# 输出markdown格式 + +from .output import Output +from .tools import getDataText + +import os + + +class OutputMD(Output): + def __init__(self, argd): + self.dir = argd["outputDir"] # 输出路径(文件夹) + self.fileName = argd["outputFileName"] # 文件名 + self.outputPath = f"{self.dir}/{self.fileName}.md" # 输出路径 + self.ignoreBlank = argd["ignoreBlank"] # 忽略空白文件 + # 创建输出文件 + try: + with open(self.outputPath, "w", encoding="utf-8") as f: # 覆盖创建文件 + f.write(f'> {argd["startDatetime"]}\n\n') + except Exception as e: + raise Exception(f"Failed to create jsonl file. {e}\n创建jsonl文件失败。") + + def print(self, res): # 输出图片结果 + if not res["code"] == 100 and self.ignoreBlank: + return # 忽略空白图片 + name = res["fileName"] + path = os.path.relpath( # 从md文件到图片的相对路径 + res["path"], os.path.dirname(self.outputPath) + ) + path = path.replace(" ", "%20") # 空格转 %20 + textOut = f""" +--- +![{name}]({path}) +[{name}]({path}) + +""" + # 正文 + if res["code"] == 100: + texts = getDataText(res["data"]).split("\n") # 获取拼接结果列表 + for t in texts: + textOut += f"> {t} \n" + elif res["code"] == 101: + pass + else: + textOut += f'> [Error] OCR failed. Code: {res["code"]}, Msg: {res["data"]} \n> 【异常】OCR识别失败。 \n' + with open(self.outputPath, "a", encoding="utf-8") as f: # 追加写入本地文件 + f.write(textOut) diff --git a/UmiOCR-data/py_src/ocr/output/output_pdf_layered.py b/UmiOCR-data/py_src/ocr/output/output_pdf_layered.py new file mode 100644 index 0000000..882f51a --- /dev/null +++ b/UmiOCR-data/py_src/ocr/output/output_pdf_layered.py @@ -0,0 +1,171 @@ +# 双层可搜索 searchable pdf +# https://github.com/pymupdf/PyMuPDF/discussions/2299 + +import os +import fitz # PyMuPDF + +from umi_log import logger +from .output import Output + + +class OutputPdfLayered(Output): + def __init__(self, argd): + self.dir = argd["outputDir"] # 输出路径(文件夹) + self.originPath = argd["originPath"] # 原始文件路径 + self.fileName = argd["outputFileName"] # 文件名 + self.password = argd["password"] # 密码 + self.outputPath = f"{self.dir}/{self.fileName}.layered.pdf" # 输出路径 + self.pdf = None + self.existentPages = [] # 已处理的页数 + self.isInsertFont = False # 是否有字体嵌入 + self.opacity = 0 # 文本透明度为0 + try: + self.font = fitz.Font("cjk") # 字体 + except Exception as e: + raise Exception(f"Failed to load cjk font. {e}\n无法加载cjk字体。") + try: + self.pdf = self._getPDF(self.originPath) # 加载pymupdf对象 + except Exception as e: + raise Exception( + f"Failed to load doc file. {e}\n无法加载文档。\n{self.originPath}" + ) + + # 获取pdf文档对象,或将其它类型的文档转为PDF对象 + def _getPDF(self, path): + # https://github.com/pymupdf/PyMuPDF-Utilities/blob/master/examples/convert-document/convert.py + doc = fitz.open(path) + # 如果已加密,则尝试解密 + if doc.is_encrypted and not doc.authenticate(self.password): + raise Exception( + f'The document is encrypted, and the password "{self.password}" is incorrect.\n文档已加密,输入密码不正确。' + ) + if doc.is_pdf: + return doc + b = doc.convert_to_pdf() # 转换为PDF格式的二进制数据 + pdf = fitz.open("pdf", b) # 创建PDF文档对象 + try: + pdf.set_toc(doc.get_toc()) # 复制原始文档的目录 + except Exception: + logger.warning("pdf.set_toc error", exc_info=True, stack_info=True) + # 复制原始文档的元数据(如作者、标题等) + meta = doc.metadata + if not meta["producer"]: + meta["producer"] = "Umi-OCR & PyMuPDF v" + fitz.VersionBind + if not meta["creator"]: + meta["creator"] = "Umi-OCR & PyMuPDF PDF converter" + pdf.set_metadata(meta) + # 复制原始文档的链接 + for pinput in doc: + links = pinput.get_links() + pout = pdf[pinput.number] + for link in links: + if link["kind"] == fitz.LINK_NAMED: # 不处理 named links + continue + pout.insert_link(link) # 写入新文档 + doc.close() # 释放原文档 + return pdf + + # 计算填满宽和高的一行字体大小 + def _calculateFontSize(self, text, w, h): + if h > w: # 竖排转为横排计算 + w, h = h, w + fontsize = round(h) # 字体大小初值,假设为行高 + minSize = 5 # 大小下限 + getLen = lambda text, s: self.font.text_length(text, fontsize=s) + while getLen(text, fontsize) > w and fontsize >= minSize: + fontsize -= 1 # 尝试减小字体,直到行宽刚好小于界限 + while getLen(text, fontsize) < w: + fontsize += 1 # 尝试增大字体,直到行宽刚好超过界限 + while getLen(text, fontsize) > w and fontsize >= minSize: + fontsize -= 0.1 # 再次减小字体,将精度提升到 0.1 + return fontsize + + def print(self, res): # 输出图片结果 + if not self.pdf: + logger.error("self.pdf 未初始化。") + return + pno = res["page"] - 1 # 当前页数 + self.existentPages.append(pno) # 记录已处理的页面 + if not res["code"] == 100: + return # 忽略空白 + + page = self.pdf[pno] # 当前页对象 + page.clean_contents() # 内容流清理、语法更正,减少错误 + protation = page.rotation # 获取页面旋转角度 + isInsertFont = False # 当前是否进行过字体注入 + # 插入文本,用shape.insert_text(可编辑)或page.insert_text(不可编辑) + for tb in res["data"]: + if self.opacity == 0 and "from" in tb and tb["from"] == "text": + continue # 双层(透明文字)模式下,跳过直接提取的文本,只写入OCR文本 + if not isInsertFont: # 页面插入字体 + self.isInsertFont = isInsertFont = True + page.insert_font(fontname="cjk", fontbuffer=self.font.buffer) + text = tb["text"] + box = tb["box"] + x0, y0 = box[0] + x2, y2 = box[2] + w = x2 - x0 + h = y2 - y0 + fontsize = self._calculateFontSize(text, w, h) + # 插入点的 旋转后的坐标 + point = fitz.Point(x0, y2) * page.derotation_matrix + page.insert_text( + point, + text, + fontsize, + fontname="cjk", + rotate=protation, # 文本角度设定 + stroke_opacity=self.opacity, # 描边透明度 + fill_opacity=self.opacity, # 填充(字体)透明度 + ) + + def onEnd(self): # 结束时保存。 + if not self.pdf: + return + # 删除未处理的页数 + for i in range(len(self.pdf) - 1, -1, -1): + if i not in self.existentPages: + self.pdf.delete_page(i) + logger.info(f"保存{len(self.pdf)}页PDF:{self.outputPath}") + if self.isInsertFont: # 有任意页面嵌入字体,则构建字体子集 + try: # 对于部分PDF,如用txt直接打印的,构建字体子集会失败。 + self.pdf.subset_fonts() # 构建字体子集,减小文件大小。需要 fontTools 库 + except Exception: # TODO: 失败原因?可能文件中实际并没有字体? + logger.error("构建字体子集失败。", exc_info=True, stack_info=True) + # 保存:压缩并进行3级垃圾回收。等同 ez_save + self.save(self.pdf, self.outputPath, deflate=True, garbage=3) + else: + # 无嵌入字体,则直接保存,不压缩 + self.save(self.pdf, self.outputPath) + + def save(self, pdf, path, **options): # 保存并关闭 pdf 对象 + try: + # 尝试保存到指定路径 + pdf.save(path, **options) + except Exception: + # 保存失败,尝试保存到 ".temp" 路径 + tempPath = self.outputPath + ".temp" + logger.warning(f"保存PDF失败。 path: {path}", exc_info=True) + try: + pdf.save(tempPath, **options) + pdf.close() + except Exception as e1: + logger.error( + f"保存PDF到临时路径失败。 tempPath: {tempPath}", exc_info=True + ) + raise Exception(f"[Error] Unable to save PDF to [{tempPath}]: {e1}") + # 已保存到 .temp 并 close 原对象,尝试替换文件 + try: + if os.path.exists(path): + os.remove(path) + os.rename(tempPath, path) + except Exception as e2: + logger.warning( + f"保存PDF文件替换失败。保存到临时文件: {tempPath}", exc_info=True + ) + + raise Exception( + f"[Warning] Unable to save PDF: [{path}]. Exception: {e2}. Saved to temporary path: [{tempPath}]." + ) + else: # 正常结束 + pdf.close() diff --git a/UmiOCR-data/py_src/ocr/output/output_pdf_one_layer.py b/UmiOCR-data/py_src/ocr/output/output_pdf_one_layer.py new file mode 100644 index 0000000..48a2b97 --- /dev/null +++ b/UmiOCR-data/py_src/ocr/output/output_pdf_one_layer.py @@ -0,0 +1,43 @@ +# 单层纯文本 PDF + +import fitz # PyMuPDF + +from umi_log import logger +from .output_pdf_layered import OutputPdfLayered + + +class OutputPdfOneLayer(OutputPdfLayered): + def __init__(self, argd): + super().__init__(argd) + self.opacity = 1 # 文本不透明 + self.outputPath = f"{self.dir}/{self.fileName}.text.pdf" # 输出路径 + + # 创建空白 PDF + def _getPDF(self, path): + source_doc = fitz.open(path) # 打开原文档 + # 如果已加密,则尝试解密 + if source_doc.is_encrypted and not source_doc.authenticate(self.password): + raise Exception( + f'The document is encrypted, and the password "{self.password}" is incorrect.\n文档已加密,输入密码不正确。' + ) + pdf = fitz.open() # 创建空白PDF文档对象 + # 复制原始文档的元数据(如作者、标题等) + meta = source_doc.metadata + if not meta["producer"]: + meta["producer"] = "Umi-OCR & PyMuPDF v" + fitz.VersionBind + if not meta["creator"]: + meta["creator"] = "Umi-OCR & PyMuPDF PDF converter" + pdf.set_metadata(meta) + # 生成空白的每一页 + for page in source_doc: + rect = page.rect # 原文档渲染尺寸 + pdf.new_page(width=rect.width, height=rect.height) + # 尝试复制原始文档的目录数据 + try: + pdf.set_toc(source_doc.get_toc()) + except Exception: + logger.warning( + f"pdf.set_toc error. path: {path}", exc_info=True, stack_info=True + ) + source_doc.close() # 释放原文档 + return pdf diff --git a/UmiOCR-data/py_src/ocr/output/output_txt.py b/UmiOCR-data/py_src/ocr/output/output_txt.py new file mode 100644 index 0000000..8da460e --- /dev/null +++ b/UmiOCR-data/py_src/ocr/output/output_txt.py @@ -0,0 +1,33 @@ +# 输出到txt文件 + +from .output import Output +from .tools import getDataText + + +class OutputTxt(Output): + def __init__(self, argd): + self.dir = argd["outputDir"] # 输出路径(文件夹) + self.fileName = argd["outputFileName"] # 文件名 + self.outputPath = f"{self.dir}/{self.fileName}.txt" # 输出路径 + self.ignoreBlank = argd["ignoreBlank"] # 忽略空白文件 + # 创建输出文件 + try: + with open(self.outputPath, "w", encoding="utf-8") as f: # 覆盖创建文件 + f.write(f'{argd["startDatetime"]}\n\n') # 写入开始时间日期 + except Exception as e: + raise Exception(f"Failed to create txt file. {e}\n创建txt文件失败。") + + def print(self, res): # 输出图片结果 + if not res["code"] == 100 and self.ignoreBlank: + return # 忽略空白图片 + textOut = f'≦ {res["fileName"]} ≧\n' + if res["code"] == 100: + textOut += getDataText(res["data"]) # 获取拼接结果 + textOut += "\n" # 结尾额外加换行 + elif res["code"] == 101: + pass + else: + textOut += f'[Error] OCR failed. Code: {res["code"]}, Msg: {res["data"]}\n【异常】OCR识别失败。\n' + textOut += "\n" # 多空一行 + with open(self.outputPath, "a", encoding="utf-8") as f: # 追加写入本地文件 + f.write(textOut) diff --git a/UmiOCR-data/py_src/ocr/output/output_txt_individual.py b/UmiOCR-data/py_src/ocr/output/output_txt_individual.py new file mode 100644 index 0000000..2c42b2e --- /dev/null +++ b/UmiOCR-data/py_src/ocr/output/output_txt_individual.py @@ -0,0 +1,35 @@ +# 单独txt文件 + +import os +from .output import Output +from .tools import getDataText + + +class OutputTxtIndividual(Output): + def __init__(self, argd): + super().__init__(argd) + # 是否输出到原目录 + self.outputSource = argd["outputDirType"] == "source" + + def openOutputFile(self): + pass # 覆盖父类方法 + + def print(self, res): # 输出图片结果 + if not res["code"] == 100 and self.ignoreBlank: + return # 忽略空白图片 + textOut = "" + if res["code"] == 100: + textOut += getDataText(res["data"]) # 获取拼接结果 + elif res["code"] == 101: + pass + else: + textOut += f'[Error] OCR failed. Code: {res["code"]}, Msg: {res["data"]}\n【异常】OCR识别失败。\n' + # 输出文件 + if self.outputSource: # 输出到原始路径 + p, _ = os.path.splitext(res["path"]) # 原路径去除扩展名 + path = p + ".txt" + else: # 输出到指定路径 + f, _ = os.path.splitext(res["fileName"]) # 原文件名去除扩展名 + path = f"{self.dir}/{f}.txt" + with open(path, "w", encoding="utf-8") as f: # 追加写入同名本地文件 + f.write(textOut) diff --git a/UmiOCR-data/py_src/ocr/output/output_txt_plain.py b/UmiOCR-data/py_src/ocr/output/output_txt_plain.py new file mode 100644 index 0000000..3258e30 --- /dev/null +++ b/UmiOCR-data/py_src/ocr/output/output_txt_plain.py @@ -0,0 +1,29 @@ +# 纯文本(无格式)txt文件 + +from .output import Output +from .tools import getDataText + + +class OutputTxtPlain(Output): + def __init__(self, argd): + self.dir = argd["outputDir"] # 输出路径(文件夹) + self.fileName = argd["outputFileName"] # 文件名 + self.outputPath = f"{self.dir}/{self.fileName}.p.txt" # 输出路径 + # 创建输出文件 + try: + open(self.outputPath, "w").close() # 覆盖创建文件 + except Exception as e: + raise Exception( + f"Failed to create plain txt file. {e}\n创建纯文本txt文件失败。" + ) + + def print(self, res): # 输出图片结果 + if not res["code"] == 100: + return # 强制忽略空白图片 + textOut = "" + if res["code"] == 100: + textOut += getDataText(res["data"]) # 获取拼接结果 + if not textOut[-1] == "\n": # 确保结尾有换行 + textOut += "\n" + with open(self.outputPath, "a", encoding="utf-8") as f: # 追加写入本地文件 + f.write(textOut) diff --git a/UmiOCR-data/py_src/ocr/output/tools.py b/UmiOCR-data/py_src/ocr/output/tools.py new file mode 100644 index 0000000..757465b --- /dev/null +++ b/UmiOCR-data/py_src/ocr/output/tools.py @@ -0,0 +1,9 @@ +# 从data中提取、拼接文本 +def getDataText(data): + textOut = "" + l = len(data) - 1 + for i, tb in enumerate(data): + textOut += tb["text"] + if i < l: + textOut += tb["end"] + return textOut diff --git a/UmiOCR-data/py_src/ocr/tbpu/__init__.py b/UmiOCR-data/py_src/ocr/tbpu/__init__.py new file mode 100644 index 0000000..2e9add2 --- /dev/null +++ b/UmiOCR-data/py_src/ocr/tbpu/__init__.py @@ -0,0 +1,32 @@ +# tbpu : text block processing unit 文本块后处理 + +from .parser_none import ParserNone + +from .ignore_area import IgnoreArea +from .parser_multi_para import MultiPara +from .parser_multi_line import MultiLine +from .parser_multi_none import MultiNone +from .parser_single_para import SinglePara +from .parser_single_line import SingleLine +from .parser_single_none import SingleNone +from .parser_single_code import SingleCode + +# 排版解析 +Parser = { + "none": ParserNone, # 不做处理 + "multi_para": MultiPara, # 多栏-自然段 + "multi_line": MultiLine, # 多栏-总是换行 + "multi_none": MultiNone, # 多栏-无换行 + "single_para": SinglePara, # 单栏-自然段 + "single_line": SingleLine, # 单栏-总是换行 + "single_none": SingleNone, # 单栏-无换行 + "single_code": SingleCode, # 单栏-代码段 +} + + +# 获取排版解析器对象 +def getParser(key): + if key in Parser: + return Parser[key]() + else: + return Parser["none"]() diff --git a/UmiOCR-data/py_src/ocr/tbpu/ignore_area.py b/UmiOCR-data/py_src/ocr/tbpu/ignore_area.py new file mode 100644 index 0000000..ac865cd --- /dev/null +++ b/UmiOCR-data/py_src/ocr/tbpu/ignore_area.py @@ -0,0 +1,32 @@ +# 忽略区域 + +from .tbpu import Tbpu + + +class IgnoreArea(Tbpu): + def __init__(self, areaList): + self.tbpuName = "忽略区域" + self.areaList = areaList + + def run(self, textBlocks): + # 返回是否矩形框 a 包含 b + def isInBox(a, b): + return ( + a[0][0] <= b[0][0] + and a[0][1] <= b[0][1] + and a[2][0] >= b[2][0] + and a[2][1] >= b[2][1] + ) + + newList = [] + for b in textBlocks: + flag = True # True 为没有被忽略 + # 检测当前文块 b 是否在任何一个检测块 a 内 + for a in self.areaList: + if isInBox(a, b["box"]): + flag = False # 踩到任何一个块,GG + break + if flag: # 没有被忽略 + newList.append(b) + + return newList diff --git a/UmiOCR-data/py_src/ocr/tbpu/parser_multi_line.py b/UmiOCR-data/py_src/ocr/tbpu/parser_multi_line.py new file mode 100644 index 0000000..0b0aeb0 --- /dev/null +++ b/UmiOCR-data/py_src/ocr/tbpu/parser_multi_line.py @@ -0,0 +1,22 @@ +# 排版解析-多栏-单行 + +from .tbpu import Tbpu +from .parser_tools.line_preprocessing import linePreprocessing # 行预处理 +from .parser_tools.gap_tree import GapTree # 间隙树排序算法 + + +class MultiLine(Tbpu): + def __init__(self): + self.tbpuName = "排版解析-多栏-单行" + + # 构建算法对象,指定包围盒的元素位置 + self.gtree = GapTree(lambda tb: tb["normalized_bbox"]) + + def run(self, textBlocks): + textBlocks = linePreprocessing(textBlocks) # 预处理 + textBlocks = self.gtree.sort(textBlocks) # 构建间隙树 + # 补充行尾间隔符 + for tb in textBlocks: + tb["end"] = "\n" + del tb["normalized_bbox"] + return textBlocks diff --git a/UmiOCR-data/py_src/ocr/tbpu/parser_multi_none.py b/UmiOCR-data/py_src/ocr/tbpu/parser_multi_none.py new file mode 100644 index 0000000..e65e358 --- /dev/null +++ b/UmiOCR-data/py_src/ocr/tbpu/parser_multi_none.py @@ -0,0 +1,29 @@ +# 排版解析-多栏-无换行 + +from .tbpu import Tbpu +from .parser_tools.line_preprocessing import linePreprocessing # 行预处理 +from .parser_tools.gap_tree import GapTree # 间隙树排序算法 +from .parser_tools.paragraph_parse import word_separator # 上下句间隔符 + + +class MultiNone(Tbpu): + def __init__(self): + self.tbpuName = "排版解析-多栏-无换行" + + # 构建算法对象,指定包围盒的元素位置 + self.gtree = GapTree(lambda tb: tb["normalized_bbox"]) + + def run(self, textBlocks): + textBlocks = linePreprocessing(textBlocks) # 预处理 + textBlocks = self.gtree.sort(textBlocks) # 构建间隙树 + # 补充行尾间隔符 + for i in range(len(textBlocks)): + tb = textBlocks[i] + if i < len(textBlocks) - 1: + letter1 = tb["text"][-1] # 行1结尾字母 + letter2 = textBlocks[i + 1]["text"][0] # 行2开头字母 + tb["end"] = word_separator(letter1, letter2) # 获取间隔符 + else: + tb["end"] = "\n" + del tb["normalized_bbox"] + return textBlocks diff --git a/UmiOCR-data/py_src/ocr/tbpu/parser_multi_para.py b/UmiOCR-data/py_src/ocr/tbpu/parser_multi_para.py new file mode 100644 index 0000000..45917e8 --- /dev/null +++ b/UmiOCR-data/py_src/ocr/tbpu/parser_multi_para.py @@ -0,0 +1,33 @@ +# 排版解析-多栏-自然段 + +from .tbpu import Tbpu +from .parser_tools.line_preprocessing import linePreprocessing # 行预处理 +from .parser_tools.gap_tree import GapTree # 间隙树排序算法 +from .parser_tools.paragraph_parse import ParagraphParse # 段内分析器 + + +class MultiPara(Tbpu): + def __init__(self): + self.tbpuName = "排版解析-多栏-自然段" + + # 间隙树对象 + self.gtree = GapTree(lambda tb: tb["normalized_bbox"]) + + # 段内分析器对象 + get_info = lambda tb: (tb["normalized_bbox"], tb["text"]) + + def set_end(tb, end): # 获取预测的块尾分隔符 + tb["end"] = end + + self.pp = ParagraphParse(get_info, set_end) + + def run(self, textBlocks): + textBlocks = linePreprocessing(textBlocks) # 预处理 + textBlocks = self.gtree.sort(textBlocks) # 构建间隙树 + nodes = self.gtree.get_nodes_text_blocks() # 获取树节点序列 + # 对每个结点,进行自然段分析 + for tbs in nodes: + self.pp.run(tbs) # 预测结尾分隔符 + for tb in tbs: + del tb["normalized_bbox"] + return textBlocks diff --git a/UmiOCR-data/py_src/ocr/tbpu/parser_none.py b/UmiOCR-data/py_src/ocr/tbpu/parser_none.py new file mode 100644 index 0000000..36fbeac --- /dev/null +++ b/UmiOCR-data/py_src/ocr/tbpu/parser_none.py @@ -0,0 +1,14 @@ +# 排版解析-不做处理 + +from .tbpu import Tbpu + + +class ParserNone(Tbpu): + def __init__(self): + self.tbpuName = "排版解析-不做处理" + + def run(self, textBlocks): + for tb in textBlocks: + if "end" not in tb: + tb["end"] = "\n" # 默认结尾间隔符为换行 + return textBlocks diff --git a/UmiOCR-data/py_src/ocr/tbpu/parser_single_code.py b/UmiOCR-data/py_src/ocr/tbpu/parser_single_code.py new file mode 100644 index 0000000..1a9ddcf --- /dev/null +++ b/UmiOCR-data/py_src/ocr/tbpu/parser_single_code.py @@ -0,0 +1,73 @@ +# 排版解析-单栏-代码段 + +from .parser_single_line import SingleLine +from .parser_tools.line_preprocessing import linePreprocessing # 行预处理 + +from bisect import bisect_left + + +class SingleCode(SingleLine): + def __init__(self): + self.tbpuName = "排版解析-单栏-代码段" + + def merge_line(self, line): # 合并一行 + A = line[0] + ba = A["box"] + ha = ba[3][1] - ba[0][1] # 块A行高 + score = A["score"] + for i in range(1, len(line)): + B = line[i] + bb = B["box"] + ha = (ha + bb[3][1] - bb[0][1]) / 2 + # 合并文字,补充与间距相同的空格数 + space = 0 + if bb[0][0] > ba[1][0]: + space = round((bb[0][0] - ba[1][0]) / ha) + A["text"] += " " * space + B["text"] + # 合并包围盒 + yTop = min(ba[0][1], ba[1][1], bb[0][1], bb[1][1]) + yBottom = max(ba[2][1], ba[3][1], bb[2][1], bb[3][1]) + xLeft = min(ba[0][0], ba[3][0], bb[0][0], bb[3][0]) + xRight = max(ba[1][0], ba[2][0], bb[1][0], bb[2][0]) + ba[0][1] = ba[1][1] = yTop # y上 + ba[2][1] = ba[3][1] = yBottom # y下 + ba[0][0] = ba[3][0] = xLeft # x左 + ba[1][0] = ba[2][0] = xRight # x右 + # 置信度 + score += B["score"] + A["score"] = score / len(line) + del A["normalized_bbox"] + A["end"] = "\n" + return A + + def indent(self, tbs): # 分析所有行,构造缩进 + lh = 0 # 平均行高 + xMin = float("inf") # 句首的最左、最右x值 + xMax = float("-inf") + for tb in tbs: + b = tb["box"] + lh += b[3][1] - b[0][1] + x = b[0][0] + xMin = min(xMin, x) + xMax = max(xMax, x) + lh /= len(tbs) + lh2 = lh / 2 + # 构建缩进层级列表 + levelList = [] + x = xMin + while x < xMax: + levelList.append(x) + x += lh + # 按照层级,为每行句首加上空格,并调整包围盒 + for tb in tbs: + b = tb["box"] + level = bisect_left(levelList, b[0][0] + lh2) - 1 # 二分查找层级点 + tb["text"] = " " * level + tb["text"] # 补充空格 + b[0][0] = b[3][0] = xMin # 左侧归零 + + def run(self, textBlocks): + textBlocks = linePreprocessing(textBlocks) # 预处理 + lines = self.get_lines(textBlocks) # 获取每一行 + tbs = [self.merge_line(line) for line in lines] # 合并所有行 + self.indent(tbs) # 为每行添加句首缩进 + return tbs diff --git a/UmiOCR-data/py_src/ocr/tbpu/parser_single_line.py b/UmiOCR-data/py_src/ocr/tbpu/parser_single_line.py new file mode 100644 index 0000000..b7e0285 --- /dev/null +++ b/UmiOCR-data/py_src/ocr/tbpu/parser_single_line.py @@ -0,0 +1,73 @@ +# 排版解析-单栏-单行 + +from .tbpu import Tbpu +from .parser_tools.line_preprocessing import linePreprocessing # 行预处理 +from .parser_tools.paragraph_parse import word_separator # 上下句间隔符 + + +class SingleLine(Tbpu): + def __init__(self): + self.tbpuName = "排版解析-单栏-单行" + + # 从文本块列表中找出所有行 + def get_lines(self, textBlocks): + # 按x排序 + textBlocks.sort(key=lambda tb: tb["normalized_bbox"][0]) + lines = [] + for i1, tb1 in enumerate(textBlocks): + if not tb1: + continue + # 最左的一个块 + l1, top1, r1, bottom1 = tb1["normalized_bbox"] + h1 = bottom1 - top1 + now_line = [tb1] + # 考察右侧哪些块符合条件 + for i2 in range(i1 + 1, len(textBlocks)): + tb2 = textBlocks[i2] + if not tb2: + continue + l2, top2, r2, bottom2 = tb2["normalized_bbox"] + h2 = bottom2 - top2 + # 行2左侧太前 + if l2 < r1 - h1: + continue + # 垂直距离太远 + if top2 < top1 - h1 * 0.5 or bottom2 > bottom1 + h1 * 0.5: + continue + # 行高差距过大 + if abs(h1 - h2) > min(h1, h2) * 0.5: + continue + # 符合条件 + now_line.append(tb2) + textBlocks[i2] = None + # 更新搜索条件 + r1 = r2 + # 处理完一行 + for i2 in range(len(now_line) - 1): + # 检查同一行内相邻文本块的水平间隙 + l1, t1, r1, b1 = now_line[i2]["normalized_bbox"] + l2, t2, r2, b2 = now_line[i2 + 1]["normalized_bbox"] + h = (b1 + b2 - t1 - l2) * 0.5 + if l2 - r1 > h * 1.5: # 间隙太大,强制设置空格 + now_line[i2]["end"] = " " + continue + letter1 = now_line[i2]["text"][-1] + letter2 = now_line[i2 + 1]["text"][0] + now_line[i2]["end"] = word_separator(letter1, letter2) + now_line[-1]["end"] = "\n" + lines.append(now_line) + textBlocks[i1] = None + # 所有行按y排序 + lines.sort(key=lambda tbs: tbs[0]["normalized_bbox"][1]) + return lines + + def run(self, textBlocks): + textBlocks = linePreprocessing(textBlocks) # 预处理 + lines = self.get_lines(textBlocks) # 获取每一行 + # 解包 + textBlocks = [] + for line in lines: + for tb in line: + del tb["normalized_bbox"] + textBlocks.append(tb) + return textBlocks diff --git a/UmiOCR-data/py_src/ocr/tbpu/parser_single_none.py b/UmiOCR-data/py_src/ocr/tbpu/parser_single_none.py new file mode 100644 index 0000000..88332f9 --- /dev/null +++ b/UmiOCR-data/py_src/ocr/tbpu/parser_single_none.py @@ -0,0 +1,19 @@ +# 排版解析-单栏-无换行 + +from .parser_single_line import SingleLine +from .parser_tools.paragraph_parse import word_separator # 上下句间隔符 + + +class SingleNone(SingleLine): + def __init__(self): + self.tbpuName = "排版解析-单栏-无换行" + + def run(self, textBlocks): + textBlocks = super().run(textBlocks) + # 找到换行符,更改为间隔符 + for i in range(len(textBlocks) - 1): + if textBlocks[i]["end"] == "\n": + letter1 = textBlocks[i]["text"][-1] + letter2 = textBlocks[i + 1]["text"][0] + textBlocks[i]["end"] = word_separator(letter1, letter2) + return textBlocks diff --git a/UmiOCR-data/py_src/ocr/tbpu/parser_single_para.py b/UmiOCR-data/py_src/ocr/tbpu/parser_single_para.py new file mode 100644 index 0000000..5b56e25 --- /dev/null +++ b/UmiOCR-data/py_src/ocr/tbpu/parser_single_para.py @@ -0,0 +1,49 @@ +# 排版解析-单栏-自然段 + +from .parser_single_line import SingleLine +from .parser_tools.line_preprocessing import linePreprocessing # 行预处理 +from .parser_tools.paragraph_parse import ParagraphParse # 段内分析器 + + +class SinglePara(SingleLine): + def __init__(self): + self.tbpuName = "排版解析-单栏-自然段" + + # 段内分析器对象 + get_info = lambda tb: (tb["normalized_bbox"], tb["text"]) + + def set_end(tb, end): # 获取预测的块尾分隔符 + tb["line"][-1]["end"] = end + + self.pp = ParagraphParse(get_info, set_end) + + def run(self, textBlocks): + textBlocks = linePreprocessing(textBlocks) # 预处理 + lines = self.get_lines(textBlocks) # 获取每一行 + # 将行封装为tb + temp_tbs = [] + for line in lines: + b0, b1, b2, b3 = line[0]["normalized_bbox"] + # 搜索bbox + for i in range(1, len(line)): + bb = line[i]["normalized_bbox"] + b1 = min(b1, bb[1]) + b2 = max(b1, bb[2]) + b3 = max(b1, bb[3]) + # 构建tb + temp_tbs.append( + { + "normalized_bbox": (b0, b1, b2, b3), + "text": line[0]["text"][0] + line[-1]["text"][-1], + "line": line, + } + ) + # 预测结尾分隔符 + self.pp.run(temp_tbs) + # 解包 + textBlocks = [] + for t in temp_tbs: + for tb in t["line"]: + del tb["normalized_bbox"] + textBlocks.append(tb) + return textBlocks diff --git a/UmiOCR-data/py_src/ocr/tbpu/parser_tools/__init__.py b/UmiOCR-data/py_src/ocr/tbpu/parser_tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/UmiOCR-data/py_src/ocr/tbpu/parser_tools/gap_tree.py b/UmiOCR-data/py_src/ocr/tbpu/parser_tools/gap_tree.py new file mode 100644 index 0000000..0b48654 --- /dev/null +++ b/UmiOCR-data/py_src/ocr/tbpu/parser_tools/gap_tree.py @@ -0,0 +1,330 @@ +# 【间隙·树·排序算法】 GapTree_Sort_Algorithm +# 对OCR结果或PDF提取的文本进行版面分析,按人类阅读顺序进行排序。 +# Author: hiroi-sora +# https://github.com/hiroi-sora/GapTree_Sort_Algorithm + +from typing import Callable + + +class GapTree: + def __init__(self, get_bbox: Callable): + """ + :param get_bbox: 函数,传入单个文本块, + 返回该文本块左上角、右下角的坐标元组 (x0, y0, x1, y1) + """ + self.get_bbox = get_bbox + + # ======================= 调用接口 ===================== + # 对文本块列表排序 + def sort(self, text_blocks: list): + """ + 对文本块列表,按人类阅读顺序进行排序。 + + :param text_blocks: 文本块对象列表 + :return: 排序后的文本块列表 + """ + + # 封装块单元,并求页面左右边缘 + units, page_l, page_r = self._get_units(text_blocks, self.get_bbox) + # 求行和竖切线 + cuts, rows = self._get_cuts_rows(units, page_l, page_r) + # 求布局树 + root = self._get_layout_tree(cuts, rows) + # 求树节点序列 + nodes = self._preorder_traversal(root) + # 求排序后的 原始文本块序列 + new_text_blocks = self._get_text_blocks(nodes) + + # 测试:缓存中间变量,以便调试输出 + self.current_rows = rows + self.current_cuts = cuts + self.current_nodes = nodes + + return new_text_blocks + + # 获取以区块为单位的文本块二层列表 + def get_nodes_text_blocks(self): + """ + 获取以区块为单位的文本块二层列表。需要在 sort 后调用。 + + :return: [ [区块1的text_blocks], [区块2的text_blocks]... ] + """ + result = [] + for node in self.current_nodes: + tbs = [] + if node["units"]: + for unit in node["units"]: + tbs.append(unit[1]) + result.append(tbs) + return result + + # ======================= 封装块单元列表 ===================== + # 将原始文本块,封装为 ( (x0,y0,x2,y2), 原始 ) 。并检查页边界。 + def _get_units(self, text_blocks, get_bbox): + # 封装单元列表 units [ ( (x0,y0,x2,y2), 原始文本块 ), ... ] + units = [] + page_l, page_r = float("inf"), -1 # 记录文本块的左右最值,作为页边界 + for tb in text_blocks: + x0, y0, x2, y2 = get_bbox(tb) + units.append(((x0, y0, x2, y2), tb)) + if x0 < page_l: + page_l = x0 + if x2 > page_r: + page_r = x2 + units.sort(key=lambda a: a[0][1]) # 按顶部从上到下排序 + return units, page_l, page_r + + # ======================= 求行和竖切线 ===================== + """ + 扫描所有文本块,获取所有行和竖切线。 + 一个行,由一组垂直位置接近的文本块所组成。 + 一条竖切线,由多个连续行中,同一位置的间隙所组成。间隙划分同一行中不同列的文本块。 + 输入:一个页面上的文本块单元列表 units=[ ( (x0,y0,x2,y2), _ ) ] 。必须按上到下排序。 + 返回: + 竖切线列表 cuts=[ ( 左边缘x, 右边缘x, 起始行号, 结束行号 ) ] 。从左到右排序 + 页面上的行 rows=[ [unit...] ] 。从上到下,从左到右排序 + """ + + def _get_cuts_rows(self, units, page_l, page_r): + # 使用间隙组 gaps2 更新 gaps1 。返回: 更新后的gaps1 , gaps1中被移除的间隙 + def update_gaps(gaps1, gaps2): + flags1 = [True for _ in gaps1] # gaps1[i] 是否彻底移除 + flags2 = [True for _ in gaps2] # gaps2[i] 是否新加入 + new_gaps1 = [] + for i1, g1 in enumerate(gaps1): + l1, r1, _ = g1 + for i2, g2 in enumerate(gaps2): # 对每一个gap1,考察所有gap2 + l2, r2, _ = g2 + # 计算交集的起点和终点 + inter_l = max(l1, l2) + inter_r = min(r1, r2) + # 如果交集有效 + if inter_l <= inter_r: + # 更新 gap1 左右边缘 + new_gaps1.append((inter_l, inter_r, g1[2])) + flags1[i1] = False # 旧的 gap1 不应移除 + flags2[i2] = False # 新的 gap2 不应添加 + # gap2 新加入 + for i2, f2 in enumerate(flags2): + if f2: + new_gaps1.append(gaps2[i2]) + # 记录 gaps1 彻底移除的项 + del_gaps1 = [] + for i1, f1 in enumerate(flags1): + if f1: + del_gaps1.append(gaps1[i1]) + + return new_gaps1, del_gaps1 + + # ======================================== + + page_l -= 1 # 保证页面左右边缘不与文本块重叠 + page_r += 1 + # 存放所有行。“row”指同一水平线上的单元块(可能属于多列)。 [ [unit...] ] + rows = [] + # 已生成完毕的竖切线。[ ( 左边缘x, 右边缘x , 起始行号, 结束行号 ) ] + completed_cuts = [] + # 考察中的间隙。 [ (左边缘x, 右边缘x , 开始行号) ] + gaps = [] + row_index = 0 # 当前行号 + unit_index = 0 # 当前块号 + # 从上到下遍历所有文本行 + l_units = len(units) + while unit_index < l_units: + # ========== 查找当前行 row ========== + unit = units[unit_index] # 当前行最顶部的块 + u_bottom = unit[0][3] + row = [unit] # 当前行 + # 查找当前行的剩余块 + for i in range(unit_index + 1, len(units)): + next_u = units[i] + next_top = next_u[0][1] + if next_top > u_bottom: + break # 下一块的顶部超过当前底部,结束本行 + row.append(next_u) # 当前行添加块 + unit_index = i # 步进 已遍历的块序号 + # ========== 查找当前行的间隙 row_gaps ========== + row.sort(key=lambda x: (x[0][0], x[0][2])) # 当前行中的块 从左到右排序 + row_gaps = [] # 当前行的间隙 [ ( ( 左边缘l, 右边缘r ), 开始行号) ] + search_start = page_l # 本轮搜索的线段起始点为页面左边缘 + for u in row: # 遍历当前行的块 + l = u[0][0] # 块左侧 + r = u[0][2] # 块右侧 + # 若块起始点大于搜索起始点,那么将这部分加入到结果 + if l > search_start: + row_gaps.append((search_start, l, row_index)) + # 若块结束点大于搜索起始点,更新搜索起始点 + if r > search_start: + search_start = r + # 页面右边缘 加入最后一个间隙 + row_gaps.append((search_start, page_r, row_index)) + # ========== 更新考察中的间隙组 ========== + gaps, del_gaps = update_gaps(gaps, row_gaps) + # gaps 中被移除的项,加入生成完毕的竖切线 completed_cuts + row_max = row_index - 1 # 竖切线结束行号 + for dg1 in del_gaps: + completed_cuts.append((*dg1, row_max)) + # ========== End ========== + rows.append(row) # 总行列表添加当前行 + unit_index += 1 + row_index += 1 + # 遍历结束,收集 gaps 中剩余的间隙,组成延伸到最后一行的竖切线 + row_max = len(rows) - 1 # 竖切线结束行号 + for g in gaps: + completed_cuts.append((*g, row_max)) + completed_cuts.sort(key=lambda c: c[0]) + return completed_cuts, rows + + # ======================= 求布局树 ===================== + """ + 一个布局树节点表示一个区块。定义: + node = { + "x_left": 节点左边缘x, + "x_right": 右边缘x, + "r_top": 顶部的行号, + "r_bottom": 底部的行号, + "units": [], # 节点内部的文本块列表(除了根节点为空,其它节点非空) + "children": [], # 子节点,有序 + } + """ + + def _get_layout_tree(self, cuts, rows): + # 竖切线,将一个横行切开,断开的区域为“间隙”。 + # 生成每一行对应的间隙 (左侧,右侧) 坐标列表 + rows_gaps = [[] for _ in rows] + for g_i, cut in enumerate(cuts): + for r_i in range(cut[2], cut[3] + 1): + rows_gaps[r_i].append((cut[0], cut[1])) + + root = { # 根节点 + "x_left": cuts[0][0] - 1, + "x_right": cuts[-1][1] + 1, + "r_top": -1, + "r_bottom": -1, + "units": [], + "children": [], + } + completed_nodes = [root] # 已经完成结束的节点 + now_nodes = [] # 当前正在考虑的节点。无顺序 + + # ========== 结束一个节点,加入节点树 ========== + def complete(node): + node_r = node["x_right"] - 2 # 当前节点右边界 + max_nodes = [] # 符合父节点条件的,最低的完成节点列表 + max_r = -2 # 符合父节点条件的最低行数 + # 在完成列表中,寻找父节点 + for com_node in completed_nodes: + # 父节点的垂直投影必须包含当前右界 + if node_r < com_node["x_left"] or node_r > com_node["x_right"] + 0.0001: + continue + # 父节点底部必须在当前之上 + if com_node["r_bottom"] >= node["r_top"]: + continue + # 遇到更低的符合条件节点 + if com_node["r_bottom"] > max_r: + max_r = com_node["r_bottom"] + max_nodes = [com_node] + continue + # 遇到同样低的符合条件节点 + if com_node["r_bottom"] == max_r: + max_nodes.append(com_node) + continue + # 在最低列表中,寻找最右的节点作为父节点 + max_node = max(max_nodes, key=lambda n: n["x_right"]) + max_node["children"].append(node) # 加入父节点 + completed_nodes.append(node) # 加入完成列表 + + # ========== 遍历每行,更新节点树 ========== + for r_i, row in enumerate(rows): + row_gaps = rows_gaps[r_i] # 当前行的间隙组 + u_i = g_i = 0 # 当前考察的 文本块、间隙下标 + + # ========== 检查是否有正在考虑的节点 可以结束 ========== + new_nodes = [] + for node in now_nodes: # 遍历节点 + l_flag = r_flag = False # 标记节点左右边缘是否延续 + completed_flag = False # 标记节点是否可以结束 + x_left = node["x_left"] # 左右边缘坐标 + x_right = node["x_right"] + for gap in row_gaps: # 遍历该行所有间隙 + if gap[1] == x_left: # 节点左边缘被间隙右侧延续 + l_flag = True + if gap[0] == x_right: # 右边缘被间隙左侧延续 + r_flag = True + # 任意间隙在本节点下方,打断本节点 + if x_left < gap[0] < x_right or x_left < gap[1] < x_right: + completed_flag = True + break + if not l_flag or not r_flag: # 左右任意一个边缘无法延续 + completed_flag = True + if completed_flag: # 节点结束,加入节点树 + complete(node) + else: # 节点继续 + node["r_bottom"] = r_i + new_nodes.append(node) + now_nodes = new_nodes + + # ========== 从左到右遍历,将文本块加入对应列的节点 ========== + while u_i < len(row): + unit = row[u_i] # 当前块 + # ========== 当前块 unit 位于间隙 g_i 与 g_i+1 之间的区间 ========== + x_l = row_gaps[g_i][1] # 左间隙 g_i 的右边界 + x_r = row_gaps[g_i + 1][0] # 右间隙 g_i+1 的左边界 + # 检查区间是否正确 + if unit[0][0] + 0.0001 > x_r: # 块比右间隙更右,说明到了下一个区间 + g_i += 1 # 间隙步进,块不步进 + continue + # ========== 检查当前块可否加入已有的节点 ========== + flag = False + for node in now_nodes: + # 若某个节点的左右侧坐标,与当前块一致,则当前块加入节点 + if node["x_left"] == x_l and node["x_right"] == x_r: + node["units"].append(unit) + flag = True + break + if flag: + u_i += 1 # 块步进 + continue + # ========== 根据当前块创建新的节点,加入待考虑节点 ========== + now_nodes.append( + { + "x_left": x_l, + "x_right": x_r, + "r_top": r_i, + "r_bottom": r_i, + "units": [unit], + "children": [], + } + ) + u_i += 1 # 块步进 + # 将剩余节点也加入节点树 + for node in now_nodes: + complete(node) + # 整理所有节点 + for node in completed_nodes: + # 所有子节点 按从左到右排序 + node["children"].sort(key=lambda n: n["x_left"]) + # 所有块单元 按从上到下排序 + node["units"].sort(key=lambda u: u[0][1]) + return root + + # ======================= 前序遍历布局树,求节点序列 ===================== + def _preorder_traversal(self, root): + if not root: + return [] + stack = [root] + result = [] + while stack: + node = stack.pop() + result.append(node) + # 将当前节点的子节点逆序压入栈中,以保证左子节点先于右子节点处理 + stack += reversed(node["children"]) + return result + + # ======================= 从节点序列中,提取原始文本块序列 ===================== + def _get_text_blocks(self, nodes): + result = [] + for node in nodes: + for unit in node["units"]: + result.append(unit[1]) + return result diff --git a/UmiOCR-data/py_src/ocr/tbpu/parser_tools/line_preprocessing.py b/UmiOCR-data/py_src/ocr/tbpu/parser_tools/line_preprocessing.py new file mode 100644 index 0000000..3915916 --- /dev/null +++ b/UmiOCR-data/py_src/ocr/tbpu/parser_tools/line_preprocessing.py @@ -0,0 +1,98 @@ +# ========================================= +# =============== 按行预处理 =============== +# ========================================= + +from statistics import median # 中位数 +from math import atan2, cos, sin, sqrt, pi, radians, degrees + +from umi_log import logger + +angle_threshold = 3 # 进行一些操作的最小角度阈值 +angle_threshold_rad = radians(angle_threshold) + + +# 计算两点之间的距离 +def _distance(point1, point2): + return sqrt((point2[0] - point1[0]) ** 2 + (point2[1] - point1[1]) ** 2) + + +# 计算一个box的旋转角度 +def _calculateAngle(box): + # 获取宽高 + width = _distance(box[0], box[1]) + height = _distance(box[1], box[2]) + # 选择距离较大的两个顶点对,计算角度弧度值 + if width < height: + angle_rad = atan2(box[2][1] - box[1][1], box[2][0] - box[1][0]) + else: + angle_rad = atan2(box[1][1] - box[0][1], box[1][0] - box[0][0]) + # 标准化角度到[-pi/2, pi/2)范围(加上阈值) + if angle_rad < -pi / 2 + angle_threshold_rad: + angle_rad += pi + elif angle_rad >= pi / 2 + angle_threshold_rad: + angle_rad -= pi + return angle_rad + + +# 估计一组文本块的旋转角度 +def _estimateRotation(textBlocks): + # blocks["box"] = [左上角,右上角,右下角,左下角] + angle_rads = (_calculateAngle(block["box"]) for block in textBlocks) + median_angle = median(angle_rads) # 中位数 + return median_angle + + +# 获取旋转后的标准bbox。angle_threshold为执行旋转的阈值(最小角度值)。 +def _getBboxes(textBlocks, rotation_rad): + # 角度低于阈值(接近0°),则不进行旋转,以提高性能。 + if abs(rotation_rad) <= angle_threshold_rad: + bboxes = [ + ( # 直接构造bbox + min(x for x, y in tb["box"]), + min(y for x, y in tb["box"]), + max(x for x, y in tb["box"]), + max(y for x, y in tb["box"]), + ) + for tb in textBlocks + ] + # 否则,进行旋转操作。 + else: + logger.debug(f"文本块预处理旋转 {degrees(rotation_rad):.2f} °") + bboxes = [] + min_x, min_y = float("inf"), float("inf") # 初始化最小的x和y坐标 + cos_angle = cos(-rotation_rad) # 计算角度正弦值 + sin_angle = sin(-rotation_rad) + for tb in textBlocks: + box = tb["box"] + rotated_box = [ # 旋转box的每个顶点 + (cos_angle * x - sin_angle * y, sin_angle * x + cos_angle * y) + for x, y in box + ] + # 解包旋转后的顶点坐标,分别得到所有x和y的值 + xs, ys = zip(*rotated_box) + # 构建标准bbox (左上角x, 左上角y, 右下角x, 右下角y) + bbox = (min(xs), min(ys), max(xs), max(ys)) + bboxes.append(bbox) + min_x, min_y = min(min_x, bbox[0]), min(min_y, bbox[1]) + # 如果旋转后存在负坐标,将所有包围盒平移,使得最小的x和y坐标为0,确保所有坐标非负 + if min_x < 0 or min_y < 0: + bboxes = [ + (x - min_x, y - min_y, x2 - min_x, y2 - min_y) + for (x, y, x2, y2) in bboxes + ] + return bboxes + + +# 预处理 textBlocks ,将包围盒 ["box"] 转为标准化 bbox ,同时去除 ["text"] 不完整的项 +def linePreprocessing(textBlocks): + textBlocks = [i for i in textBlocks if i.get("text", False)] + # 判断角度 + rotation_rad = _estimateRotation(textBlocks) + # 获取标准化bbox + bboxes = _getBboxes(textBlocks, rotation_rad) + # 写入tb + for i, tb in enumerate(textBlocks): + tb["normalized_bbox"] = bboxes[i] + # 按y排序 + textBlocks.sort(key=lambda tb: tb["normalized_bbox"][1]) + return textBlocks diff --git a/UmiOCR-data/py_src/ocr/tbpu/parser_tools/paragraph_parse.py b/UmiOCR-data/py_src/ocr/tbpu/parser_tools/paragraph_parse.py new file mode 100644 index 0000000..c9545f2 --- /dev/null +++ b/UmiOCR-data/py_src/ocr/tbpu/parser_tools/paragraph_parse.py @@ -0,0 +1,173 @@ +# 段落分析器 +# 对已经是一个列区块之内的文本块,判断其段落关系。 + +from typing import Callable +import unicodedata + + +# 传入前句尾字符和后句首字符,返回分隔符 +def word_separator(letter1, letter2): + + # 判断Unicode字符是否属于中文、日文或韩文字符集 + def is_cjk(character): + cjk_unicode_ranges = [ + (0x4E00, 0x9FFF), # 中文 + (0x3040, 0x30FF), # 日文 + (0x1100, 0x11FF), # 韩文 + (0x3130, 0x318F), # 韩文兼容字母 + (0xAC00, 0xD7AF), # 韩文音节 + # 全角符号 + (0x3000, 0x303F), # 中文符号和标点 + (0xFE30, 0xFE4F), # 中文兼容形式标点 + (0xFF00, 0xFFEF), # 半角和全角形式字符 + ] + return any(start <= ord(character) <= end for start, end in cjk_unicode_ranges) + + if is_cjk(letter1) and is_cjk(letter2): + return "" + + # 特殊情况:前文为连字符。 + if letter1 == "-": + return "" + # 特殊情况:后文为任意标点符号。 + if unicodedata.category(letter2).startswith("P"): + return "" + # 其它正常情况加空格 + return " " + + +TH = 1.2 # 行高用作对比的阈值 + + +class ParagraphParse: + def __init__(self, get_info: Callable, set_end: Callable) -> None: + """ + :param get_info: 函数,传入单个文本块, + 返回该文本块的信息元组: ( (x0, y0, x1, y1), "文本" ) + :param set_end: 函数,传入单个文本块 和文本尾部的分隔符,该函数要将分隔符保存。 + """ + self.get_info = get_info + self.set_end = set_end + + # ======================= 调用接口:对文本块列表进行结尾分隔符预测 ===================== + def run(self, text_blocks: list): + """ + 对属于一个区块内的文本块列表,进行段落分析,预测每个文本块结尾的分隔符。 + + :param text_blocks: 文本块对象列表 + :return: 排序后的文本块列表 + """ + # 封装块单元 + units = self._get_units(text_blocks, self.get_info) + # 执行分析 + self._parse(units) + return text_blocks + + # ======================= 封装块单元列表 ===================== + # 将原始文本块,封装为 ( (x0,y0,x2,y2), ("开头","结尾"), 原始 ) 。 + def _get_units(self, text_blocks, get_info): + units = [] + for tb in text_blocks: + bbox, text = get_info(tb) + units.append((bbox, (text[0], text[-1]), tb)) + return units + + # ======================= 分析 ===================== + + # 执行分析 + def _parse(self, units): + units.sort(key=lambda a: a[0][1]) # 确保从上到下有序 + para_l, para_top, para_r, para_bottom = units[0][0] # 当前段的左右 + para_line_h = para_bottom - para_top # 当前段行高 + para_line_s = None # 当前段行间距 + now_para = [units[0]] # 当前段的块 + paras = [] # 总的段 + paras_line_space = [] # 总的段的行间距 + # 取 左右相等为一个自然段的主体 + for i in range(1, len(units)): + l, top, r, bottom = units[i][0] # 当前块上下左右边缘 + h = bottom - top + ls = top - para_bottom # 行间距 + # 检测是否同一段 + if ( # 左右边缘都相等 + abs(para_l - l) <= para_line_h * TH + and abs(para_r - r) <= para_line_h * TH + # 行间距不大 + and (para_line_s == None or ls < para_line_s + para_line_h * 0.5) + ): + # 更新数据 + para_l = (para_l + l) / 2 + para_r = (para_r + r) / 2 + para_line_h = (para_line_h + h) / 2 + para_line_s = ls if para_line_s == None else (para_line_s + ls) / 2 + # 添加到当前段 + now_para.append(units[i]) + else: # 非同一段,归档上一段,创建新一段 + paras.append(now_para) + paras_line_space.append(para_line_s) + now_para = [units[i]] + para_l, para_r, para_line_h = l, r, bottom - top + para_line_s = None + para_bottom = bottom + # 归档最后一段 + paras.append(now_para) + paras_line_space.append(para_line_s) + + # 合并只有1行的段,添加到上/下段作为首/尾句 + for i1 in reversed(range(len(paras))): + para = paras[i1] + if len(para) == 1: + l, top, r, bottom = para[0][0] + up_flag = down_flag = False + # 上段末尾条件:左对齐,右不超,行间距够小 + if i1 > 0: + # 检查左右 + up_l, up_top, up_r, up_bottom = paras[i1 - 1][-1][0] + up_dist, up_h = abs(up_l - l), up_bottom - up_top + up_flag = up_dist <= up_h * TH and r <= up_r + up_h * TH + # 检查行间距 + if ( + paras_line_space[i1 - 1] != None + and top - up_bottom > paras_line_space[i1 - 1] + up_h * 0.5 + ): + up_flag = False + # 下段开头条件:右对齐/单行超出,左缩进 + if i1 < len(paras) - 1: + down_l, down_top, down_r, down_bottom = paras[i1 + 1][0][0] + down_h = down_bottom - down_top + # 左对齐或缩进 + if down_l - down_h * TH <= l <= down_l + down_h * (1 + TH): + if len(paras[i1 + 1]) > 1: # 多行,右对齐 + down_flag = abs(down_r - r) <= down_h * TH + else: # 单行,右可超出 + down_flag = down_r - down_h * TH < r + # 检查行间距 + if ( + paras_line_space[i1 + 1] != None + and down_top - bottom > paras_line_space[i1 + 1] + down_h * 0.5 + ): + down_flag = False + + # 选择添加到上还是下段 + if up_flag and down_flag: # 两段都符合,则选择垂直距离更近的 + if top - up_bottom < down_top - bottom: + paras[i1 - 1].append(para[0]) + else: + paras[i1 + 1].insert(0, para[0]) + elif up_flag: # 只有一段符合,直接选择 + paras[i1 - 1].append(para[0]) + elif down_flag: + paras[i1 + 1].insert(0, para[0]) + if up_flag or down_flag: + del paras[i1] + del paras_line_space[i1] + + # 刷新所有段,添加end + for para in paras: + for i1 in range(len(para) - 1): + letter1 = para[i1][1][1] # 行1结尾字母 + letter2 = para[i1 + 1][1][0] # 行2开头字母 + sep = word_separator(letter1, letter2) + self.set_end(para[i1][2], sep) + self.set_end(para[-1][2], "\n") + return units diff --git a/UmiOCR-data/py_src/ocr/tbpu/tbpu.py b/UmiOCR-data/py_src/ocr/tbpu/tbpu.py new file mode 100644 index 0000000..255529f --- /dev/null +++ b/UmiOCR-data/py_src/ocr/tbpu/tbpu.py @@ -0,0 +1,22 @@ +# tbpu : text block processing unit +# 文块处理器的基类。 +# OCR返回的结果中,一项包含文字、包围盒、置信度的元素,称为一个“文块” - text block 。 +# 文块不一定是完整的一句话或一个段落。反之,一般是零散的文字。 +# 一个OCR结果常由多个文块组成。 +# 文块处理器就是:将传入的多个文块进行处理,比如合并、排序、删除文块。 + + +class Tbpu: + def __init__(self): + self.tbpuName = "文块处理单元-未知" + + def run(self, textBlocks): + """输入:textBlocks文块列表。例:\n + [ + {'box': [[29, 19], [172, 19], [172, 44], [29, 44]], 'score': 0.89, 'text': '文本111'}, + {'box': [[29, 60], [161, 60], [161, 86], [29, 86]], 'score': 0.75, 'text': '文本222'}, + ] + 输出:排序后的textBlocks文块列表,每个块增加键: + 'end' 结尾间隔符 + """ + return textBlocks diff --git a/UmiOCR-data/py_src/platform/__init__.py b/UmiOCR-data/py_src/platform/__init__.py new file mode 100644 index 0000000..9f0c43d --- /dev/null +++ b/UmiOCR-data/py_src/platform/__init__.py @@ -0,0 +1,16 @@ +# 根据当前环境,提供不同平台对象 +import sys + +# 根据当前操作系统选择要导入的模块 +_plat = sys.platform +if _plat.startswith("win32"): + from .win32.win32_api import Api as _Platform +elif _plat.startswith("linux"): + from .linux.linux_api import Api as _Platform +elif _plat.startswith("darwin"): + raise ImportError("尚未支持macos系统!") +else: + raise ImportError(f"未知系统:{_plat}") + +# 构造单例:平台对象 +Platform = _Platform() diff --git a/UmiOCR-data/py_src/platform/linux/__init__.py b/UmiOCR-data/py_src/platform/linux/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/UmiOCR-data/py_src/platform/linux/linux_api.py b/UmiOCR-data/py_src/platform/linux/linux_api.py new file mode 100644 index 0000000..1346ff3 --- /dev/null +++ b/UmiOCR-data/py_src/platform/linux/linux_api.py @@ -0,0 +1,159 @@ +# ============================================== +# =============== Linux系统API =============== +# ============================================== + +import os +import shlex +import subprocess +from PySide2.QtCore import QStandardPaths as Qsp + +from umi_log import logger +from umi_about import UmiAbout + + +# ==================== 快捷方式 ==================== +class _Shortcut: + @staticmethod + def _getPath(position): # 获取路径 + # 桌面 + if position == "desktop": + return Qsp.writableLocation(Qsp.DesktopLocation) + # 开始菜单 + if position == "startMenu": + return Qsp.writableLocation(Qsp.ApplicationsLocation) + # 开机自启 TODO + elif position == "startup": + raise ValueError("Linux 暂不支持自动添加开机自启。请手动设置。") + + # 创建快捷方式,返回成功与否的字符串。position取值: + # desktop 桌面 + # startMenu 开始菜单 + # startup 开机自启 + @staticmethod + def createShortcut(position): + if position == "startup": # TODO + return "[Warning] Linux 暂不支持自动添加开机自启。请手动设置。\nAutomatically adding startup functions is not supported at this time. Please set it manually." + try: + lnkName = UmiAbout["name"] + appPath = UmiAbout["app"]["path"] + appDir = UmiAbout["app"]["dir"] + if not appPath: + return f"[Error] 未找到程序入口文件。请尝试手动创建快捷方式。\n[Error] Umi-OCR app path not exist. Please try creating a shortcut manually.\n\n{appPath}" + + lnkPathBase = _Shortcut._getPath(position) + lnkPathBase = os.path.join(lnkPathBase, lnkName) + lnkPath = lnkPathBase + ".desktop" + i = 1 + while os.path.exists(lnkPath): # 快捷方式已存在 + lnkPath = lnkPathBase + f" ({i}).desktop" # 添加序号 + i += 1 + except Exception as e: + return f"[Error] 无法获取应用信息。\n[Error] Unable to obtain application information.\n\n{e}" + + # 快捷方式 文件内容 + desktop_entry = f""" +[Desktop Entry] +Version={UmiAbout["version"]["string"]} +Type=Application +Name={lnkName} +Exec={appPath} +Path={appDir} +Icon={appDir}/UmiOCR-data/qt_res/images/icons/umiocr.ico +Terminal=false +""" + + try: + with open(lnkPath, "w") as f: + f.write(desktop_entry) + os.chmod(lnkPath, 0o755) # 赋予执行权限 + logger.info(f"创建快捷方式: {lnkPath}") + return "[Success]" + except Exception as e: + return f"[Error] 创建快捷方式失败。\n[Error] Failed to create shortcut.\n {lnkPath}: {e}" + + # 删除快捷方式,返回删除文件的个数 + @staticmethod + def deleteShortcut(position): + try: + appName = UmiAbout["name"] + lnkDir = _Shortcut._getPath(position) + except Exception: + logger.error("无法获取应用信息", exc_info=True, stack_info=True) + return 0 + + num = 0 + for fileName in os.listdir(lnkDir): + try: + lnkPath = os.path.join(lnkDir, fileName) + if not os.path.isfile(lnkPath): # 排除非文件 + continue + if fileName.startswith(appName) and fileName.endswith(".desktop"): + os.remove(lnkPath) + num += 1 + logger.info(f"删除快捷方式: {lnkPath}") + except Exception: + logger.error( + f"删除快捷方式失败。 lnkPath: {lnkPath}", + exc_info=True, + stack_info=True, + ) + continue + return num + + +# ==================== 硬件控制 ==================== +class _HardwareCtrl: + # 关机 + @staticmethod + def shutdown(): + # TODO: sudo权限? + os.system("sudo shutdown -h now") + + # 休眠 + @staticmethod + def hibernate(): + os.system("sudo systemctl hibernate") + + +# ==================== 对外接口 ==================== +class Api: + # 快捷方式。接口: createShortcut deleteShortcut + # 参数:快捷方式位置, desktop startMenu startup + Shortcut = _Shortcut() + + # 硬件控制。接口: shutdown hibernate + HardwareCtrl = _HardwareCtrl() + + # TODO: 根据系统及硬件,判断最适合的渲染器类型 + @staticmethod + def getOpenGLUse(): + return "AA_UseOpenGLES" # 默认使用GLES。 + # return "AA_UseSoftwareOpenGL" # 如果不兼容?则使用软渲染 + + # 键值转键名 + @staticmethod + def getKeyName(key): + # 传入 pynput 按键事件对象,返回键名字符串 + if not isinstance(key, str): + key = str(key) + if not key: # 错误,未获取键值 + return "unknown" + if key.startswith("'") and key.endswith("'"): # 去除自带引号 + key = key[1:-1] + if key.startswith("Key."): # 修饰建 + key = key[4:] + if not key: # 再检查一次 + return "unknown" + return key + + # 让系统运行一个程序,不堵塞当前进程 + @staticmethod + def runNewProcess(path, args=""): + # TODO: 测试、完善 + command_line = [path] + shlex.split(args) + subprocess.Popen(command_line) + + # 用系统默认应用打开一个文件或目录,不堵塞当前进程 + @staticmethod + def startfile(path): + os.startfile(path) diff --git a/UmiOCR-data/py_src/platform/win32/__init__.py b/UmiOCR-data/py_src/platform/win32/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/UmiOCR-data/py_src/platform/win32/key_translator.py b/UmiOCR-data/py_src/platform/win32/key_translator.py new file mode 100644 index 0000000..7337350 --- /dev/null +++ b/UmiOCR-data/py_src/platform/win32/key_translator.py @@ -0,0 +1,48 @@ +# 负责 pynput 的按键转换 +# 封装 keyTranslator ,负责key、char、vk的转换 + +from pynput._util.win32 import KeyTranslator + +from umi_log import logger + + +# ==================== 按键转换器 ==================== +# 封装 keyTranslator ,负责key、char、vk的转换 +class _KeyTranslatorApi: + def __init__(self): + self._kt = KeyTranslator() + self._layout, _layoutData = self._kt._generate_layout() + self._normalLayout = _layoutData[ + (False, False, False) + ] # 选取常规布局,不受修饰键影响 + + def __call__(self, key): + """传入pynput的Key对象,返回与修饰键无关的键名char""" + # 比如,就算按下Shift再按“=”,依然返回“=”而不是“+” + try: + if hasattr(key, "name"): # 若为控制键 + name = key.name.replace("cmd", "win") # win键名称修正 + if "_" in name: # 清除 _l _r 标记后缀 + name = name.split("_", 1)[0].lower() + return name.lower() + else: # 若为非控制键,通过vk获取键名,避免组合键的char为空 + scan = self._kt._to_scan(key.vk, self._layout) # vk转扫描码 + char = self._normalLayout[scan][0] # 扫描码转char + return char.lower() + except Exception: # 特殊键(如Fn)没有对应字符,会跳到这里 + if key and hasattr(key, "vk"): + return f"<{key.vk}>" # 未知键值,无对应字符,返回键值本身 + else: + logger.error( + f"键值转换异常,未知键值!key: {str(key)}, type: {type(key)}.", + exc_info=True, + stack_info=True, + ) + return str(key) + + +_KTA = _KeyTranslatorApi() + + +def getKeyName(key): # 键值转键名 + return _KTA(key) diff --git a/UmiOCR-data/py_src/platform/win32/win32_api.py b/UmiOCR-data/py_src/platform/win32/win32_api.py new file mode 100644 index 0000000..2e9de29 --- /dev/null +++ b/UmiOCR-data/py_src/platform/win32/win32_api.py @@ -0,0 +1,141 @@ +# ============================================== +# =============== Windows系统API =============== +# ============================================== + +import os +import subprocess +from PySide2.QtCore import QStandardPaths as Qsp, QFile, QFileInfo + +from umi_log import logger +from umi_about import UmiAbout +from .key_translator import getKeyName + +# 环境的类型: +# "APPDATA" 用户,低权限要求 +# "ProgramData" 全局,高权限要求 +EnvType = "APPDATA" # or "ProgramData" + + +# ==================== 快捷方式 ==================== +class _Shortcut: + @staticmethod # 获取地址 + def _getPath(position): + # 桌面 + if position == "desktop": + return Qsp.writableLocation(Qsp.DesktopLocation) + + startMenu = os.path.join( + os.getenv(EnvType), "Microsoft", "Windows", "Start Menu" + ) + # 开始菜单 + if position == "startMenu": + return startMenu + # 开机自启 + elif position == "startup": + return os.path.join(startMenu, "Programs", "Startup") + + # 创建快捷方式,返回成功与否的字符串。position取值: + # desktop 桌面 + # startMenu 开始菜单 + # startup 开机自启 + @staticmethod + def createShortcut(position): + lnkName = UmiAbout["name"] + appPath = UmiAbout["app"]["path"] + if not appPath or not os.path.exists(appPath): + return f"[Error] 未找到程序exe文件。请尝试手动创建快捷方式。\n[Error] Exe path not exist. Please try creating a shortcut manually.\n\n{appPath}" + lnkPathBase = _Shortcut._getPath(position) + lnkPathBase = os.path.join(lnkPathBase, lnkName) + lnkPath = lnkPathBase + ".lnk" + i = 1 + while os.path.exists(lnkPath): # 快捷方式已存在 + lnkPath = lnkPathBase + f" ({i}).lnk" # 添加序号 + i += 1 + appFile = QFile(appPath) + res = appFile.link(lnkPath) + if not res: + return f"[Error] {appFile.errorString()}\n请尝试以管理员权限启动软件。\nPlease try starting the software as an administrator.\nappPath: {appPath}\nlnkPath: {lnkPath}" + return "[Success]" + + # 删除快捷方式,返回删除文件的个数 + @staticmethod + def deleteShortcut(position): + lnkName = UmiAbout["name"] + lnkDir = _Shortcut._getPath(position) + num = 0 + for fileName in os.listdir(lnkDir): + lnkPath = os.path.join(lnkDir, fileName) + try: + if not os.path.isfile(lnkPath): # 排除非文件 + continue + info = QFileInfo(lnkPath) + if not info.isSymLink(): # 排除非快捷方式 + continue + originName = os.path.basename(info.symLinkTarget()) + if lnkName in originName: # 快捷方式指向的文件名包含appName,删之 + os.remove(lnkPath) + num += 1 + except Exception: + logger.error( + f"删除快捷方式失败。 lnkPath: {lnkPath}", + exc_info=True, + stack_info=True, + ) + continue + return num + + +# ==================== 硬件控制 ==================== +class _HardwareCtrl: + # 关机 + @staticmethod + def shutdown(): + os.system("shutdown /s /t 0") + + # 休眠 + @staticmethod + def hibernate(): + os.system("shutdown /h") + + +# ==================== 对外接口 ==================== +class Api: + # 快捷方式。接口: createShortcut deleteShortcut + # 参数:快捷方式位置, desktop startMenu startup + Shortcut = _Shortcut() + + # 硬件控制。接口: shutdown hibernate + HardwareCtrl = _HardwareCtrl() + + # 根据系统及硬件,判断最适合的渲染器类型 + @staticmethod + def getOpenGLUse(): + import platform + + # 判断系统版本,若 >win10 则使用 GLES ,否则使用软渲染 + version = platform.version() + if "." in version: + ver = version.split(".")[0] + if ver.isdigit() and int(ver) >= 10: + return "AA_UseOpenGLES" + return "AA_UseSoftwareOpenGL" + + # 键值转键名 + @staticmethod + def getKeyName(key): + return getKeyName(key) + + # 让系统运行一个程序,不堵塞当前进程 + @staticmethod + def runNewProcess(path, args=""): + subprocess.Popen( + f'start "" "{path}" {args}', + shell=True, + # 确保在一个新的控制台窗口中运行,与当前进程完全独立。 + creationflags=subprocess.CREATE_NEW_CONSOLE, + ) + + # 用系统默认应用打开一个文件或目录,不堵塞当前进程 + @staticmethod + def startfile(path): + os.startfile(path) diff --git a/UmiOCR-data/py_src/plugins_controller/plugins_connector.py b/UmiOCR-data/py_src/plugins_controller/plugins_connector.py new file mode 100644 index 0000000..bbb6cba --- /dev/null +++ b/UmiOCR-data/py_src/plugins_controller/plugins_connector.py @@ -0,0 +1,16 @@ +# ============================================= +# =============== 插件管理连接器 =============== +# ============================================= + +from PySide2.QtCore import QObject, Slot, Signal + +from .plugins_controller import PluginsController + + +# 插件连接器 +class PluginsConnector(QObject): + # 初始化插件,返回字典: + # {"options": ["ocr":{"key":成功加载的插件的配置}], "errors": {"key": "失败信息"} } + @Slot(result="QVariant") + def init(self): + return PluginsController.init() diff --git a/UmiOCR-data/py_src/plugins_controller/plugins_controller.py b/UmiOCR-data/py_src/plugins_controller/plugins_controller.py new file mode 100644 index 0000000..e56b1a0 --- /dev/null +++ b/UmiOCR-data/py_src/plugins_controller/plugins_controller.py @@ -0,0 +1,84 @@ +# ======================================= +# =============== 加载插件 =============== +# ======================================= + +import os +import site +import importlib + +from umi_log import logger +from ..ocr.api import initOcrPlugins + +# 插件 总目录 +PLUGINS_PATH = "plugins" +# 插件组 组名 +PLUGINS_GROUPS = ["ocr"] + + +# 插件控制器 +class _PluginsControllerClass: + def __init__(self): + self.pluginsDict = {} + self.optionsDict = {} + for group in PLUGINS_GROUPS: + self.pluginsDict[group] = {} + self.optionsDict[group] = {} + + # 初始化并加载插件 + def init(self): + # 动态插件 + errors = {} # 保存失败信息 + # 添加包搜索路径 + if not os.path.exists(PLUGINS_PATH): + os.makedirs(PLUGINS_PATH) + logger.error(f"插件目录不存在: {PLUGINS_PATH} ") + return None + site.addsitedir(PLUGINS_PATH) # 添加插件搜索路径 + # 加载所有插件。在插件目录中搜索合法的包 + plugList = os.listdir(PLUGINS_PATH) + for name in plugList: + initPath = os.path.join(PLUGINS_PATH, name, "__init__.py") + if os.path.exists(initPath): # 若包路径下存在 __init__.py ,则尝试加载该包 + flag, res = self._loadPlugin(name) + if not flag: # 失败 + errors[name] = res + logger.error(f"加载插件 {name} 失败:{res}") + # TODO: 静态插件 + # 动态插件导入API管理器 + ocrErrs = initOcrPlugins(self.pluginsDict["ocr"]) + if ocrErrs: + errors.update(ocrErrs) + return {"options": self.optionsDict, "errors": errors} + + # 加载一个组件python包。成功返回True,失败返回 False, 错误信息 + def _loadPlugin(self, name): + # 加载包 + try: + module = importlib.import_module(name) + except Exception as e: + return False, f"动态导入包失败:{e}" + # 验证信息 + if not hasattr(module, "PluginInfo"): + return False, f"__init__.py 中未定义 PluginInfo 。" + pluginInfo = module.PluginInfo + if "group" not in pluginInfo: + return False, f"__init__.py 中未定义 group 。" + if "api_class" not in pluginInfo: + return False, f"__init__.py 中未定义 api_class 。" + if "global_options" not in pluginInfo: + pluginInfo["global_options"] = None + if "local_options" not in pluginInfo: + pluginInfo["local_options"] = None + group = pluginInfo["group"] + if not group or group not in PLUGINS_GROUPS: + return False, f'__init__.py group "{group}" 不属于已定义的插件类型。' + # 加载成功、验证成功,则记录信息 + self.pluginsDict[group][name] = pluginInfo + self.optionsDict[group][name] = { + "global_options": pluginInfo["global_options"], + "local_options": pluginInfo["local_options"], + } + return True, "" + + +PluginsController = _PluginsControllerClass() diff --git a/UmiOCR-data/py_src/run.py b/UmiOCR-data/py_src/run.py new file mode 100644 index 0000000..da30578 --- /dev/null +++ b/UmiOCR-data/py_src/run.py @@ -0,0 +1,152 @@ +# Umi-OCR +# OCR software, free and offline. 开源、免费的离线OCR软件。 +# Website - https://github.com/hiroi-sora/Umi-OCR +# Author - hiroi-sora +# +# You are free to use, modify, and distribute Umi-OCR, but it must include +# the original author's copyright statement and the following license statement. +# 您可以免费地使用、修改和分发 Umi-OCR ,但必须包含原始作者的版权声明和下列许可声明。 +""" +Copyright (c) 2023 hiroi-sora + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + + +""" +========================================= +========== Umi-OCR 软件启动入口 ========== +========================================= + +说明: +本文件必须在运行环境初始化之后调用。 +负责 Umi-OCR 的信息初始化并启动主事件循环。 +""" + +import os +import sys +import site + + +# 启动主qml。工作路径必须为 UmiOCR-data +def runQml(engineAddImportPath): + # ==================== 0. 导入包 ==================== + from PySide2.QtCore import Qt, qInstallMessageHandler + from PySide2.QtGui import QGuiApplication + from PySide2.QtQml import QQmlApplicationEngine, qmlRegisterType + + from umi_about import UmiAbout # 项目信息 + from umi_log import get_qt_message_handler, logger # 日志 + from .utils import app_opengl # 渲染器 + + # ==================== 0. QT 日志重定向到 Umi 日志模块 ==================== + qInstallMessageHandler(get_qt_message_handler()) + + # ==================== 1. 全局参数设置 ==================== + # 启用 OpenGL 上下文之间的资源共享 + QGuiApplication.setAttribute(Qt.AA_ShareOpenGLContexts, True) + # 启用高分屏自动缩放 + QGuiApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) + # 初始化渲染器 + app_opengl.initOpengl() + + # ==================== 2. 启动qt ==================== + qtApp = QGuiApplication(sys.argv) + qtApp.setApplicationName(UmiAbout["name"]) + qtApp.setOrganizationName(UmiAbout["authors"][0]["name"]) + qtApp.setOrganizationDomain(UmiAbout["url"]["home"]) + + # ==================== 3. OpenGlES 兼容性检查 ==================== + app_opengl.checkOpengl() + + # ==================== 4. 注册连接器Python类 ==================== + from .tag_pages.tag_pages_connector import TagPageConnector # 页面连接器 + from .mission.mission_connector import MissionConnector # 任务连接器 + from .mission.doc_preview_connector import DocPreviewConnector # 文档预览连接器 + from .event_bus.pubsub_connector import PubSubConnector # 发布/订阅连接器 + from .event_bus.key_mouse.key_mouse_connector import ( # 键盘/鼠标连接器 + KeyMouseConnector, + ) + from .plugins_controller.plugins_connector import PluginsConnector # 插件连接器 + from .utils.utils_connector import UtilsConnector # 通用连接器 + from .utils.global_configs_connector import GlobalConfigsConnector # 全局配置连接器 + from .utils.theme_connector import ThemeConnector # 主题连接器 + from .image_controller.image_connector import ImageConnector # 图片处理连接器 + from .image_controller.image_provider import PixmapProvider # 图片提供器 + from .utils.i18n_configs import I18n # 语言 + + qmlRegisterType(TagPageConnector, "TagPageConnector", 1, 0, "TagPageConnector") + qmlRegisterType(MissionConnector, "MissionConnector", 1, 0, "MissionConnector") + qmlRegisterType(PubSubConnector, "PubSubConnector", 1, 0, "PubSubConnector") + qmlRegisterType(KeyMouseConnector, "KeyMouseConnector", 1, 0, "KeyMouseConnector") + qmlRegisterType(UtilsConnector, "UtilsConnector", 1, 0, "UtilsConnector") + qmlRegisterType(PluginsConnector, "PluginsConnector", 1, 0, "PluginsConnector") + qmlRegisterType(ThemeConnector, "ThemeConnector", 1, 0, "ThemeConnector") + qmlRegisterType(ImageConnector, "ImageConnector", 1, 0, "ImageConnector") + qmlRegisterType( + GlobalConfigsConnector, "GlobalConfigsConnector", 1, 0, "GlobalConfigsConnector" + ) + qmlRegisterType( + DocPreviewConnector, "DocPreviewConnector", 1, 0, "DocPreviewConnector" + ) + + # ==================== 5. 启动翻译 ==================== + I18n.init(qtApp) + + # ==================== 6. 启动qml引擎 ==================== + engine = QQmlApplicationEngine() + if engineAddImportPath: + engine.addImportPath(engineAddImportPath) # 相对路径重新导入包 + engine.addImageProvider("pixmapprovider", PixmapProvider) # 注册图片提供器 + rootContext = engine.rootContext() # 注册常量 + rootContext.setContextProperty("UmiAbout", UmiAbout) + engine.load("qt_res/qml/Main.qml") # 通过本地文件启动 + if not engine.rootObjects(): + return 1 + res = qtApp.exec_() + if res != 0: + msg = f"Umi-OCR 异常退出。代码:{str(res)}\nUmi-OCR exited abnormally. Code: {str(res)}" + os.MessageBox(msg) + logger.info("QML引擎关闭") + + +def main(app_path, engineAddImportPath=""): + """ + `app_path`: 程序入口文件 路径\n + `engineAddImportPath`: 可选,qml包路径\n + """ + # 初始化运行信息 + site.addsitedir("./py_src/imports") # 自定义库添加到搜索路径 + import umi_about + + if not umi_about.init(app_path): # 初始化版本信息,失败则结束运行 + sys.exit(0) + + from .utils import pre_configs + from .server.cmd_client import initCmd + + # 安装某些软件时可能在系统中写入 QMLSCENE_DEVICE 环境变量,影响本软件的渲染方式,因此屏蔽该环境变量 + if "QMLSCENE_DEVICE" in os.environ: + del os.environ["QMLSCENE_DEVICE"] + + pre_configs.readConfigs() # 初始化预配置项 + if not initCmd(): # 初始化命令行,如果已有Umi-OCR在运行则结束运行 + sys.exit(0) + runQml(engineAddImportPath) # 启动qml + sys.exit(0) diff --git a/UmiOCR-data/py_src/server/bottle.py b/UmiOCR-data/py_src/server/bottle.py new file mode 100644 index 0000000..5e35303 --- /dev/null +++ b/UmiOCR-data/py_src/server/bottle.py @@ -0,0 +1,4204 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Bottle is a fast and simple micro-framework for small web applications. It +offers request dispatching (Routes) with url parameter support, templates, +a built-in HTTP Server and adapters for many third party WSGI/HTTP-server and +template engines - all in a single file and with no dependencies other than the +Python Standard Library. + +Homepage and documentation: http://bottlepy.org/ + +Copyright (c) 2016, Marcel Hellkamp. +License: MIT (see LICENSE for details) + +适用于Umi-OCR项目的Bottle,经由hiroi-sora修改。 +遵守原作者的开源协议,修改和定制同样遵循原协议。 +""" + +from __future__ import with_statement + +__author__ = "Marcel Hellkamp" +__version__ = "0.12.25" +__license__ = "MIT" + +# The gevent server adapter needs to patch some modules before they are imported +# This is why we parse the commandline parameters here but handle them later +if __name__ == "__main__": + from optparse import OptionParser + + _cmd_parser = OptionParser(usage="usage: %prog [options] package.module:app") + _opt = _cmd_parser.add_option + _opt("--version", action="store_true", help="show version number.") + _opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.") + _opt("-s", "--server", default="wsgiref", help="use SERVER as backend.") + _opt("-p", "--plugin", action="append", help="install additional plugin/s.") + _opt("--debug", action="store_true", help="start server in debug mode.") + _opt("--reload", action="store_true", help="auto-reload on file changes.") + _cmd_options, _cmd_args = _cmd_parser.parse_args() + if _cmd_options.server and _cmd_options.server.startswith("gevent"): + import gevent.monkey + + gevent.monkey.patch_all() + +import base64, cgi, email.utils, functools, hmac, itertools, mimetypes, os, re, subprocess, sys, tempfile, threading, time, warnings, hashlib + +from datetime import date as datedate, datetime, timedelta +from tempfile import TemporaryFile +from traceback import format_exc, print_exc +from unicodedata import normalize + + +try: + from simplejson import dumps as json_dumps, loads as json_lds +except ImportError: # pragma: no cover + try: + from json import dumps as json_dumps, loads as json_lds + except ImportError: + try: + from django.utils.simplejson import dumps as json_dumps, loads as json_lds + except ImportError: + + def json_dumps(data): + raise ImportError("JSON support requires Python 2.6 or simplejson.") + + json_lds = json_dumps + + +# We now try to fix 2.5/2.6/3.1/3.2 incompatibilities. +# It ain't pretty but it works... Sorry for the mess. + +py = sys.version_info +py3k = py >= (3, 0, 0) +py25 = py < (2, 6, 0) +py31 = (3, 1, 0) <= py < (3, 2, 0) + + +# Workaround for the missing "as" keyword in py3k. +def _e(): + return sys.exc_info()[1] + + +# Workaround for the "print is a keyword/function" Python 2/3 dilemma +# and a fallback for mod_wsgi (resticts stdout/err attribute access) +try: + _stdout, _stderr = sys.stdout.write, sys.stderr.write +except IOError: + _stdout = lambda x: sys.stdout.write(x) + _stderr = lambda x: sys.stderr.write(x) + +# Lots of stdlib and builtin differences. +if py3k: + import http.client as httplib + import _thread as thread + from urllib.parse import urljoin, SplitResult as UrlSplitResult + from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote + + urlunquote = functools.partial(urlunquote, encoding="latin1") + from http.cookies import SimpleCookie + + if py >= (3, 3, 0): + from collections.abc import MutableMapping as DictMixin + from types import ModuleType as new_module + else: + from collections import MutableMapping as DictMixin + from imp import new_module + import pickle + from io import BytesIO + from configparser import ConfigParser + from inspect import getfullargspec + + def getargspec(func): + spec = getfullargspec(func) + kwargs = makelist(spec[0]) + makelist(spec.kwonlyargs) + return kwargs, spec[1], spec[2], spec[3] + + basestring = str + unicode = str + json_loads = lambda s: json_lds(touni(s)) + callable = lambda x: hasattr(x, "__call__") + imap = map + + def _raise(*a): + raise a[0](a[1]).with_traceback(a[2]) + +else: # 2.x + import httplib + import thread + from urlparse import urljoin, SplitResult as UrlSplitResult + from urllib import urlencode, quote as urlquote, unquote as urlunquote + from Cookie import SimpleCookie + from itertools import imap + import cPickle as pickle + from imp import new_module + from StringIO import StringIO as BytesIO + from ConfigParser import SafeConfigParser as ConfigParser + from inspect import getargspec + + if py25: + msg = "Python 2.5 support may be dropped in future versions of Bottle." + warnings.warn(msg, DeprecationWarning) + from UserDict import DictMixin + + def next(it): + return it.next() + + bytes = str + else: # 2.6, 2.7 + from collections import MutableMapping as DictMixin + unicode = unicode + json_loads = json_lds + eval(compile("def _raise(*a): raise a[0], a[1], a[2]", "", "exec")) + + +# Some helpers for string/byte handling +def tob(s, enc="utf8"): + return s.encode(enc) if isinstance(s, unicode) else bytes(s) + + +def touni(s, enc="utf8", err="strict"): + return s.decode(enc, err) if isinstance(s, bytes) else unicode(s) + + +tonat = touni if py3k else tob + +# 3.2 fixes cgi.FieldStorage to accept bytes (which makes a lot of sense). +# 3.1 needs a workaround. +if py31: + from io import TextIOWrapper + + class NCTextIOWrapper(TextIOWrapper): + def close(self): + pass # Keep wrapped buffer open. + + +# A bug in functools causes it to break if the wrapper is an instance method +def update_wrapper(wrapper, wrapped, *a, **ka): + try: + functools.update_wrapper(wrapper, wrapped, *a, **ka) + except AttributeError: + pass + + +# These helpers are used at module level and need to be defined first. +# And yes, I know PEP-8, but sometimes a lower-case classname makes more sense. + + +def depr(message, hard=False): + warnings.warn(message, DeprecationWarning, stacklevel=3) + + +def makelist(data): # This is just to handy + if isinstance(data, (tuple, list, set, dict)): + return list(data) + elif data: + return [data] + else: + return [] + + +class DictProperty(object): + """Property that maps to a key in a local dict-like attribute.""" + + def __init__(self, attr, key=None, read_only=False): + self.attr, self.key, self.read_only = attr, key, read_only + + def __call__(self, func): + functools.update_wrapper(self, func, updated=[]) + self.getter, self.key = func, self.key or func.__name__ + return self + + def __get__(self, obj, cls): + if obj is None: + return self + key, storage = self.key, getattr(obj, self.attr) + if key not in storage: + storage[key] = self.getter(obj) + return storage[key] + + def __set__(self, obj, value): + if self.read_only: + raise AttributeError("Read-Only property.") + getattr(obj, self.attr)[self.key] = value + + def __delete__(self, obj): + if self.read_only: + raise AttributeError("Read-Only property.") + del getattr(obj, self.attr)[self.key] + + +class cached_property(object): + """A property that is only computed once per instance and then replaces + itself with an ordinary attribute. Deleting the attribute resets the + property.""" + + def __init__(self, func): + self.__doc__ = getattr(func, "__doc__") + self.func = func + + def __get__(self, obj, cls): + if obj is None: + return self + value = obj.__dict__[self.func.__name__] = self.func(obj) + return value + + +class lazy_attribute(object): + """A property that caches itself to the class object.""" + + def __init__(self, func): + functools.update_wrapper(self, func, updated=[]) + self.getter = func + + def __get__(self, obj, cls): + value = self.getter(cls) + setattr(cls, self.__name__, value) + return value + + +############################################################################### +# Exceptions and Events ######################################################## +############################################################################### + + +class BottleException(Exception): + """A base class for exceptions used by bottle.""" + + pass + + +############################################################################### +# Routing ###################################################################### +############################################################################### + + +class RouteError(BottleException): + """This is a base class for all routing related exceptions""" + + +class RouteReset(BottleException): + """If raised by a plugin or request handler, the route is reset and all + plugins are re-applied.""" + + +class RouterUnknownModeError(RouteError): + pass + + +class RouteSyntaxError(RouteError): + """The route parser found something not supported by this router.""" + + +class RouteBuildError(RouteError): + """The route could not be built.""" + + +def _re_flatten(p): + """Turn all capturing groups in a regular expression pattern into + non-capturing groups.""" + if "(" not in p: + return p + return re.sub( + r"(\\*)(\(\?P<[^>]+>|\((?!\?))", + lambda m: m.group(0) if len(m.group(1)) % 2 else m.group(1) + "(?:", + p, + ) + + +class Router(object): + """A Router is an ordered collection of route->target pairs. It is used to + efficiently match WSGI requests against a number of routes and return + the first target that satisfies the request. The target may be anything, + usually a string, ID or callable object. A route consists of a path-rule + and a HTTP method. + + The path-rule is either a static path (e.g. `/contact`) or a dynamic + path that contains wildcards (e.g. `/wiki/`). The wildcard syntax + and details on the matching order are described in docs:`routing`. + """ + + default_pattern = "[^/]+" + default_filter = "re" + + #: The current CPython regexp implementation does not allow more + #: than 99 matching groups per regular expression. + _MAX_GROUPS_PER_PATTERN = 99 + + def __init__(self, strict=False): + self.rules = [] # All rules in order + self._groups = {} # index of regexes to find them in dyna_routes + self.builder = {} # Data structure for the url builder + self.static = {} # Search structure for static routes + self.dyna_routes = {} + self.dyna_regexes = {} # Search structure for dynamic routes + #: If true, static routes are no longer checked first. + self.strict_order = strict + self.filters = { + "re": lambda conf: (_re_flatten(conf or self.default_pattern), None, None), + "int": lambda conf: (r"-?\d+", int, lambda x: str(int(x))), + "float": lambda conf: (r"-?[\d.]+", float, lambda x: str(float(x))), + "path": lambda conf: (r".+?", None, None), + } + + def add_filter(self, name, func): + """Add a filter. The provided function is called with the configuration + string as parameter and must return a (regexp, to_python, to_url) tuple. + The first element is a string, the last two are callables or None.""" + self.filters[name] = func + + rule_syntax = re.compile( + "(\\\\*)" + "(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)" + "|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)" + "(?::((?:\\\\.|[^\\\\>])+)?)?)?>))" + ) + + def _itertokens(self, rule): + offset, prefix = 0, "" + for match in self.rule_syntax.finditer(rule): + prefix += rule[offset : match.start()] + g = match.groups() + if len(g[0]) % 2: # Escaped wildcard + prefix += match.group(0)[len(g[0]) :] + offset = match.end() + continue + if prefix: + yield prefix, None, None + name, filtr, conf = g[4:7] if g[2] is None else g[1:4] + yield name, filtr or "default", conf or None + offset, prefix = match.end(), "" + if offset <= len(rule) or prefix: + yield prefix + rule[offset:], None, None + + def add(self, rule, method, target, name=None): + """Add a new rule or replace the target for an existing rule.""" + anons = 0 # Number of anonymous wildcards found + keys = [] # Names of keys + pattern = "" # Regular expression pattern with named groups + filters = [] # Lists of wildcard input filters + builder = [] # Data structure for the URL builder + is_static = True + + for key, mode, conf in self._itertokens(rule): + if mode: + is_static = False + if mode == "default": + mode = self.default_filter + mask, in_filter, out_filter = self.filters[mode](conf) + if not key: + pattern += "(?:%s)" % mask + key = "anon%d" % anons + anons += 1 + else: + pattern += "(?P<%s>%s)" % (key, mask) + keys.append(key) + if in_filter: + filters.append((key, in_filter)) + builder.append((key, out_filter or str)) + elif key: + pattern += re.escape(key) + builder.append((None, key)) + + self.builder[rule] = builder + if name: + self.builder[name] = builder + + if is_static and not self.strict_order: + self.static.setdefault(method, {}) + self.static[method][self.build(rule)] = (target, None) + return + + try: + re_pattern = re.compile("^(%s)$" % pattern) + re_match = re_pattern.match + except re.error: + raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, _e())) + + if filters: + + def getargs(path): + url_args = re_match(path).groupdict() + for name, wildcard_filter in filters: + try: + url_args[name] = wildcard_filter(url_args[name]) + except ValueError: + raise HTTPError(400, "Path has wrong format.") + return url_args + + elif re_pattern.groupindex: + + def getargs(path): + return re_match(path).groupdict() + + else: + getargs = None + + flatpat = _re_flatten(pattern) + whole_rule = (rule, flatpat, target, getargs) + + if (flatpat, method) in self._groups: + if DEBUG: + msg = "Route <%s %s> overwrites a previously defined route" + warnings.warn(msg % (method, rule), RuntimeWarning) + self.dyna_routes[method][self._groups[flatpat, method]] = whole_rule + else: + self.dyna_routes.setdefault(method, []).append(whole_rule) + self._groups[flatpat, method] = len(self.dyna_routes[method]) - 1 + + self._compile(method) + + def _compile(self, method): + all_rules = self.dyna_routes[method] + comborules = self.dyna_regexes[method] = [] + maxgroups = self._MAX_GROUPS_PER_PATTERN + for x in range(0, len(all_rules), maxgroups): + some = all_rules[x : x + maxgroups] + combined = (flatpat for (_, flatpat, _, _) in some) + combined = "|".join("(^%s$)" % flatpat for flatpat in combined) + combined = re.compile(combined).match + rules = [(target, getargs) for (_, _, target, getargs) in some] + comborules.append((combined, rules)) + + def build(self, _name, *anons, **query): + """Build an URL by filling the wildcards in a rule.""" + builder = self.builder.get(_name) + if not builder: + raise RouteBuildError("No route with that name.", _name) + try: + for i, value in enumerate(anons): + query["anon%d" % i] = value + url = "".join([f(query.pop(n)) if n else f for (n, f) in builder]) + return url if not query else url + "?" + urlencode(query) + except KeyError: + raise RouteBuildError("Missing URL argument: %r" % _e().args[0]) + + def match(self, environ): + """Return a (target, url_agrs) tuple or raise HTTPError(400/404/405).""" + verb = environ["REQUEST_METHOD"].upper() + path = environ["PATH_INFO"] or "/" + target = None + if verb == "HEAD": + methods = ["PROXY", verb, "GET", "ANY"] + else: + methods = ["PROXY", verb, "ANY"] + + for method in methods: + if method in self.static and path in self.static[method]: + target, getargs = self.static[method][path] + return target, getargs(path) if getargs else {} + elif method in self.dyna_regexes: + for combined, rules in self.dyna_regexes[method]: + match = combined(path) + if match: + target, getargs = rules[match.lastindex - 1] + return target, getargs(path) if getargs else {} + + # No matching route found. Collect alternative methods for 405 response + allowed = set([]) + nocheck = set(methods) + for method in set(self.static) - nocheck: + if path in self.static[method]: + allowed.add(method) + for method in set(self.dyna_regexes) - allowed - nocheck: + for combined, rules in self.dyna_regexes[method]: + match = combined(path) + if match: + allowed.add(method) + if allowed: + allow_header = ",".join(sorted(allowed)) + raise HTTPError(405, "Method not allowed.", Allow=allow_header) + + # No matching route and no alternative method found. We give up + raise HTTPError(404, "Not found: " + repr(path)) + + +class Route(object): + """This class wraps a route callback along with route specific metadata and + configuration and applies Plugins on demand. It is also responsible for + turing an URL path rule into a regular expression usable by the Router. + """ + + def __init__( + self, + app, + rule, + method, + callback, + name=None, + plugins=None, + skiplist=None, + **config + ): + #: The application this route is installed to. + self.app = app + #: The path-rule string (e.g. ``/wiki/:page``). + self.rule = rule + #: The HTTP method as a string (e.g. ``GET``). + self.method = method + #: The original callback with no plugins applied. Useful for introspection. + self.callback = callback + #: The name of the route (if specified) or ``None``. + self.name = name or None + #: A list of route-specific plugins (see :meth:`Bottle.route`). + self.plugins = plugins or [] + #: A list of plugins to not apply to this route (see :meth:`Bottle.route`). + self.skiplist = skiplist or [] + #: Additional keyword arguments passed to the :meth:`Bottle.route` + #: decorator are stored in this dictionary. Used for route-specific + #: plugin configuration and meta-data. + self.config = ConfigDict().load_dict(config, make_namespaces=True) + + def __call__(self, *a, **ka): + depr( + "Some APIs changed to return Route() instances instead of" + " callables. Make sure to use the Route.call method and not to" + " call Route instances directly." + ) # 0.12 + return self.call(*a, **ka) + + @cached_property + def call(self): + """The route callback with all plugins applied. This property is + created on demand and then cached to speed up subsequent requests.""" + return self._make_callback() + + def reset(self): + """Forget any cached values. The next time :attr:`call` is accessed, + all plugins are re-applied.""" + self.__dict__.pop("call", None) + + def prepare(self): + """Do all on-demand work immediately (useful for debugging).""" + self.call + + @property + def _context(self): + depr("Switch to Plugin API v2 and access the Route object directly.") # 0.12 + return dict( + rule=self.rule, + method=self.method, + callback=self.callback, + name=self.name, + app=self.app, + config=self.config, + apply=self.plugins, + skip=self.skiplist, + ) + + def all_plugins(self): + """Yield all Plugins affecting this route.""" + unique = set() + for p in reversed(self.app.plugins + self.plugins): + if True in self.skiplist: + break + name = getattr(p, "name", False) + if name and (name in self.skiplist or name in unique): + continue + if p in self.skiplist or type(p) in self.skiplist: + continue + if name: + unique.add(name) + yield p + + def _make_callback(self): + callback = self.callback + for plugin in self.all_plugins(): + try: + if hasattr(plugin, "apply"): + api = getattr(plugin, "api", 1) + context = self if api > 1 else self._context + callback = plugin.apply(callback, context) + else: + callback = plugin(callback) + except RouteReset: # Try again with changed configuration. + return self._make_callback() + if not callback is self.callback: + update_wrapper(callback, self.callback) + return callback + + def get_undecorated_callback(self): + """Return the callback. If the callback is a decorated function, try to + recover the original function.""" + func = self.callback + func = getattr(func, "__func__" if py3k else "im_func", func) + closure_attr = "__closure__" if py3k else "func_closure" + while hasattr(func, closure_attr) and getattr(func, closure_attr): + func = getattr(func, closure_attr)[0].cell_contents + return func + + def get_callback_args(self): + """Return a list of argument names the callback (most likely) accepts + as keyword arguments. If the callback is a decorated function, try + to recover the original function before inspection.""" + return getargspec(self.get_undecorated_callback())[0] + + def get_config(self, key, default=None): + """Lookup a config field and return its value, first checking the + route.config, then route.app.config.""" + for conf in (self.config, self.app.config): + if key in conf: + return conf[key] + return default + + def __repr__(self): + cb = self.get_undecorated_callback() + return "<%s %r %r>" % (self.method, self.rule, cb) + + +############################################################################### +# Application Object ########################################################### +############################################################################### + + +class Bottle(object): + """Each Bottle object represents a single, distinct web application and + consists of routes, callbacks, plugins, resources and configuration. + Instances are callable WSGI applications. + + :param catchall: If true (default), handle all exceptions. Turn off to + let debugging middleware handle exceptions. + """ + + def __init__(self, catchall=True, autojson=True): + #: A :class:`ConfigDict` for app specific configuration. + self.config = ConfigDict() + self.config._on_change = functools.partial(self.trigger_hook, "config") + self.config.meta_set("autojson", "validate", bool) + self.config.meta_set("catchall", "validate", bool) + self.config["catchall"] = catchall + self.config["autojson"] = autojson + + #: A :class:`ResourceManager` for application files + self.resources = ResourceManager() + + self.routes = [] # List of installed :class:`Route` instances. + self.router = Router() # Maps requests to :class:`Route` instances. + self.error_handler = {} + + # Core plugins + self.plugins = [] # List of installed plugins. + if self.config["autojson"]: + self.install(JSONPlugin()) + self.install(TemplatePlugin()) + + #: If true, most exceptions are caught and returned as :exc:`HTTPError` + catchall = DictProperty("config", "catchall") + + __hook_names = "before_request", "after_request", "app_reset", "config" + __hook_reversed = "after_request" + + @cached_property + def _hooks(self): + return dict((name, []) for name in self.__hook_names) + + def add_hook(self, name, func): + """Attach a callback to a hook. Three hooks are currently implemented: + + before_request + Executed once before each request. The request context is + available, but no routing has happened yet. + after_request + Executed once after each request regardless of its outcome. + app_reset + Called whenever :meth:`Bottle.reset` is called. + """ + if name in self.__hook_reversed: + self._hooks[name].insert(0, func) + else: + self._hooks[name].append(func) + + def remove_hook(self, name, func): + """Remove a callback from a hook.""" + if name in self._hooks and func in self._hooks[name]: + self._hooks[name].remove(func) + return True + + def trigger_hook(self, __name, *args, **kwargs): + """Trigger a hook and return a list of results.""" + return [hook(*args, **kwargs) for hook in self._hooks[__name][:]] + + def hook(self, name): + """Return a decorator that attaches a callback to a hook. See + :meth:`add_hook` for details.""" + + def decorator(func): + self.add_hook(name, func) + return func + + return decorator + + def mount(self, prefix, app, **options): + """Mount an application (:class:`Bottle` or plain WSGI) to a specific + URL prefix. Example:: + + root_app.mount('/admin/', admin_app) + + :param prefix: path prefix or `mount-point`. If it ends in a slash, + that slash is mandatory. + :param app: an instance of :class:`Bottle` or a WSGI application. + + All other parameters are passed to the underlying :meth:`route` call. + """ + if isinstance(app, basestring): + depr("Parameter order of Bottle.mount() changed.", True) # 0.10 + + segments = [p for p in prefix.split("/") if p] + if not segments: + raise ValueError("Empty path prefix.") + path_depth = len(segments) + + def mountpoint_wrapper(): + try: + request.path_shift(path_depth) + rs = HTTPResponse([]) + + def start_response(status, headerlist, exc_info=None): + if exc_info: + try: + _raise(*exc_info) + finally: + exc_info = None + rs.status = status + for name, value in headerlist: + rs.add_header(name, value) + return rs.body.append + + body = app(request.environ, start_response) + if body and rs.body: + body = itertools.chain(rs.body, body) + rs.body = body or rs.body + return rs + finally: + request.path_shift(-path_depth) + + options.setdefault("skip", True) + options.setdefault("method", "PROXY") + options.setdefault("mountpoint", {"prefix": prefix, "target": app}) + options["callback"] = mountpoint_wrapper + + self.route("/%s/<:re:.*>" % "/".join(segments), **options) + if not prefix.endswith("/"): + self.route("/" + "/".join(segments), **options) + + def merge(self, routes): + """Merge the routes of another :class:`Bottle` application or a list of + :class:`Route` objects into this application. The routes keep their + 'owner', meaning that the :data:`Route.app` attribute is not + changed.""" + if isinstance(routes, Bottle): + routes = routes.routes + for route in routes: + self.add_route(route) + + def install(self, plugin): + """Add a plugin to the list of plugins and prepare it for being + applied to all routes of this application. A plugin may be a simple + decorator or an object that implements the :class:`Plugin` API. + """ + if hasattr(plugin, "setup"): + plugin.setup(self) + if not callable(plugin) and not hasattr(plugin, "apply"): + raise TypeError("Plugins must be callable or implement .apply()") + self.plugins.append(plugin) + self.reset() + return plugin + + def uninstall(self, plugin): + """Uninstall plugins. Pass an instance to remove a specific plugin, a type + object to remove all plugins that match that type, a string to remove + all plugins with a matching ``name`` attribute or ``True`` to remove all + plugins. Return the list of removed plugins.""" + removed, remove = [], plugin + for i, plugin in list(enumerate(self.plugins))[::-1]: + if ( + remove is True + or remove is plugin + or remove is type(plugin) + or getattr(plugin, "name", True) == remove + ): + removed.append(plugin) + del self.plugins[i] + if hasattr(plugin, "close"): + plugin.close() + if removed: + self.reset() + return removed + + def reset(self, route=None): + """Reset all routes (force plugins to be re-applied) and clear all + caches. If an ID or route object is given, only that specific route + is affected.""" + if route is None: + routes = self.routes + elif isinstance(route, Route): + routes = [route] + else: + routes = [self.routes[route]] + for route in routes: + route.reset() + if DEBUG: + for route in routes: + route.prepare() + self.trigger_hook("app_reset") + + def close(self): + """Close the application and all installed plugins.""" + for plugin in self.plugins: + if hasattr(plugin, "close"): + plugin.close() + self.stopped = True + + def run(self, **kwargs): + """Calls :func:`run` with the same parameters.""" + run(self, **kwargs) + + def match(self, environ): + """Search for a matching route and return a (:class:`Route` , urlargs) + tuple. The second value is a dictionary with parameters extracted + from the URL. Raise :exc:`HTTPError` (404/405) on a non-match.""" + return self.router.match(environ) + + def get_url(self, routename, **kargs): + """Return a string that matches a named route""" + scriptname = request.environ.get("SCRIPT_NAME", "").strip("/") + "/" + location = self.router.build(routename, **kargs).lstrip("/") + return urljoin(urljoin("/", scriptname), location) + + def add_route(self, route): + """Add a route object, but do not change the :data:`Route.app` + attribute.""" + self.routes.append(route) + self.router.add(route.rule, route.method, route, name=route.name) + if DEBUG: + route.prepare() + + def route( + self, + path=None, + method="GET", + callback=None, + name=None, + apply=None, + skip=None, + **config + ): + """A decorator to bind a function to a request URL. Example:: + + @app.route('/hello/:name') + def hello(name): + return 'Hello %s' % name + + The ``:name`` part is a wildcard. See :class:`Router` for syntax + details. + + :param path: Request path or a list of paths to listen to. If no + path is specified, it is automatically generated from the + signature of the function. + :param method: HTTP method (`GET`, `POST`, `PUT`, ...) or a list of + methods to listen to. (default: `GET`) + :param callback: An optional shortcut to avoid the decorator + syntax. ``route(..., callback=func)`` equals ``route(...)(func)`` + :param name: The name for this route. (default: None) + :param apply: A decorator or plugin or a list of plugins. These are + applied to the route callback in addition to installed plugins. + :param skip: A list of plugins, plugin classes or names. Matching + plugins are not installed to this route. ``True`` skips all. + + Any additional keyword arguments are stored as route-specific + configuration and passed to plugins (see :meth:`Plugin.apply`). + """ + if callable(path): + path, callback = None, path + plugins = makelist(apply) + skiplist = makelist(skip) + + def decorator(callback): + # TODO: Documentation and tests + if isinstance(callback, basestring): + callback = load(callback) + for rule in makelist(path) or yieldroutes(callback): + for verb in makelist(method): + verb = verb.upper() + route = Route( + self, + rule, + verb, + callback, + name=name, + plugins=plugins, + skiplist=skiplist, + **config + ) + self.add_route(route) + return callback + + return decorator(callback) if callback else decorator + + def get(self, path=None, method="GET", **options): + """Equals :meth:`route`.""" + return self.route(path, method, **options) + + def post(self, path=None, method="POST", **options): + """Equals :meth:`route` with a ``POST`` method parameter.""" + return self.route(path, method, **options) + + def put(self, path=None, method="PUT", **options): + """Equals :meth:`route` with a ``PUT`` method parameter.""" + return self.route(path, method, **options) + + def delete(self, path=None, method="DELETE", **options): + """Equals :meth:`route` with a ``DELETE`` method parameter.""" + return self.route(path, method, **options) + + def error(self, code=500): + """Decorator: Register an output handler for a HTTP error code""" + + def wrapper(handler): + self.error_handler[int(code)] = handler + return handler + + return wrapper + + def default_error_handler(self, res): + return tob(template(ERROR_PAGE_TEMPLATE, e=res)) + + def _handle(self, environ): + try: + environ["bottle.app"] = self + request.bind(environ) + response.bind() + + path = environ["bottle.raw_path"] = environ["PATH_INFO"] + if py3k: + try: + environ["PATH_INFO"] = path.encode("latin1").decode("utf8") + except UnicodeError: + return HTTPError(400, "Invalid path string. Expected UTF-8") + + try: + self.trigger_hook("before_request") + route, args = self.router.match(environ) + environ["route.handle"] = route + environ["bottle.route"] = route + environ["route.url_args"] = args + return route.call(**args) + finally: + self.trigger_hook("after_request") + + except HTTPResponse: + return _e() + except RouteReset: + route.reset() + return self._handle(environ) + except (KeyboardInterrupt, SystemExit, MemoryError): + raise + except Exception: + if not self.catchall: + raise + stacktrace = format_exc() + environ["wsgi.errors"].write(stacktrace) + return HTTPError(500, "Internal Server Error", _e(), stacktrace) + + def _cast(self, out, peek=None): + """Try to convert the parameter into something WSGI compatible and set + correct HTTP headers when possible. + Support: False, str, unicode, dict, HTTPResponse, HTTPError, file-like, + iterable of strings and iterable of unicodes + """ + + # Empty output is done here + if not out: + if "Content-Length" not in response: + response["Content-Length"] = 0 + return [] + # Join lists of byte or unicode strings. Mixed lists are NOT supported + if isinstance(out, (tuple, list)) and isinstance(out[0], (bytes, unicode)): + out = out[0][0:0].join(out) # b'abc'[0:0] -> b'' + # Encode unicode strings + if isinstance(out, unicode): + out = out.encode(response.charset) + # Byte Strings are just returned + if isinstance(out, bytes): + if "Content-Length" not in response: + response["Content-Length"] = len(out) + return [out] + # HTTPError or HTTPException (recursive, because they may wrap anything) + # TODO: Handle these explicitly in handle() or make them iterable. + if isinstance(out, HTTPError): + out.apply(response) + out = self.error_handler.get(out.status_code, self.default_error_handler)( + out + ) + return self._cast(out) + if isinstance(out, HTTPResponse): + out.apply(response) + return self._cast(out.body) + + # File-like objects. + if hasattr(out, "read"): + if "wsgi.file_wrapper" in request.environ: + return request.environ["wsgi.file_wrapper"](out) + elif hasattr(out, "close") or not hasattr(out, "__iter__"): + return WSGIFileWrapper(out) + + # Handle Iterables. We peek into them to detect their inner type. + try: + iout = iter(out) + first = next(iout) + while not first: + first = next(iout) + except StopIteration: + return self._cast("") + except HTTPResponse: + first = _e() + except (KeyboardInterrupt, SystemExit, MemoryError): + raise + except Exception: + if not self.catchall: + raise + first = HTTPError(500, "Unhandled exception", _e(), format_exc()) + + # These are the inner types allowed in iterator or generator objects. + if isinstance(first, HTTPResponse): + return self._cast(first) + elif isinstance(first, bytes): + new_iter = itertools.chain([first], iout) + elif isinstance(first, unicode): + encoder = lambda x: x.encode(response.charset) + new_iter = imap(encoder, itertools.chain([first], iout)) + else: + msg = "Unsupported response type: %s" % type(first) + return self._cast(HTTPError(500, msg)) + if hasattr(out, "close"): + new_iter = _closeiter(new_iter, out.close) + return new_iter + + def wsgi(self, environ, start_response): + """The bottle WSGI-interface.""" + try: + out = self._cast(self._handle(environ)) + # rfc2616 section 4.3 + if ( + response._status_code in (100, 101, 204, 304) + or environ["REQUEST_METHOD"] == "HEAD" + ): + if hasattr(out, "close"): + out.close() + out = [] + start_response(response._status_line, response.headerlist) + return out + except (KeyboardInterrupt, SystemExit, MemoryError): + raise + except Exception: + if not self.catchall: + raise + err = "

Critical error while processing request: %s

" % html_escape( + environ.get("PATH_INFO", "/") + ) + if DEBUG: + err += ( + "

Error:

\n
\n%s\n
\n" + "

Traceback:

\n
\n%s\n
\n" + % (html_escape(repr(_e())), html_escape(format_exc())) + ) + environ["wsgi.errors"].write(err) + headers = [("Content-Type", "text/html; charset=UTF-8")] + start_response("500 INTERNAL SERVER ERROR", headers, sys.exc_info()) + return [tob(err)] + + def __call__(self, environ, start_response): + """Each instance of :class:'Bottle' is a WSGI application.""" + return self.wsgi(environ, start_response) + + +############################################################################### +# HTTP and WSGI Tools ########################################################## +############################################################################### + + +class BaseRequest(object): + """A wrapper for WSGI environment dictionaries that adds a lot of + convenient access methods and properties. Most of them are read-only. + + Adding new attributes to a request actually adds them to the environ + dictionary (as 'bottle.request.ext.'). This is the recommended + way to store and access request-specific data. + """ + + __slots__ = "environ" + + #: Maximum size of memory buffer for :attr:`body` in bytes. + MEMFILE_MAX = 102400 + + def __init__(self, environ=None): + """Wrap a WSGI environ dictionary.""" + #: The wrapped WSGI environ dictionary. This is the only real attribute. + #: All other attributes actually are read-only properties. + self.environ = {} if environ is None else environ + self.environ["bottle.request"] = self + + @DictProperty("environ", "bottle.app", read_only=True) + def app(self): + """Bottle application handling this request.""" + raise RuntimeError("This request is not connected to an application.") + + @DictProperty("environ", "bottle.route", read_only=True) + def route(self): + """The bottle :class:`Route` object that matches this request.""" + raise RuntimeError("This request is not connected to a route.") + + @DictProperty("environ", "route.url_args", read_only=True) + def url_args(self): + """The arguments extracted from the URL.""" + raise RuntimeError("This request is not connected to a route.") + + @property + def path(self): + """The value of ``PATH_INFO`` with exactly one prefixed slash (to fix + broken clients and avoid the "empty path" edge case).""" + return "/" + self.environ.get("PATH_INFO", "").lstrip("/") + + @property + def method(self): + """The ``REQUEST_METHOD`` value as an uppercase string.""" + return self.environ.get("REQUEST_METHOD", "GET").upper() + + @DictProperty("environ", "bottle.request.headers", read_only=True) + def headers(self): + """A :class:`WSGIHeaderDict` that provides case-insensitive access to + HTTP request headers.""" + return WSGIHeaderDict(self.environ) + + def get_header(self, name, default=None): + """Return the value of a request header, or a given default value.""" + return self.headers.get(name, default) + + @DictProperty("environ", "bottle.request.cookies", read_only=True) + def cookies(self): + """Cookies parsed into a :class:`FormsDict`. Signed cookies are NOT + decoded. Use :meth:`get_cookie` if you expect signed cookies.""" + cookies = SimpleCookie(self.environ.get("HTTP_COOKIE", "")).values() + return FormsDict((c.key, c.value) for c in cookies) + + def get_cookie(self, key, default=None, secret=None): + """Return the content of a cookie. To read a `Signed Cookie`, the + `secret` must match the one used to create the cookie (see + :meth:`BaseResponse.set_cookie`). If anything goes wrong (missing + cookie or wrong signature), return a default value.""" + value = self.cookies.get(key) + if secret and value: + dec = cookie_decode(value, secret) # (key, value) tuple or None + return dec[1] if dec and dec[0] == key else default + return value or default + + @DictProperty("environ", "bottle.request.query", read_only=True) + def query(self): + """The :attr:`query_string` parsed into a :class:`FormsDict`. These + values are sometimes called "URL arguments" or "GET parameters", but + not to be confused with "URL wildcards" as they are provided by the + :class:`Router`.""" + get = self.environ["bottle.get"] = FormsDict() + pairs = _parse_qsl(self.environ.get("QUERY_STRING", "")) + for key, value in pairs: + get[key] = value + return get + + @DictProperty("environ", "bottle.request.forms", read_only=True) + def forms(self): + """Form values parsed from an `url-encoded` or `multipart/form-data` + encoded POST or PUT request body. The result is returned as a + :class:`FormsDict`. All keys and values are strings. File uploads + are stored separately in :attr:`files`.""" + forms = FormsDict() + forms.recode_unicode = self.POST.recode_unicode + for name, item in self.POST.allitems(): + if not isinstance(item, FileUpload): + forms[name] = item + return forms + + @DictProperty("environ", "bottle.request.params", read_only=True) + def params(self): + """A :class:`FormsDict` with the combined values of :attr:`query` and + :attr:`forms`. File uploads are stored in :attr:`files`.""" + params = FormsDict() + for key, value in self.query.allitems(): + params[key] = value + for key, value in self.forms.allitems(): + params[key] = value + return params + + @DictProperty("environ", "bottle.request.files", read_only=True) + def files(self): + """File uploads parsed from `multipart/form-data` encoded POST or PUT + request body. The values are instances of :class:`FileUpload`. + + """ + files = FormsDict() + files.recode_unicode = self.POST.recode_unicode + for name, item in self.POST.allitems(): + if isinstance(item, FileUpload): + files[name] = item + return files + + @DictProperty("environ", "bottle.request.json", read_only=True) + def json(self): + """If the ``Content-Type`` header is ``application/json``, this + property holds the parsed content of the request body. Only requests + smaller than :attr:`MEMFILE_MAX` are processed to avoid memory + exhaustion.""" + ctype = self.environ.get("CONTENT_TYPE", "").lower().split(";")[0] + if ctype == "application/json": + b = self._get_body_string() + if not b: + return None + return json_loads(b) + return None + + def _iter_body(self, read, bufsize): + maxread = max(0, self.content_length) + while maxread: + part = read(min(maxread, bufsize)) + if not part: + break + yield part + maxread -= len(part) + + def _iter_chunked(self, read, bufsize): + err = HTTPError(400, "Error while parsing chunked transfer body.") + rn, sem, bs = tob("\r\n"), tob(";"), tob("") + while True: + header = read(1) + while header[-2:] != rn: + c = read(1) + header += c + if not c: + raise err + if len(header) > bufsize: + raise err + size, _, _ = header.partition(sem) + try: + maxread = int(tonat(size.strip()), 16) + except ValueError: + raise err + if maxread == 0: + break + buff = bs + while maxread > 0: + if not buff: + buff = read(min(maxread, bufsize)) + part, buff = buff[:maxread], buff[maxread:] + if not part: + raise err + yield part + maxread -= len(part) + if read(2) != rn: + raise err + + @DictProperty("environ", "bottle.request.body", read_only=True) + def _body(self): + body_iter = self._iter_chunked if self.chunked else self._iter_body + read_func = self.environ["wsgi.input"].read + body, body_size, is_temp_file = BytesIO(), 0, False + for part in body_iter(read_func, self.MEMFILE_MAX): + body.write(part) + body_size += len(part) + if not is_temp_file and body_size > self.MEMFILE_MAX: + body, tmp = TemporaryFile(mode="w+b"), body + body.write(tmp.getvalue()) + del tmp + is_temp_file = True + self.environ["wsgi.input"] = body + body.seek(0) + return body + + def _get_body_string(self): + """read body until content-length or MEMFILE_MAX into a string. Raise + HTTPError(413) on requests that are to large.""" + clen = self.content_length + if clen > self.MEMFILE_MAX: + raise HTTPError(413, "Request to large") + if clen < 0: + clen = self.MEMFILE_MAX + 1 + data = self.body.read(clen) + if len(data) > self.MEMFILE_MAX: # Fail fast + raise HTTPError(413, "Request to large") + return data + + @property + def body(self): + """The HTTP request body as a seek-able file-like object. Depending on + :attr:`MEMFILE_MAX`, this is either a temporary file or a + :class:`io.BytesIO` instance. Accessing this property for the first + time reads and replaces the ``wsgi.input`` environ variable. + Subsequent accesses just do a `seek(0)` on the file object.""" + self._body.seek(0) + return self._body + + @property + def chunked(self): + """True if Chunked transfer encoding was.""" + return "chunked" in self.environ.get("HTTP_TRANSFER_ENCODING", "").lower() + + #: An alias for :attr:`query`. + GET = query + + @DictProperty("environ", "bottle.request.post", read_only=True) + def POST(self): + """The values of :attr:`forms` and :attr:`files` combined into a single + :class:`FormsDict`. Values are either strings (form values) or + instances of :class:`cgi.FieldStorage` (file uploads). + """ + post = FormsDict() + # We default to application/x-www-form-urlencoded for everything that + # is not multipart and take the fast path (also: 3.1 workaround) + if not self.content_type.startswith("multipart/"): + pairs = _parse_qsl(tonat(self._get_body_string(), "latin1")) + for key, value in pairs: + post[key] = value + return post + + safe_env = {"QUERY_STRING": ""} # Build a safe environment for cgi + for key in ("REQUEST_METHOD", "CONTENT_TYPE", "CONTENT_LENGTH"): + if key in self.environ: + safe_env[key] = self.environ[key] + args = dict(fp=self.body, environ=safe_env, keep_blank_values=True) + if py31: + args["fp"] = NCTextIOWrapper(args["fp"], encoding="utf8", newline="\n") + elif py3k: + args["encoding"] = "utf8" + post.recode_unicode = False + data = cgi.FieldStorage(**args) + self["_cgi.FieldStorage"] = data # http://bugs.python.org/issue18394#msg207958 + data = data.list or [] + for item in data: + if item.filename is None: + post[item.name] = item.value + else: + post[item.name] = FileUpload( + item.file, item.name, item.filename, item.headers + ) + return post + + @property + def url(self): + """The full request URI including hostname and scheme. If your app + lives behind a reverse proxy or load balancer and you get confusing + results, make sure that the ``X-Forwarded-Host`` header is set + correctly.""" + return self.urlparts.geturl() + + @DictProperty("environ", "bottle.request.urlparts", read_only=True) + def urlparts(self): + """The :attr:`url` string as an :class:`urlparse.SplitResult` tuple. + The tuple contains (scheme, host, path, query_string and fragment), + but the fragment is always empty because it is not visible to the + server.""" + env = self.environ + http = env.get("HTTP_X_FORWARDED_PROTO") or env.get("wsgi.url_scheme", "http") + host = env.get("HTTP_X_FORWARDED_HOST") or env.get("HTTP_HOST") + if not host: + # HTTP 1.1 requires a Host-header. This is for HTTP/1.0 clients. + host = env.get("SERVER_NAME", "127.0.0.1") + port = env.get("SERVER_PORT") + if port and port != ("80" if http == "http" else "443"): + host += ":" + port + path = urlquote(self.fullpath) + return UrlSplitResult(http, host, path, env.get("QUERY_STRING"), "") + + @property + def fullpath(self): + """Request path including :attr:`script_name` (if present).""" + return urljoin(self.script_name, self.path.lstrip("/")) + + @property + def query_string(self): + """The raw :attr:`query` part of the URL (everything in between ``?`` + and ``#``) as a string.""" + return self.environ.get("QUERY_STRING", "") + + @property + def script_name(self): + """The initial portion of the URL's `path` that was removed by a higher + level (server or routing middleware) before the application was + called. This script path is returned with leading and tailing + slashes.""" + script_name = self.environ.get("SCRIPT_NAME", "").strip("/") + return "/" + script_name + "/" if script_name else "/" + + def path_shift(self, shift=1): + """Shift path segments from :attr:`path` to :attr:`script_name` and + vice versa. + + :param shift: The number of path segments to shift. May be negative + to change the shift direction. (default: 1) + """ + script = self.environ.get("SCRIPT_NAME", "/") + self["SCRIPT_NAME"], self["PATH_INFO"] = path_shift(script, self.path, shift) + + @property + def content_length(self): + """The request body length as an integer. The client is responsible to + set this header. Otherwise, the real length of the body is unknown + and -1 is returned. In this case, :attr:`body` will be empty.""" + return int(self.environ.get("CONTENT_LENGTH") or -1) + + @property + def content_type(self): + """The Content-Type header as a lowercase-string (default: empty).""" + return self.environ.get("CONTENT_TYPE", "").lower() + + @property + def is_xhr(self): + """True if the request was triggered by a XMLHttpRequest. This only + works with JavaScript libraries that support the `X-Requested-With` + header (most of the popular libraries do).""" + requested_with = self.environ.get("HTTP_X_REQUESTED_WITH", "") + return requested_with.lower() == "xmlhttprequest" + + @property + def is_ajax(self): + """Alias for :attr:`is_xhr`. "Ajax" is not the right term.""" + return self.is_xhr + + @property + def auth(self): + """HTTP authentication data as a (user, password) tuple. This + implementation currently supports basic (not digest) authentication + only. If the authentication happened at a higher level (e.g. in the + front web-server or a middleware), the password field is None, but + the user field is looked up from the ``REMOTE_USER`` environ + variable. On any errors, None is returned.""" + basic = parse_auth(self.environ.get("HTTP_AUTHORIZATION", "")) + if basic: + return basic + ruser = self.environ.get("REMOTE_USER") + if ruser: + return (ruser, None) + return None + + @property + def remote_route(self): + """A list of all IPs that were involved in this request, starting with + the client IP and followed by zero or more proxies. This does only + work if all proxies support the ```X-Forwarded-For`` header. Note + that this information can be forged by malicious clients.""" + proxy = self.environ.get("HTTP_X_FORWARDED_FOR") + if proxy: + return [ip.strip() for ip in proxy.split(",")] + remote = self.environ.get("REMOTE_ADDR") + return [remote] if remote else [] + + @property + def remote_addr(self): + """The client IP as a string. Note that this information can be forged + by malicious clients.""" + route = self.remote_route + return route[0] if route else None + + def copy(self): + """Return a new :class:`Request` with a shallow :attr:`environ` copy.""" + return Request(self.environ.copy()) + + def get(self, value, default=None): + return self.environ.get(value, default) + + def __getitem__(self, key): + return self.environ[key] + + def __delitem__(self, key): + self[key] = "" + del self.environ[key] + + def __iter__(self): + return iter(self.environ) + + def __len__(self): + return len(self.environ) + + def keys(self): + return self.environ.keys() + + def __setitem__(self, key, value): + """Change an environ value and clear all caches that depend on it.""" + + if self.environ.get("bottle.request.readonly"): + raise KeyError("The environ dictionary is read-only.") + + self.environ[key] = value + todelete = () + + if key == "wsgi.input": + todelete = ("body", "forms", "files", "params", "post", "json") + elif key == "QUERY_STRING": + todelete = ("query", "params") + elif key.startswith("HTTP_"): + todelete = ("headers", "cookies") + + for key in todelete: + self.environ.pop("bottle.request." + key, None) + + def __repr__(self): + return "<%s: %s %s>" % (self.__class__.__name__, self.method, self.url) + + def __getattr__(self, name): + """Search in self.environ for additional user defined attributes.""" + try: + var = self.environ["bottle.request.ext.%s" % name] + return var.__get__(self) if hasattr(var, "__get__") else var + except KeyError: + raise AttributeError("Attribute %r not defined." % name) + + def __setattr__(self, name, value): + if name == "environ": + return object.__setattr__(self, name, value) + self.environ["bottle.request.ext.%s" % name] = value + + +def _hkey(key): + if "\n" in key or "\r" in key or "\0" in key: + raise ValueError("Header names must not contain control characters: %r" % key) + return key.title().replace("_", "-") + + +def _hval(value): + value = tonat(value) + if "\n" in value or "\r" in value or "\0" in value: + raise ValueError("Header value must not contain control characters: %r" % value) + return value + + +class HeaderProperty(object): + def __init__(self, name, reader=None, writer=None, default=""): + self.name, self.default = name, default + self.reader, self.writer = reader, writer + self.__doc__ = "Current value of the %r header." % name.title() + + def __get__(self, obj, cls): + if obj is None: + return self + value = obj.get_header(self.name, self.default) + return self.reader(value) if self.reader else value + + def __set__(self, obj, value): + obj[self.name] = self.writer(value) if self.writer else value + + def __delete__(self, obj): + del obj[self.name] + + +class BaseResponse(object): + """Storage class for a response body as well as headers and cookies. + + This class does support dict-like case-insensitive item-access to + headers, but is NOT a dict. Most notably, iterating over a response + yields parts of the body and not the headers. + + :param body: The response body as one of the supported types. + :param status: Either an HTTP status code (e.g. 200) or a status line + including the reason phrase (e.g. '200 OK'). + :param headers: A dictionary or a list of name-value pairs. + + Additional keyword arguments are added to the list of headers. + Underscores in the header name are replaced with dashes. + """ + + default_status = 200 + default_content_type = "text/html; charset=UTF-8" + + # Header blacklist for specific response codes + # (rfc2616 section 10.2.3 and 10.3.5) + bad_headers = { + 204: set(("Content-Type",)), + 304: set( + ( + "Allow", + "Content-Encoding", + "Content-Language", + "Content-Length", + "Content-Range", + "Content-Type", + "Content-Md5", + "Last-Modified", + ) + ), + } + + def __init__(self, body="", status=None, headers=None, **more_headers): + self._cookies = None + self._headers = {} + self.body = body + self.status = status or self.default_status + if headers: + if isinstance(headers, dict): + headers = headers.items() + for name, value in headers: + self.add_header(name, value) + if more_headers: + for name, value in more_headers.items(): + self.add_header(name, value) + + def copy(self, cls=None): + """Returns a copy of self.""" + cls = cls or BaseResponse + assert issubclass(cls, BaseResponse) + copy = cls() + copy.status = self.status + copy._headers = dict((k, v[:]) for (k, v) in self._headers.items()) + if self._cookies: + copy._cookies = SimpleCookie() + copy._cookies.load(self._cookies.output(header="")) + return copy + + def __iter__(self): + return iter(self.body) + + def close(self): + if hasattr(self.body, "close"): + self.body.close() + + @property + def status_line(self): + """The HTTP status line as a string (e.g. ``404 Not Found``).""" + return self._status_line + + @property + def status_code(self): + """The HTTP status code as an integer (e.g. 404).""" + return self._status_code + + def _set_status(self, status): + if isinstance(status, int): + code, status = status, _HTTP_STATUS_LINES.get(status) + elif " " in status: + status = status.strip() + code = int(status.split()[0]) + else: + raise ValueError("String status line without a reason phrase.") + if not 100 <= code <= 999: + raise ValueError("Status code out of range.") + self._status_code = code + self._status_line = str(status or ("%d Unknown" % code)) + + def _get_status(self): + return self._status_line + + status = property( + _get_status, + _set_status, + None, + """ A writeable property to change the HTTP response status. It accepts + either a numeric code (100-999) or a string with a custom reason + phrase (e.g. "404 Brain not found"). Both :data:`status_line` and + :data:`status_code` are updated accordingly. The return value is + always a status string. """, + ) + del _get_status, _set_status + + @property + def headers(self): + """An instance of :class:`HeaderDict`, a case-insensitive dict-like + view on the response headers.""" + hdict = HeaderDict() + hdict.dict = self._headers + return hdict + + def __contains__(self, name): + return _hkey(name) in self._headers + + def __delitem__(self, name): + del self._headers[_hkey(name)] + + def __getitem__(self, name): + return self._headers[_hkey(name)][-1] + + def __setitem__(self, name, value): + self._headers[_hkey(name)] = [_hval(value)] + + def get_header(self, name, default=None): + """Return the value of a previously defined header. If there is no + header with that name, return a default value.""" + return self._headers.get(_hkey(name), [default])[-1] + + def set_header(self, name, value): + """Create a new response header, replacing any previously defined + headers with the same name.""" + self._headers[_hkey(name)] = [_hval(value)] + + def add_header(self, name, value): + """Add an additional response header, not removing duplicates.""" + self._headers.setdefault(_hkey(name), []).append(_hval(value)) + + def iter_headers(self): + """Yield (header, value) tuples, skipping headers that are not + allowed with the current response status code.""" + return self.headerlist + + @property + def headerlist(self): + """WSGI conform list of (header, value) tuples.""" + out = [] + headers = list(self._headers.items()) + if "Content-Type" not in self._headers: + headers.append(("Content-Type", [self.default_content_type])) + if self._status_code in self.bad_headers: + bad_headers = self.bad_headers[self._status_code] + headers = [h for h in headers if h[0] not in bad_headers] + out += [(name, val) for (name, vals) in headers for val in vals] + if self._cookies: + for c in self._cookies.values(): + out.append(("Set-Cookie", _hval(c.OutputString()))) + if py3k: + out = [(k, v.encode("utf8").decode("latin1")) for (k, v) in out] + return out + + content_type = HeaderProperty("Content-Type") + content_length = HeaderProperty("Content-Length", reader=int) + expires = HeaderProperty( + "Expires", + reader=lambda x: datetime.utcfromtimestamp(parse_date(x)), + writer=lambda x: http_date(x), + ) + + @property + def charset(self, default="UTF-8"): + """Return the charset specified in the content-type header (default: utf8).""" + if "charset=" in self.content_type: + return self.content_type.split("charset=")[-1].split(";")[0].strip() + return default + + def set_cookie(self, name, value, secret=None, **options): + """Create a new cookie or replace an old one. If the `secret` parameter is + set, create a `Signed Cookie` (described below). + + :param name: the name of the cookie. + :param value: the value of the cookie. + :param secret: a signature key required for signed cookies. + + Additionally, this method accepts all RFC 2109 attributes that are + supported by :class:`cookie.Morsel`, including: + + :param max_age: maximum age in seconds. (default: None) + :param expires: a datetime object or UNIX timestamp. (default: None) + :param domain: the domain that is allowed to read the cookie. + (default: current domain) + :param path: limits the cookie to a given path (default: current path) + :param secure: limit the cookie to HTTPS connections (default: off). + :param httponly: prevents client-side javascript to read this cookie + (default: off, requires Python 2.6 or newer). + + If neither `expires` nor `max_age` is set (default), the cookie will + expire at the end of the browser session (as soon as the browser + window is closed). + + Signed cookies may store any pickle-able object and are + cryptographically signed to prevent manipulation. Keep in mind that + cookies are limited to 4kb in most browsers. + + Warning: Signed cookies are not encrypted (the client can still see + the content) and not copy-protected (the client can restore an old + cookie). The main intention is to make pickling and unpickling + save, not to store secret information at client side. + """ + if not self._cookies: + self._cookies = SimpleCookie() + + if secret: + value = touni(cookie_encode((name, value), secret)) + elif not isinstance(value, basestring): + raise TypeError("Secret key missing for non-string Cookie.") + + if len(value) > 4096: + raise ValueError("Cookie value to long.") + self._cookies[name] = value + + for key, value in options.items(): + if key == "max_age": + if isinstance(value, timedelta): + value = value.seconds + value.days * 24 * 3600 + if key == "expires": + if isinstance(value, (datedate, datetime)): + value = value.timetuple() + elif isinstance(value, (int, float)): + value = time.gmtime(value) + value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) + self._cookies[name][key.replace("_", "-")] = value + + def delete_cookie(self, key, **kwargs): + """Delete a cookie. Be sure to use the same `domain` and `path` + settings as used to create the cookie.""" + kwargs["max_age"] = -1 + kwargs["expires"] = 0 + self.set_cookie(key, "", **kwargs) + + def __repr__(self): + out = "" + for name, value in self.headerlist: + out += "%s: %s\n" % (name.title(), value.strip()) + return out + + +def local_property(name=None): + if name: + depr("local_property() is deprecated and will be removed.") # 0.12 + ls = threading.local() + + def fget(self): + try: + return ls.var + except AttributeError: + raise RuntimeError("Request context not initialized.") + + def fset(self, value): + ls.var = value + + def fdel(self): + del ls.var + + return property(fget, fset, fdel, "Thread-local property") + + +class LocalRequest(BaseRequest): + """A thread-local subclass of :class:`BaseRequest` with a different + set of attributes for each thread. There is usually only one global + instance of this class (:data:`request`). If accessed during a + request/response cycle, this instance always refers to the *current* + request (even on a multithreaded server).""" + + bind = BaseRequest.__init__ + environ = local_property() + + +class LocalResponse(BaseResponse): + """A thread-local subclass of :class:`BaseResponse` with a different + set of attributes for each thread. There is usually only one global + instance of this class (:data:`response`). Its attributes are used + to build the HTTP response at the end of the request/response cycle. + """ + + bind = BaseResponse.__init__ + _status_line = local_property() + _status_code = local_property() + _cookies = local_property() + _headers = local_property() + body = local_property() + + +Request = BaseRequest +Response = BaseResponse + + +class HTTPResponse(Response, BottleException): + def __init__(self, body="", status=None, headers=None, **more_headers): + super(HTTPResponse, self).__init__(body, status, headers, **more_headers) + + def apply(self, response): + response._status_code = self._status_code + response._status_line = self._status_line + response._headers = self._headers + response._cookies = self._cookies + response.body = self.body + + +class HTTPError(HTTPResponse): + default_status = 500 + + def __init__( + self, status=None, body=None, exception=None, traceback=None, **options + ): + self.exception = exception + self.traceback = traceback + super(HTTPError, self).__init__(body, status, **options) + + +############################################################################### +# Plugins ###################################################################### +############################################################################### + + +class PluginError(BottleException): + pass + + +class JSONPlugin(object): + name = "json" + api = 2 + + def __init__(self, json_dumps=json_dumps): + self.json_dumps = json_dumps + + def apply(self, callback, route): + dumps = self.json_dumps + if not dumps: + return callback + + def wrapper(*a, **ka): + try: + rv = callback(*a, **ka) + except HTTPResponse: + rv = _e() + + if isinstance(rv, dict): + # Attempt to serialize, raises exception on failure + json_response = dumps(rv) + # Set content type only if serialization successful + response.content_type = "application/json" + return json_response + elif isinstance(rv, HTTPResponse) and isinstance(rv.body, dict): + rv.body = dumps(rv.body) + rv.content_type = "application/json" + return rv + + return wrapper + + +class TemplatePlugin(object): + """This plugin applies the :func:`view` decorator to all routes with a + `template` config parameter. If the parameter is a tuple, the second + element must be a dict with additional options (e.g. `template_engine`) + or default variables for the template.""" + + name = "template" + api = 2 + + def apply(self, callback, route): + conf = route.config.get("template") + if isinstance(conf, (tuple, list)) and len(conf) == 2: + return view(conf[0], **conf[1])(callback) + elif isinstance(conf, str): + return view(conf)(callback) + else: + return callback + + +#: Not a plugin, but part of the plugin API. TODO: Find a better place. +class _ImportRedirect(object): + def __init__(self, name, impmask): + """Create a virtual package that redirects imports (see PEP 302).""" + self.name = name + self.impmask = impmask + self.module = sys.modules.setdefault(name, new_module(name)) + self.module.__dict__.update( + {"__file__": __file__, "__path__": [], "__all__": [], "__loader__": self} + ) + sys.meta_path.append(self) + + def find_module(self, fullname, path=None): + if "." not in fullname: + return + packname = fullname.rsplit(".", 1)[0] + if packname != self.name: + return + return self + + def load_module(self, fullname): + if fullname in sys.modules: + return sys.modules[fullname] + modname = fullname.rsplit(".", 1)[1] + realname = self.impmask % modname + __import__(realname) + module = sys.modules[fullname] = sys.modules[realname] + setattr(self.module, modname, module) + module.__loader__ = self + return module + + +############################################################################### +# Common Utilities ############################################################# +############################################################################### + + +class MultiDict(DictMixin): + """This dict stores multiple values per key, but behaves exactly like a + normal dict in that it returns only the newest value for any given key. + There are special methods available to access the full list of values. + """ + + def __init__(self, *a, **k): + self.dict = dict((k, [v]) for (k, v) in dict(*a, **k).items()) + + def __len__(self): + return len(self.dict) + + def __iter__(self): + return iter(self.dict) + + def __contains__(self, key): + return key in self.dict + + def __delitem__(self, key): + del self.dict[key] + + def __getitem__(self, key): + return self.dict[key][-1] + + def __setitem__(self, key, value): + self.append(key, value) + + def keys(self): + return self.dict.keys() + + if py3k: + + def values(self): + return (v[-1] for v in self.dict.values()) + + def items(self): + return ((k, v[-1]) for k, v in self.dict.items()) + + def allitems(self): + return ((k, v) for k, vl in self.dict.items() for v in vl) + + iterkeys = keys + itervalues = values + iteritems = items + iterallitems = allitems + + else: + + def values(self): + return [v[-1] for v in self.dict.values()] + + def items(self): + return [(k, v[-1]) for k, v in self.dict.items()] + + def iterkeys(self): + return self.dict.iterkeys() + + def itervalues(self): + return (v[-1] for v in self.dict.itervalues()) + + def iteritems(self): + return ((k, v[-1]) for k, v in self.dict.iteritems()) + + def iterallitems(self): + return ((k, v) for k, vl in self.dict.iteritems() for v in vl) + + def allitems(self): + return [(k, v) for k, vl in self.dict.iteritems() for v in vl] + + def get(self, key, default=None, index=-1, type=None): + """Return the most recent value for a key. + + :param default: The default value to be returned if the key is not + present or the type conversion fails. + :param index: An index for the list of available values. + :param type: If defined, this callable is used to cast the value + into a specific type. Exception are suppressed and result in + the default value to be returned. + """ + try: + val = self.dict[key][index] + return type(val) if type else val + except Exception: + pass + return default + + def append(self, key, value): + """Add a new value to the list of values for this key.""" + self.dict.setdefault(key, []).append(value) + + def replace(self, key, value): + """Replace the list of values with a single value.""" + self.dict[key] = [value] + + def getall(self, key): + """Return a (possibly empty) list of values for a key.""" + return self.dict.get(key) or [] + + #: Aliases for WTForms to mimic other multi-dict APIs (Django) + getone = get + getlist = getall + + +class FormsDict(MultiDict): + """This :class:`MultiDict` subclass is used to store request form data. + Additionally to the normal dict-like item access methods (which return + unmodified data as native strings), this container also supports + attribute-like access to its values. Attributes are automatically de- + or recoded to match :attr:`input_encoding` (default: 'utf8'). Missing + attributes default to an empty string.""" + + #: Encoding used for attribute values. + input_encoding = "utf8" + #: If true (default), unicode strings are first encoded with `latin1` + #: and then decoded to match :attr:`input_encoding`. + recode_unicode = True + + def _fix(self, s, encoding=None): + if isinstance(s, unicode) and self.recode_unicode: # Python 3 WSGI + return s.encode("latin1").decode(encoding or self.input_encoding) + elif isinstance(s, bytes): # Python 2 WSGI + return s.decode(encoding or self.input_encoding) + else: + return s + + def decode(self, encoding=None): + """Returns a copy with all keys and values de- or recoded to match + :attr:`input_encoding`. Some libraries (e.g. WTForms) want a + unicode dictionary.""" + copy = FormsDict() + enc = copy.input_encoding = encoding or self.input_encoding + copy.recode_unicode = False + for key, value in self.allitems(): + copy.append(self._fix(key, enc), self._fix(value, enc)) + return copy + + def getunicode(self, name, default=None, encoding=None): + """Return the value as a unicode string, or the default.""" + try: + return self._fix(self[name], encoding) + except (UnicodeError, KeyError): + return default + + def __getattr__(self, name, default=unicode()): + # Without this guard, pickle generates a cryptic TypeError: + if name.startswith("__") and name.endswith("__"): + return super(FormsDict, self).__getattr__(name) + return self.getunicode(name, default=default) + + +class HeaderDict(MultiDict): + """A case-insensitive version of :class:`MultiDict` that defaults to + replace the old value instead of appending it.""" + + def __init__(self, *a, **ka): + self.dict = {} + if a or ka: + self.update(*a, **ka) + + def __contains__(self, key): + return _hkey(key) in self.dict + + def __delitem__(self, key): + del self.dict[_hkey(key)] + + def __getitem__(self, key): + return self.dict[_hkey(key)][-1] + + def __setitem__(self, key, value): + self.dict[_hkey(key)] = [_hval(value)] + + def append(self, key, value): + self.dict.setdefault(_hkey(key), []).append(_hval(value)) + + def replace(self, key, value): + self.dict[_hkey(key)] = [_hval(value)] + + def getall(self, key): + return self.dict.get(_hkey(key)) or [] + + def get(self, key, default=None, index=-1): + return MultiDict.get(self, _hkey(key), default, index) + + def filter(self, names): + for name in (_hkey(n) for n in names): + if name in self.dict: + del self.dict[name] + + +class WSGIHeaderDict(DictMixin): + """This dict-like class wraps a WSGI environ dict and provides convenient + access to HTTP_* fields. Keys and values are native strings + (2.x bytes or 3.x unicode) and keys are case-insensitive. If the WSGI + environment contains non-native string values, these are de- or encoded + using a lossless 'latin1' character set. + + The API will remain stable even on changes to the relevant PEPs. + Currently PEP 333, 444 and 3333 are supported. (PEP 444 is the only one + that uses non-native strings.) + """ + + #: List of keys that do not have a ``HTTP_`` prefix. + cgikeys = ("CONTENT_TYPE", "CONTENT_LENGTH") + + def __init__(self, environ): + self.environ = environ + + def _ekey(self, key): + """Translate header field name to CGI/WSGI environ key.""" + key = key.replace("-", "_").upper() + if key in self.cgikeys: + return key + return "HTTP_" + key + + def raw(self, key, default=None): + """Return the header value as is (may be bytes or unicode).""" + return self.environ.get(self._ekey(key), default) + + def __getitem__(self, key): + return tonat(self.environ[self._ekey(key)], "latin1") + + def __setitem__(self, key, value): + raise TypeError("%s is read-only." % self.__class__) + + def __delitem__(self, key): + raise TypeError("%s is read-only." % self.__class__) + + def __iter__(self): + for key in self.environ: + if key[:5] == "HTTP_": + yield key[5:].replace("_", "-").title() + elif key in self.cgikeys: + yield key.replace("_", "-").title() + + def keys(self): + return [x for x in self] + + def __len__(self): + return len(self.keys()) + + def __contains__(self, key): + return self._ekey(key) in self.environ + + +class ConfigDict(dict): + """A dict-like configuration storage with additional support for + namespaces, validators, meta-data, on_change listeners and more. + + This storage is optimized for fast read access. Retrieving a key + or using non-altering dict methods (e.g. `dict.get()`) has no overhead + compared to a native dict. + """ + + __slots__ = ("_meta", "_on_change") + + class Namespace(DictMixin): + def __init__(self, config, namespace): + self._config = config + self._prefix = namespace + + def __getitem__(self, key): + depr( + "Accessing namespaces as dicts is discouraged. " + "Only use flat item access: " + 'cfg["names"]["pace"]["key"] -> cfg["name.space.key"]' + ) # 0.12 + return self._config[self._prefix + "." + key] + + def __setitem__(self, key, value): + self._config[self._prefix + "." + key] = value + + def __delitem__(self, key): + del self._config[self._prefix + "." + key] + + def __iter__(self): + ns_prefix = self._prefix + "." + for key in self._config: + ns, dot, name = key.rpartition(".") + if ns == self._prefix and name: + yield name + + def keys(self): + return [x for x in self] + + def __len__(self): + return len(self.keys()) + + def __contains__(self, key): + return self._prefix + "." + key in self._config + + def __repr__(self): + return "" % self._prefix + + def __str__(self): + return "" % self._prefix + + # Deprecated ConfigDict features + def __getattr__(self, key): + depr("Attribute access is deprecated.") # 0.12 + if key not in self and key[0].isupper(): + self[key] = ConfigDict.Namespace(self._config, self._prefix + "." + key) + if key not in self and key.startswith("__"): + raise AttributeError(key) + return self.get(key) + + def __setattr__(self, key, value): + if key in ("_config", "_prefix"): + self.__dict__[key] = value + return + depr("Attribute assignment is deprecated.") # 0.12 + if hasattr(DictMixin, key): + raise AttributeError("Read-only attribute.") + if key in self and self[key] and isinstance(self[key], self.__class__): + raise AttributeError("Non-empty namespace attribute.") + self[key] = value + + def __delattr__(self, key): + if key in self: + val = self.pop(key) + if isinstance(val, self.__class__): + prefix = key + "." + for key in self: + if key.startswith(prefix): + del self[prefix + key] + + def __call__(self, *a, **ka): + depr("Calling ConfDict is deprecated. Use the update() method.") # 0.12 + self.update(*a, **ka) + return self + + def __init__(self, *a, **ka): + self._meta = {} + self._on_change = lambda name, value: None + if a or ka: + depr("Constructor does no longer accept parameters.") # 0.12 + self.update(*a, **ka) + + def load_config(self, filename): + """Load values from an *.ini style config file. + + If the config file contains sections, their names are used as + namespaces for the values within. The two special sections + ``DEFAULT`` and ``bottle`` refer to the root namespace (no prefix). + """ + conf = ConfigParser() + conf.read(filename) + for section in conf.sections(): + for key, value in conf.items(section): + if section not in ("DEFAULT", "bottle"): + key = section + "." + key + self[key] = value + return self + + def load_dict(self, source, namespace="", make_namespaces=False): + """Import values from a dictionary structure. Nesting can be used to + represent namespaces. + + >>> ConfigDict().load_dict({'name': {'space': {'key': 'value'}}}) + {'name.space.key': 'value'} + """ + stack = [(namespace, source)] + while stack: + prefix, source = stack.pop() + if not isinstance(source, dict): + raise TypeError("Source is not a dict (r)" % type(key)) + for key, value in source.items(): + if not isinstance(key, basestring): + raise TypeError("Key is not a string (%r)" % type(key)) + full_key = prefix + "." + key if prefix else key + if isinstance(value, dict): + stack.append((full_key, value)) + if make_namespaces: + self[full_key] = self.Namespace(self, full_key) + else: + self[full_key] = value + return self + + def update(self, *a, **ka): + """If the first parameter is a string, all keys are prefixed with this + namespace. Apart from that it works just as the usual dict.update(). + Example: ``update('some.namespace', key='value')``""" + prefix = "" + if a and isinstance(a[0], basestring): + prefix = a[0].strip(".") + "." + a = a[1:] + for key, value in dict(*a, **ka).items(): + self[prefix + key] = value + + def setdefault(self, key, value): + if key not in self: + self[key] = value + return self[key] + + def __setitem__(self, key, value): + if not isinstance(key, basestring): + raise TypeError("Key has type %r (not a string)" % type(key)) + + value = self.meta_get(key, "filter", lambda x: x)(value) + if key in self and self[key] is value: + return + self._on_change(key, value) + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + dict.__delitem__(self, key) + + def clear(self): + for key in self: + del self[key] + + def meta_get(self, key, metafield, default=None): + """Return the value of a meta field for a key.""" + return self._meta.get(key, {}).get(metafield, default) + + def meta_set(self, key, metafield, value): + """Set the meta field for a key to a new value. This triggers the + on-change handler for existing keys.""" + self._meta.setdefault(key, {})[metafield] = value + if key in self: + self[key] = self[key] + + def meta_list(self, key): + """Return an iterable of meta field names defined for a key.""" + return self._meta.get(key, {}).keys() + + # Deprecated ConfigDict features + def __getattr__(self, key): + depr("Attribute access is deprecated.") # 0.12 + if key not in self and key[0].isupper(): + self[key] = self.Namespace(self, key) + if key not in self and key.startswith("__"): + raise AttributeError(key) + return self.get(key) + + def __setattr__(self, key, value): + if key in self.__slots__: + return dict.__setattr__(self, key, value) + depr("Attribute assignment is deprecated.") # 0.12 + if hasattr(dict, key): + raise AttributeError("Read-only attribute.") + if key in self and self[key] and isinstance(self[key], self.Namespace): + raise AttributeError("Non-empty namespace attribute.") + self[key] = value + + def __delattr__(self, key): + if key in self: + val = self.pop(key) + if isinstance(val, self.Namespace): + prefix = key + "." + for key in self: + if key.startswith(prefix): + del self[prefix + key] + + def __call__(self, *a, **ka): + depr("Calling ConfDict is deprecated. Use the update() method.") # 0.12 + self.update(*a, **ka) + return self + + +class AppStack(list): + """A stack-like list. Calling it returns the head of the stack.""" + + def __call__(self): + """Return the current default application.""" + return self[-1] + + def push(self, value=None): + """Add a new :class:`Bottle` instance to the stack""" + if not isinstance(value, Bottle): + value = Bottle() + self.append(value) + return value + + +class WSGIFileWrapper(object): + def __init__(self, fp, buffer_size=1024 * 64): + self.fp, self.buffer_size = fp, buffer_size + for attr in ("fileno", "close", "read", "readlines", "tell", "seek"): + if hasattr(fp, attr): + setattr(self, attr, getattr(fp, attr)) + + def __iter__(self): + buff, read = self.buffer_size, self.read + while True: + part = read(buff) + if not part: + return + yield part + + +class _closeiter(object): + """This only exists to be able to attach a .close method to iterators that + do not support attribute assignment (most of itertools).""" + + def __init__(self, iterator, close=None): + self.iterator = iterator + self.close_callbacks = makelist(close) + + def __iter__(self): + return iter(self.iterator) + + def close(self): + for func in self.close_callbacks: + func() + + +class ResourceManager(object): + """This class manages a list of search paths and helps to find and open + application-bound resources (files). + + :param base: default value for :meth:`add_path` calls. + :param opener: callable used to open resources. + :param cachemode: controls which lookups are cached. One of 'all', + 'found' or 'none'. + """ + + def __init__(self, base="./", opener=open, cachemode="all"): + self.opener = open + self.base = base + self.cachemode = cachemode + + #: A list of search paths. See :meth:`add_path` for details. + self.path = [] + #: A cache for resolved paths. ``res.cache.clear()`` clears the cache. + self.cache = {} + + def add_path(self, path, base=None, index=None, create=False): + """Add a new path to the list of search paths. Return False if the + path does not exist. + + :param path: The new search path. Relative paths are turned into + an absolute and normalized form. If the path looks like a file + (not ending in `/`), the filename is stripped off. + :param base: Path used to absolutize relative search paths. + Defaults to :attr:`base` which defaults to ``os.getcwd()``. + :param index: Position within the list of search paths. Defaults + to last index (appends to the list). + + The `base` parameter makes it easy to reference files installed + along with a python module or package:: + + res.add_path('./resources/', __file__) + """ + base = os.path.abspath(os.path.dirname(base or self.base)) + path = os.path.abspath(os.path.join(base, os.path.dirname(path))) + path += os.sep + if path in self.path: + self.path.remove(path) + if create and not os.path.isdir(path): + os.makedirs(path) + if index is None: + self.path.append(path) + else: + self.path.insert(index, path) + self.cache.clear() + return os.path.exists(path) + + def __iter__(self): + """Iterate over all existing files in all registered paths.""" + search = self.path[:] + while search: + path = search.pop() + if not os.path.isdir(path): + continue + for name in os.listdir(path): + full = os.path.join(path, name) + if os.path.isdir(full): + search.append(full) + else: + yield full + + def lookup(self, name): + """Search for a resource and return an absolute file path, or `None`. + + The :attr:`path` list is searched in order. The first match is + returend. Symlinks are followed. The result is cached to speed up + future lookups.""" + if name not in self.cache or DEBUG: + for path in self.path: + fpath = os.path.join(path, name) + if os.path.isfile(fpath): + if self.cachemode in ("all", "found"): + self.cache[name] = fpath + return fpath + if self.cachemode == "all": + self.cache[name] = None + return self.cache[name] + + def open(self, name, mode="r", *args, **kwargs): + """Find a resource and return a file object, or raise IOError.""" + fname = self.lookup(name) + if not fname: + raise IOError("Resource %r not found." % name) + return self.opener(fname, mode=mode, *args, **kwargs) + + +class FileUpload(object): + def __init__(self, fileobj, name, filename, headers=None): + """Wrapper for file uploads.""" + #: Open file(-like) object (BytesIO buffer or temporary file) + self.file = fileobj + #: Name of the upload form field + self.name = name + #: Raw filename as sent by the client (may contain unsafe characters) + self.raw_filename = filename + #: A :class:`HeaderDict` with additional headers (e.g. content-type) + self.headers = HeaderDict(headers) if headers else HeaderDict() + + content_type = HeaderProperty("Content-Type") + content_length = HeaderProperty("Content-Length", reader=int, default=-1) + + def get_header(self, name, default=None): + """Return the value of a header within the mulripart part.""" + return self.headers.get(name, default) + + @cached_property + def filename(self): + """Name of the file on the client file system, but normalized to ensure + file system compatibility. An empty filename is returned as 'empty'. + + Only ASCII letters, digits, dashes, underscores and dots are + allowed in the final filename. Accents are removed, if possible. + Whitespace is replaced by a single dash. Leading or tailing dots + or dashes are removed. The filename is limited to 255 characters. + """ + fname = self.raw_filename + if not isinstance(fname, unicode): + fname = fname.decode("utf8", "ignore") + fname = normalize("NFKD", fname).encode("ASCII", "ignore").decode("ASCII") + fname = os.path.basename(fname.replace("\\", os.path.sep)) + fname = re.sub(r"[^a-zA-Z0-9-_.\s]", "", fname).strip() + fname = re.sub(r"[-\s]+", "-", fname).strip(".-") + return fname[:255] or "empty" + + def _copy_file(self, fp, chunk_size=2**16): + read, write, offset = self.file.read, fp.write, self.file.tell() + while 1: + buf = read(chunk_size) + if not buf: + break + write(buf) + self.file.seek(offset) + + def save(self, destination, overwrite=False, chunk_size=2**16): + """Save file to disk or copy its content to an open file(-like) object. + If *destination* is a directory, :attr:`filename` is added to the + path. Existing files are not overwritten by default (IOError). + + :param destination: File path, directory or file(-like) object. + :param overwrite: If True, replace existing files. (default: False) + :param chunk_size: Bytes to read at a time. (default: 64kb) + """ + if isinstance(destination, basestring): # Except file-likes here + if os.path.isdir(destination): + destination = os.path.join(destination, self.filename) + if not overwrite and os.path.exists(destination): + raise IOError("File exists.") + with open(destination, "wb") as fp: + self._copy_file(fp, chunk_size) + else: + self._copy_file(destination, chunk_size) + + +############################################################################### +# Application Helper ########################################################### +############################################################################### + + +def abort(code=500, text="Unknown Error."): + """Aborts execution and causes a HTTP error.""" + raise HTTPError(code, text) + + +def redirect(url, code=None): + """Aborts execution and causes a 303 or 302 redirect, depending on + the HTTP protocol version.""" + if not code: + code = 303 if request.get("SERVER_PROTOCOL") == "HTTP/1.1" else 302 + res = response.copy(cls=HTTPResponse) + res.status = code + res.body = "" + res.set_header("Location", urljoin(request.url, url)) + raise res + + +def _file_iter_range(fp, offset, bytes, maxread=1024 * 1024): + """Yield chunks from a range in a file. No chunk is bigger than maxread.""" + fp.seek(offset) + while bytes > 0: + part = fp.read(min(bytes, maxread)) + if not part: + break + bytes -= len(part) + yield part + + +def static_file(filename, root, mimetype="auto", download=False, charset="UTF-8"): + """Open a file in a safe way and return :exc:`HTTPResponse` with status + code 200, 305, 403 or 404. The ``Content-Type``, ``Content-Encoding``, + ``Content-Length`` and ``Last-Modified`` headers are set if possible. + Special support for ``If-Modified-Since``, ``Range`` and ``HEAD`` + requests. + + :param filename: Name or path of the file to send. + :param root: Root path for file lookups. Should be an absolute directory + path. + :param mimetype: Defines the content-type header (default: guess from + file extension) + :param download: If True, ask the browser to open a `Save as...` dialog + instead of opening the file with the associated program. You can + specify a custom filename as a string. If not specified, the + original filename is used (default: False). + :param charset: The charset to use for files with a ``text/*`` + mime-type. (default: UTF-8) + """ + + root = os.path.abspath(root) + os.sep + filename = os.path.abspath(os.path.join(root, filename.strip("/\\"))) + headers = dict() + + if not filename.startswith(root): + return HTTPError(403, "Access denied.") + if not os.path.exists(filename) or not os.path.isfile(filename): + return HTTPError(404, "File does not exist.") + if not os.access(filename, os.R_OK): + return HTTPError(403, "You do not have permission to access this file.") + + if mimetype == "auto": + mimetype, encoding = mimetypes.guess_type(filename) + if encoding: + headers["Content-Encoding"] = encoding + + if mimetype: + if mimetype[:5] == "text/" and charset and "charset" not in mimetype: + mimetype += "; charset=%s" % charset + headers["Content-Type"] = mimetype + + if download: + download = os.path.basename(filename if download == True else download) + headers["Content-Disposition"] = 'attachment; filename="%s"' % download + + stats = os.stat(filename) + headers["Content-Length"] = clen = stats.st_size + lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime)) + headers["Last-Modified"] = lm + + ims = request.environ.get("HTTP_IF_MODIFIED_SINCE") + if ims: + ims = parse_date(ims.split(";")[0].strip()) + if ims is not None and ims >= int(stats.st_mtime): + headers["Date"] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) + return HTTPResponse(status=304, **headers) + + body = "" if request.method == "HEAD" else open(filename, "rb") + + headers["Accept-Ranges"] = "bytes" + ranges = request.environ.get("HTTP_RANGE") + if "HTTP_RANGE" in request.environ: + ranges = list(parse_range_header(request.environ["HTTP_RANGE"], clen)) + if not ranges: + return HTTPError(416, "Requested Range Not Satisfiable") + offset, end = ranges[0] + headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, clen) + headers["Content-Length"] = str(end - offset) + if body: + body = _file_iter_range(body, offset, end - offset) + return HTTPResponse(body, status=206, **headers) + return HTTPResponse(body, **headers) + + +############################################################################### +# HTTP Utilities and MISC (TODO) ############################################### +############################################################################### + + +def debug(mode=True): + """Change the debug level. + There is only one debug level supported at the moment.""" + global DEBUG + if mode: + warnings.simplefilter("default") + DEBUG = bool(mode) + + +def http_date(value): + if isinstance(value, (datedate, datetime)): + value = value.utctimetuple() + elif isinstance(value, (int, float)): + value = time.gmtime(value) + if not isinstance(value, basestring): + value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) + return value + + +def parse_date(ims): + """Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch.""" + try: + ts = email.utils.parsedate_tz(ims) + return time.mktime(ts[:8] + (0,)) - (ts[9] or 0) - time.timezone + except (TypeError, ValueError, IndexError, OverflowError): + return None + + +def parse_auth(header): + """Parse rfc2617 HTTP authentication header string (basic) and return (user,pass) tuple or None""" + try: + method, data = header.split(None, 1) + if method.lower() == "basic": + user, pwd = touni(base64.b64decode(tob(data))).split(":", 1) + return user, pwd + except (KeyError, ValueError): + return None + + +def parse_range_header(header, maxlen=0): + """Yield (start, end) ranges parsed from a HTTP Range header. Skip + unsatisfiable ranges. The end index is non-inclusive.""" + if not header or header[:6] != "bytes=": + return + ranges = [r.split("-", 1) for r in header[6:].split(",") if "-" in r] + for start, end in ranges: + try: + if not start: # bytes=-100 -> last 100 bytes + start, end = max(0, maxlen - int(end)), maxlen + elif not end: # bytes=100- -> all but the first 99 bytes + start, end = int(start), maxlen + else: # bytes=100-200 -> bytes 100-200 (inclusive) + start, end = int(start), min(int(end) + 1, maxlen) + if 0 <= start < end <= maxlen: + yield start, end + except ValueError: + pass + + +def _parse_qsl(qs): + r = [] + for pair in qs.split("&"): + if not pair: + continue + nv = pair.split("=", 1) + if len(nv) != 2: + nv.append("") + key = urlunquote(nv[0].replace("+", " ")) + value = urlunquote(nv[1].replace("+", " ")) + r.append((key, value)) + return r + + +def _lscmp(a, b): + """Compares two strings in a cryptographically safe way: + Runtime is not affected by length of common prefix.""" + return not sum(0 if x == y else 1 for x, y in zip(a, b)) and len(a) == len(b) + + +def cookie_encode(data, key): + """Encode and sign a pickle-able object. Return a (byte) string""" + msg = base64.b64encode(pickle.dumps(data, -1)) + sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=hashlib.md5).digest()) + return tob("!") + sig + tob("?") + msg + + +def cookie_decode(data, key): + """Verify and decode an encoded string. Return an object or None.""" + data = tob(data) + if cookie_is_encoded(data): + sig, msg = data.split(tob("?"), 1) + if _lscmp( + sig[1:], + base64.b64encode(hmac.new(tob(key), msg, digestmod=hashlib.md5).digest()), + ): + return pickle.loads(base64.b64decode(msg)) + return None + + +def cookie_is_encoded(data): + """Return True if the argument looks like a encoded cookie.""" + return bool(data.startswith(tob("!")) and tob("?") in data) + + +def html_escape(string): + """Escape HTML special characters ``&<>`` and quotes ``'"``.""" + return ( + string.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace('"', """) + .replace("'", "'") + ) + + +def html_quote(string): + """Escape and quote a string to be used as an HTTP attribute.""" + return '"%s"' % html_escape(string).replace("\n", " ").replace( + "\r", " " + ).replace("\t", " ") + + +def yieldroutes(func): + """Return a generator for routes that match the signature (name, args) + of the func parameter. This may yield more than one route if the function + takes optional keyword arguments. The output is best described by example:: + + a() -> '/a' + b(x, y) -> '/b//' + c(x, y=5) -> '/c/' and '/c//' + d(x=5, y=6) -> '/d' and '/d/' and '/d//' + """ + path = "/" + func.__name__.replace("__", "/").lstrip("/") + spec = getargspec(func) + argc = len(spec[0]) - len(spec[3] or []) + path += ("/<%s>" * argc) % tuple(spec[0][:argc]) + yield path + for arg in spec[0][argc:]: + path += "/<%s>" % arg + yield path + + +def path_shift(script_name, path_info, shift=1): + """Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa. + + :return: The modified paths. + :param script_name: The SCRIPT_NAME path. + :param script_name: The PATH_INFO path. + :param shift: The number of path fragments to shift. May be negative to + change the shift direction. (default: 1) + """ + if shift == 0: + return script_name, path_info + pathlist = path_info.strip("/").split("/") + scriptlist = script_name.strip("/").split("/") + if pathlist and pathlist[0] == "": + pathlist = [] + if scriptlist and scriptlist[0] == "": + scriptlist = [] + if shift > 0 and shift <= len(pathlist): + moved = pathlist[:shift] + scriptlist = scriptlist + moved + pathlist = pathlist[shift:] + elif shift < 0 and shift >= -len(scriptlist): + moved = scriptlist[shift:] + pathlist = moved + pathlist + scriptlist = scriptlist[:shift] + else: + empty = "SCRIPT_NAME" if shift < 0 else "PATH_INFO" + raise AssertionError("Cannot shift. Nothing left from %s" % empty) + new_script_name = "/" + "/".join(scriptlist) + new_path_info = "/" + "/".join(pathlist) + if path_info.endswith("/") and pathlist: + new_path_info += "/" + return new_script_name, new_path_info + + +def auth_basic(check, realm="private", text="Access denied"): + """Callback decorator to require HTTP auth (basic). + TODO: Add route(check_auth=...) parameter.""" + + def decorator(func): + @functools.wraps(func) + def wrapper(*a, **ka): + user, password = request.auth or (None, None) + if user is None or not check(user, password): + err = HTTPError(401, text) + err.add_header("WWW-Authenticate", 'Basic realm="%s"' % realm) + return err + return func(*a, **ka) + + return wrapper + + return decorator + + +# Shortcuts for common Bottle methods. +# They all refer to the current default application. + + +def make_default_app_wrapper(name): + """Return a callable that relays calls to the current default app.""" + + @functools.wraps(getattr(Bottle, name)) + def wrapper(*a, **ka): + return getattr(app(), name)(*a, **ka) + + return wrapper + + +route = make_default_app_wrapper("route") +get = make_default_app_wrapper("get") +post = make_default_app_wrapper("post") +put = make_default_app_wrapper("put") +delete = make_default_app_wrapper("delete") +error = make_default_app_wrapper("error") +mount = make_default_app_wrapper("mount") +hook = make_default_app_wrapper("hook") +install = make_default_app_wrapper("install") +uninstall = make_default_app_wrapper("uninstall") +url = make_default_app_wrapper("get_url") + + +############################################################################### +# Server Adapter ############################################################### +############################################################################### + + +class ServerAdapter(object): + quiet = False + + def __init__(self, host="127.0.0.1", port=8080, **options): + self.options = options + self.host = host + self.port = int(port) + + def run(self, handler): # pragma: no cover + pass + + def __repr__(self): + args = ", ".join(["%s=%s" % (k, repr(v)) for k, v in self.options.items()]) + return "%s(%s)" % (self.__class__.__name__, args) + + +class CGIServer(ServerAdapter): + quiet = True + + def run(self, handler): # pragma: no cover + from wsgiref.handlers import CGIHandler + + def fixed_environ(environ, start_response): + environ.setdefault("PATH_INFO", "") + return handler(environ, start_response) + + CGIHandler().run(fixed_environ) + + +class FlupFCGIServer(ServerAdapter): + def run(self, handler): # pragma: no cover + import flup.server.fcgi + + self.options.setdefault("bindAddress", (self.host, self.port)) + flup.server.fcgi.WSGIServer(handler, **self.options).run() + + +class WSGIRefServer(ServerAdapter): + def run(self, app): # pragma: no cover + from wsgiref.simple_server import WSGIRequestHandler, WSGIServer + from wsgiref.simple_server import make_server + import socket + + class FixedHandler(WSGIRequestHandler): + def address_string(self): # Prevent reverse DNS lookups please. + return self.client_address[0] + + def log_request(*args, **kw): + if not self.quiet: + return WSGIRequestHandler.log_request(*args, **kw) + + handler_cls = self.options.get("handler_class", FixedHandler) + server_cls = self.options.get("server_class", WSGIServer) + + if ":" in self.host: # Fix wsgiref for IPv6 addresses. + if getattr(server_cls, "address_family") == socket.AF_INET: + + class server_cls(server_cls): + address_family = socket.AF_INET6 + + srv = make_server(self.host, self.port, app, server_cls, handler_cls) + srv.serve_forever() + + +class CherryPyServer(ServerAdapter): + def run(self, handler): # pragma: no cover + depr( + "The wsgi server part of cherrypy was split into a new " + "project called 'cheroot'. Use the 'cheroot' server " + "adapter instead of cherrypy." + ) + from cherrypy import wsgiserver # This will fail for CherryPy >= 9 + + self.options["bind_addr"] = (self.host, self.port) + self.options["wsgi_app"] = handler + + certfile = self.options.get("certfile") + if certfile: + del self.options["certfile"] + keyfile = self.options.get("keyfile") + if keyfile: + del self.options["keyfile"] + + server = wsgiserver.CherryPyWSGIServer(**self.options) + if certfile: + server.ssl_certificate = certfile + if keyfile: + server.ssl_private_key = keyfile + + try: + server.start() + finally: + server.stop() + + +class CherootServer(ServerAdapter): + def run(self, handler): # pragma: no cover + from cheroot import wsgi + from cheroot.ssl import builtin + + self.options["bind_addr"] = (self.host, self.port) + self.options["wsgi_app"] = handler + certfile = self.options.pop("certfile", None) + keyfile = self.options.pop("keyfile", None) + chainfile = self.options.pop("chainfile", None) + server = wsgi.Server(**self.options) + if certfile and keyfile: + server.ssl_adapter = builtin.BuiltinSSLAdapter(certfile, keyfile, chainfile) + try: + server.start() + finally: + server.stop() + + +class WaitressServer(ServerAdapter): + def run(self, handler): + from waitress import serve + + serve(handler, host=self.host, port=self.port) + + +class PasteServer(ServerAdapter): + def run(self, handler): # pragma: no cover + from paste import httpserver + from paste.translogger import TransLogger + + handler = TransLogger(handler, setup_console_handler=(not self.quiet)) + httpserver.serve(handler, host=self.host, port=str(self.port), **self.options) + + +class MeinheldServer(ServerAdapter): + def run(self, handler): + from meinheld import server + + server.listen((self.host, self.port)) + server.run(handler) + + +class FapwsServer(ServerAdapter): + """Extremely fast webserver using libev. See https://github.com/william-os4y/fapws3""" + + def run(self, handler): # pragma: no cover + import fapws._evwsgi as evwsgi + from fapws import base, config + + port = self.port + if float(config.SERVER_IDENT[-2:]) > 0.4: + # fapws3 silently changed its API in 0.5 + port = str(port) + evwsgi.start(self.host, port) + # fapws3 never releases the GIL. Complain upstream. I tried. No luck. + if "BOTTLE_CHILD" in os.environ and not self.quiet: + _stderr("WARNING: Auto-reloading does not work with Fapws3.\n") + _stderr(" (Fapws3 breaks python thread support)\n") + evwsgi.set_base_module(base) + + def app(environ, start_response): + environ["wsgi.multiprocess"] = False + return handler(environ, start_response) + + evwsgi.wsgi_cb(("", app)) + evwsgi.run() + + +class TornadoServer(ServerAdapter): + """The super hyped asynchronous server by facebook. Untested.""" + + def run(self, handler): # pragma: no cover + import tornado.wsgi, tornado.httpserver, tornado.ioloop + + container = tornado.wsgi.WSGIContainer(handler) + server = tornado.httpserver.HTTPServer(container) + server.listen(port=self.port, address=self.host) + tornado.ioloop.IOLoop.instance().start() + + +class AppEngineServer(ServerAdapter): + """Adapter for Google App Engine.""" + + quiet = True + + def run(self, handler): + from google.appengine.ext.webapp import util + + # A main() function in the handler script enables 'App Caching'. + # Lets makes sure it is there. This _really_ improves performance. + module = sys.modules.get("__main__") + if module and not hasattr(module, "main"): + module.main = lambda: util.run_wsgi_app(handler) + util.run_wsgi_app(handler) + + +class TwistedServer(ServerAdapter): + """Untested.""" + + def run(self, handler): + from twisted.web import server, wsgi + from twisted.python.threadpool import ThreadPool + from twisted.internet import reactor + + thread_pool = ThreadPool() + thread_pool.start() + reactor.addSystemEventTrigger("after", "shutdown", thread_pool.stop) + factory = server.Site(wsgi.WSGIResource(reactor, thread_pool, handler)) + reactor.listenTCP(self.port, factory, interface=self.host) + reactor.run() + + +class DieselServer(ServerAdapter): + """Untested.""" + + def run(self, handler): + from diesel.protocols.wsgi import WSGIApplication + + app = WSGIApplication(handler, port=self.port) + app.run() + + +class GeventServer(ServerAdapter): + """Untested. Options: + + * `fast` (default: False) uses libevent's http server, but has some + issues: No streaming, no pipelining, no SSL. + * See gevent.wsgi.WSGIServer() documentation for more options. + """ + + def run(self, handler): + from gevent import pywsgi, local + + if not isinstance(threading.local(), local.local): + msg = "Bottle requires gevent.monkey.patch_all() (before import)" + raise RuntimeError(msg) + if self.options.pop("fast", None): + depr('The "fast" option has been deprecated and removed by Gevent.') + if self.quiet: + self.options["log"] = None + address = (self.host, self.port) + server = pywsgi.WSGIServer(address, handler, **self.options) + if "BOTTLE_CHILD" in os.environ: + import signal + + signal.signal(signal.SIGINT, lambda s, f: server.stop()) + server.serve_forever() + + +class GeventSocketIOServer(ServerAdapter): + def run(self, handler): + from socketio import server + + address = (self.host, self.port) + server.SocketIOServer(address, handler, **self.options).serve_forever() + + +class GunicornServer(ServerAdapter): + """Untested. See http://gunicorn.org/configure.html for options.""" + + def run(self, handler): + from gunicorn.app.base import Application + + config = {"bind": "%s:%d" % (self.host, int(self.port))} + config.update(self.options) + + class GunicornApplication(Application): + def init(self, parser, opts, args): + return config + + def load(self): + return handler + + GunicornApplication().run() + + +class EventletServer(ServerAdapter): + """Untested""" + + def run(self, handler): + from eventlet import wsgi, listen + + try: + wsgi.server( + listen((self.host, self.port)), handler, log_output=(not self.quiet) + ) + except TypeError: + # Fallback, if we have old version of eventlet + wsgi.server(listen((self.host, self.port)), handler) + + +class RocketServer(ServerAdapter): + """Untested.""" + + def run(self, handler): + from rocket import Rocket + + server = Rocket((self.host, self.port), "wsgi", {"wsgi_app": handler}) + server.start() + + +class BjoernServer(ServerAdapter): + """Fast server written in C: https://github.com/jonashaag/bjoern""" + + def run(self, handler): + from bjoern import run + + run(handler, self.host, self.port) + + +class AutoServer(ServerAdapter): + """Untested.""" + + adapters = [ + WaitressServer, + PasteServer, + TwistedServer, + CherryPyServer, + CherootServer, + WSGIRefServer, + ] + + def run(self, handler): + for sa in self.adapters: + try: + return sa(self.host, self.port, **self.options).run(handler) + except ImportError: + pass + + +server_names = { + "cgi": CGIServer, + "flup": FlupFCGIServer, + "wsgiref": WSGIRefServer, + "waitress": WaitressServer, + "cherrypy": CherryPyServer, + "cheroot": CherootServer, + "paste": PasteServer, + "fapws3": FapwsServer, + "tornado": TornadoServer, + "gae": AppEngineServer, + "twisted": TwistedServer, + "diesel": DieselServer, + "meinheld": MeinheldServer, + "gunicorn": GunicornServer, + "eventlet": EventletServer, + "gevent": GeventServer, + "geventSocketIO": GeventSocketIOServer, + "rocket": RocketServer, + "bjoern": BjoernServer, + "auto": AutoServer, +} + + +############################################################################### +# Application Control ########################################################## +############################################################################### + + +def load(target, **namespace): + """Import a module or fetch an object from a module. + + * ``package.module`` returns `module` as a module object. + * ``pack.mod:name`` returns the module variable `name` from `pack.mod`. + * ``pack.mod:func()`` calls `pack.mod.func()` and returns the result. + + The last form accepts not only function calls, but any type of + expression. Keyword arguments passed to this function are available as + local variables. Example: ``import_string('re:compile(x)', x='[a-z]')`` + """ + module, target = target.split(":", 1) if ":" in target else (target, None) + if module not in sys.modules: + __import__(module) + if not target: + return sys.modules[module] + if target.isalnum(): + return getattr(sys.modules[module], target) + package_name = module.split(".")[0] + namespace[package_name] = sys.modules[package_name] + return eval("%s.%s" % (module, target), namespace) + + +def load_app(target): + """Load a bottle application from a module and make sure that the import + does not affect the current default application, but returns a separate + application object. See :func:`load` for the target parameter.""" + global NORUN + NORUN, nr_old = True, NORUN + try: + tmp = default_app.push() # Create a new "default application" + rv = load(target) # Import the target module + return rv if callable(rv) else tmp + finally: + default_app.remove(tmp) # Remove the temporary added default application + NORUN = nr_old + + +_debug = debug + + +def run( + app=None, + server="wsgiref", + host="127.0.0.1", + port=8080, + interval=1, + reloader=False, + quiet=False, + plugins=None, + debug=None, + **kargs +): + """Start a server instance. This method blocks until the server terminates. + + :param app: WSGI application or target string supported by + :func:`load_app`. (default: :func:`default_app`) + :param server: Server adapter to use. See :data:`server_names` keys + for valid names or pass a :class:`ServerAdapter` subclass. + (default: `wsgiref`) + :param host: Server address to bind to. Pass ``0.0.0.0`` to listens on + all interfaces including the external one. (default: 127.0.0.1) + :param port: Server port to bind to. Values below 1024 require root + privileges. (default: 8080) + :param reloader: Start auto-reloading server? (default: False) + :param interval: Auto-reloader interval in seconds (default: 1) + :param quiet: Suppress output to stdout and stderr? (default: False) + :param options: Options passed to the server adapter. + """ + if NORUN: + return + if reloader and not os.environ.get("BOTTLE_CHILD"): + try: + lockfile = None + fd, lockfile = tempfile.mkstemp(prefix="bottle.", suffix=".lock") + os.close(fd) # We only need this file to exist. We never write to it + while os.path.exists(lockfile): + args = [sys.executable] + sys.argv + environ = os.environ.copy() + environ["BOTTLE_CHILD"] = "true" + environ["BOTTLE_LOCKFILE"] = lockfile + p = subprocess.Popen(args, env=environ) + while p.poll() is None: # Busy wait... + os.utime(lockfile, None) # I am alive! + time.sleep(interval) + if p.poll() != 3: + if os.path.exists(lockfile): + os.unlink(lockfile) + sys.exit(p.poll()) + except KeyboardInterrupt: + pass + finally: + if os.path.exists(lockfile): + os.unlink(lockfile) + return + + try: + if debug is not None: + _debug(debug) + app = app or default_app() + if isinstance(app, basestring): + app = load_app(app) + if not callable(app): + raise ValueError("Application is not callable: %r" % app) + + for plugin in plugins or []: + app.install(plugin) + + if server in server_names: + server = server_names.get(server) + if isinstance(server, basestring): + server = load(server) + if isinstance(server, type): + server = server(host=host, port=port, **kargs) + if not isinstance(server, ServerAdapter): + raise ValueError("Unknown or unsupported server: %r" % server) + + server.quiet = server.quiet or quiet + if not server.quiet: + _stderr( + "Bottle v%s server starting up (using %s)...\n" + % (__version__, repr(server)) + ) + # _stderr("Listening on http://%s:%d/\n" % (server.host, server.port)) + # _stderr("Hit Ctrl-C to quit.\n\n") + + if reloader: + lockfile = os.environ.get("BOTTLE_LOCKFILE") + bgcheck = FileCheckerThread(lockfile, interval) + with bgcheck: + server.run(app) + if bgcheck.status == "reload": + sys.exit(3) + else: + server.run(app) + except KeyboardInterrupt: + pass + except (SystemExit, MemoryError): + raise + except: + if not reloader: + raise + if not getattr(server, "quiet", quiet): + print_exc() + time.sleep(interval) + sys.exit(3) + + +class FileCheckerThread(threading.Thread): + """Interrupt main-thread as soon as a changed module file is detected, + the lockfile gets deleted or gets to old.""" + + def __init__(self, lockfile, interval): + threading.Thread.__init__(self) + self.lockfile, self.interval = lockfile, interval + #: Is one of 'reload', 'error' or 'exit' + self.status = None + + def run(self): + exists = os.path.exists + mtime = lambda path: os.stat(path).st_mtime + files = dict() + + for module in list(sys.modules.values()): + path = getattr(module, "__file__", "") or "" + if path[-4:] in (".pyo", ".pyc"): + path = path[:-1] + if path and exists(path): + files[path] = mtime(path) + + while not self.status: + if ( + not exists(self.lockfile) + or mtime(self.lockfile) < time.time() - self.interval - 5 + ): + self.status = "error" + thread.interrupt_main() + for path, lmtime in list(files.items()): + if not exists(path) or mtime(path) > lmtime: + self.status = "reload" + thread.interrupt_main() + break + time.sleep(self.interval) + + def __enter__(self): + self.start() + + def __exit__(self, exc_type, exc_val, exc_tb): + if not self.status: + self.status = "exit" # silent exit + self.join() + return exc_type is not None and issubclass(exc_type, KeyboardInterrupt) + + +############################################################################### +# Template Adapters ############################################################ +############################################################################### + + +class TemplateError(HTTPError): + def __init__(self, message): + HTTPError.__init__(self, 500, message) + + +class BaseTemplate(object): + """Base class and minimal API for template adapters""" + + extensions = ["tpl", "html", "thtml", "stpl"] + settings = {} # used in prepare() + defaults = {} # used in render() + + def __init__(self, source=None, name=None, lookup=[], encoding="utf8", **settings): + """Create a new template. + If the source parameter (str or buffer) is missing, the name argument + is used to guess a template filename. Subclasses can assume that + self.source and/or self.filename are set. Both are strings. + The lookup, encoding and settings parameters are stored as instance + variables. + The lookup parameter stores a list containing directory paths. + The encoding parameter should be used to decode byte strings or files. + The settings parameter contains a dict for engine-specific settings. + """ + self.name = name + self.source = source.read() if hasattr(source, "read") else source + self.filename = source.filename if hasattr(source, "filename") else None + self.lookup = [os.path.abspath(x) for x in lookup] + self.encoding = encoding + self.settings = self.settings.copy() # Copy from class variable + self.settings.update(settings) # Apply + if not self.source and self.name: + self.filename = self.search(self.name, self.lookup) + if not self.filename: + raise TemplateError("Template %s not found." % repr(name)) + if not self.source and not self.filename: + raise TemplateError("No template specified.") + self.prepare(**self.settings) + + @classmethod + def search(cls, name, lookup=[]): + """Search name in all directories specified in lookup. + First without, then with common extensions. Return first hit.""" + if not lookup: + depr("The template lookup path list should not be empty.") # 0.12 + lookup = ["."] + + if os.path.isabs(name) and os.path.isfile(name): + depr("Absolute template path names are deprecated.") # 0.12 + return os.path.abspath(name) + + for spath in lookup: + spath = os.path.abspath(spath) + os.sep + fname = os.path.abspath(os.path.join(spath, name)) + if not fname.startswith(spath): + continue + if os.path.isfile(fname): + return fname + for ext in cls.extensions: + if os.path.isfile("%s.%s" % (fname, ext)): + return "%s.%s" % (fname, ext) + + @classmethod + def global_config(cls, key, *args): + """This reads or sets the global settings stored in class.settings.""" + if args: + cls.settings = cls.settings.copy() # Make settings local to class + cls.settings[key] = args[0] + else: + return cls.settings[key] + + def prepare(self, **options): + """Run preparations (parsing, caching, ...). + It should be possible to call this again to refresh a template or to + update settings. + """ + raise NotImplementedError + + def render(self, *args, **kwargs): + """Render the template with the specified local variables and return + a single byte or unicode string. If it is a byte string, the encoding + must match self.encoding. This method must be thread-safe! + Local variables may be provided in dictionaries (args) + or directly, as keywords (kwargs). + """ + raise NotImplementedError + + +class MakoTemplate(BaseTemplate): + def prepare(self, **options): + from mako.template import Template + from mako.lookup import TemplateLookup + + options.update({"input_encoding": self.encoding}) + options.setdefault("format_exceptions", bool(DEBUG)) + lookup = TemplateLookup(directories=self.lookup, **options) + if self.source: + self.tpl = Template(self.source, lookup=lookup, **options) + else: + self.tpl = Template( + uri=self.name, filename=self.filename, lookup=lookup, **options + ) + + def render(self, *args, **kwargs): + for dictarg in args: + kwargs.update(dictarg) + _defaults = self.defaults.copy() + _defaults.update(kwargs) + return self.tpl.render(**_defaults) + + +class CheetahTemplate(BaseTemplate): + def prepare(self, **options): + from Cheetah.Template import Template + + self.context = threading.local() + self.context.vars = {} + options["searchList"] = [self.context.vars] + if self.source: + self.tpl = Template(source=self.source, **options) + else: + self.tpl = Template(file=self.filename, **options) + + def render(self, *args, **kwargs): + for dictarg in args: + kwargs.update(dictarg) + self.context.vars.update(self.defaults) + self.context.vars.update(kwargs) + out = str(self.tpl) + self.context.vars.clear() + return out + + +class Jinja2Template(BaseTemplate): + def prepare(self, filters=None, tests=None, globals={}, **kwargs): + from jinja2 import Environment, FunctionLoader + + if "prefix" in kwargs: # TODO: to be removed after a while + raise RuntimeError( + "The keyword argument `prefix` has been removed. " + "Use the full jinja2 environment name line_statement_prefix instead." + ) + self.env = Environment(loader=FunctionLoader(self.loader), **kwargs) + if filters: + self.env.filters.update(filters) + if tests: + self.env.tests.update(tests) + if globals: + self.env.globals.update(globals) + if self.source: + self.tpl = self.env.from_string(self.source) + else: + self.tpl = self.env.get_template(self.filename) + + def render(self, *args, **kwargs): + for dictarg in args: + kwargs.update(dictarg) + _defaults = self.defaults.copy() + _defaults.update(kwargs) + return self.tpl.render(**_defaults) + + def loader(self, name): + fname = self.search(name, self.lookup) + if not fname: + return + with open(fname, "rb") as f: + return f.read().decode(self.encoding) + + +class SimpleTemplate(BaseTemplate): + def prepare(self, escape_func=html_escape, noescape=False, syntax=None, **ka): + self.cache = {} + enc = self.encoding + self._str = lambda x: touni(x, enc) + self._escape = lambda x: escape_func(touni(x, enc)) + self.syntax = syntax + if noescape: + self._str, self._escape = self._escape, self._str + + @cached_property + def co(self): + return compile(self.code, self.filename or "", "exec") + + @cached_property + def code(self): + source = self.source + if not source: + with open(self.filename, "rb") as f: + source = f.read() + try: + source, encoding = touni(source), "utf8" + except UnicodeError: + depr("Template encodings other than utf8 are no longer supported.") # 0.11 + source, encoding = touni(source, "latin1"), "latin1" + parser = StplParser(source, encoding=encoding, syntax=self.syntax) + code = parser.translate() + self.encoding = parser.encoding + return code + + def _rebase(self, _env, _name=None, **kwargs): + if _name is None: + depr( + "Rebase function called without arguments." + " You were probably looking for {{base}}?", + True, + ) # 0.12 + _env["_rebase"] = (_name, kwargs) + + def _include(self, _env, _name=None, **kwargs): + if _name is None: + depr( + "Rebase function called without arguments." + " You were probably looking for {{base}}?", + True, + ) # 0.12 + env = _env.copy() + env.update(kwargs) + if _name not in self.cache: + self.cache[_name] = self.__class__(name=_name, lookup=self.lookup) + return self.cache[_name].execute(env["_stdout"], env) + + def execute(self, _stdout, kwargs): + env = self.defaults.copy() + env.update(kwargs) + env.update( + { + "_stdout": _stdout, + "_printlist": _stdout.extend, + "include": functools.partial(self._include, env), + "rebase": functools.partial(self._rebase, env), + "_rebase": None, + "_str": self._str, + "_escape": self._escape, + "get": env.get, + "setdefault": env.setdefault, + "defined": env.__contains__, + } + ) + eval(self.co, env) + if env.get("_rebase"): + subtpl, rargs = env.pop("_rebase") + rargs["base"] = "".join(_stdout) # copy stdout + del _stdout[:] # clear stdout + return self._include(env, subtpl, **rargs) + return env + + def render(self, *args, **kwargs): + """Render the template using keyword arguments as local variables.""" + env = {} + stdout = [] + for dictarg in args: + env.update(dictarg) + env.update(kwargs) + self.execute(stdout, env) + return "".join(stdout) + + +class StplSyntaxError(TemplateError): + pass + + +class StplParser(object): + """Parser for stpl templates.""" + + _re_cache = {} #: Cache for compiled re patterns + # This huge pile of voodoo magic splits python code into 8 different tokens. + # 1: All kinds of python strings (trust me, it works) + _re_tok = ( + "([urbURB]?(?:''(?!')|\"\"(?!\")|'{6}|\"{6}" + "|'(?:[^\\\\']|\\\\.)+?'|\"(?:[^\\\\\"]|\\\\.)+?\"" + "|'{3}(?:[^\\\\]|\\\\.|\\n)+?'{3}" + '|"{3}(?:[^\\\\]|\\\\.|\\n)+?"{3}))' + ) + _re_inl = _re_tok.replace("|\\n", "") # We re-use this string pattern later + # 2: Comments (until end of line, but not the newline itself) + _re_tok += "|(#.*)" + # 3,4: Open and close grouping tokens + _re_tok += "|([\\[\\{\\(])" + _re_tok += "|([\\]\\}\\)])" + # 5,6: Keywords that start or continue a python block (only start of line) + _re_tok += ( + "|^([ \\t]*(?:if|for|while|with|try|def|class)\\b)" + "|^([ \\t]*(?:elif|else|except|finally)\\b)" + ) + # 7: Our special 'end' keyword (but only if it stands alone) + _re_tok += "|((?:^|;)[ \\t]*end[ \\t]*(?=(?:%(block_close)s[ \\t]*)?\\r?$|;|#))" + # 8: A customizable end-of-code-block template token (only end of line) + _re_tok += "|(%(block_close)s[ \\t]*(?=\\r?$))" + # 9: And finally, a single newline. The 10th token is 'everything else' + _re_tok += "|(\\r?\\n)" + + # Match the start tokens of code areas in a template + _re_split = "(?m)^[ \t]*(\\\\?)((%(line_start)s)|(%(block_start)s))(%%?)" + # Match inline statements (may contain python strings) + _re_inl = "(?m)%%(inline_start)s((?:%s|[^'\"\n])*?)%%(inline_end)s" % _re_inl + _re_tok = "(?m)" + _re_tok + + default_syntax = "<% %> % {{ }}" + + def __init__(self, source, syntax=None, encoding="utf8"): + self.source, self.encoding = touni(source, encoding), encoding + self.set_syntax(syntax or self.default_syntax) + self.code_buffer, self.text_buffer = [], [] + self.lineno, self.offset = 1, 0 + self.indent, self.indent_mod = 0, 0 + self.paren_depth = 0 + + def get_syntax(self): + """Tokens as a space separated string (default: <% %> % {{ }})""" + return self._syntax + + def set_syntax(self, syntax): + self._syntax = syntax + self._tokens = syntax.split() + if not syntax in self._re_cache: + names = "block_start block_close line_start inline_start inline_end" + etokens = map(re.escape, self._tokens) + pattern_vars = dict(zip(names.split(), etokens)) + patterns = (self._re_split, self._re_tok, self._re_inl) + patterns = [re.compile(p % pattern_vars) for p in patterns] + self._re_cache[syntax] = patterns + self.re_split, self.re_tok, self.re_inl = self._re_cache[syntax] + + syntax = property(get_syntax, set_syntax) + + def translate(self): + if self.offset: + raise RuntimeError("Parser is a one time instance.") + while True: + m = self.re_split.search(self.source[self.offset :]) + if m: + text = self.source[self.offset : self.offset + m.start()] + self.text_buffer.append(text) + self.offset += m.end() + if m.group(1): # New escape syntax + line, sep, _ = self.source[self.offset :].partition("\n") + self.text_buffer.append(m.group(2) + m.group(5) + line + sep) + self.offset += len(line + sep) + 1 + continue + elif m.group(5): # Old escape syntax + depr("Escape code lines with a backslash.") # 0.12 + line, sep, _ = self.source[self.offset :].partition("\n") + self.text_buffer.append(m.group(2) + line + sep) + self.offset += len(line + sep) + 1 + continue + self.flush_text() + self.read_code(multiline=bool(m.group(4))) + else: + break + self.text_buffer.append(self.source[self.offset :]) + self.flush_text() + return "".join(self.code_buffer) + + def read_code(self, multiline): + code_line, comment = "", "" + while True: + m = self.re_tok.search(self.source[self.offset :]) + if not m: + code_line += self.source[self.offset :] + self.offset = len(self.source) + self.write_code(code_line.strip(), comment) + return + code_line += self.source[self.offset : self.offset + m.start()] + self.offset += m.end() + _str, _com, _po, _pc, _blk1, _blk2, _end, _cend, _nl = m.groups() + if (code_line or self.paren_depth > 0) and ( + _blk1 or _blk2 + ): # a if b else c + code_line += _blk1 or _blk2 + continue + if _str: # Python string + code_line += _str + elif _com: # Python comment (up to EOL) + comment = _com + if multiline and _com.strip().endswith(self._tokens[1]): + multiline = False # Allow end-of-block in comments + elif _po: # open parenthesis + self.paren_depth += 1 + code_line += _po + elif _pc: # close parenthesis + if self.paren_depth > 0: + # we could check for matching parentheses here, but it's + # easier to leave that to python - just check counts + self.paren_depth -= 1 + code_line += _pc + elif _blk1: # Start-block keyword (if/for/while/def/try/...) + code_line, self.indent_mod = _blk1, -1 + self.indent += 1 + elif _blk2: # Continue-block keyword (else/elif/except/...) + code_line, self.indent_mod = _blk2, -1 + elif _end: # The non-standard 'end'-keyword (ends a block) + self.indent -= 1 + elif _cend: # The end-code-block template token (usually '%>') + if multiline: + multiline = False + else: + code_line += _cend + else: # \n + self.write_code(code_line.strip(), comment) + self.lineno += 1 + code_line, comment, self.indent_mod = "", "", 0 + if not multiline: + break + + def flush_text(self): + text = "".join(self.text_buffer) + del self.text_buffer[:] + if not text: + return + parts, pos, nl = [], 0, "\\\n" + " " * self.indent + for m in self.re_inl.finditer(text): + prefix, pos = text[pos : m.start()], m.end() + if prefix: + parts.append(nl.join(map(repr, prefix.splitlines(True)))) + if prefix.endswith("\n"): + parts[-1] += nl + parts.append(self.process_inline(m.group(1).strip())) + if pos < len(text): + prefix = text[pos:] + lines = prefix.splitlines(True) + if lines[-1].endswith("\\\\\n"): + lines[-1] = lines[-1][:-3] + elif lines[-1].endswith("\\\\\r\n"): + lines[-1] = lines[-1][:-4] + parts.append(nl.join(map(repr, lines))) + code = "_printlist((%s,))" % ", ".join(parts) + self.lineno += code.count("\n") + 1 + self.write_code(code) + + def process_inline(self, chunk): + if chunk[0] == "!": + return "_str(%s)" % chunk[1:] + return "_escape(%s)" % chunk + + def write_code(self, line, comment=""): + line, comment = self.fix_backward_compatibility(line, comment) + code = " " * (self.indent + self.indent_mod) + code += line.lstrip() + comment + "\n" + self.code_buffer.append(code) + + def fix_backward_compatibility(self, line, comment): + parts = line.strip().split(None, 2) + if parts and parts[0] in ("include", "rebase"): + depr("The include and rebase keywords are functions now.") # 0.12 + if len(parts) == 1: + return "_printlist([base])", comment + elif len(parts) == 2: + return "_=%s(%r)" % tuple(parts), comment + else: + return "_=%s(%r, %s)" % tuple(parts), comment + if self.lineno <= 2 and not line.strip() and "coding" in comment: + m = re.match(r"#.*coding[:=]\s*([-\w.]+)", comment) + if m: + depr("PEP263 encoding strings in templates are deprecated.") # 0.12 + enc = m.group(1) + self.source = self.source.encode(self.encoding).decode(enc) + self.encoding = enc + return line, comment.replace("coding", "coding*") + return line, comment + + +def template(*args, **kwargs): + """ + Get a rendered template as a string iterator. + You can use a name, a filename or a template string as first parameter. + Template rendering arguments can be passed as dictionaries + or directly (as keyword arguments). + """ + tpl = args[0] if args else None + adapter = kwargs.pop("template_adapter", SimpleTemplate) + lookup = kwargs.pop("template_lookup", TEMPLATE_PATH) + tplid = (id(lookup), tpl) + if tplid not in TEMPLATES or DEBUG: + settings = kwargs.pop("template_settings", {}) + if isinstance(tpl, adapter): + TEMPLATES[tplid] = tpl + if settings: + TEMPLATES[tplid].prepare(**settings) + elif "\n" in tpl or "{" in tpl or "%" in tpl or "$" in tpl: + TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings) + else: + TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings) + if not TEMPLATES[tplid]: + abort(500, "Template (%s) not found" % tpl) + for dictarg in args[1:]: + kwargs.update(dictarg) + return TEMPLATES[tplid].render(kwargs) + + +mako_template = functools.partial(template, template_adapter=MakoTemplate) +cheetah_template = functools.partial(template, template_adapter=CheetahTemplate) +jinja2_template = functools.partial(template, template_adapter=Jinja2Template) + + +def view(tpl_name, **defaults): + """Decorator: renders a template for a handler. + The handler can control its behavior like that: + + - return a dict of template vars to fill out the template + - return something other than a dict and the view decorator will not + process the template, but return the handler result as is. + This includes returning a HTTPResponse(dict) to get, + for instance, JSON with autojson or other castfilters. + """ + + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + if isinstance(result, (dict, DictMixin)): + tplvars = defaults.copy() + tplvars.update(result) + return template(tpl_name, **tplvars) + elif result is None: + return template(tpl_name, **defaults) + return result + + return wrapper + + return decorator + + +mako_view = functools.partial(view, template_adapter=MakoTemplate) +cheetah_view = functools.partial(view, template_adapter=CheetahTemplate) +jinja2_view = functools.partial(view, template_adapter=Jinja2Template) + + +############################################################################### +# Constants and Globals ######################################################## +############################################################################### + + +TEMPLATE_PATH = ["./", "./views/"] +TEMPLATES = {} +DEBUG = False +NORUN = False # If set, run() does nothing. Used by load_app() + +#: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found') +HTTP_CODES = httplib.responses +HTTP_CODES[418] = "I'm a teapot" # RFC 2324 +HTTP_CODES[422] = "Unprocessable Entity" # RFC 4918 +HTTP_CODES[428] = "Precondition Required" +HTTP_CODES[429] = "Too Many Requests" +HTTP_CODES[431] = "Request Header Fields Too Large" +HTTP_CODES[511] = "Network Authentication Required" +_HTTP_STATUS_LINES = dict((k, "%d %s" % (k, v)) for (k, v) in HTTP_CODES.items()) + +#: The default template used for error pages. Override with @error() +ERROR_PAGE_TEMPLATE = ( + """ +%%try: + %%from %s import DEBUG, HTTP_CODES, request, touni + + + + Error: {{e.status}} + + + +

Error: {{e.status}}

+

Sorry, the requested URL {{repr(request.url)}} + caused an error:

+
{{e.body}}
+ %%if DEBUG and e.exception: +

Exception:

+
{{repr(e.exception)}}
+ %%end + %%if DEBUG and e.traceback: +

Traceback:

+
{{e.traceback}}
+ %%end + + +%%except ImportError: + ImportError: Could not generate the error page. Please add bottle to + the import path. +%%end +""" + % __name__ +) + +#: A thread-safe instance of :class:`LocalRequest`. If accessed from within a +#: request callback, this instance always refers to the *current* request +#: (even on a multithreaded server). +request = LocalRequest() + +#: A thread-safe instance of :class:`LocalResponse`. It is used to change the +#: HTTP response for the *current* request. +response = LocalResponse() + +#: A thread-safe namespace. Not used by Bottle. +local = threading.local() + +# Initialize app stack (create first empty Bottle app) +# BC: 0.6.4 and needed for run() +app = default_app = AppStack() +app.push() + +#: A virtual package that redirects import statements. +#: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`. +ext = _ImportRedirect( + "bottle.ext" if __name__ == "__main__" else __name__ + ".ext", "bottle_%s" +).module + +if __name__ == "__main__": + opt, args, parser = _cmd_options, _cmd_args, _cmd_parser + if opt.version: + _stdout("Bottle %s\n" % __version__) + sys.exit(0) + if not args: + parser.print_help() + _stderr("\nError: No application specified.\n") + sys.exit(1) + + sys.path.insert(0, ".") + sys.modules.setdefault("bottle", sys.modules["__main__"]) + + host, port = (opt.bind or "localhost"), 8080 + if ":" in host and host.rfind("]") < host.rfind(":"): + host, port = host.rsplit(":", 1) + host = host.strip("[]") + + run( + args[0], + host=host, + port=int(port), + server=opt.server, + reloader=opt.reload, + plugins=opt.plugin, + debug=opt.debug, + ) + + +# THE END diff --git a/UmiOCR-data/py_src/server/cmd_client.py b/UmiOCR-data/py_src/server/cmd_client.py new file mode 100644 index 0000000..8b9fc6b --- /dev/null +++ b/UmiOCR-data/py_src/server/cmd_client.py @@ -0,0 +1,183 @@ +# ============================================ +# =============== 命令行-客户端 =============== +# ============================================ + +import os +import sys +import time +import psutil + +from umi_log import logger +from ..utils import pre_configs +from ..platform import Platform + + +# 获取进程的创建时间 +def getPidTime(pid): + try: + return str(psutil.Process(pid).create_time()) + except psutil.NoSuchProcess: + logger.warning( + "psutil.pid_exists(pid) 存在,但 Process 无法生成对象", + exc_info=True, + stack_info=True, + ) + return "" + except Exception: + logger.error("psutil.Process(pid) error", exc_info=True, stack_info=True) + return "" + + +# 检查软件多开 +def _isMultiOpen(): + # 检查上次记录的pid和key是否还在运行 + recordPID = pre_configs.getValue("last_pid") + recordPTime = pre_configs.getValue("last_ptime") + if psutil.pid_exists(recordPID): # 上次记录的pid如今存在 + processTime = getPidTime(recordPID) + if recordPTime == processTime: # 当前该进程启动时间与记录的相同,则为多开 + return True + return False + + +# 输出 +def _output(argv, argument, mode, text): + if argument not in argv: + return + path = "" + # 提取路径参数 + try: + i = argv.index(argument) + path = argv[i + 1] + del argv[i : i + 2] + except Exception as e: + # logger 输出到 stderr , print 输出到 stdout + logger.error(f"argument {argument} cannot be resolved.", exc_info=True) + print(f"[Error] argument {argument} cannot be resolved. \n{e}") + return + # 相对路径转绝对路径 + if not os.path.isabs(path): + # 获取当前工作目录的上一级目录 + current_dir = os.getcwd() + parent_dir = os.path.abspath(os.path.join(current_dir, os.pardir)) + # 将 path 转为绝对路径,且以上一级目录为基准 + path = os.path.abspath(os.path.join(parent_dir, path)) + try: + with open(path, mode, encoding="utf-8") as f: + f.write(text) + print(f"\nSuccess output to file: {path}") + except Exception as e: + logger.error(f"failed to write file {path} .", exc_info=True) + print(f"[Error] failed to write file {path} : \n{e}") + return + + +# 复制文本到剪贴板,不依赖第三方库 +def _clip(text): + import subprocess + import platform + import tempfile + + os_type = platform.system() + try: + if os_type == "Windows": + # 创建一个临时文件,并立即关闭它,以便其他进程可以访问 + with tempfile.NamedTemporaryFile( + delete=False, mode="w+", newline="\n" + ) as temp_file: + temp_file.write(text) + temp_file_name = temp_file.name + temp_file.close() + try: + subprocess.run(f"clip < {temp_file_name}", check=True, shell=True) + finally: + os.unlink(temp_file_name) # 删除临时文件 + print("\nSuccess copy to clipboard.") + else: + print(f"[Error] clip unsupported OS: {os_type}") + except Exception as e: + logger.error("failed to copy to clipboard.", exc_info=True) + print(f"[Error] failed to copy to clipboard: {e}") + + +# 跨进程发送指令 +def _sendCmd(argv): + port = pre_configs.getValue("server_port") # 记录的端口号 + url = f"http://127.0.0.1:{port}/argv" # argv,指令列表接口 + errStr = f"Umi-OCR 已在运行,HTTP跨进程传输指令失败。\n[Error] Umi-OCR is already running, HTTP cross process transmission instruction failed.\n{url}" + import urllib.request + import json + + # 向后台工作进程发送指令 + res = "" + try: + data = json.dumps(argv, ensure_ascii=True).encode("utf-8") + req = urllib.request.Request( + url, data=data, headers={"Content-Type": "application/json"} + ) + # response = urllib.request.urlopen(req) + # 创建一个不使用代理的 opener ,发送请求 + opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) + response = opener.open(req) + if response.status == 200: + res = response.read().decode("utf-8") + else: + res = f"{errStr}\nstatus_code: {response.status}" + except Exception as e: + res = f"{errStr}\nerror: {e}" + # 输出 + print(res) + _output(argv, "-->", "w", res) + _output(argv, "--output", "w", res) + _output(argv, "-->>", "a", res) + _output(argv, "--output_append", "a", res) + if "--clip" in argv: + _clip(res) + + +# 启动新进程,并发送指令 +def _newSend(argv): + from umi_about import UmiAbout # 项目信息 + + appPath = UmiAbout["app"]["path"] + if not appPath: + msg = "未找到 Umi-OCR 入口路径,无法启动新进程。请手动启动 Umi-OCR 后发送指令。\nUmi-OCR path not found, unable to start a new process." + os.MessageBox(msg) + return + # 启动进程,传入强制参数,避免递归无限启动进程 + Platform.runNewProcess(appPath, " --force") + # 等待并检查 服务进程初始化完毕 + for i in range(60): # 检测轮次 + time.sleep(0.5) # 每次等待时间 + pre_configs.readConfigs() # 重新读取预配置 + if _isMultiOpen(): # 检测新进程是否启动 + _sendCmd(argv) # 发送指令 + return + print( + "服务进程初始化失败,等待时间超时。\n[Error] The service process initialization failed and the waiting time timed out." + ) + + +# 初始化命令行 +def initCmd(): + argv = sys.argv[1:] + force = False + if "--force" in argv: + argv.remove("--force") + force = True + # 检查,发现软件多开,则向已在运行的进程发送初始指令 + if _isMultiOpen(): + _sendCmd(argv) + return False + # 未多开,则启动进程 + else: + # 无参数或强制启动,则正常运行本进程,刷新pid和ptime + if not argv or force: + nowPid = os.getpid() + nowPTime = getPidTime(nowPid) + pre_configs.setValue("last_pid", nowPid) + pre_configs.setValue("last_ptime", nowPTime) + return True + else: # 有参数,则启动新进程并发送参数 + _newSend(argv) + return False diff --git a/UmiOCR-data/py_src/server/cmd_server.py b/UmiOCR-data/py_src/server/cmd_server.py new file mode 100644 index 0000000..a4d63a5 --- /dev/null +++ b/UmiOCR-data/py_src/server/cmd_server.py @@ -0,0 +1,524 @@ +# =============================================== +# =============== 命令行-解析和执行 =============== +# =============================================== + +import time +import json +import argparse +from threading import Condition +from ..utils.call_func import CallFunc +from ..utils.file_finder import findFiles +from ..event_bus.pubsub_service import PubSubService # 发布/订阅管理器 + + +# 命令执行器 +class _Actuator: + def __init__(self): + self.pyDict = {} # python模块字典 + self.qmlDict = {} # qml模块字典 + self.tagPageConn = None # 页面连接器的引用 + + # 初始化,并收集信息。传入qml模块字典 + def initCollect(self, qmlModuleDict): + qmlModuleDict = qmlModuleDict.toVariant() + self.qmlDict.update(qmlModuleDict) + # 获取页面连接器实例 + from ..tag_pages.tag_pages_connector import TagPageConnObj + + self.tagPageConn = TagPageConnObj + + # ============================== 页面管理 ============================== + + # 返回当前 [可创建的页面模板] 和 [已创建的页面] 的信息 + def getAllPages(self): + TabViewManager = self.qmlDict["TabViewManager"] + pageList = TabViewManager.getPageList().toVariant() + infoStr = "All opened pages:\npage_index\tkey\ttitle\n" + for index, value in enumerate(pageList): + infoStr += f'{index}\t{value["ctrlKey"]}\t{value["info"]["title"]}\n' + + infoList = TabViewManager.getInfoList().toVariant() + infoStr += ( + "\nAll page templates that can be opened:\ntemplate_index\tkey\ttitle\n" + ) + for index, value in enumerate(infoList): + infoStr += f'{index}\t{value["key"]}\t{value["title"]}\n' + + infoStr += "\nUsage of create a page:\n" + infoStr += " Umi-OCR --add_page [template_index]\n" + infoStr += "Usage of delete a page:\n" + infoStr += " Umi-OCR --del_page [page_index]\n" + infoStr += "Usage of query the modules that can be called:\n" + infoStr += " Umi-OCR --all_modules\n" + + return infoStr + + # 创建页面 + def addPage(self, index): + try: + index = int(index) + except ValueError: + return f"[Error] template_index must be integer, not {index}." + TabViewManager = self.qmlDict["TabViewManager"] + infoList = TabViewManager.getInfoList().toVariant() + l = len(infoList) - 1 + if index < 0 or index > l: + return f"[Error] template_index {index} out of range (0~{l})." + return self.call("TabViewManager", "qml", "addTabPage", False, -1, index) + + # 删除页面 + def delPage(self, index): + try: + index = int(index) + except ValueError: + return f"[Error] page_index must be integer, not {index}." + TabViewManager = self.qmlDict["TabViewManager"] + pageList = TabViewManager.getPageList().toVariant() + l = len(pageList) - 1 + if index < 0 or index > l: + return f"[Error] page_index {index} out of range (0~{l})." + return self.call("TabViewManager", "qml", "delTabPage", False, index) + + # 通过key创建页面 + def addPageByKey(self, key): + # 1. 检查截图标签页,如果未创建则创建 + module, _ = self.getModuleFromName(key, "qml") + if module == None: + tvm = self.qmlDict["TabViewManager"] + infoList = tvm.getInfoList().toVariant() + f2 = False + for i, v in enumerate(infoList): + if v["key"] == key: + f2 = True + self.addPage(i) + break + if not f2: + return f"[Error] Template {key} not found." + for i in range(10): + time.sleep(0.5) + module, _ = self.getModuleFromName(key, "qml") + if module != None: + break + if module == None: + return f"[Error] Unable to create template {key}." + return "[Success]" + + # ============================== 动态模块调用 ============================== + + # 返回所有可调用模块 + def getModules(self): + pyd, qmld = {}, {} + pages = self.tagPageConn.pages + for p in pages: + if pages[p]["qmlObj"]: + qmld[p] = pages[p]["qmlObj"] + if pages[p]["pyObj"]: + pyd[p] = pages[p]["pyObj"] + pyd.update(self.pyDict) + qmld.update(self.qmlDict) + return {"py": pyd, "qml": qmld} + + # 传入(不完整的)模块名,搜索并返回模块实例。type: py / qml + def getModuleFromName(self, moduleName, type_): + d = self.getModules()[type_] + module = None + if moduleName in d: + module = d[moduleName] + else: + for name in d.keys(): # 若输入模块名的前几个字母,也可以匹配 + if name.startswith(moduleName): + moduleName = name + module = d[name] + break + return module, moduleName + + # 返回所有可调用模块的帮助信息 + def getModulesHelp(self): + modules = self.getModules() + help = "\nPython modules: (Usage: Umi-OCR --call_py [module name])\n" + for k in modules["py"].keys(): + help += f" {k}\n" + help += "\nQml modules: (Usage: Umi-OCR --call_qml [module name])\n" + for k in modules["qml"].keys(): + help += f" {k}\n" + help += f"\nTips: module name can only write the first letters, such as [ScreenshotOCR_1] → [Scr]" + return help + + # 返回一个模块的所有函数的帮助信息 + def getModuleFuncsHelp(self, moduleName, type_): + module, moduleName = self.getModuleFromName(moduleName, type_) + typeStr = "Python" if type_ == "py" else "qml" + if not module: + return f'[Error] {typeStr} module "{moduleName}" non-existent.' + funcs = [ + func + for func in vars(type(module)).keys() + if callable(getattr(module, func)) + ] + help = f'All functions in {typeStr} module "{moduleName}":\n' + for f in funcs: + f = str(f) + if not f.startswith("_"): + help += f" {f}\n" + help += f"Usage: Umi-OCR --call_qml {moduleName} --func [function name]\n" + return help + + # 调用一个模块函数。type: py / qml , thread: True 同步在子线程 / False 异步在主线程 + def call(self, moduleName, type_, funcName, thread, *paras): + module, moduleName = self.getModuleFromName(moduleName, type_) + typeStr = "Python" if type_ == "py" else "qml" + if not module: + return f'[Error] {typeStr} module "{moduleName}" non-existent.' + func = getattr(module, funcName, None) + if not func: + return f'[Error] func "{funcName}" not exist in {typeStr} module "{moduleName}".' + try: + if thread: # 在子线程执行,返回结果 + return func(*paras) + else: # 在主线程执行,返回标志文本 + CallFunc.now(func, *paras) # 在主线程中调用回调函数 + return f'Calling "{funcName}" in main thread.' + except Exception as e: + return f'[Error] calling {typeStr} module "{moduleName}" - "{funcName}" {paras}: {e}' + + # ============================== 便捷指令 ============================== + + # 控制主窗口 + def ctrlWindow(self, show, hide, quit): + if show: + self.call("MainWindow", "qml", "setVisibility", False, True) + return "Umi-OCR show." + elif hide: + self.call("MainWindow", "qml", "setVisibility", False, False) + return "Umi-OCR hide." + elif quit: + self.call("MainWindow", "qml", "quit", False) + return "Umi-OCR quit." + + # 快捷OCR:截图/粘贴/路径,并获取返回结果 + def quick_ocr(self, ss, clip, paras): + # 1. 检查截图标签页,如果未创建则创建 + msg = self.addPageByKey("ScreenshotOCR") + if msg != "[Success]": + return msg + + # 2. 订阅事件,监听 <> + isOcrEnd = False + resList = [] + condition = Condition() # 线程同步器 + + def onOcrEnd(recentResult): + nonlocal isOcrEnd, resList + isOcrEnd = True + resList = recentResult + with condition: # 释放线程阻塞 + condition.notify() + + PubSubService.subscribe("<>", onOcrEnd) + + # 3. 调用截图标签页的函数 + if ss: # 截图 + if not paras: # 无参数,手动截图 + self.call("ScreenshotOCR", "qml", "screenshot", False) + else: # 有参数,自动截图 umi-ocr --screenshot screen=0 rect=0,100,500,200 + rect = [0, 0, 0, 0] # 截图矩形框 + screen = 0 # 显示器编号 + para_args = [] + try: + for para in paras: # 空格分隔 + para_args.extend(para.split()) + for part in para_args: + if part.startswith("rect="): + rect_values = part[len("rect=") :].split(",") + rect_values = [int(v) for v in rect_values] + rect[: len(rect_values)] = rect_values # 补齐rect的值 + elif part.startswith("screen="): + screen = int(part[len("screen=") :]) + self.call( + "ScreenshotOCR", "qml", "autoScreenshot", False, rect, screen + ) + except Exception as e: + return f"[Error] {e}" + elif clip: # 粘贴 + self.call("ScreenshotOCR", "qml", "paste", False) + else: # 路径 + if not paras: + return "[Error] Paths is empty." + paths = findFiles(paras, "image", True) # 递归搜索 + if not paths: + return "[Error] No valid path." + self.call("ScreenshotOCR", "qml", "ocrPaths", False, paths) + + # 4. 堵塞等待任务完成,注销事件订阅 + with condition: + while not isOcrEnd: + condition.wait() + PubSubService.unsubscribe("<>", onOcrEnd) + + # 5. 处理结果列表,转文本 + text = "" + for i, r in enumerate(resList): # 遍历图片 + if text and not text.endswith("\n"): # 如果上次结果结尾没有换行,则补换行 + text += "\n" + if r["code"] == 100: + for d in r["data"]: # 遍历文本块 + text += d["text"] + d["end"] + elif r["code"] != 101 and isinstance(r["data"], str): + text += r["data"] + if not text: + text = "[Message] No text in OCR result." + return text + + # 创建二维码 + def qrcode_create(self, paras): + if len(paras) < 2: + return ( + '[Error] Not enough arguments passed! Must pass "text" "save_image.jpg"' + ) + text, path = paras[0], paras[1] + if len(paras) == 3: + w = h = int(paras[2]) + elif len(paras) == 4: + w, h = int(paras[2]), int(paras[3]) + else: + w = h = 0 + try: + from ..mission.mission_qrcode import MissionQRCode + + pil = MissionQRCode.createImage( + text, + format="QRCode", # 格式 + w=w, # 宽高 + h=h, + quiet_zone=-1, # 边缘宽度 + ec_level=-1, # 纠错等级 + ) + if isinstance(pil, str): + return pil + pil.save(path) + return f"Successfully saved to {path}" + except Exception as e: + return f"[Error] {str(e)}" + + # 识别二维码 + def qrcode_read(self, paras): + if len(paras) < 1: + return '[Error] Not enough arguments passed! Must pass "image_to_recognize.jpg"' + try: + from ..mission.mission_qrcode import MissionQRCode + from PIL import Image + except Exception as e: + return f"[Error] {str(e)}" + resText = "" + paths = findFiles(paras, "image", True) # 递归搜索图片 + for index, path in enumerate(paths): + if index != 0: + resText += "\n" + try: + pil = Image.open(path) + res = MissionQRCode.addMissionWait({}, [{"pil": pil}]) + res = res[0]["result"] + if res["code"] == 100: + t = "" + for i, d in enumerate(res["data"]): + if i != 0: + t += "\n" + t += d["text"] + resText += t + elif res["code"] == 101: + resText += "No code in image." + else: + resText += f"[Error] Code: {res['code']}\nMessage: {res['data']}" + except Exception as e: + resText += f"[Error] {str(e)}" + return resText + + +CmdActuator = _Actuator() + + +# 命令解析器 +class _Cmd: + def __init__(self): + self._parser = None + + def init(self): + if self._parser: + return + self._parser = argparse.ArgumentParser(prog="Umi-OCR") + # 便捷指令 + self._parser.add_argument( + "--show", action="store_true", help="Make the app appear in the foreground." + ) + self._parser.add_argument( + "--hide", action="store_true", help="Hide app in the background." + ) + self._parser.add_argument("--quit", action="store_true", help="Quit app.") + self._parser.add_argument( + "--screenshot", + action="store_true", + help="Screenshot OCR and output the result.", + ) + self._parser.add_argument( + "--clipboard", + action="store_true", + help="Clipboard OCR and output the result.", + ) + self._parser.add_argument( + "--path", + action="store_true", + help="OCR the image in path and output the result.", + ) + self._parser.add_argument( + "--qrcode_create", + action="store_true", + help='Create a QR code from the text. Use --qrcode_create "text" "save_image.jpg"', + ) + self._parser.add_argument( + "--qrcode_read", + action="store_true", + help='Read the QR code. Use --qrcode_read "image_to_recognize.jpg"', + ) + self._parser.add_argument( + "--reload", + action="store_true", + help='Reload settings from the configuration file ".settings"', + ) + # 页面管理 + self._parser.add_argument( + "--all_pages", + action="store_true", + help="Output all template and page information.", + ) + self._parser.add_argument( + "--add_page", type=int, help="usage: Umi-OCR --all_pages" + ) + self._parser.add_argument( + "--del_page", type=int, help="usage: Umi-OCR --all_pages" + ) + # 函数调用 + self._parser.add_argument( + "--all_modules", + action="store_true", + help="Output all module names that can be called.", + ) + self._parser.add_argument( + "--call_py", help="Calling a function on a Python module." + ) + self._parser.add_argument( + "--call_qml", help="Calling a function on a Qml module." + ) + self._parser.add_argument( + "--func", help="The name of the function to be called." + ) + self._parser.add_argument( + "--thread", + action="store_true", + help="The function will be called on the child thread and return the result, but it may be unstable or cause QML to crash.", + ) + # 输出 + self._parser.add_argument( + "--clip", + action="store_true", + help="Copy the results to the clipboard.", + ) + self._parser.add_argument( + "--output", + help="The path to the file where results will be saved. (overwrite)", + ) + self._parser.add_argument( + "--output_append", + help="The path to the file where results will be saved. (append)", + ) + self._parser.add_argument("-->", help='"-->" equivalent to "--output"') + self._parser.add_argument("-->>", help='"-->>" equivalent to "--output_append"') + self._parser.add_argument("paras", nargs="*", help="parameters of [--func].") + + # 分析指令,返回指令对象或报错字符串 + def parse(self, argv): + self.init() + # 特殊情况 + if "-h" in argv or "--help" in argv: # 帮助 + return self._parser.format_help() + if len(argv) == 0: # 空指令 + CmdActuator.ctrlWindow(True, False, False) # 展示主窗 + return self._parser.format_help() # 返回帮助 + # 正常解析 + try: + return self._parser.parse_args(argv) + except SystemExit as e: + return f"Your argv: {argv}\n[Error]: {e}\nusage: Umi-OCR --help" + except Exception as e: + return f"Your argv: {argv}\n[Error]: {e}\nusage: Umi-OCR --help" + + # 执行指令,返回执行结果字符串 + def execute(self, argv): + args = self.parse(argv) + if isinstance(args, str): + return args + if args.all_modules: + return CmdActuator.getModulesHelp() + # 便捷指令 + if args.show or args.hide or args.quit: # 控制主窗 + return CmdActuator.ctrlWindow(args.show, args.hide, args.quit) + if args.screenshot or args.clipboard or args.path: # 快捷识图 + return CmdActuator.quick_ocr(args.screenshot, args.clipboard, args.paras) + if args.qrcode_create: # 写二维码 + return CmdActuator.qrcode_create(args.paras) + if args.qrcode_read: # 读二维码 + return CmdActuator.qrcode_read(args.paras) + if args.reload: # 重新加载配置 + PubSubService.publish("<>") + return "Settings reload." + # 页面管理 + if args.all_pages: + return CmdActuator.getAllPages() + if not args.add_page is None: + return CmdActuator.addPage(args.add_page) + if not args.del_page is None: + return CmdActuator.delPage(args.del_page) + # 动态模块调用 + if args.call_py: + if args.func: + return CmdActuator.call( + args.call_py, + "py", + args.func, + args.thread, + *self.format_paras(args.paras), + ) + else: + return CmdActuator.getModuleFuncsHelp(args.call_py, "py") + if args.call_qml: + if args.func: + return CmdActuator.call( + args.call_qml, + "qml", + args.func, + args.thread, + *self.format_paras(args.paras), + ) + else: + return CmdActuator.getModuleFuncsHelp(args.call_qml, "qml") + + # paras 格式化 + def format_paras(self, paras): + def convert_param(param): + try: + return int(param) + except ValueError: + pass + try: + return float(param) + except ValueError: + pass + try: + return json.loads(param) + except json.JSONDecodeError: + pass + return param + + return [convert_param(p) for p in paras] + + +CmdServer = _Cmd() diff --git a/UmiOCR-data/py_src/server/doc_server.py b/UmiOCR-data/py_src/server/doc_server.py new file mode 100644 index 0000000..50a1fc6 --- /dev/null +++ b/UmiOCR-data/py_src/server/doc_server.py @@ -0,0 +1,580 @@ +import os +import re +import json +import time +import shutil +import zipfile +from urllib.parse import urlparse +from uuid import uuid4 +from PySide2.QtCore import QMutex +from typing import Dict + +from umi_log import logger +from call_func import CallFunc +from .bottle import request, static_file, HTTPError +from .ocr_server import get_ocr_options +from ..ocr.output import Output +from ..mission.mission_doc import MissionDOC, DocSuf +from ..utils.utils import initConfigDict +from ..ocr.output.tools import getDataText + +UPLOAD_DIR = "./temp_doc" # 上传文件临时目录 +TEMP_FILE_RETENTION_DURATION = 24 # 任务临时文件保留时长,小时 +TEMP_FILE_CLEANUP_INTERVAL = 0.5 # 自动清理临时文件的间隔,小时 + + +# 获取参数模板字典 +def get_doc_options(): + opts = get_ocr_options(is_format=False) + opts["tbpu.ignoreRangeStart"] = { + "title": "忽略区域起始", + "toolTip": "忽略区域生效的页数范围起始。从1开始。", + "default": 1, + "isInt": True, + } + opts["tbpu.ignoreRangeEnd"] = { + "title": "忽略区域结束", + "toolTip": "忽略区域生效的页数范围结束。可以用负数表示倒数第X页。", + "default": -1, + "isInt": True, + } + opts["pageRangeStart"] = { + "title": "OCR页数起始", + "toolTip": "OCR的页数范围起始。从1开始。", + "default": 1, + "isInt": True, + } + opts["pageRangeEnd"] = { + "title": "OCR页数结束", + "toolTip": "OCR的页数范围结束。可以用负数表示倒数第X页。", + "default": -1, + "isInt": True, + } + opts["pageList"] = { + "title": "OCR页数列表", + "toolTip": "数组,可指定单个或多个页数。例:[1,2,5]表示对第1、2、5页进行OCR。如果与页数范围同时填写,则 pageList 优先。", + "default": [], + "type": "var", + } + opts["password"] = { + "title": "密码", + "toolTip": "如果文档已加密,则填写文档密码。", + "default": "", + } + opts["doc.extractionMode"] = { + "title": "内容提取模式", + "toolTip": "若一页文档既存在图片又存在文本,如何进行处理。", + "optionsList": [ + ["mixed", "混合OCR/原文本"], + ["fullPage", "整页强制OCR"], + ["imageOnly", "仅OCR图片"], + ["textOnly", "仅拷贝原有文本"], + ], + } + opts = initConfigDict(opts) # 格式化 + return opts + + +UPLOAD_DIR = os.path.abspath(UPLOAD_DIR) # 路径转绝对 +TEMP_FILE_RETENTION_DURATION *= 3600 # 小时转为秒 +TEMP_FILE_CLEANUP_INTERVAL *= 3600 + + +# 异常类 +class DocUnitError(Exception): + def __init__(self, data): + self.data = data + + +# 单个任务单元 +class _DocUnit: + def __init__( + self, dir_id, dir_path, origin_path, origin_name, origin_prefix, options + ): + # 提取文档信息 + doc_info = MissionDOC.getDocInfo(origin_path) + if "error" in doc_info.keys(): + raise DocUnitError({"code": 201, "data": doc_info["error"]}) + + # 补充缺失的默认参数 + default = get_doc_options() + for key in default: + if key not in options: + options[key] = default[key]["default"] + + # 提取参数 + page_range = [options["pageRangeStart"], options["pageRangeEnd"]] # 识别范围 + page_list = options["pageList"] # 页数列表 + if page_list: # 下标起始由1转为0 + page_list = [x - 1 for x in page_list] + password = options["password"] # 密码 + if not password and doc_info["is_encrypted"]: + raise DocUnitError( + { + "code": 202, + "data": "The doc is encrypted, please fill in the password.", + } + ) + + # 从 options 中提取一些条目,组装 docArgd 作为 MissionDoc 任务参数字典 + prefixes = ["ocr.", "doc.", "tbpu."] # 要提取的条目前缀 + doc_argd = {} + for k, v in options.items(): + for prefix in prefixes: + if k.startswith(prefix): + doc_argd[k] = v + break + + # 任务信息 + msnInfo = { + "onStart": self._onStart, + "onGet": self._onGet, + "onEnd": self._onEnd, + "argd": doc_argd, + } + + # 提交任务 + self.msnID = "" + msg = MissionDOC.addMission( + msnInfo, origin_path, page_range, page_list, password + ) + if not msg: + raise DocUnitError({"code": 203, "data": "addMission unknow."}) + if msg.startswith("["): + raise DocUnitError({"code": 204, "data": msg}) + page_list = msnInfo["pageList"] + + self.password = password + self.dir_id = dir_id + self.dir_path = dir_path + self.origin_prefix = origin_prefix + self.origin_name = origin_name + self.origin_path = origin_path + self.msnID = msg # 任务ID + self.results = {} # 任务结果原始字典,键为页数 + self.pages_count = len(page_list) # 任务总页数 + self.processed_count = 0 # 已处理的页数 + self.unread_list = [] # 未读的任务列表 + self.is_done = False # 当前任务是否完成 + self.state = "waiting" # 任务状态, waiting running success failure + self.message = "" # 如果任务失败,则记录失败信息 + self.start_timestamp = time.time() # 开始时间戳 + self.end_timestamp = time.time() # 任务结束的时间戳 + self._mutex = QMutex() # 主锁 + + # ========================= 【接口】 ========================= + + # 获取结果 + def get_result( + self, + is_data=False, # True 时返回识别内容data + format="dict", # 识别内容格式, "dict", "text" + is_unread=False, # True 时只返回未读过的识别内容 + ): + self._mutex.lock() + data = { + "code": 100, + "processed_count": self.processed_count, # 已处理的数量 + "pages_count": self.pages_count, # 总页数 + "is_done": self.is_done, # 是否已结束 + "state": self.state, # 任务状态 + "data": [], # 结果 + } + if self.state == "failure": + data["message"] = self.message + # 需要返回识别内容 + if is_data: + datas = [] + # 增量式 + if is_unread: + for page in self.unread_list: + datas.append(self.results[page]) + self.unread_list = [] + # 全量式 + else: + for _, res in self.results.items(): + datas.append(res) + # 需要转为纯文本 + if format == "text": + datas_text = "" + for res in datas: + if res["code"] == 100: + datas_text += getDataText(res["data"]) + datas = datas_text + data["data"] = datas + self._mutex.unlock() + return data + + # 获取文件 + def get_files( + self, + base_url, # 下载基础url + file_types=["pdfLayered"], # 输出文件类型,可选: + # txt, txtPlain, jsonl, csv, pdfLayered, pdfOneLayer + ignore_blank=True, # 忽略空白页数 + ): + if not self.is_done: + return {"code": 201, "data": f"{self.msnID} 任务尚未结束,无法获取文件"} + if not self.state == "success": + return {"code": 201, "data": f"{self.msnID} 任务处理失败,无法获取文件"} + if not isinstance(file_types, list) or not isinstance(ignore_blank, bool): + return { + "code": 202, + "data": f"参数类型错误: file_types={file_types} , ignore_blank={ignore_blank}", + } + + # 删除旧的文件 + for filename in os.listdir(self.dir_path): + file_path = os.path.join(self.dir_path, filename) + if filename != self.origin_name and os.path.isfile(file_path): + os.remove(file_path) + + # 准备参数 + startDatetime = time.strftime( # 日期时间字符串(标准格式) + r"%Y-%m-%d %H:%M:%S", time.localtime(self.start_timestamp) + ) + outputArgd = { + "outputDir": self.dir_path, # 输出路径 + "outputDirType": "specify", + "outputFileName": "[OCR]_" + self.origin_prefix, # 输出文件名(前缀) + "startDatetime": startDatetime, # 开始日期 + "ignoreBlank": ignore_blank, # 忽略空白页数 + "originPath": self.origin_path, # 原始文件 + "password": self.password, # 文档密码 + } + + # 创建输出器 + output = [] + try: + for f in file_types: + output.append(Output[f](outputArgd)) + except Exception as e: + return {"code": 203, "data": f"初始化输出器失败。{e}"} + + # 输出 + for o in output: + for _, res in self.results.items(): + try: + o.print(res) + except Exception as e: + return {"code": 204, "data": f"输出失败:{o}\n{e}"} + try: + o.onEnd() # 保存 + except Exception as e: + return {"code": 205, "data": f"保存失败:{o}\n{e}"} + + # 收集新的文件 + download_paths = [] + for filename in os.listdir(self.dir_path): + file_path = os.path.join(self.dir_path, filename) + if filename != self.origin_name and os.path.isfile(file_path): + download_paths.append(file_path) + # 如果文件多,则打包zip + if not download_paths: + return {"code": 206, "data": "未找到生成的文件"} + elif len(download_paths) == 1: + download_name = os.path.basename(download_paths[0]) + else: + download_name = f"[OCR]_{self.origin_prefix}.zip" + zip_path = os.path.join(self.dir_path, download_name) + # 将 download_list 中的所有文件打包为 zip + try: + with zipfile.ZipFile(zip_path, "w") as zipf: + for p in download_paths: + zipf.write(p, os.path.basename(p)) + except Exception as e: + return {"code": 207, "data": f"无法打包zip:{e}"} + + # 组合下载地址 + url = f"{base_url}/api/doc/download/{self.dir_id}/{download_name}" + + return {"code": 100, "data": url, "name": download_name} + + # 清理任务 + def clear(self): + # 停止任务 + if not self.is_done: + MissionDOC.stopMissionList([self.msnID]) + time.sleep(0.1) # 给一些时间收尾 + # 尝试删除目录。如果权限原因无法删除,说明OCR线程还在占用,等待OCR结束。 + for i in range(20): + try: + if os.path.exists(self.dir_path): + shutil.rmtree(self.dir_path) + except PermissionError: + time.sleep(1) + + # ========================= 【任务控制器的异步回调】 ========================= + + def _onStart(self, msnInfo): # 一个文档 开始 + self.state = "running" + + def _onGet(self, msnInfo, page, res): # 一个文档的一页 获取结果 + page += 1 + res["page"] = page + res["path"] = f"{self.origin_name} - {page}" + res["fileName"] = f"{self.origin_name} - {page}" + + # 记录信息 + self._mutex.lock() + self.results[page] = res + self.processed_count += 1 + self.unread_list.append(page) + self._mutex.unlock() + + def _onEnd(self, msnInfo, msg): # 一个文档处理完毕 + # msg: [Success] [Warning] [Error] + + # 记录信息 + self._mutex.lock() + self.is_done = True + if msg == "[Success]": + self.state = "success" + else: + self.state = "failure" + self.message = msg + self.end_timestamp = time.time() # 刷新结束时间戳 + self._mutex.unlock() + + +# 管理所有任务单元 +class _DocUnitManagerClass: + def __init__(self): + self.doc_units: Dict[str, _DocUnit] = {} + + # 添加一个任务单元 + def add(self, id: str, unit: _DocUnit): + self.doc_units[id] = unit + + # 获取一个任务单元 + def get(self, id: str): + if id not in self.doc_units: + return None + return self.doc_units[id] + + # 手动清理一个任务 + def clear(self, id: str): + if id in self.doc_units: + self.doc_units[id].clear() + del self.doc_units[id] + return True + return False + + # 自动清理 + def auto_clear(self): + # 清理超时的任务和文件 + if self.doc_units: + now = time.time() # 当前时间戳 + del_list = [] # 要清理的id + for id, unit in self.doc_units.items(): + if now - unit.end_timestamp > TEMP_FILE_RETENTION_DURATION: + logger.info(f"超时自动清理 {id}") + unit.clear() # 清理文件 + del_list.append(id) + for id in del_list: + del self.doc_units[id] # 清理任务对象 + # 计划下一次清理 + CallFunc.delay(self.auto_clear, TEMP_FILE_CLEANUP_INTERVAL) + + +_DocUnitManager = _DocUnitManagerClass() + + +# 路由函数 +def init(UmiWeb): + # 清空上传文件目录内容 + if os.path.exists(UPLOAD_DIR): + shutil.rmtree(UPLOAD_DIR) + os.makedirs(UPLOAD_DIR) + # 启动自动清理循环 + _DocUnitManager.auto_clear() + + @UmiWeb.route("/api/doc/get_options") + def _get_options_json(): + opts = get_doc_options() + res = json.dumps(opts) + return res + + """ + 上传文档,方法:POST + 参数:文档内容 + 返回值: + 成功: {"code": 100, "data": "任务id"} + 失败: {"code": 不是100的值, "data": "失败原因"} + """ + + @UmiWeb.route("/api/doc/upload", method="POST") + def _upload(): + # 1. 获取上传文件 + upload = request.files.get("file") + if not upload: + return {"code": 101, "data": "[Error] No file was uploaded."} + + # 2. 获取文件名,检查文件后缀 + # origin_name = upload.filename + # 将原始文件名转为合法文件名 + def filename_convert(raw_filename: str): + # 去除前后的空格 + raw_filename = raw_filename.strip() + # 定义非法字符 + illegal_chars = r'[\/:*?"<>|]' + # 替换非法字符 + sanitized_filename = re.sub(illegal_chars, "_", raw_filename) + # 限制文件名长度为255个字符 + max_length = 255 + if len(sanitized_filename) > max_length: + sanitized_filename = sanitized_filename[:max_length] + return sanitized_filename + + try: + origin_name = filename_convert(upload.raw_filename) + origin_prefix, ext = os.path.splitext(origin_name) + ext = ext.lower() + except Exception as e: + return {"code": 102, "data": f"[Error] Unable to obtain raw_filename: {e}"} + if ext not in DocSuf: + return { + "code": 103, + "data": f"[Error] File extension '{ext}' is not allowed.", + } + + # 3. 指定文件编号。创建对应目录,保存文件到 ./temp/dir_id/原文件名 + dir_id = str(uuid4()) + dir_path = os.path.join(UPLOAD_DIR, f"{dir_id}") + dir_path = os.path.abspath(dir_path) # 将路径转为绝对路径 + file_path = os.path.join(dir_path, origin_name) + # 安全检测: file_path 是否在 UPLOAD_DIR 中 + if os.path.commonpath([UPLOAD_DIR]) != os.path.commonpath( + [UPLOAD_DIR, file_path] + ): + return {"code": 104, "data": f"[Error] Unauthorized path"} + + try: + if os.path.exists(dir_path): # 如果目录存在,则删除该目录 + shutil.rmtree(dir_path) + os.makedirs(dir_path) # 重新创建目录 + except Exception as e: + return {"code": 105, "data": f"[Error] Failed to create dir_id: {e}"} + try: + upload.save(file_path, overwrite=True) # 保存文件 + except Exception as e: + return {"code": 106, "data": f"[Error] Failed to save file: {e}"} + + # 4. 提取 options 参数 + options = request.forms.get("json") + if options: + try: + options = json.loads(options) + except Exception as e: + shutil.rmtree(dir_path) + return { + "code": 107, + "data": f"[Error] Invalid JSON format: {options} | {e}", + } + + # 检查忽略区域参数是否存在, 若存在则补全坐标 (兼容桌面程序) + if "tbpu.ignoreArea" in options: + ignore_area = list() + for area in options["tbpu.ignoreArea"]: + ignore_area.append([ + [area[0][0], area[0][1]], # 左上角 + [area[1][0], area[0][1]], # 右上角 + [area[1][0], area[1][1]], # 右下角 + [area[0][0], area[1][1]], # 左下角 + ]) + options["tbpu.ignoreArea"] = ignore_area + + if not isinstance(options, dict): + options = {} + + # 5. 构造任务对象 + try: + doc_unit = _DocUnit( + dir_id, dir_path, file_path, origin_name, origin_prefix, options + ) + msnID = doc_unit.msnID + _DocUnitManager.add(msnID, doc_unit) + logger.info(f"添加 HTTP 文档任务: {origin_name}") + return {"code": 100, "data": msnID} + except DocUnitError as e: + shutil.rmtree(dir_path) + return e.data + except Exception as e: + shutil.rmtree(dir_path) + return {"code": 108, "data": f"[Error] Failed to submit mission: {e}"} + + """ + 获取结果,方法:POST + json参数: + "id"="", # 任务ID + "is_data"=False, # True 时返回识别内容data + "format"="dict", # 识别内容格式, "dict", "text" + "is_unread"=False, # True 时只返回未读过的识别内容 + + 返回值: {} + """ + + @UmiWeb.route("/api/doc/result", method="POST") + def _result(): + try: + user_data = request.json + except Exception as e: + return {"code": 101, "data": f"请求无法解析为json。"} + if not user_data or "id" not in user_data: + return {"code": 102, "data": f"未填写id。"} + msnID = user_data["id"] + doc_unit = _DocUnitManager.get(msnID) + if not doc_unit: + return {"code": 103, "data": f"任务 {msnID} 不存在。"} + is_data = user_data.get("is_data", False) + format = user_data.get("format", "dict") + is_unread = user_data.get("is_unread", True) + return doc_unit.get_result(is_data, format, is_unread) + + """ + 获取文件,方法:POST + json参数: + "id"="", # 任务ID + "file_types"=["pdfLayered"], # 输出文件类型,可选: + # ["txt", "txtPlain", "jsonl", "csv", "pdfLayered", "pdfOneLayer"] + "ignore_blank"=True, # 忽略空白页数 + + 返回值: {} + """ + + @UmiWeb.route("/api/doc/download", method="POST") + def _download_build(): + try: + user_data = request.json + except Exception as e: + return {"code": 101, "data": f"请求无法解析为json。"} + if not user_data or "id" not in user_data: + return {"code": 102, "data": f"未填写id。"} + msnID = user_data["id"] + doc_unit = _DocUnitManager.get(msnID) + if not doc_unit: + return {"code": 103, "data": f"任务 {msnID} 不存在。"} + + file_types = user_data.get("file_types", ["pdfLayered"]) + ignore_blank = user_data.get("ignore_blank", True) + parsed_url = urlparse(request.url) + base_url = f"{parsed_url.scheme}://{parsed_url.netloc}" + return doc_unit.get_files(base_url, file_types, ignore_blank) + + # 下载文件 + @UmiWeb.route("/api/doc/download//") + def _download_get(id, download_name): + dir = os.path.join(UPLOAD_DIR, id) + path = os.path.join(dir, download_name) + # 安全检测: path 是否在 UPLOAD_DIR 中 + if os.path.commonpath([UPLOAD_DIR]) != os.path.commonpath([UPLOAD_DIR, path]): + raise HTTPError(103, "[Error] Unauthorized path.") + return static_file(download_name, root=dir) + + # 清理任务 + @UmiWeb.route("/api/doc/clear/") + def _clear(id): + flag = _DocUnitManager.clear(id) + if flag: + return {"code": 100, "data": "Success"} + return {"code": 101, "data": f"{id} does not exist."} diff --git a/UmiOCR-data/py_src/server/ocr_server.py b/UmiOCR-data/py_src/server/ocr_server.py new file mode 100644 index 0000000..20e303f --- /dev/null +++ b/UmiOCR-data/py_src/server/ocr_server.py @@ -0,0 +1,176 @@ +import json + +from .bottle import request +from ..mission.mission_ocr import MissionOCR +from ..utils.utils import initConfigDict +from ..ocr.output.tools import getDataText + + +# 获取ocr配置字典。 is_format=False 时不含 format 选项。 +def get_ocr_options(is_format=True): + opts = {} + # OCR 的参数 + ocr_opts = MissionOCR.getLocalOptions() + for key in ocr_opts: + opts[f"ocr.{key}"] = ocr_opts[key] + # 排版解析的参数 + opts["tbpu.parser"] = { + "title": "排版解析方案", + "toolTip": "按什么方式,解析和排序图片中的文字块", + "default": "multi_para", + "optionsList": [ + ["multi_para", "多栏-按自然段换行"], + ["multi_line", "多栏-总是换行"], + ["multi_none", "多栏-无换行"], + ["single_para", "单栏-按自然段换行"], + ["single_line", "单栏-总是换行"], + ["single_none", "单栏-无换行"], + ["single_code", "单栏-保留缩进"], + ["none", "不做处理"], + ], + } + # 忽略区域 + opts["tbpu.ignoreArea"] = { + "title": "忽略区域", + "toolTip": "数组,每一项为[[左上角x,y],[右下角x,y]]。", + "default": [], + "type": "var", + } + # 输出格式 + if is_format: + opts["data.format"] = { + "title": "数据返回格式", + "toolTip": '返回值字典中,["data"] 按什么格式表示OCR结果数据', + "default": "dict", + "optionsList": [ + ["dict", "含有位置等信息的原始字典"], + ["text", "纯文本"], + ], + } + opts = initConfigDict(opts) # 格式化 + return opts + + +# 检查ocr参数字典,返回修改后字典 +def check_ocr_options(opts): + # 检查忽略区域参数 + if opts["tbpu.ignoreArea"]: + new_ia = [] + ia = opts["tbpu.ignoreArea"] + for a in ia: + if ( + not isinstance(a, list) + or len(a) != 2 + or not isinstance(a[0], list) + or len(a[0]) != 2 + or not isinstance(a[1], list) + or len(a[1]) != 2 + or not all( + isinstance(x, (int, float)) + for x in [a[0][0], a[0][1], a[1][0], a[1][1]] + ) + ): + raise Exception( + f"tbpu.ignoreArea 中,每一项的格式必须是 [[x1,y1],[x2,y2]] 。当前值不合法: {ia}" + ) + new_ia.append([[a[0][0], a[0][1]], [], [a[1][0], a[1][1]], []]) + opts["tbpu.ignoreArea"] = new_ia + return opts + + +# 路由函数 +def init(UmiWeb): + @UmiWeb.route("/api/ocr/get_options") + def _get_options_json(): + opts = get_ocr_options() + res = json.dumps(opts) + return res + + """ + 执行OCR,方法:POST + 参数: + "base64": "", # 必填 + "options": {}, # 选填,内容与 _get_options 的对应。 + """ + + @UmiWeb.route("/api/ocr", method="POST") + def _ocr(): + try: + data = request.json + except Exception as e: + return json.dumps({"code": 800, "data": f"请求无法解析为json。 {e}"}) + if not data: + return json.dumps({"code": 801, "data": "请求为空。"}) + if "base64" not in data: + return json.dumps({"code": 802, "data": "请求中缺少 base64 字段。"}) + if "options" not in data: + data["options"] = {} + elif not isinstance(data["options"], dict): + return json.dumps({"code": 803, "data": "请求中 options 字段必须为字典。"}) + try: + # 补充缺失的默认参数 + opt = data["options"] + default = get_ocr_options() + for key in default: + if key not in opt: + opt[key] = default[key]["default"] + # 检查OCR参数 + check_ocr_options(opt) + except Exception as e: + return json.dumps({"code": 804, "data": f"options 解释失败。 {e}"}) + # 同步执行 + resList = MissionOCR.addMissionWait(opt, {"base64": data["base64"]}) + res = resList[0]["result"] + if opt["data.format"] == "text": # 转纯文本 + if res["code"] == 100: + res["data"] = getDataText(res["data"]) + res = json.dumps(res) + return res + + +""" +const url = "http://127.0.0.1:1224/api/ocr"; +const data = { + // 必填 + "base64": "iVBORw0KGgoAAAANSUhEUgAAAC4AAAAXCAIAAAD7ruoFAAAACXBIWXMAABnWAAAZ1gEY0crtAAAAEXRFWHRTb2Z0d2FyZQBTbmlwYXN0ZV0Xzt0AAAHjSURBVEiJ7ZYrcsMwEEBXnR7FLuj0BPIJHJOi0DAZ2qSsMCxEgjYrDQqJdALrBJ2ASndRgeNI8ledutOCLrLl1e7T/mRkjIG/IXe/DWBldRTNEoQSpgNURe5puiiaJehrMuJSXSTgbaby0A1WzLrCCQCmyn0FwoN0V06QONWAt1nUxfnjHYA8p65GjhDKxcjedVH6JOejBPwYh21eE0Wzfe0tqIsEkGXcVcpoMH4CRZ+P0lsQp/pWJ4ripf1XFDFe8GHSHlYcSo9Es31t60RdFlN1RUmrma5oTzTVB8ZUaeeYEC9GmL6kNkDw9BANAQYo3xTNdqUkvHq+rYhDKW0Bj3RSEIpmyWyBaZaMTCrCK+tJ5Jsa07fs3E7esE66HzralRLgJKp0/BD6fJRSxvmDsb6joqkcFXGqMVVFFEHDL2gTxwCAaTabnkFUWhDCHTd9iYrGcAL1ZnqIp5Vpiqh7bCfua7FA4qN0INMcN1+cgCzj+UFxtbmvwdZvGIrI41JiqhZBWhhF8WxorkYPpQwJiWYJeA3rXE4hzcwJ+B96F9zCFHC0FcVegghvFul7oeEE8PvHeJqC0w0AUbbFIT8JnEwGbPKcS2OxU3HMTqD0r4wgEIuiKJ7i4MS16+og8/+bPZRPLa+6Ld2DSzcAAAAASUVORK5CYII=", + "options": { + "ocr.angle": false, + "ocr.language": "简体中文", + "ocr.maxSideLen": 1024, + "tbpu.parser": "multi_para", + "data.format": "text", + } +}; + +fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(data) + }) + .then(response => response.json()) + .then(data => { + console.log(data); + }) + .catch(error => { + console.error(error); + }); + + + +const url = "http://127.0.0.1:1224/api/ocr/get_options"; +fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json" + }, + }) + .then(response => response.json()) + .then(data => { + console.log(data); + }) + .catch(error => { + console.error(error); + }); +""" diff --git a/UmiOCR-data/py_src/server/qrcode_server.py b/UmiOCR-data/py_src/server/qrcode_server.py new file mode 100644 index 0000000..3339d14 --- /dev/null +++ b/UmiOCR-data/py_src/server/qrcode_server.py @@ -0,0 +1,127 @@ +import json +import base64 +from io import BytesIO + +from .bottle import request +from ..mission.mission_qrcode import MissionQRCode + + +# 从base64识别二维码图片。传入data指令字典 {"base64", "options"} +# 返回字典 {"code", "data"} +def base2text(data): + base64 = data["base64"] + opt = data.get("options", {}) + res = MissionQRCode.addMissionWait(opt, [{"base64": base64}]) + return res[0]["result"] + + +# 从文本生成base64。传入data指令字典 {"text", "xxx"} +# 返回 {"code", "data"} +def text2base(data): + text = data["text"] + opt = data.get("options", {}) + format = opt.get("format", "QRCode") + w = opt.get("w", 0) + h = opt.get("h", 0) + quiet_zone = opt.get("quiet_zone", -1) + ec_level = opt.get("ec_level", -1) + try: + pil = MissionQRCode.createImage(text, format, w, h, quiet_zone, ec_level) + buffered = BytesIO() + pil.save(buffered, format="JPEG") + b64 = base64.b64encode(buffered.getvalue()).decode("ascii") + res = {"code": 100, "data": b64} + except Exception as e: + res = {"code": 200, "data": f"[Error] {str(e)}"} + return res + + +# 路由函数 +def init(UmiWeb): + + @UmiWeb.route("/api/qrcode", method="POST") + def _qrcode(): + try: + data = request.json + except Exception as e: + return json.dumps({"code": 800, "data": f"请求无法解析为json。"}) + if not data: + return json.dumps({"code": 801, "data": f"请求为空。"}) + + if "base64" in data: + return json.dumps(base2text(data)) + elif "text" in data: + return json.dumps(text2base(data)) + return json.dumps({"code": 802, "data": '指令中不存在 "base64" 或 "text"'}) + + +""" +// 文本 → 二维码base64 +const url = "http://127.0.0.1:1224/api/qrcode"; +const text = "测试文本"; +const data = { + // 必填 + "text": text, + // 选填 + "options": { + "format": "QRCode", // 码类型 + "w": 0, // 图像宽度,0为自动设为最小宽度 + "h": 0, // 图像高度 + "quiet_zone": -1, // 码四周的空白边缘宽度,-1为自动 + "ec_level": -1, // 纠错等级,-1为自动 + } +}; + +fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(data) + }) + .then(response => response.json()) + .then(data => { + if(data.code === 100) { + console.log("生成二维码base64:", data.data); + } + else { + console.log("生成二维码base64失败!错误码:", data.code, " 错误信息:", data.data); + } + }) + .catch(error => { + console.error(error); + }); + + +// 二维码base64 → 文本 +const url = "http://127.0.0.1:1224/api/qrcode"; +const base64 = "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAALCAAdAB0BAREA/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/9oACAEBAAA/APU/GfjM+EjAzW9o0DW8txNPdXEkSxKkkMYAEcUjMS069hjBrn3+K0yi3B0/RozO52y3OtG3gaPy7WRWV5IVJO27DFSoIEbYycCrF18Sb2z1a20u70rTbO8uLiKzigutRl3NcNDBIyAxW7rhTcIu4sAcE8Cu00LU/wC2/D2mat5Pk/brSK58rdu2b0Dbc4GcZxnAri/iSdPGs6AuqySW+nzpcW11dg27xwIzQspkimikDIZUiG/5QhK5PzCuPI1qz8ISalajUtNu1czLGsxnt7tHhhhiijNmkSF22W8aFeFWZ2RjIjeVXvrq0t/EWmaTpq3d9rTXFpCqpa2iRW92sCJOUP2WZYjEsNszrG7Bd/GNhr2zQtP/ALI8PaZpuMfY7SK3x5nmY2IF+9tXd067Vz6DpXH/ABK1LVrN7SLTIr6622k159isYYnknkjuLVUI8yGXGzzWfhc5UHPFeeSyav4dtI9R8O+Ho5dYS4WNrSK1EV2sb29ncFJY7aOPzIkkYhjhSGaME7WdHy72y8NWthbfDxrrfDDdpdXH2eVvtIu/IcStcOUaCGFMqGKNKUELZDEsU+g/DUcMXhXSI7cRrAllCsYjIKhQgxgh3BGP9t/95upk1PQtH1vyv7W0qxv/ACc+X9rt0l2ZxnG4HGcDp6Co7Xw1oNiipaaJptuiPvVYbVEAbcjZGB13RxnPqin+EYksdC0fTIo4rDSrG0jjlM6JBbpGFkKlC4AHDFSVz1wcdKuQQQ2tvFb28UcMESBI441CqigYAAHAAHGK/9k=" +const data = { "base64": base64 }; + +fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(data) + }) + .then(response => response.json()) + .then(data => { + if(data.code === 100) { + console.log("识别二维码成功!图片中的二维码数量:", data.data.length); + console.log("所有码的内容:"); + for (let d of data.data) { + console.log(" 文本:", d.text); + console.log(" 格式:", d.format); + console.log(" 方向:", d.orientation); + console.log(" ===="); + } + } + else { + console.log("识别二维码失败!错误码:", data.code, " 错误信息:", data.data); + } + }) + .catch(error => { + console.error(error); + }); +""" diff --git a/UmiOCR-data/py_src/server/web_server.py b/UmiOCR-data/py_src/server/web_server.py new file mode 100644 index 0000000..40ed227 --- /dev/null +++ b/UmiOCR-data/py_src/server/web_server.py @@ -0,0 +1,163 @@ +# ========================================================= +# ======= Web服务器 ======== +# ======= http接口可复用于跨进程命令行、防止多开等方面 ======== +# ========================================================= + +from PySide2.QtCore import QThreadPool, QRunnable +from wsgiref.simple_server import make_server, WSGIServer + +from umi_log import logger +from ..utils import pre_configs +from ..utils.call_func import CallFunc +from .bottle import Bottle, ServerAdapter, request, HTTPResponse, response, BaseRequest +from .cmd_server import CmdServer +from . import ocr_server +from . import qrcode_server +from . import doc_server + +BaseRequest.MEMFILE_MAX = 104857600 # 设置单次请求大小上限: 100 MB + +UmiWeb = Bottle() +Host = "127.0.0.1" # 由qml设置 + + +# 允许跨域 +@UmiWeb.hook("before_request") +def _validate_before(): + re_method = request.environ.get("REQUEST_METHOD") + hacrm = request.environ.get("HTTP_ACCESS_CONTROL_REQUEST_METHOD") + if re_method == "OPTIONS" and hacrm: + request.environ["REQUEST_METHOD"] = hacrm + + +@UmiWeb.hook("after_request") +def _validate_after(): + response.headers["Access-Control-Allow-Origin"] = "*" + response.headers["Access-Control-Allow-Headers"] = "*" + response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, OPTIONS" + + +# ============================== 基础路由 ============================== + + +@UmiWeb.route("/") +@UmiWeb.route("/umiocr") +def _umiocr(): + from umi_about import UmiAbout # 项目信息 + + return UmiAbout["fullname"] + + +# 跨进程接收命令行参数 +@UmiWeb.route("/argv", method="POST") +def _argv(): + addr = request.environ.get("REMOTE_ADDR") + if addr == "127.0.0.1": + data = request.json + res = CmdServer.execute(data) + return res + else: + msg = "Unauthorized access. Only local requests are allowed.\n此接口只允许本机访问。" + return HTTPResponse(msg, status=401) + + +ocr_server.init(UmiWeb) +qrcode_server.init(UmiWeb) +doc_server.init(UmiWeb) + +# =============== 自定义服务器适配器,方便控制服务终止 ============================== +QmlCallback = None # qml回调函数 + + +class _WSGIRefServer(ServerAdapter): + # https://stackoverflow.com/questions/11282218/bottle-web-framework-how-to-stop + + class CustomWSGIServer(WSGIServer): # 定制服务器 + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.activeConnections = set() # 当前活跃连接 + + def process_request(self, request, client_address): + # 记录活跃的连接 + self.activeConnections.add(request) + super().process_request(request, client_address) + + def close_all_request(self): # 关闭所有活跃的连接 + import socket + + for request in self.activeConnections: + try: + request.shutdown(socket.SHUT_RDWR) + request.close() + logger.info(f"强制关闭连接。 request: {request}") + except OSError: + pass + except Exception: + logger.error( + f"强制连接异常。 request: {request}", + exc_info=True, + stack_info=True, + ) + + def run(self, handler): + import atexit # 退出处理 + + atexit.register(self.stop) # 注册程序终止时停止线程 + self.port = pre_configs.getValue("server_port") # 提取记录的端口号 + self.host = Host + # 找到一个可用的端口号 + while True: + try: + self.server = make_server( + self.host, + self.port, + handler, + server_class=self.CustomWSGIServer, + **self.options, + ) + break + except OSError: # 当前端口号已占用,测试下一位端口号 + logger.warning(f"服务器端口号 {self.port} 已被占用") + self.port += 1 + if self.port > 65535: + self.port = 1024 + pre_configs.setValue("server_port", self.port) # 写入记录 + + logger.info(f"Listening on http://{self.host}:{self.port}") + print(f"Listening on http://{self.host}:{self.port}") + CallFunc.now(QmlCallback, self.port) # 在主线程中调用回调函数,告知实际端口号 + self.server.serve_forever() + + def stop(self): # 服务终止 + # self.server.server_close() # 备选方案,但会导致 bad fd 异常 + logger.debug("WEB服务器准备关闭!") + self.server.close_all_request() # 强制关闭客户端连接 + self.server.shutdown() # 关闭服务器 + logger.info("WEB服务器已关闭!") + + +# ============================== 线程类 ============================== +class _WorkerClass(QRunnable): + def run(self): + self._server = _WSGIRefServer() + UmiWeb.run(server=self._server) + + +_Worker = _WorkerClass() + + +# ============================== 控制接口 ============================== + + +# 启动web服务。传入qml对象,回调函数名,主机地址 +def runUmiWeb(qmlObj, callback, host): + global QmlCallback, Host + Host = host + QmlCallback = getattr(qmlObj, callback, None) # 提取qml回调函数 + threadPool = QThreadPool.globalInstance() # 获取全局线程池 + threadPool.start(_Worker) # 启动服务器线程 + + +# 切换端口号(下次启动生效) +def setPort(port): + pre_configs.setValue("server_port", port) diff --git a/UmiOCR-data/py_src/tag_pages/BatchDOC.py b/UmiOCR-data/py_src/tag_pages/BatchDOC.py new file mode 100644 index 0000000..a11fda1 --- /dev/null +++ b/UmiOCR-data/py_src/tag_pages/BatchDOC.py @@ -0,0 +1,230 @@ +# ======================================== +# =============== 批量文档页 =============== +# ======================================== + +import os +import time + +from umi_log import logger +from .page import Page # 页基类 +from ..mission.mission_doc import MissionDOC # 任务管理器 +from ..utils import utils +from ..ocr.output import Output + + +class BatchDOC(Page): + def __init__(self, *args): + super().__init__(*args) + self._msnID = "" # 当前正在进行的任务ID(已提交至任务处理器) + self._pauseMsnID = "" # 当前正在暂停的任务ID + self._queuedDocs = [] # 当前正在排队的文档信息(未提交) + self._argd = None + self._docArgd = None + + # 进行任务。 + # docs为列表,每一项为: {path:文档路径, range_start:范围起始, range_end: 范围结束, page_count:总页数, password:密码} + # 返回一个列表,每项为: {path:文档路径, msnID:任务ID。若[Error]开头则为失败。} + def msnDocs(self, docs, argd): + if self._msnID or self._queuedDocs: + return "[Error] 有任务进行中,不允许提交新任务。" + + # 从 argd 中提取一些条目,组装 docArgd + prefixes = ["ocr.", "doc.", "tbpu."] # 要提取的条目前缀 + docArgd = {} + for k, v in argd.items(): + for prefix in prefixes: + if k.startswith(prefix): + docArgd[k] = v + break + + # 记录任务参数 + self._queuedDocs = docs + self._argd = argd + self._docArgd = docArgd + # 提交第一个任务 + self._runNewDoc() + + def msnStop(self): # 停止当前所有任务 + MissionDOC.stopMissionList(self._msnID) + self._msnID = "" + self._queuedDocs = [] + self._argd = None + self._docArgd = None + + def msnPause(self): # 任务暂停 + self._pauseMsnID = self._msnID + MissionDOC.pauseMissionList(self._msnID) + + def msnResume(self): # 任务恢复 + self._pauseMsnID = "" + MissionDOC.resumeMissionList(self._msnID) + + # 初始化输出器列表。成功返回两个输出器列表 output 。失败返回 "失败信息" + def _initOutputList(self, argd, path, pageRange, pageCount, password=""): + # =============== 提取输出路径 outputDir, outputDirName =============== + if argd["mission.dirType"] == "source": # 若保存到原目录 + outputDir = os.path.dirname(path) # 则保存路径设为文档的目录 + else: # 若保存到用户指定目录 + d = os.path.abspath(argd["mission.dir"]) # 转绝对地址 + if not os.path.exists(d): # 检查地址是否存在 + try: # 不存在,尝试创建地址 + os.makedirs(d) + except OSError as e: + return f"[Error] 无法创建路径 {d}" + outputDir = d + + # =============== 提取时间信息和文件名 outputFileName =============== + startTimestamp = time.time() # 开始时间戳 + startDatetime = time.strftime( # 日期时间字符串(标准格式) + r"%Y-%m-%d %H:%M:%S", time.localtime(startTimestamp) + ) + # 日期时间字符串(用户指定格式):先替换时间戳,再strftime + startDatetimeUser = argd["mission.datetimeFormat"].replace( + r"%unix", str(startTimestamp) + ) + startDatetimeUser = time.strftime( + startDatetimeUser, time.localtime(startTimestamp) + ) + # 替换时间 %date + nameTemplate = argd["mission.fileNameFormat"] + nameTemplate = nameTemplate.replace(r"%date", startDatetimeUser) # 替换时间 + # 替换范围 %range + rangeStr = "" # 范围全本 + if pageRange[1] - pageRange[0] + 1 < pageCount: # 范围非全本 + if pageRange[1] == pageRange[0]: # 只识别一页,写入单个页数 + rangeStr = f"(p{pageRange[0]})" + else: # 识别多页,写入开头和结尾页数 + rangeStr = f"(p{pageRange[0]}-{pageRange[1]})" + nameTemplate = nameTemplate.replace(r"%range", rangeStr) + # 替换文件名 #name + fileNameStr = os.path.splitext(os.path.basename(path))[0] + outputFileName = nameTemplate.replace(r"%name", fileNameStr) # 替换名称元素 + if not utils.allowedFileName(outputFileName): # 文件名不合法 + return f'[Error] 文件名【{outputFileName}】含有不允许的字符。\n不允许含有下列字符: \ / : * ? " < > |' + + # =============== 组装输出参数字典 =============== + outputArgd = { + "outputDir": outputDir, # 输出路径 + "outputDirType": argd[ + "mission.dirType" + ], # 输出目录类型,"source" 为原文件目录 + "outputFileName": outputFileName, # 输出文件名(前缀) + "startDatetime": startDatetime, # 开始日期 + "ignoreBlank": argd["mission.ignoreBlank"], # 忽略空白文件 + "originPath": path, # 原始文件名 + "password": password, # 文档密码 + } + + # =============== 实例化输出器对象 =============== + output = [] + try: + for key in argd.keys(): + if "mission.filesType" in key and argd[key]: + output.append(Output[key[18:]](outputArgd)) + except Exception as e: + return f"[Error] 初始化输出器失败。{e}" + return output + + def _runNewDoc(self): # 取 self._queuedList 首位任务,提交执行 + if not self._queuedDocs: + logger.warning("文档任务: queuedDocs 已空") + return + d = self._queuedDocs.pop(0) # 取首位任务 + path = d["path"] # 取地址 + password = d["password"] # 密码 + pageRange = [int(d["range_start"]), int(d["range_end"])] # 识别范围 + pageCount = int(d["page_count"]) # 总页数 + # 构造输出器 + output = self._initOutputList(self._argd, path, pageRange, pageCount, password) + if isinstance(output, str): # 创建输出器失败 + self._onEnd({"path": path}, "[Error] 无法创建输出器。") + return + # 任务信息 + msnInfo = { + "onStart": self._onStart, + "onReady": self._onReady, + "onGet": self._onGet, + "onEnd": self._onEnd, + "argd": self._docArgd, + # 交给 self._onGet 的参数 + "get_output": output, + } + msnID = MissionDOC.addMission(msnInfo, path, pageRange, password=password) + if msnID.startswith("["): # 添加任务失败 + self._msnID = "" + self._onEnd({"path": path}, msnID) + else: + self._msnID = msnID + + # ========================= 【任务控制器的异步回调】 ========================= + + def _onStart(self, msnInfo): # 一个文档 开始 + msnID = msnInfo["msnID"] + if not msnID == self._msnID: + logger.warning(f"_onStart 任务ID未在记录。{msnID}") + return + self.callQmlInMain("onDocStart", msnInfo["path"]) + + def _onReady(self, msnInfo, page): # 一个文档的一页 准备开始 + page += 1 + + def _onGet(self, msnInfo, page, res): # 一个文档的一页 获取结果 + page += 1 + msnID = msnInfo["msnID"] + if not msnID == self._msnID: + logger.warning(f"_onGet 任务ID未在记录。{msnID}") + return + + # 为 res 添加信息 + res["page"] = page + res["fileName"] = f"{page}" + res["path"] = msnInfo["path"] + + # 输出 + for o in msnInfo["get_output"]: + try: + o.print(res) + except Exception: + logger.error(f"文档结果输出失败:{o}", exc_info=True, stack_info=True) + + self.callQmlInMain("onDocGet", msnInfo["path"], page, res) + + def _onEnd(self, msnInfo, msg): # 一个文档处理完毕 + # msg: [Success] [Warning] [Error] + + if "path" not in msnInfo: + raise Exception('[Error] BatchDOC onEnd(): "path" not in msnInfo') + + msnID = "" # 该任务的ID + if "msnID" in msnInfo: + msnID = msnInfo["msnID"] + if not msnID == self._msnID: + logger.warning(f"_onEnd 任务ID未在记录。{msnID}") + return + + # 结束输出器,保存文件。 + if "get_output" in msnInfo: + output = msnInfo["get_output"] + for o in output: + try: + o.onEnd() + except Exception as e: + msg = f"[Error] 输出器异常:{e}" + msg + + # 上报 + isAll = False if self._queuedDocs else True # 是否所有文档处理完毕 + self.callQmlInMain("onDocEnd", msnInfo["path"], msg, isAll) + + # 所有任务完毕 + if isAll: + self._msnID = "" + self._argd = None + self._docArgd = None + # 还有排队中的任务,则提交新任务 + else: + self._runNewDoc() + # 如果新任务 self._msnID 已提交成功,且上一个任务 msnID 处于暂停状态, + # 也就是在保存文件的过程中,用户点了暂停, + # 那么将新的任务设为暂停。 + if self._msnID and msnID and self._pauseMsnID == msnID: + self.msnPause() diff --git a/UmiOCR-data/py_src/tag_pages/BatchOCR.py b/UmiOCR-data/py_src/tag_pages/BatchOCR.py new file mode 100644 index 0000000..92bb580 --- /dev/null +++ b/UmiOCR-data/py_src/tag_pages/BatchOCR.py @@ -0,0 +1,180 @@ +# ======================================== +# =============== 批量OCR页 =============== +# ======================================== + +import os +import time + +from umi_log import logger +from .page import Page # 页基类 +from ..mission.mission_ocr import MissionOCR # 任务管理器 +from ..utils.utils import allowedFileName +from ..ocr.output import Output # 输出器 + + +class BatchOCR(Page): + def __init__(self, *args): + super().__init__(*args) + self.argd = None + self.msnID = "" + self.outputList = [] # 输出器列表 + + # ========================= 【qml调用python】 ========================= + + def msnPaths(self, paths, argd): # 接收路径列表和配置参数字典,开始OCR任务 + # 任务信息 + msnInfo = { + "onStart": self._onStart, + "onReady": self._onReady, + "onGet": self._onGet, + "onEnd": self._onEnd, + "argd": argd, + } + # 预处理参数字典 + if not self._preprocessArgd(argd, paths[0]): + return "" + # 构造输出器 + if not self._initOutputList(argd): + return "" + # 路径转为任务列表格式,加载进任务管理器 + msnList = [{"path": x} for x in paths] + self.msnID = MissionOCR.addMissionList(msnInfo, msnList) + if self.msnID.startswith("[Error]"): # 添加任务失败 + self._onEnd(None, f"{self.msnID}\n添加任务失败。") + else: # 添加成功,通知前端刷新UI + logger.debug(f"添加任务成功 {self.msnID}") + return self.msnID + + def _preprocessArgd(self, argd, path0): # 预处理参数字典,无异常返回True + self.argd = None + if argd["mission.dirType"] == "source": # 若保存到原目录 + # 则保存路径设为第1张图片的目录 + argd["mission.dir"] = os.path.dirname(path0) + else: # 若保存到用户指定目录 + d = os.path.abspath(argd["mission.dir"]) # 转绝对地址 + if not os.path.exists(d): # 检查地址是否存在 + try: # 不存在,尝试创建地址 + os.makedirs(d) + except OSError: # 创建地址失败,报错 + logger.warning(f"批量OCR无法创建目录: {d}", exc_info=True) + self._onEnd( + None, + f'[Error] Failed to create directory: "{d}"\n【异常】无法创建目录。', + ) + return False + argd["mission.dir"] = d # 写回字典 + startTimestamp = time.time() # 开始时间戳 + argd["startTimestamp"] = startTimestamp + # 格式化日期时间(标准格式) + argd["startDatetime"] = time.strftime( + r"%Y-%m-%d %H:%M:%S", time.localtime(startTimestamp) + ) + # 添加格式化日期时间(用户指定格式):先替换时间戳,再strftime + startDatetimeUser = argd["mission.datetimeFormat"].replace( + r"%unix", str(startTimestamp) + ) + startDatetimeUser = time.strftime( + startDatetimeUser, time.localtime(startTimestamp) + ) + # 处理文件名 + fileName = argd["mission.fileNameFormat"] + fileName = fileName.replace(r"%date", startDatetimeUser) # 替换时间 + fileNameEle = os.path.basename(os.path.dirname(path0)) + fileName = fileName.replace("%name", fileNameEle) # 替换名称元素 + if not allowedFileName(fileName): # 文件名不合法 + self._onEnd( + None, + f'[Error] The file name is illegal.\n【错误】文件名【{fileName}】含有不允许的字符。\n不允许含有下列字符: \ / : * ? " < > |', + ) + return False + argd["mission.fileName"] = fileName # 回填文件名 + self.argd = argd + return True + + def _initOutputList(self, argd): # 初始化输出器列表,无异常返回True + self.outputList = [] + outputArgd = { # 数据转换,封装有需要的值 + "outputDir": argd["mission.dir"], # 输出目录 + # 输出目录类型,"source" 为原文件目录 + "outputDirType": argd["mission.dirType"], + "outputFileName": argd["mission.fileName"], # 输出文件名(前缀) + "startDatetime": argd["startDatetime"], # 开始日期 + "ignoreBlank": argd["mission.ignoreBlank"], # 忽略空白文件 + } + try: + for key in argd.keys(): + if "mission.filesType" in key and argd[key]: + self.outputList.append(Output[key[18:]](outputArgd)) + except Exception as e: + self._onEnd( + None, + f"[Error] Failed to initialize output file.\n【错误】初始化输出文件失败。\n{e}", + ) + return False + return True + + def msnStop(self): # 任务停止 + MissionOCR.stopMissionList(self.msnID) + + def msnPause(self): # 任务暂停 + MissionOCR.pauseMissionList(self.msnID) + + def msnResume(self): # 任务恢复 + MissionOCR.resumeMissionList(self.msnID) + + def msnPreview(self, path, argd): # 快速进行一次任务,主要用于预览 + msnInfo = { + "onGet": self._onPreview, + "argd": argd, + } + msnList = [{"path": path}] + self.msnID = MissionOCR.addMissionList(msnInfo, msnList) + + # ========================= 【任务控制器的异步回调】 ========================= + + def _onStart(self, msnInfo): # 任务队列开始 + pass + + def _onReady(self, msnInfo, msn): # 单个任务准备 + msnID = msnInfo["msnID"] + if msnID != self.msnID: + logger.warning(f"_onReady 任务ID未在记录。{msnID}") + return + self.callQmlInMain("onOcrReady", msn["path"]) + + def _onGet(self, msnInfo, msn, res): # 单个任务完成 + msnID = msnInfo["msnID"] + if msnID != self.msnID: + logger.warning(f"_onGet 任务ID未在记录。{msnID}") + return + # 补充参数 + res["fileName"] = os.path.basename(msn["path"]) + res["dir"] = os.path.dirname(msn["path"]) + # 输出器输出 + for o in self.outputList: + try: + o.print(res) + except Exception: + logger.error(f"结果输出失败:{o}", exc_info=True, stack_info=True) + # 通知qml更新UI + self.callQmlInMain("onOcrGet", msn["path"], res) # 在主线程中调用qml + + def _onEnd(self, msnInfo, msg): # 任务队列完成或失败 + if msnInfo: + msnID = msnInfo["msnID"] + if msnID != self.msnID: + logger.warning(f"_onEnd 任务ID未在记录。{msnID}") + return + else: + msnID = "" + # 结束输出器,保存文件。 + for o in self.outputList: + try: + o.onEnd() + except Exception as e: + msg = f"[Error] 输出器异常:{e}" + msg + # msg: [Success] [Warning] [Error] + self.callQmlInMain("onOcrEnd", msg, msnID) + + def _onPreview(self, msnInfo, msn, res): + self.callQmlInMain("onPreview", msn["path"], res) diff --git a/UmiOCR-data/py_src/tag_pages/QRCode.py b/UmiOCR-data/py_src/tag_pages/QRCode.py new file mode 100644 index 0000000..8ac2faf --- /dev/null +++ b/UmiOCR-data/py_src/tag_pages/QRCode.py @@ -0,0 +1,78 @@ +# ======================================= +# =============== 二维码页 =============== +# ======================================= + +from .page import Page # 页基类 +from ..image_controller.image_provider import PixmapProvider # 图片提供器 +from ..mission.mission_qrcode import MissionQRCode + +import os +import time +from PIL import Image, ImageEnhance, ImageFilter +import base64 + +try: + import zxingcpp +except Exception as e: + zxingcpp = None + zxingcppErr = str(e) + + +class QRCode(Page): + # def __init__(self, *args): + # super().__init__(*args) + + # 对一个imgID进行扫码 + def scanImgID(self, imgID, configDict): + msnInfo = { + "onStart": self._onStart, + "onReady": self._onReady, + "onGet": self._onGet, + "onEnd": self._onEnd, + "argd": configDict, + } + msnList = [{"pil": PixmapProvider.getPilImage(imgID), "imgID": imgID}] + MissionQRCode.addMissionList(msnInfo, msnList) + + # 对一串path进行扫码 + def scanPaths(self, paths, configDict): + msnInfo = { + "onStart": self._onStart, + "onReady": self._onReady, + "onGet": self._onGet, + "onEnd": self._onEnd, + "argd": configDict, + } + msnList = [{"path": x} for x in paths] + MissionQRCode.addMissionList(msnInfo, msnList) + + # 生成二维码 + # format: "Aztec","Codabar","Code128","Code39","Code93","DataBar","DataBarExpanded","DataMatrix","EAN13","EAN8","ITF","LinearCodes","MatrixCodes","MaxiCode","MicroQRCode","PDF417","QRCode","UPCA","UPCE", + # quiet_zone: 四周的空闲区域 + # ec_level:纠错等级,-1 - 自动, 1- L-7% , 0 - M-15%, 3 - Q-25%, 2 - H-30% + # 纠错仅用于Aztec、PDF417和QRCode + def writeBarcode(self, text, format, w=0, h=0, quiet_zone=-1, ec_level=-1): + img = MissionQRCode.createImage(text, format, w, h, quiet_zone, ec_level) + if isinstance(img, str): + return img + imgID = PixmapProvider.setPilImage(img) + # 若 setPilImage 失败, imgID.startswith("[Error]") + return imgID + + # ========================= 【扫码处理】 ========================= + + def _onStart(self, msnInfo): # 任务队列开始 + self.callQmlInMain("setRunning", True) + + def _onReady(self, msnInfo, msn): # 单个任务准备 + pass + + def _onGet(self, msnInfo, msn, res): # 单个任务完成 + # 通知qml更新UI + imgID = msn.get("imgID", "") + imgPath = msn.get("path", "") + self.callQmlInMain("onQRCodeGet", res, imgID, imgPath) # 在主线程中调用qml + + def _onEnd(self, msnInfo, msg): # 任务队列完成或失败 + # msg: [Success] [Warning] [Error] + self.callQmlInMain("setRunning", False) diff --git a/UmiOCR-data/py_src/tag_pages/ScreenshotOCR.py b/UmiOCR-data/py_src/tag_pages/ScreenshotOCR.py new file mode 100644 index 0000000..54edfe1 --- /dev/null +++ b/UmiOCR-data/py_src/tag_pages/ScreenshotOCR.py @@ -0,0 +1,124 @@ +# ======================================== +# =============== 截图OCR页 =============== +# ======================================== + +from PySide2.QtGui import QClipboard # 截图 剪贴板 + +from umi_log import logger +from .page import Page # 页基类 +from ..image_controller.image_provider import PixmapProvider # 图片提供器 +from ..mission.mission_ocr import MissionOCR # 任务管理器 +from ..event_bus.pubsub_service import PubSubService # 发布/订阅管理器 + +# 只要触发了截图/粘贴/图片识图任务,并结束任务(无论是否成功),都发送 <> 事件。 + +Clipboard = QClipboard() # 剪贴板 + + +class ScreenshotOCR(Page): + def __init__(self, *args): + super().__init__(*args) + self.msnDict = {} + self.recentResult = [] # 缓存本轮任务的识别结果,提交给 <> + + # ========================= 【qml调用python】 ========================= + + # 对一个imgID进行OCR + def ocrImgID(self, imgID, configDict): + self.recentResult = [] + if not imgID or not configDict: # 截图取消 + PubSubService.publish("<>", []) + return + if imgID.startswith("["): # 截图失败 + PubSubService.publish( + "<>", [{"code": 301, "data": imgID}] + ) + return + pixmap = PixmapProvider.getPixmap(imgID) + if not pixmap: + logger.error(f'ScreenshotOCR: imgID "{imgID}" 不存在 PixmapProvider 中') + return + self._msnImage(pixmap, imgID, configDict) # 开始OCR + + # 对一批路径进行OCR + def ocrPaths(self, paths, configDict): + self.recentResult = [] + self._msnPaths(paths, configDict) + + # 停止全部任务 + def msnStop(self): + self.callQml("setMsnState", "none") + for i in self.msnDict: + MissionOCR.stopMissionList(i) + self.msnDict = {} + PubSubService.publish("<>", self.recentResult) + + # ========================= 【OCR 任务控制】 ========================= + + # 传入 QImage或QPixmap图片, 图片id, 配置字典。 提交OCR任务。 + def _msnImage(self, img, imgID, configDict): + # 图片转字节,构造任务队列 + bytesData = PixmapProvider.toBytes(img) + msnList = [{"bytes": bytesData, "imgID": imgID}] + self._msn(msnList, configDict) + + # 传入路径列表,提交OCR任务,返回图片缓存ID + def _msnPaths(self, paths, configDict): + msnList = [{"path": x} for x in paths] + self._msn(msnList, configDict) + + # 开始任务 + def _msn(self, msnList, configDict): + # 任务信息 + msnInfo = { + "onStart": self._onStart, + "onReady": self._onReady, + "onGet": self._onGet, + "onEnd": self._onEnd, + "argd": configDict, + } + msnID = MissionOCR.addMissionList(msnInfo, msnList) + if msnID.startswith("[Error]"): # 添加任务失败 + self._onEnd(None, f"{self.msnID}\n添加任务失败。") + else: # 添加成功 + self.msnDict[msnID] = None + self.callQml("setMsnState", "run") + + def _onStart(self, msnInfo): # 任务队列开始 + pass + + def _onReady(self, msnInfo, msn): # 单个任务准备 + pass + + def _onGet(self, msnInfo, msn, res): # 单个任务完成 + # 补充平均置信度 + score = 0 + num = 0 + if res["code"] == 100: + for r in res["data"]: + score += r["score"] + num += 1 + if num > 0: + score /= num + res["score"] = score + # 通知qml更新UI + imgID = msn.get("imgID", "") + imgPath = msn.get("path", "") + self.recentResult.append(res) # 记录结果 + self.callQmlInMain("onOcrGet", res, imgID, imgPath) # 在主线程中调用qml + + def _onEnd(self, msnInfo, msg): # 任务队列完成或失败 + # msg: [Success] [Warning] [Error] + PubSubService.publish("<>", self.recentResult) + + def update(): + # 清除任务id + if msnInfo and msnInfo["msnID"] in self.msnDict: + del self.msnDict[msnInfo["msnID"]] + # 所有任务都完成了 + if not self.msnDict: + # 停止前端显示 + self.callQml("setMsnState", "none") + self.callQml("onOcrEnd", msg) + + self.callFunc(update) # 在主线程中执行 diff --git a/UmiOCR-data/py_src/tag_pages/page.py b/UmiOCR-data/py_src/tag_pages/page.py new file mode 100644 index 0000000..ebaa75d --- /dev/null +++ b/UmiOCR-data/py_src/tag_pages/page.py @@ -0,0 +1,36 @@ +# ============================================= +# =============== 页面控制器基类 =============== +# ============================================= + +from PySide2.QtCore import QObject + +from umi_log import logger + + +class Page(QObject): + def __init__(self, ctrlKey, controller): + super().__init__() + self.ctrlKey = ctrlKey + self.controller = controller + logger.debug(f"py控制器 {self.ctrlKey} 实例化!") + + def __del__(self): + logger.debug(f"py控制器 {self.ctrlKey} 销毁!") + + def callQml(self, funcName, *args): # python调用qml函数 + return self.controller.callQml(self.ctrlKey, funcName, *args) + + def callQmlInMain(self, funcName, *args): # python调用qml函数,可在子线程调用 + self.callFunc(self.callQml, funcName, *args) + + def callFunc(self, func, *args): # 在主线程中调用py函数 + return self.controller.callFunc(func, *args) + + def getQmlValueDict(self): # python获取qml配置字典 + return self.controller.callQml(self.ctrlKey, "getValueDict").toVariant() + + def getQmlOriginDict(self): + return self.controller.callQml(self.ctrlKey, "getOriginDict").toVariant() + + def setQmlValue(self, key, val): # python获取qml配置字典 + return self.controller.callQml(self.ctrlKey, "setValue", key, val).toVariant() diff --git a/UmiOCR-data/py_src/tag_pages/tag_pages_connector.py b/UmiOCR-data/py_src/tag_pages/tag_pages_connector.py new file mode 100644 index 0000000..31ed693 --- /dev/null +++ b/UmiOCR-data/py_src/tag_pages/tag_pages_connector.py @@ -0,0 +1,149 @@ +# =========================================================================== +# =============== 【前端qml标签页】与【后端Python控制器】的连接器 =============== +# =========================================================================== + +""" +每一个qml页面,都可以拥有一个对应的Python控制器实例。 +前端页面访问各种后端功能,必须靠这个控制器作为中转。 +""" + +from PySide2.QtCore import QObject, Slot + +from umi_log import logger + +# 导入本模块内定义的控制器类 +from .BatchOCR import BatchOCR +from .BatchDOC import BatchDOC +from .ScreenshotOCR import ScreenshotOCR +from .QRCode import QRCode +from ..utils.call_func import CallFunc + +# 控制器类列表 +PageClass = [BatchOCR, ScreenshotOCR, QRCode, BatchDOC] + + +TagPageConnObj = None # 记录实例 +# PySide2 没有 qmlRegisterSingletonType,PyQt5或者PySide6中才有。 +# 不过没关系,我们手动维护控制器的单例状态就是了 + + +# 页面连接器类(手动单例) +class TagPageConnector(QObject): + def __init__(self): + global TagPageConnObj + # 1. 检查是否单例 + if not TagPageConnObj == None: + raise Exception("【Error】TagPageConnector只允许创建一个实例!") + TagPageConnObj = self + super().__init__() + # 2. 收集所有已导入的控制器类,聚集为dict。 ["类名"]=类 + self.pageClass = {} + for i in PageClass: + self.pageClass[i.__name__] = i + # 属性 + # 当前已实例化的控制器 + self.pages = {} + self._keysNum = {} # 记录每个key被生成了多少次 + + # ========================= 【增删改查】 ========================= + + # 增: 新增一个class为key的控制器,返回这个控制器的标识符。失败返回空字符串 + @Slot(str, result=str) + def addPage(self, key): + if key not in self.pageClass: + return "" + ctrlKey = self._getCtrlKey(key) + obj = self.pageClass[key](ctrlKey, self) # 实例化 页控制器对象 + self.pages[ctrlKey] = { # key为控制器id + "pyObj": obj, # py对象 + "qmlObj": None, # qml对象 + "pyCache": {}, # py方法缓存 + "qmlCache": {}, # qml方法缓存 + } + return ctrlKey + + # 增: 新增一个不带控制器的简单页 + @Slot(str, result=str) + def addSimplePage(self, key): + ctrlKey = self._getCtrlKey(key) + self.pages[ctrlKey] = { + "pyObj": None, + "qmlObj": None, + "pyCache": {}, + "qmlCache": {}, + } + return ctrlKey + + # 增2: qml回调,设置标识符为ctrlKey的控制器,对应的qml页面对象 + @Slot(str, "QVariant") + def setPageQmlObj(self, ctrlKey, qmlObj): + if ctrlKey not in self.pages: + return False + self.pages[ctrlKey]["qmlObj"] = qmlObj + + # 删: 删除标识符为ctrlKey的控制器。成功返回true + @Slot(str, result=bool) + def delPage(self, ctrlKey): + if ctrlKey not in self.pages: + return False + del self.pages[ctrlKey] + return True + + # ========================= 【与qml的通信】 ========================= + + # qml调用Python的方法(同步) + # qml调用ctrlKey的方法funcName,入参为列表(对应参数顺序),返回值为可变类型。 + @Slot(str, str, list, result="QVariant") + def callPy(self, ctrlKey, funcName, args): + if ctrlKey not in self.pages: + logger.warning(f"调用py方法{funcName},但{ctrlKey}不存在!") + return None + page = self.pages[ctrlKey] + # 获取方法的引用 + method = None + if funcName in page["pyCache"]: # 缓存中存在,直接取缓存 + method = page["pyCache"][funcName] + else: # 否则,搜索该方法,并写入缓存 + method = getattr(page["pyObj"], funcName, None) + page["pyCache"][funcName] = method + # 查询失败 + if not method: + logger.error(f"调用了{ctrlKey}的不存在的py方法{funcName}!") + return None + # 调用方法,参数不对的话让系统抛出错误 + return method(*args) + + # python调用qml的函数(同步) + def callQml(self, ctrlKey, funcName, *args): + if ctrlKey not in self.pages: + logger.warning(f"调用qml方法{funcName},但{ctrlKey}不存在!") + return None + page = self.pages[ctrlKey] + # 获取方法的引用 + method = None + if funcName in page["qmlCache"]: # 缓存中存在,直接取缓存 + method = page["qmlCache"][funcName] + else: # 否则,搜索该方法,并写入缓存 + method = getattr(page["qmlObj"], funcName, None) + page["qmlCache"][funcName] = method + # 查询失败 + if not method: + logger.error(f"调用了{ctrlKey}的不存在的qml方法{funcName}!") + return None + # 调用方法,参数不对的话让系统抛出错误 + return method(*args) + + # 在子线程中调用,到主线程中调用python函数 + def callFunc(self, func, *args): + CallFunc.now(func, *args) + + # ================================================== + + # 生成一个ctrlKey + def _getCtrlKey(self, key): + if key not in self._keysNum: + n = 1 + else: + n = self._keysNum[key] + 1 + self._keysNum[key] = n + return f"{key}_{n}" diff --git a/UmiOCR-data/py_src/utils/__init__.py b/UmiOCR-data/py_src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/UmiOCR-data/py_src/utils/app_opengl.py b/UmiOCR-data/py_src/utils/app_opengl.py new file mode 100644 index 0000000..97ded51 --- /dev/null +++ b/UmiOCR-data/py_src/utils/app_opengl.py @@ -0,0 +1,64 @@ +# 软件渲染选项 + +from PySide2.QtGui import QGuiApplication, QOpenGLContext +from PySide2.QtCore import Qt +import os + +from umi_log import logger +from . import pre_configs +from ..platform import Platform + +_GLDict = { + "AA_UseDesktopOpenGL": Qt.AA_UseDesktopOpenGL, + "AA_UseOpenGLES": Qt.AA_UseOpenGLES, + "AA_UseSoftwareOpenGL": Qt.AA_UseSoftwareOpenGL, +} +_Opt = "" + + +def initOpengl(): + global _Opt + opt = getOpengl() + if opt not in _GLDict: + opt = Platform.getOpenGLUse() + setOpengl(opt) + QGuiApplication.setAttribute(_GLDict[opt], True) + _Opt = opt + + +def checkOpengl(): + global _Opt + if _Opt == "AA_UseOpenGLES": # GLES需要检查,有些win7不支持 + if not QOpenGLContext.openGLModuleType() == QOpenGLContext.LibGLES: + QGuiApplication.setAttribute(Qt.AA_UseOpenGLES, False) + _Opt = "AA_UseSoftwareOpenGL" # 既然不支持opengl,那就软渲染吧 + setOpengl(_Opt) + msg = "当前系统不支持OpenGLES,已禁用此渲染器。\n若本次运行中程序崩溃或报错,请重新启动程序。\n\n" + msg += "The current system does not support OpenGLES and has disabled the program from using this renderer. \nIf there are crashes or errors during this run, please restarting the program." + logger.warning(msg) + os.MessageBox(msg, type_="warning") + + +def setOpengl(opt): + if opt not in _GLDict: + raise ValueError + pre_configs.setValue("opengl", opt) + + +def getOpengl(): + return pre_configs.getValue("opengl") + + +# OpenGL渲染模式 +# 启用 OpenGL 上下文之间的资源共享 +# QGuiApplication.setAttribute(Qt.AA_ShareOpenGLContexts, True) +# 渲染模式,【减少窗口调整大小时内容的抖动】 +# 方式一:启用OpenGL软件渲染。性能最差,CPU占用率大幅提升,效果最好。 +# QGuiApplication.setAttribute(Qt.AA_UseSoftwareOpenGL, True) +# 方式二:使用 桌面 OpenGL(例如 opengl32.dll 或 libGL.so)。性能最好,效果较差。 +# QGuiApplication.setAttribute(Qt.AA_UseDesktopOpenGL, True) +# 方式三:使用 OpenGL ES 2.0 或更高版本,用d3d接口抽象成Opengl。性能和效果都很好。但兼容性很差: +# 1. ColorOverlay必须开启cache,否则无法渲染透明层。 +# 2. 需要系统安装dx9和OpenGL3。虚拟机中可能无法使用。需要检查兼容性!!! +# 必须做兼容性判定,兼容时才启用AA_UseOpenGLES。 +# QGuiApplication.setAttribute(Qt.AA_UseOpenGLES, True) diff --git a/UmiOCR-data/py_src/utils/call_func.py b/UmiOCR-data/py_src/utils/call_func.py new file mode 100644 index 0000000..89bdd90 --- /dev/null +++ b/UmiOCR-data/py_src/utils/call_func.py @@ -0,0 +1,63 @@ +# 提供在主线程中调用指定函数 + +from PySide2.QtCore import QObject, Slot, Signal, QTimer, QMutex +from uuid import uuid4 # 唯一ID + + +class __CallFunc(QObject): + def __init__(self): + super().__init__() + # 信号 在主线程中调用函数 + self._callFuncSignal = self.cSignal() + self._callFuncSignal.signal.connect(self._cFunc) + # 计时器停止字典 + self._timerStopDict = {} + self._timerLock = QMutex() + + # ========================= 【接口】 ========================= + + # 立刻:在主线程中调用python函数 + def now(self, func, *args): + self._callFuncSignal.signal.emit((func, args)) + + # 延时:在主线程中调用python函数。返回计时器ID + def delay(self, func, time, *args): + timerID = str(uuid4()) + + def go(): + timer = QTimer(self) + timer.setSingleShot(True) # 单次运行 + timer.timeout.connect(lambda: self._timerFunc(timerID, func, args)) + timer.start(time * 1000) + + self.now(go) + return timerID + + # 取消已启用的延时 + def delayStop(self, timerID): + self._timerLock.lock() + self._timerStopDict[timerID] = True # 记录停止 + self._timerLock.unlock() + + # ================================================== + # 计时器调用的函数 + def _timerFunc(self, timerID, func, args): + self._timerLock.lock() + if timerID in self._timerStopDict: + del self._timerStopDict[timerID] + self._timerLock.unlock() + return + self._timerLock.unlock() + func(*args) + + # 异步调用的槽函数 + @Slot("QVariant") + def _cFunc(self, args): + args[0](*args[1]) + + # 信号类 + class cSignal(QObject): + signal = Signal("QVariant") + + +CallFunc = __CallFunc() diff --git a/UmiOCR-data/py_src/utils/file_finder.py b/UmiOCR-data/py_src/utils/file_finder.py new file mode 100644 index 0000000..f2ef0cd --- /dev/null +++ b/UmiOCR-data/py_src/utils/file_finder.py @@ -0,0 +1,131 @@ +# ============================================ +# =============== 文件查找/加载 =============== +# ============================================ +# 从指定路径中,查找符合的文件 + +import re +import os +import time +from PySide2.QtQml import QJSValue +from typing import List + +from ..event_bus.pubsub_service import PubSubService # 发布事件 +from ..mission.mission_doc import MissionDOC, DocSuf +from ..mission.mission_ocr import ImageSuf +from umi_log import logger + + +FileSuf = { # 合法文件后缀 + "image": ImageSuf, + "doc": DocSuf, +} + + +# 同步从路径中搜索后缀符合要求的文件,返回路径列表。 +def findFiles( + paths: List, # 初始路径列表 + sufType: str, # 后缀类型,FileSuf的key + isRecurrence: bool, # 若为True,则递归搜索 +): + if isinstance(paths, QJSValue): + paths = paths.toVariant() + if not isinstance(paths, list): + logger.error(f"不合法的路径列表:{paths}, {type(paths)}") + return [] + sufs = FileSuf.get(sufType, "") + if not sufs: + logger.error(f"不合法的后缀类型:{sufs}") + return [] + + def _sufMatching(path): + return os.path.splitext(path)[-1].lower() in sufs + + filePaths = [] + for p in paths: + if os.path.isfile(p) and _sufMatching(p): # 是文件,直接判断 + filePaths.append(os.path.abspath(p)) + elif os.path.isdir(p): # 是目录 + if isRecurrence: # 需要递归 + for root, dirs, files in os.walk(p): + for file in files: + if _sufMatching(file): # 收集子文件 + filePaths.append( + os.path.abspath(os.path.join(root, file)) + ) # 将路径转换为绝对路径 + else: # 不递归读取子文件夹 + for file in os.listdir(p): + if os.path.isfile(os.path.join(p, file)) and _sufMatching(file): + filePaths.append(os.path.abspath(os.path.join(p, file))) + for i, p in enumerate(filePaths): # 规范化正斜杠 + filePaths[i] = p.replace("\\", "/") + return filePaths + + +# 异步从路径中搜索后缀符合要求的文件,并定时刷新UI。 +# image: 返回路径列表 +# doc: 返回 MissionDOC.getDocInfo 的信息字典列表 +def asynFindFiles( + paths: List, # 初始路径列表 + sufType: str, # 后缀类型,FileSuf的key + isRecurrence: bool, # 若为True,则递归搜索 + completeKey: str, # 全部完成后的事件key。向事件传入合法路径列表。 + updateKey: str, # UI刷新进度的事件key。填""则不刷新。向事件传入 (已完成的路径数量, 最近一条路径) + updateTime: float, # UI刷新进度的间距 +): + if isinstance(paths, QJSValue): + paths = paths.toVariant() + if not isinstance(paths, list): + logger.error(f"不合法的路径列表:{paths}, {type(paths)}") + PubSubService.publish(completeKey, []) + return + sufs = FileSuf.get(sufType, "") + if not sufs: + logger.error(f"不合法的后缀类型:{sufs}") + PubSubService.publish(completeKey, []) + return + + def _sufMatching(path): + return os.path.splitext(path)[-1].lower() in sufs + + if not updateKey: # 如果没有刷新事件,则刷新间隔为无穷大 + updateTime = float("inf") + filePaths = [] + lastTime = 0 # 上一次update事件的时间 + + def updateEvent(fp): + nonlocal lastTime + now = time.time() + if now - lastTime > updateTime: + PubSubService.publish(updateKey, len(filePaths), fp) + lastTime = now + + def addFile(fp): + fp = fp.replace("\\", "/") # 规范化正斜杠 + if sufType == "doc": # 文档读取信息 + info = MissionDOC.getDocInfo(fp) + if "error" in info: + logger.warning(f'读入文档失败:{fp}, {info["error"]}') + else: + filePaths.append(info) + else: + filePaths.append(fp) + updateEvent(fp) + + for p in paths: + if os.path.isfile(p) and _sufMatching(p): # 是文件,直接判断 + addFile(os.path.abspath(p)) + elif os.path.isdir(p): # 是目录 + if isRecurrence: # 需要递归 + for root, dirs, files in os.walk(p): + for file in files: + if _sufMatching(file): # 收集子文件 + # 转换为绝对路径 + fp = os.path.abspath(os.path.join(root, file)) + addFile(fp) + else: # 不递归读取子文件夹 + for file in os.listdir(p): + if os.path.isfile(os.path.join(p, file)) and _sufMatching(file): + fp = os.path.abspath(os.path.join(p, file)) + addFile(fp) + + PubSubService.publish(completeKey, filePaths) diff --git a/UmiOCR-data/py_src/utils/global_configs_connector.py b/UmiOCR-data/py_src/utils/global_configs_connector.py new file mode 100644 index 0000000..4fe2a72 --- /dev/null +++ b/UmiOCR-data/py_src/utils/global_configs_connector.py @@ -0,0 +1,86 @@ +# 全局设置连接器 + +import os +from PySide2.QtCore import QObject, Slot + +from . import app_opengl +from .i18n_configs import I18n +from ..platform import Platform +from .pre_configs import getErrorStr +from ..server import web_server +from ..server.cmd_server import CmdActuator +from umi_log import change_save_log_level, open_logs_dir + + +class GlobalConfigsConnector(QObject): + def __init__(self): + super().__init__() + + # 创建快捷方式 + @Slot(str, result=str) + def createShortcut(self, position): + return Platform.Shortcut.createShortcut(position) + + # 删除快捷方式 + @Slot(str, result=int) + def deleteShortcut(self, position): + return Platform.Shortcut.deleteShortcut(position) + + # 获取UI语言信息 + @Slot(result="QVariant") + def i18nGetInfos(self): + return I18n.getInfos() + + # 设置UI语言 + @Slot(str, result=bool) + def i18nSetLanguage(self, lang): + return I18n.setLanguage(lang) + + # 获取Opengl渲染器选项 + @Slot(result=str) + def getOpengl(self): + return app_opengl.getOpengl() + + # 设置Opengl渲染器选项 + @Slot(str) + def setOpengl(self, opt): + app_opengl.setOpengl(opt) + + # 修改日志级别,成功返回T + @Slot(str, result=bool) + def change_save_log_level(self, levelname): + return change_save_log_level(levelname) + + # 打开日志保存目录 + @Slot() + def open_logs_dir(self): + open_logs_dir() + + # 启动web服务器,传入qml对象及回调函数名。 + @Slot("QVariant", str, str, result=int) + def runUmiWeb(self, qmlObj, callback, host): + web_server.runUmiWeb(qmlObj, callback, host) + + # 设置服务端口号 + @Slot(int) + def setServerPort(self, port): + web_server.setPort(port) + + # 将qml模块字典传入cmd执行器 + @Slot("QVariant") + def setQmlToCmd(self, moduleDict): + CmdActuator.initCollect(moduleDict) + + # 检查权限,返回检查结果 + @Slot(result=str) + def checkAccess(self): + cwd = os.getcwd() # 当前工作路径 + err = getErrorStr() # 读写异常情况 + if not err: # 没有异常,则再检查一遍权限 + if not os.access(cwd, os.R_OK): + err += "在当前路径不具有可读权限。\nDo not have read permission on the current path." + if not os.access(cwd, os.W_OK): + err += "在当前路径不具有可写权限。\nDo not have write permission on the current path." + if err: + err = cwd + "\n" + err + return err diff --git a/UmiOCR-data/py_src/utils/i18n_configs.py b/UmiOCR-data/py_src/utils/i18n_configs.py new file mode 100644 index 0000000..c085227 --- /dev/null +++ b/UmiOCR-data/py_src/utils/i18n_configs.py @@ -0,0 +1,146 @@ +# UI语言设置 + +import os +from PySide2.QtCore import QTranslator + +from . import pre_configs +from plugin_i18n import setLangCode +from umi_log import logger + +I18nDir = "i18n" # 翻译文件 目录 +DefaultLang = "zh_CN" # 默认语言,项目中qsTr()标记的原生语言,无翻译文件。 + +# 语言表。每个语种只有第一个语言代码是有效的(对应到翻译文件.qm), +# 其余的语言代码会映射到第一个。如zh_HK会映射到zh_TW,en_CA映射到en_US。 +# https://www.science.co.il/language/Locale-codes.php +LanguageCodes = { + # ===== 简中 ===== + "zh_CN": "简体中文", + "zh": "简体中文", + # ===== 繁中 ===== + "zh_TW": "繁體中文", # 中国台湾 + "zh_HK": "繁體中文", # 中国香港 + "zh_MO": "繁體中文", # 中国澳门 + "zh_SG": "繁體中文", # 新加坡 + # ===== 英语 ===== + "en_US": "English", # 美国 + "en": "English", + "en_GB": "English", # 英国 + "en_AU": "English", # 澳大利亚 + "en_CA": "English", # 加拿大 + # ===== 日语 ===== + "ja_JP": "日本語", # 日本 + # ===== 俄语 ===== + "ru_RU": "Русский", # 俄罗斯 + "ru": "Русский", + # ===== 葡萄牙语 ===== + "pt": "Português", + "pt_BR": "Português", # 巴西 + "pt_PT": "Português", # 葡萄牙 + # ===== 泰米尔语 ===== + "ta": "தமிழ்", + "ta_TA": "தமிழ்", +} + +""" 暂未启用的语言 + # ===== 韩语 ===== + "ko_KR": "한국어", # 韩国 + # ===== 法语 ===== + "fr_FR": "Français", # 法国 + "fr": "Français", + "fr_CA": "Français", # 加拿大(魁北克) + "fr_BE": "Français", # 比利时 + # ===== 意大利语 ===== + "it_IT": "Italiano", + # ===== 挪威语 ===== + "nb_NO": "norsk", + # ===== 德语 ===== + "de_DE": "Deutsch", + "de": "Deutsch", + "de_AT": "Deutsch", + "de_CH": "Deutsch", + # ===== 西班牙语 Spanish ===== + "es_ES": "Español", + "es_MX": "Español", +""" + + +class _I18n: + def init(self, qtApp): + translator = QTranslator() + qtApp.translators = [translator] + + self.langCode = "" + self.langDict = {} + # 获取信息 + self._getLangPath() + text, path = self.langDict[self.langCode] + setLangCode(self.langCode) # 设置插件翻译 + if not path: + logger.debug("使用默认文本,未加载翻译。") + return + if not translator.load(path): + msg = f"无法加载UI语言!\n[Error] Unable to load UI language: {path}" + logger.warning(msg) + os.MessageBox(msg, type_="warning") + return + if not qtApp.installTranslator(translator): # 安装翻译器 + msg = f"无法加载翻译模块!\n[Error] Unable to installTranslator: {path}" + logger.warning(msg) + os.MessageBox(msg, type_="warning") + return + logger.info(f"i18n file loaded successfully. {self.langCode} - {text}") + + # 切换语言 + def setLanguage(self, code): + if code in self.langDict: + self.langCode = code + pre_configs.setValue("i18n", code) # 写入预配置项 + return True + return False + + # 获取语言参数 + def getInfos(self): + return [self.langCode, self.langDict] + + # 获取当前翻译文件路径,如果没有配置文件则初始化 + def _getLangPath(self): + self.langDict = {} + self.langCode = "" + # 搜索本地翻译文件 + for file in os.listdir(I18nDir): + if file.endswith(".qm"): + code = os.path.splitext(file)[0] + path = os.path.join(I18nDir, file) + text = LanguageCodes.get(code, code) + self.langDict[code] = [text, path] + if DefaultLang not in self.langDict: + text = LanguageCodes[DefaultLang] + self.langDict[DefaultLang] = [text, ""] + # 加载预配置项 + code = pre_configs.getValue("i18n") + if code in self.langDict: + self.langCode = code + # 未能加载,则初始化预配置 + if not self.langCode: + import locale + + # 取得当前系统语言 + code, encoding = locale.getdefaultlocale() + # 映射首位代号 + if code in LanguageCodes: + langStr = LanguageCodes[code] + for c, l in LanguageCodes.items(): + if l == langStr: + code = c + break + # 尝试写入配置 + if not self.setLanguage(code): + # 写入配置失败,则使用默认语言 + self.setLanguage(DefaultLang) + logger.warning( + f"The current system language is {code} and there is no corresponding i18n file. The default language used is {DefaultLang}." + ) + + +I18n = _I18n() diff --git a/UmiOCR-data/py_src/utils/pre_configs.py b/UmiOCR-data/py_src/utils/pre_configs.py new file mode 100644 index 0000000..9f1609c --- /dev/null +++ b/UmiOCR-data/py_src/utils/pre_configs.py @@ -0,0 +1,76 @@ +# 程序的配置分为两部分,一部分是由qml引擎控制的主配置,必须启动qml才能访问。 +# 而这里是第二部分的配置项,单独存放少量关键配置,可以在未启动qml之前访问。 + +import os +import json + +_FileName = "./.pre_settings" + +_Configs = { + "i18n": "", # 界面语言 + "opengl": "", # 界面OpenGL渲染类型 + "server_port": 1224, # 服务端口号 + "last_pid": -1, # 最后一次运行时的进程号 + "last_ptime": -1, # 最后一次运行时的进程创建时间 +} + +_Errors = {} # 记录读写预配置文件的异常情况 + + +def getValue(key): + if key in _Configs: + return _Configs[key] + else: + raise ValueError + + +def setValue(key, value): + if key in _Configs: + _Configs[key] = value + writeConfigs() + else: + raise ValueError + + +def writeConfigs(): + global _Errors + try: + with open(_FileName, "w", encoding="utf-8") as file: + json.dump(_Configs, file, ensure_ascii=False, indent=4) + except PermissionError: + _Errors[ + "Write PermissionError" + ] = "权限不足,无法写入配置文件。\nInsufficient permissions, unable to write to the configuration file." + except Exception as e: + _Errors[ + "Write Error" + ] = f"无法写入配置文件。\nUnable to write to the configuration file: {e}" + + +def readConfigs(): + global _Errors + if not os.path.exists(_FileName): + return + try: + with open(_FileName, "r") as file: + data = json.load(file) + for key in _Configs: + _Configs[key] = data[key] + except PermissionError: + _Errors[ + "Write PermissionError" + ] = "权限不足,无法读取配置文件。\nInsufficient permissions, unable to read to the configuration file." + except Exception as e: + _Errors[ + "Write Error" + ] = f"无法读取配置文件。\nUnable to read to the configuration file: {e}" + + +# 返回异常情况字符串 +def getErrorStr(): + global _Errors + err = "" + if _Errors: + for e in _Errors.values(): + err += e + "\n" + return err diff --git a/UmiOCR-data/py_src/utils/theme_connector.py b/UmiOCR-data/py_src/utils/theme_connector.py new file mode 100644 index 0000000..ce12b14 --- /dev/null +++ b/UmiOCR-data/py_src/utils/theme_connector.py @@ -0,0 +1,33 @@ +# ========================================= +# =============== 主题连接器 =============== +# ========================================= + +from PySide2.QtCore import QObject, Slot + +from umi_log import logger + +ThemePath = "themes.json" + + +class ThemeConnector(QObject): + # 读取主题 + @Slot(result=str) + def loadThemeStr(self): + try: + with open(ThemePath, "r", encoding="utf-8") as f: + r = f.read() + return r + except FileNotFoundError: + pass + except Exception: + logger.warning("读取主题文件失败。", exc_info=True) + return "" + + # 保存主题 + @Slot(str) + def saveThemeStr(self, tstr): + try: + with open(ThemePath, "w", encoding="utf-8") as f: + f.write(tstr) + except Exception: + logger.warning("写入主题文件失败。", exc_info=True) diff --git a/UmiOCR-data/py_src/utils/thread_pool.py b/UmiOCR-data/py_src/utils/thread_pool.py new file mode 100644 index 0000000..80b13e8 --- /dev/null +++ b/UmiOCR-data/py_src/utils/thread_pool.py @@ -0,0 +1,42 @@ +# ===================================================== +# =============== 全局线程池 异步任务接口 =============== +# ===================================================== + +from PySide2.QtCore import QThreadPool, QRunnable + +from umi_log import logger + +# 全局线程池 +GlobalThreadPool = QThreadPool.globalInstance() + + +# 异步类 +class Runnable(QRunnable): + def __init__(self, taskFunc, *args, **kwargs): + super().__init__() + self._taskFunc = taskFunc + self._args = args + self._kwargs = kwargs + + def run(self): + try: + self._taskFunc(*self._args, **self._kwargs) + except Exception: + logger.error("异步运行发生错误。", exc_info=True, stack_info=True) + + +# 启动异步类 +def threadPoolStart(runnable: QRunnable): + # 检查线程池是否满,并扩充 + activeThreadCount = GlobalThreadPool.activeThreadCount() + if activeThreadCount >= GlobalThreadPool.maxThreadCount(): + logger.debug(f"线程池已满 {activeThreadCount} !自动扩充+1。") + GlobalThreadPool.setMaxThreadCount(activeThreadCount + 1) + GlobalThreadPool.start(runnable) + + +# 快捷接口:异步运行函数,返回异步类的对象 +def threadRun(taskFunc, *args, **kwargs): + runnable = Runnable(taskFunc, *args, **kwargs) + threadPoolStart(runnable) + return runnable diff --git a/UmiOCR-data/py_src/utils/utils.py b/UmiOCR-data/py_src/utils/utils.py new file mode 100644 index 0000000..4d805b4 --- /dev/null +++ b/UmiOCR-data/py_src/utils/utils.py @@ -0,0 +1,104 @@ +# ======================================= +# =============== 通用工具 =============== +# ======================================= + +import re +import os +from PySide2.QtGui import QClipboard +from PySide2.QtCore import QFileInfo +from PySide2.QtQml import QJSValue +from urllib.parse import unquote # 路径解码 + +from umi_log import logger + +Clipboard = QClipboard() # 剪贴板 + + +# 传入文件名,检测是否含非法字符。没问题返回True +def allowedFileName(fn): + pattern = r'[\\/:*?"<>|]' + if re.search(pattern, fn): + return False # 转布尔值 + else: + return True + + +# 复制文本到剪贴板 +def copyText(text): + Clipboard.setText(text) + + +# QUrl列表 转 String列表 +def QUrl2String(urls): + resList = [] + for url in urls: + if url.isLocalFile(): + u = unquote(url.toLocalFile()) # 解码路径 + if QFileInfo(u).exists(): # 检查路径是否真的存在 + resList.append(u) + return resList + + +# 初始化配置项字典数值,等同于 Configs.qml 的 function initConfigDict +# 主要是为了补充type和default +def initConfigDict(dic): + toDict = {} + + def handleConfigItem(config, key): # 处理一个配置项 + # 类型:指定type + if not config["type"] == "": + if config["type"] == "file": # 文件选择 + config["default"] = "" if not config["default"] is None else None + elif config["type"] == "var" and config["default"] is None: # 任意类型 + config["default"] = "" + # 类型:省略type + else: + if isinstance(config["default"], bool): # 布尔 + config["type"] = "boolean" + elif "optionsList" in config: # 枚举 + config["type"] = "enum" + if len(config["optionsList"]) == 0: + logger.error(f"处理配置项异常:{key}枚举列表为空。") + return + if config["default"] is None: + config["default"] = config["optionsList"][0][0] + elif isinstance(config["default"], str): # 文本 + config["type"] = "text" + elif isinstance(config["default"], (int, float)): # 数字 + config["type"] = "number" + elif "btnsList" in config: # 按钮组 + config["type"] = "buttons" + return + else: + logger.error(f"未知类型的配置项:{key}") + return + + def handleConfigGroup(group, prefix=""): # 处理一个配置组 + for key in group: + config = group[key] + if not isinstance(config, dict): + continue + # 补充空白参数 + if "type" not in config: # 类型 + config["type"] = "" + if "default" not in config: # 默认值 + config["default"] = None + if "advanced" not in config: # 是否为高级选项 + config["advanced"] = False + # 记录完整key + fullKey = prefix + key + if config["type"] == "group": # 若是配置项组,递归遍历 + handleConfigGroup(config, fullKey + ".") # 前缀加深一层 + else: # 若是配置项 + toDict[fullKey] = config + handleConfigItem(config, fullKey) + + handleConfigGroup(dic) + return toDict + + +# 整理 argd 参数字典,将 float 恢复 int 类型,如 12.0 → 12 +def argdIntConvert(argd): + for k, v in argd.items(): + if isinstance(v, float) and v.is_integer(): + argd[k] = int(v) diff --git a/UmiOCR-data/py_src/utils/utils_connector.py b/UmiOCR-data/py_src/utils/utils_connector.py new file mode 100644 index 0000000..5fe3c6e --- /dev/null +++ b/UmiOCR-data/py_src/utils/utils_connector.py @@ -0,0 +1,63 @@ +# 通用工具连接器 + +from typing import List +from PySide2.QtCore import QObject, Slot + +from . import utils +from . import file_finder # 文件搜索器 +from ..platform import Platform # 跨平台 +from .thread_pool import threadRun # 异步执行函数 + + +class UtilsConnector(QObject): + def __init__(self): + super().__init__() + + # 将文本写入剪贴板 + @Slot(str) + def copyText(self, text): + utils.copyText(text) + + # 用系统应用打开文件或目录 + @Slot(str) + def startfile(self, path): + Platform.startfile(path) + + # 硬件控制 + @Slot(str) + def hardwareCtrl(self, key): + if key == "shutdown": # 关机 + Platform.HardwareCtrl.shutdown() + elif key == "hibernate": # 休眠 + Platform.HardwareCtrl.hibernate() + + # 同步搜索文件,返回合法的文件路径列表 + @Slot("QVariant", bool, str, result="QVariant") + def findFiles(self, paths, sufType, isRecurrence): + return file_finder.findFiles(paths, sufType, isRecurrence) + + # 异步搜索文件 + @Slot("QVariant", str, bool, str, str, float) + def asynFindFiles( + self, + paths: List, # 初始路径列表 + sufType: str, # 后缀类型,FileSuf的key + isRecurrence: bool, # 若为True,则递归搜索 + completeKey: str, # 全部完成后的事件key。向事件传入合法路径列表。 + updateKey: str, # 加载中刷新进度的key,不填则无。向事件传入 (已完成的路径数量, 最近一条路径) + updateTime: float, # 刷新进度的间距 + ): + threadRun( + file_finder.asynFindFiles, + paths, + sufType, + isRecurrence, + completeKey, + updateKey, + updateTime, + ) + + # QUrl列表 转 String列表 + @Slot("QVariant", result="QVariant") + def QUrl2String(self, fileUrls): + return utils.QUrl2String(fileUrls) diff --git a/UmiOCR-data/qt_res/images/Umi-OCR_logo_full.png b/UmiOCR-data/qt_res/images/Umi-OCR_logo_full.png new file mode 100644 index 0000000..e19e042 Binary files /dev/null and b/UmiOCR-data/qt_res/images/Umi-OCR_logo_full.png differ diff --git a/UmiOCR-data/qt_res/images/icons/add.svg b/UmiOCR-data/qt_res/images/icons/add.svg new file mode 100644 index 0000000..e636b39 --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/arrow_to_center.svg b/UmiOCR-data/qt_res/images/icons/arrow_to_center.svg new file mode 100644 index 0000000..3f3e910 --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/arrow_to_center.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/arrow_to_left.svg b/UmiOCR-data/qt_res/images/icons/arrow_to_left.svg new file mode 100644 index 0000000..e4c7e7e --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/arrow_to_left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/bell.svg b/UmiOCR-data/qt_res/images/icons/bell.svg new file mode 100644 index 0000000..704beca --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/bell.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/camera-rear.svg b/UmiOCR-data/qt_res/images/icons/camera-rear.svg new file mode 100644 index 0000000..9ed9e41 --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/camera-rear.svg @@ -0,0 +1 @@ + Switch the Cameras \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/clear.svg b/UmiOCR-data/qt_res/images/icons/clear.svg new file mode 100644 index 0000000..1b72e70 --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/clear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/dango_right.png b/UmiOCR-data/qt_res/images/icons/dango_right.png new file mode 100644 index 0000000..bd3813e Binary files /dev/null and b/UmiOCR-data/qt_res/images/icons/dango_right.png differ diff --git a/UmiOCR-data/qt_res/images/icons/dash.svg b/UmiOCR-data/qt_res/images/icons/dash.svg new file mode 100644 index 0000000..7216883 --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/dash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/down.svg b/UmiOCR-data/qt_res/images/icons/down.svg new file mode 100644 index 0000000..0cc3b29 --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/folder.svg b/UmiOCR-data/qt_res/images/icons/folder.svg new file mode 100644 index 0000000..e52d15b --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/full_screen.svg b/UmiOCR-data/qt_res/images/icons/full_screen.svg new file mode 100644 index 0000000..706e1d6 --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/full_screen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/line_feed.svg b/UmiOCR-data/qt_res/images/icons/line_feed.svg new file mode 100644 index 0000000..6446da3 --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/line_feed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/lock.svg b/UmiOCR-data/qt_res/images/icons/lock.svg new file mode 100644 index 0000000..dc3d586 --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/menu.svg b/UmiOCR-data/qt_res/images/icons/menu.svg new file mode 100644 index 0000000..023226d --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/menu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/no.svg b/UmiOCR-data/qt_res/images/icons/no.svg new file mode 100644 index 0000000..e905fb1 --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/no.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/one_to_one.svg b/UmiOCR-data/qt_res/images/icons/one_to_one.svg new file mode 100644 index 0000000..dc981e7 --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/one_to_one.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/open_image.svg b/UmiOCR-data/qt_res/images/icons/open_image.svg new file mode 100644 index 0000000..601f464 --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/open_image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/paste.svg b/UmiOCR-data/qt_res/images/icons/paste.svg new file mode 100644 index 0000000..17a688b --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/paste.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/pause.svg b/UmiOCR-data/qt_res/images/icons/pause.svg new file mode 100644 index 0000000..e74baaa --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/pin.svg b/UmiOCR-data/qt_res/images/icons/pin.svg new file mode 100644 index 0000000..c484087 --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/pin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/run.svg b/UmiOCR-data/qt_res/images/icons/run.svg new file mode 100644 index 0000000..bfd6a33 --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/run.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/save.svg b/UmiOCR-data/qt_res/images/icons/save.svg new file mode 100644 index 0000000..da3c85c --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/save.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/screenshot.svg b/UmiOCR-data/qt_res/images/icons/screenshot.svg new file mode 100644 index 0000000..5e4930b --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/screenshot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/split_view.svg b/UmiOCR-data/qt_res/images/icons/split_view.svg new file mode 100644 index 0000000..e8e0658 --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/split_view.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/stop.svg b/UmiOCR-data/qt_res/images/icons/stop.svg new file mode 100644 index 0000000..2017574 --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/stop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/umiocr.ico b/UmiOCR-data/qt_res/images/icons/umiocr.ico new file mode 100644 index 0000000..ce45400 Binary files /dev/null and b/UmiOCR-data/qt_res/images/icons/umiocr.ico differ diff --git a/UmiOCR-data/qt_res/images/icons/wait.svg b/UmiOCR-data/qt_res/images/icons/wait.svg new file mode 100644 index 0000000..203cfab --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/wait.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/warning.svg b/UmiOCR-data/qt_res/images/icons/warning.svg new file mode 100644 index 0000000..cb35b4c --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/images/icons/yes.svg b/UmiOCR-data/qt_res/images/icons/yes.svg new file mode 100644 index 0000000..7a05a4e --- /dev/null +++ b/UmiOCR-data/qt_res/images/icons/yes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/ApiManager/OcrManager.qml b/UmiOCR-data/qt_res/qml/ApiManager/OcrManager.qml new file mode 100644 index 0000000..b6819ce --- /dev/null +++ b/UmiOCR-data/qt_res/qml/ApiManager/OcrManager.qml @@ -0,0 +1,177 @@ +// ========================================== +// =============== OCR接口管理 =============== +// ========================================== + +// qml的 api key 与 python api.ocr 中的字典要一致 + +import QtQuick 2.15 + +Item { + + // 配置在python那边编写 + // 缓存全局配置,如引擎路径、账号密钥等 + property var globalOptions: { + "title": qsTr("文字识别"), + "type": "group", + + "btns": { + "title": qsTr("操作"), + "btnsList": [ + {"text":qsTr("强制终止任务"), "onClicked": stopAllMissions, "textColorKey":"noColor"}, + // {"text":qsTr("测试API"), "onClicked":()=>{}}, + {"text":qsTr("应用修改"), "onClicked": applyConfigs, "textColorKey":"yesColor"}, + ], + }, + "api": { + "title": qsTr("当前接口"), + "optionsList": [], + }, + } + + // ========================= 【外部接口】 ========================= + + // 应用更改,showSuccess=false时不显示成功提示 + function applyConfigs(showSuccess=true) { + + // 成功应用修改之后的刷新函数 + function successUpdate() { + // 刷新qml各个页面的独立配置 + for (let key in deployDict) { + const p = deployDict[key].page + if(!p.configDict) { // 页面已经不存在了,则从记录字典中删除 + delete deployDict[key] + continue + } + const k = deployDict[key].configKey + p.configDict[k] = localOptions[apiKey] // 刷新页面设置 + p.reload() // 刷新页面UI + } + } + + // 验证 + if(Object.keys(localOptions).length === 0){ + const s = qsTr("没有可用的 OCR 插件。") + qmlapp.popup.message("", s, "error") + return + } + + // 获取当前全局 apiKey ,验证在本字典中的存在性 + const nowKey = qmlapp.globalConfigs.getValue("ocr.api") + if(!localOptions.hasOwnProperty(nowKey)) { + const s = qsTr("OCR API 列表中不存在%1").arg(nowKey) + qmlapp.popup.message("", s, "error") + return + } + // 验证 py 是否有执行中的任务 + const pyStatus = qmlapp.msnConnector.callPy("ocr", "getStatus", []) + const msnLen = Object.keys(pyStatus.missionListsLength).length + if(msnLen > 0) { // 当前执行中的任务队列数量 > 0 + let n = 0 + for(let k in pyStatus.missionListsLength) + n += pyStatus.missionListsLength[k] + const s = qsTr("当前已有%1组任务队列、共%2个任务正在执行。您可【强制终止任务】后修改API。").arg(msnLen).arg(n) + qmlapp.popup.message(qsTr("无法修改 文字识别接口设置"), s, "warning") + return + } + // 从全局配置中,提取出目前apiKey对应的配置项 + const allDict = qmlapp.globalConfigs.getValueDict() + const ocrk = "ocr."+nowKey + const info = {} // 汇聚为配置信息 + for(let k in allDict) { // 从全局配置中,提取以该api开头的键/值 + if(k.startsWith(ocrk)) { + info[k] = allDict[k] + } + } + // 将配置信息发送给py,然后验证操作是否成功 + const msg = qmlapp.msnConnector.callPy("ocr", "setApi", [nowKey, info]) + + // 成功,写入记录 + if(msg.startsWith("[Success]")) { + apiKey = nowKey + successUpdate() + if(showSuccess) { // 显示弹窗 + qmlapp.popup.simple(qsTr("文字识别接口应用成功"), qsTr("当前API为【%1】").arg(nowKey)) + } + } + else { + qmlapp.popup.message(qsTr("文字识别接口应用失败"), msg, "error") + } + } + + // 终止所有任务 + function stopAllMissions() { + const pyStatus = qmlapp.msnConnector.callPy("ocr", "getStatus", []) + const msnLen = Object.keys(pyStatus.missionListsLength).length + if(msnLen == 0) { // 无任务 + qmlapp.popup.simple(qsTr("当前没有运行中的任务"), "") + return + } + + let n = 0 + for(let k in pyStatus.missionListsLength) + n += pyStatus.missionListsLength[k] + const s = qsTr("当前已有%1组任务队列、共%2个任务正在执行。\n要强制终止全部任务吗?").arg(msnLen).arg(n) + + const argd = {yesText: qsTr("强制终止任务")} + const callback = (flag)=>{ if(flag) qmlapp.msnConnector.callPy("ocr", "stopAllMissions", []) } + qmlapp.popup.dialog("", s, callback, "warning", argd) + } + + // 部署进一个Configs的配置项里,可动态改变配置页。 + // 传入configs页引用和所在字典键(只能在最外层) + function deploy(page, configKey) { + // 记录已部署页面 + const pageId = page.toString() + deployDict[pageId] = { + "page": page, + "configKey": configKey, + } + // 返回初始配置字典 + if(apiKey === ""){ + return { // apiKey未初始化,先返回空的占位 + "title": "", + "type": "group", + } + } + else{ // apiKey已初始化,返回对应配置 + return localOptions[apiKey] + } + } + + // 初始化1:传入python ocr信息,整理信息,返回全局配置字典 + function init1(options) { + localOptions = {} + for (var key in options) { + const gOpt = options[key].global_options + const lOpt = options[key].local_options + if(gOpt) { // 有全局配置 + globalOptions.api.optionsList.push([key, gOpt.title]) + globalOptions[key] = gOpt + } + else { // 无全局配置 + globalOptions.api.optionsList.push([key, key]) + globalOptions[key] = {"title":key, "type":"group"} + } + if(lOpt) // 有局部配置 + localOptions[key] = lOpt + else // 无局部配置 + localOptions[key] = {"title":key, "type":"group"} + } + qmlapp.globalConfigs.configDict.ocr = globalOptions // 写入全局预配置 + } + // 初始化2:应用更改 + function init2() { + applyConfigs(false) + console.log("OcrManager 初始化OCR管理器完毕!") + } + + // ========================= 【内部】 ========================= + property string apiKey: "" // 当前选定的apiKey + property var deployDict: {} // 存放 部署了配置的页面 + property var localOptions: {} // 缓存局部配置 + property var pyOptions: undefined // 由py定义的配置参数的原始内容 + + Component.onCompleted: { + deployDict = {} + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Configs/ConfigItemComp.qml b/UmiOCR-data/qt_res/qml/Configs/ConfigItemComp.qml new file mode 100644 index 0000000..246406c --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Configs/ConfigItemComp.qml @@ -0,0 +1,77 @@ +// ============================================= +// =============== 参数项组件基类 =============== +// ============================================= + +import QtQuick 2.15 +import "../Widgets" + +Item { + property string key: "" // valueDict的键 + property var configs: undefined // 保存对Configs组件的引用 + property var updateUI: ()=>{console.log(`${key} 更新设置UI为${value()}`)} // 更新UI函数,子类重写,通过value()获取最新值 + property int cursorShape_: Qt.ArrowCursor // 鼠标指针 + + property string title: "" // 标题,可不填 + property var origin: undefined // 起源参数(静态) + property bool advanced: false // true时是高级选项 + + anchors.left: parent.left + anchors.right: parent.right + clip: true + // 这个选项是高级选项,且设置页未开启高级模式,则高度为0隐藏 + height: (advanced&&!configs.advanced) ? 0 : (size_.line + size_.spacing) + visible: !(advanced&&!configs.advanced) + + // 初始化 + Component.onCompleted: { + origin = configs.originDict[key] + title = origin.title + updateUI() + // 如果设定了提示,则加载提示组件 + if(origin.toolTip) { + toolTipLoader.sourceComponent = toolTip + } + if(origin.advanced) { + advanced = origin.advanced + title = "* "+title + } + Qt.callLater(() => { // 延迟一个事件循环,再激活背景鼠标响应 + mouseAreaBackgroud.hoverEnabled = true + }) + } + // 获取或设置值 + function value(v=undefined) { + if(v===undefined) { // 获取 + return configs.getValue(key) + } + else { // 设置 + configs.setValue(key, v) + } + } + + // 左边 标题 + Text_ { + text: title + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: size_.smallSpacing + anchors.rightMargin: size_.line + anchors.verticalCenter: parent.verticalCenter + clip: true + } + // 背景 + MouseAreaBackgroud { + id: mouseAreaBackgroud + cursorShape: cursorShape_ + hoverEnabled: false + } + // 提示 + Component { + id: toolTip + ToolTip_ { + visible: mouseAreaBackgroud.hovered + text: origin.toolTip + } + } + Loader { id: toolTipLoader } // 默认不加载 +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Configs/Configs.qml b/UmiOCR-data/qt_res/qml/Configs/Configs.qml new file mode 100644 index 0000000..6b502d2 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Configs/Configs.qml @@ -0,0 +1,1222 @@ +// ============================================ +// =============== 参数配置 逻辑 =============== +// ============================================ + +/* +定义规则: + +configDict: { + + "配置项组": { + "title": 若填单个空格“ ”,则不显示标题栏 + "type": "group", + "配置项或配置项组" + // 折叠属性,仅内层组生效 + "enabledFold": 填true时显示折叠标签 + "fold": 填true时初始折叠 + }, + + "布尔 boolean (开关)": { + "title": , + "default": true / false, + }, + "文本 text (文本框)": { + "title": , + "default": "文本", + }, + "数字 number (输入框)": { + "title": , + "isInt": true 整数 / false 浮点数, + "default": 233, + "max": 可选,上限, + "min": 可选,下限, + "unit": 可选,单位。qsTr("秒"), + }, + "枚举 enum (下拉框)": { + "title": , + "optionsList": [ + ["键1", "名称1"], + ["键2", "名称2"], + ], + }, + "文件路径 file (文件选择框)": { + "title": , + "type": "file", + "default": "默认路径", + "selectExisting": true 选择现有文件 / false 新创建文件(夹), + "selectFolder": true 选择文件夹 / false 选择文件, + "dialogTitle": 对话框标题, + "nameFilters": ["图片 (*.jpg *.jpeg)", "类型2..."] 文件夹类型可不需要 + }, + "热键 hotkey": { + "title": , + "type": "hotkey", + "default": "win+alt+c", // 默认热键 + "eventTitle": "<>", // 触发事件标题 + }, + "按钮组 buttons": { + "title": , + "btnsList": [ + {"text":"名称1", "onClicked":函数1, "textColorKey": 字体颜色名}}, + {"text":"名称2", "onClicked":函数2, "bgColorKey": 背景颜色名}}, + ], + // 颜色名 ColorKey 为 theme.keys 的值 + }, + "任意变量 var": { // 程序缓存任意变量 + "type": "var", + "save": false, + } + + 通用配置元素: + "title": 显示名称。不填(或undefined)时不生成组件。填写(包括空字符串"")时自动生成控件。 + "type": 控件类型, + "save": 可选,填false时不保存(每次初始化为默认值), + "toolTip": 可选,字符串,鼠标悬停时的提示, + "advanced": 可选,填true时为高级选项,平时隐藏 + "onChanged": 可选,值变化时的回调函数, (newVal, oldVal)=>{console.log(`值从 ${oldVal} 变为 ${newVal}`)} + onChanged可以有返回值。默认返回 undefined 表示允许变动,返回 true 表示阻止这次变动。 +} + +configDict为嵌套形式,而originDict与valueDict为展开形式的单层字典。例: + configDict["aaa"]["bbb"]["ccc"] === originDict["aaa.bbb.ccc"] + configDict["aaa"]["bbb"]["ccc"]的当前取值 === valueDict["aaa.bbb.ccc"] +*/ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Dialogs 1.3 // 文件对话框 +import "../Widgets" + +Item { + id: configs + + // ========================= 【对外接口】 ========================= + + property string category_: "" // 配置名 + property var configDict: { } // 定义字典,静态参数,key为嵌套 + property alias panelComponent: panelComponent // 自动生成的组件 + property bool autoLoad: true // 自动加载设置项 + + // 重置所有设置为默认值 + function reset() { + for (let key in originDict) { + setValue(key, originDict[key].default, true) // 刷新值 + } + } + // 重新从 configDict 加载设置项和UI + function reload() { + isChangedInit = false + initConfigDict() + initPanelComponent() + initChangedFuncs() + console.log(`% 加载配置 ${category_} !`) + // console.log(`% 加载配置 ${category_} !: ${JSON.stringify(valueDict, null, 2)}`) + } + // 重新从 settings 文件中加载设置项和UI + function settingsReload() { + settings.sync() + reload() + } + // 获取配置值字典 + function getValueDict() { + return valueDict + } + // 获取原始值字典 + function getOriginDict() { + return originDict + } + + // ========================= 【内部变量】 ========================= + + // 配置存储字典,key为展开形式,三个字典key对应 + property var originDict: { } // 键字典,键为展开形式,值指向configDict的项 + property var valueDict: { } // 值字典,动态变化 + property var compDict: { } // 组件字典(不包括组)。可能不是所有配置项都有组件 + property var compList: [] // 保存所有组件(包括组)的列表,便于删除 + + property var cacheDict: {} // 缓存 + property int cacheInterval: 500 // 缓存写入本地时间 + + property bool enabledAdvanced: false // true时显示高级模式的按钮。只有任意设置项设了高级模式,此项才会被置true + property bool advanced: false // true时进入高级模式 + + // ========================= 【数值逻辑(内部调用)】 ========================= + + // 初始化 + Component.onCompleted: { + if(autoLoad) reload() // 自动加载设置项 + // 注册重载事件 + Qt.callLater(()=>{ + qmlapp.pubSub.subscribeGroup("<>", configs, "settingsReload", + "settingsReload"+category_ // 将配置名传入组名,作为注销事件时的唯一标识符 + ) + }) + } + // 销毁,注销重载事件 + Component.onDestruction: { + qmlapp.pubSub.unsubscribeGroup("settingsReload"+category_) + } + // 初始化数值 + function initConfigDict() { + originDict = {} + valueDict = {} + cacheDict = {} + function handleConfigItem(config, key) { // 处理一个配置项 + // 类型:指定type + if (config.type !== "") { + if(config.type === "file") { // 文件选择 + if(! config.hasOwnProperty("default")) + config.default = "" + if(! config.hasOwnProperty("nameFilters")) { + config.nameFilters = [] + } + } + else if(config.type === "var") { // 缓存任意类型 + if(!config.hasOwnProperty("default")) + config.default = undefined + } + } + // 类型判断:省略type + else{ + if (typeof config.default === "boolean") { // 布尔 + config.type = "boolean" + } + else if (config.hasOwnProperty("optionsList")) { // 枚举 + if(config.optionsList.length==0) { + qmlapp.popup.message("", qsTr("%1 处理配置项异常:\n%2枚举列表为空。").arg(category_).arg(key), "error") + return + } + config.type = "enum" + if(config.default == undefined) + config.default = config.optionsList[0][0] + } + else if (typeof config.default === "string") { // 文本 + config.type = "text" + } + else if (typeof config.default === "number") { // 数字 + config.type = "number" + } + else if (config.hasOwnProperty("btnsList")) { // 按钮组 + config.type = "buttons" + config.fullKey = key // 记录完整key + return + } + else { + console.error("【Error】未知类型的配置项:"+key) + return + } + } + let flag = false + // 从配置文件中取值 + let val = settings.value(key, undefined) + // 检查和格式化存储值类型 + if(val !== undefined) { + switch(config.type) { + case "boolean": // 布尔,记录参数字符串转布尔值 + if(typeof val === "string") + val = val=="true" + flag = true + break + case "number": // 数字,字符串转数字 + if(typeof val === "string") { + if(config.isInt) + val = parseInt(val) + else + val = parseFloat(val) + } + flag = !(val==null || isNaN(val)) // 若非数字,则设为默认数值 + break + case "enum": // 枚举,检查记录参数是否在列表内 + val = str2var(val) // 尝试转为合适类型 + for(let i in config.optionsList) { + if(config.optionsList[i][0] == val) { + flag = true + break + } + } + break + // 无需检查 + case "var": // 任意 + case "file": // 文件 + case "text": // 文本 + case "hotkey": // 热键 + flag = true + break + } + } + if(!flag) { // 未有存储项或类型检查不合格,则取默认值 + val = config.default + setValue(key, val) // 存储 + console.log(`${key} 取默认值 ${val}`) + } + config.fullKey = key // 记录完整key + valueDict[key] = val // 设当前值 + } + function handleConfigGroup(group, prefix="") { // 处理一个配置组 + for(let key in group) { + const config = group[key] + if(typeof config !== "object"){ + continue + } + // 补充空白参数 + supplyDefaultParams(config) + // 记录完整key + const fullKey = prefix+key + config.fullKey = fullKey + if(config.type==="group") { // 若是配置项组,递归遍历 + handleConfigGroup(config, fullKey+".") // 前缀加深一层 + originDict[fullKey] = config + } + else { // 若是配置项 + originDict[fullKey] = config + handleConfigItem(config, fullKey) + } + } + } + handleConfigGroup(configDict) + } + // 尝试将字符串类型的变量转为合适的类型 + function str2var(str) { + // 非字符串 + if(typeof str !== "string") return str + // 尝试转数字 + const num = Number(str) + if(!isNaN(num)) return num + // 尝试转布尔 + if(str === "true") return true + if(str === "false") return false + // 都不符合,保持为字符串 + return str + } + // 补充空白参数 + function supplyDefaultParams(config) { + if(!config.hasOwnProperty("type")) // 类型 + config.type = "" + if(!config.hasOwnProperty("save")) // 保存 + config.save = true + } + // 获取值 + function getValue(key) { + return valueDict[key] + } + // 设置值 键, 值, 是否刷新UI, 是否立刻写入本地(还是缓存写入) + function setValue(key, val, isupdateUI=false, saveNow=false) { + if(typeof val !== "object" && valueDict[key] === val) // 如果val不是数组或字典,排除相同值 + return + let res = onChangedFunc(key, val, valueDict[key]) // 触发函数,传入新值和旧值 + if(res !== undefined) { // 阻止这次变动 + compDict[key].updateUI() + return + } + valueDict[key] = val + if(originDict[key].save) { // 需要保存值 + if(saveNow) // 立刻保存 + settings.setValue(key, val) + else // 缓存保存 + saveValue(key) + } + if(isupdateUI && compDict.hasOwnProperty(key)) { // 刷新UI + compDict[key].updateUI() + } + } + // 初始化期间。不执行触发函数 + property bool isChangedInit: false + // 触发函数 + function onChangedFunc(key, newVal, oldVal) { + if(!isChangedInit) // 初始化期间。不执行触发函数 + return undefined + // 配置项存在触发函数,则执行 + if(originDict[key].hasOwnProperty("onChanged")) + return originDict[key].onChanged(newVal, oldVal) + } + // 初始化,执行全部触发函数 + function initChangedFuncs() { + isChangedInit = true + for(let k in originDict) { + onChangedFunc(k, valueDict[k], undefined) // 传入空旧值 + } + } + // 带缓存的存储值 + function saveValue(key) { + cacheDict[key] = valueDict[key] + cacheTimer.restart() + } + // 保存计时器 + Timer { + id: "cacheTimer" + running: false + interval: cacheInterval + onTriggered: { + for(let k in cacheDict) { + settings.setValue(k, cacheDict[k]) // 缓存写入本地 + } + cacheDict = {} // 清空缓存 + } + } + + // 存储配置项 + Settings_ { + id: settings + category: category_ // 自定义类别名称 + } + // 存储UI项 + Settings_ { + id: uiSettings + category: category_ + property alias configs_advanced: configs.advanced + } + + // ========================= 【自动生成组件】 ========================= + + // 初始化 自动生成组件 + function initPanelComponent() { + enabledAdvanced = false + const compListLength = compList.length + if(compListLength !== 0) { // 外层组件列表非空,先删除旧的组件 + for(let i = compListLength-1; i>=0; i--) { // 倒序遍历,从内层往外层删 + compList[i].destroy() + } + compList = [] + } + compDict = {} + + function handleConfigGroup(group, parent=panelContainer) { // 处理一个配置组 + for(let key in group) { + const config = group[key] + if(typeof config !== "object") + continue + if(! (typeof config.title === "string")) // 无标题,则表示不生成组件 + continue + if(config.advanced) // 任意一个选项是高级选项,则总体开启高级模式 + enabledAdvanced = true + // 若是配置项组,递归遍历 + if(config.type === "group") { + // 若是外层,则生成外层group组件;若是内层则生成内层组件。 + const comp = parent===panelContainer ? compGroup : compGroupInner + const fold = config.fold?true:false // 是否折叠,转布尔值 + const obj = comp.createObject(parent, {"key":config.fullKey, "configs":configs}) + compList.push(obj) // 保存组件引用 + handleConfigGroup(config, obj.container) // 递归下一层,父级变成本层 + } + // 若是配置项 + else { + if(componentDict.hasOwnProperty(config.type)) { + const comp = componentDict[config.type] + const obj = comp.createObject(parent, {"key":config.fullKey, "configs":configs}) + compList.push(obj) // 保存组件引用 + compDict[config.fullKey] = obj + } + } + } + } + handleConfigGroup(configDict) + } + + // 总体 滚动视图 + ScrollView { + id: panelComponent + anchors.fill: parent + contentWidth: width // 内容宽度 + clip: true // 溢出隐藏 + property alias ctrlBar: ctrlBar // 控制栏的引用 + property alias advanced: configs.advanced // 高级标志的引用 + // 获取所有外层配置组的信息 + function getGroupList() { + const c = panelContainer.children + let itemList = [] + for(let i in c) { + if(c[i].title===undefined) + return + itemList.push({ + title: c[i].title, + advanced: c[i].advanced + }) + } + return itemList + } + // 滚到一个外层配置组的位置 + function scrollToGroup(index) { + const children = panelContainer.children + if(index < 0 || index >= children.length) { + console.error(`无法滚动到${index},超出范围!`) + return + } + const c = children[index] + let y = c.y - size_.line + let max = panelContainer.height - panelComponent.height + if(y < 0) y = 0 + if(y > max) y = max + y = y / panelContainer.height + ScrollBar.vertical.position = y + c.kirakira && c.kirakira() // 闪烁 + } + + Column { + id: panelContainer + anchors.fill: parent + anchors.rightMargin: size_.spacing + spacing: size_.spacing + + // 顶部控制栏 + Item { + id: ctrlBar + height: size_.line*1.5 + anchors.left: parent ? parent.left : undefined + anchors.right: parent ? parent.right : undefined + + Button_ { + id: ctrlBtn1 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + text_: qsTr("重置") + toolTip: qsTr("重置本页上的设定") + textColor_: theme.noColor + onClicked: { + const argd = {yesText: qsTr("重置设定")} + const callback = (flag)=>{ if(flag) reset() } + qmlapp.popup.dialog("", qsTr("要重置本页的设定吗?"), callback, "warning", argd) + } + } + CheckButton { + visible: enabledAdvanced + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: ctrlBtn1.left + text_: qsTr("高级") + toolTip: qsTr("显示更多高级选项。它们标有 * 号。\n请谨慎修改高级选项。") + textColor_: configs.advanced ? theme.textColor : theme.subTextColor + checked: configs.advanced + enabledAnime: true + onCheckedChanged: configs.advanced = checked + } + } + } + } + // 配置项组(外层) + Component { + id: compGroup + + Item { + id: groupRoot + property string key: "" // 键 + property var configs: undefined // 保存对Configs组件的引用 + property var origin: undefined // 起源参数(静态) + property string title: "" // 标题 + property alias container: panelContainer // 容器 + property bool advanced: false // true时是高级选项 + anchors.left: parent.left + anchors.right: parent.right + // 高级模式,整组隐藏 + height: (advanced&&!configs.advanced) ? 0 : groupText.height+groupRectangle.height + visible: !(advanced&&!configs.advanced) + // 边框闪烁 + function kirakira() { + if(qmlapp.enabledEffect) + blinkAnimation.start() + } + + Component.onCompleted: { + origin = configs.originDict[key] + title = origin.title + if(origin.advanced) { + advanced = origin.advanced + title = "* "+title + } + } + // 标题 + Text_ { + id: groupText + text: title + anchors.left: parent.left + anchors.leftMargin: size_.spacing + // 显示标题时,自动高度;否则高度为0 + height: (title) ? undefined:0 + + } + // 内容 + Rectangle { + id: groupRectangle + anchors.left: parent.left + anchors.right: parent.right + anchors.top: groupText.bottom + anchors.topMargin: size_.smallSpacing + color: theme.bgColor + radius: size_.panelRadius + height: childrenRect.height + size_.smallSpacing + + Column { + id: panelContainer + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: size_.smallSpacing + spacing: size_.smallSpacing + } + + Item { // 底部占位 + anchors.top: panelContainer.bottom + height: size_.smallSpacing + } + } + // 闪烁边框 + Panel { + id: groupBorder + anchors.fill: parent + color: "#00000000" + border.width: 2 + border.color: theme.specialTextColor + visible: false + opacity: 0 + // 颜色闪烁动画 + SequentialAnimation { + id: blinkAnimation + running: false + loops: 3 + onStarted: groupBorder.visible = true + NumberAnimation { + target: groupBorder; property: "opacity" + from: 0; to: 1; duration: 200 + } + NumberAnimation { + target: groupBorder; property: "opacity" + from: 1; to: 0; duration: 200 + } + onStopped: { + groupBorder.visible = false + } + } + } + } + } + // 配置项组(内层) + Component { + id: compGroupInner + + Item { + property string key: "" // 键 + property var configs: undefined // 保存对Configs组件的引用 + property var origin: undefined // 起源参数(静态) + property string title: "" // 标题 + property alias container: panelContainer // 容器 + property bool enabledFold: false // 启用折叠机制 + property bool fold: false // 折叠状态 + property string foldKey: key+".fold" // 折叠键 + property alias isFold: foldBtn.checked // 折叠 + anchors.left: parent.left + anchors.right: parent.right + clip: true + // 折叠时高度=标题+0,展开时高度=标题+内容 + height: groupText.height + (fold ? 0:panelContainer.height) + + Component.onCompleted: { + origin = configs.originDict[key] + title = origin.title + // 折叠属性。origin值转布尔,undefined当成false + enabledFold = origin.enabledFold?true:false + const f = origin.fold?true:false + if(enabledFold) { // 若启用折叠按钮,则取记录值,无记录则使用设定值 + const readf = uiSettings.value(foldKey, undefined) + // 字符串转bool + if(readf===undefined) fold = f + else if(readf===true || readf==="true") fold = true + else if(readf===false || readf==="false") fold = false + } + else { // 未启用折叠按钮,则使用设定值 + fold = f + } + // 如果设定了提示,则加载提示组件 + if(origin.toolTip) { + toolTipLoader.sourceComponent = toolTip + } + } + + // 提示 + Component { + id: toolTip + ToolTip_ { + visible: mouseAreaBackgroud.hovered + text: origin.toolTip + } + } + Loader { id: toolTipLoader } + // 背景 + MouseAreaBackgroud { id: mouseAreaBackgroud } + // 标题 + Text_ { + id: groupText + text: title+":" + anchors.left: parent.left + anchors.leftMargin: size_.smallSpacing + height: size_.line+size_.smallSpacing*2 + verticalAlignment: Text.AlignVCenter + } + // 折叠按钮 + Button_ { + id: foldBtn + visible: enabledFold + anchors.right: parent.right + anchors.rightMargin: size_.smallSpacing + anchors.verticalCenter: groupText.verticalCenter + height: groupText.height + textSize: size_.smallLine + textColor_: theme.subTextColor + text_: fold ? qsTr("展开")+" 🔽" : qsTr("折叠")+" 🔼" + bgColor_: theme.bgColor + onClicked: { + fold=!fold + uiSettings.setValue(foldKey, fold) // 折叠状态写入本地 + } + } + // 内容 + Column { + id: panelContainer + visible: !fold + anchors.left: panelLeftBorder.right + anchors.right: parent.right + anchors.top: groupText.bottom + anchors.leftMargin: size_.smallSpacing*0.5 // 子项右偏移 + } + // 内容左边的边框 + Rectangle { + id: panelLeftBorder + anchors.left: parent.left + anchors.top: panelContainer.top + anchors.bottom: panelContainer.bottom + anchors.leftMargin: size_.smallSpacing*2 + width: size_.smallSpacing*0.7 + color: theme.coverColor2 + } + } + } + // ========== 生成组件字典 ========== + property var componentDict: { + "boolean": compBoolean, + "enum": compEnum, + "file": compFile, + "text": compText, + "number": compNumber, + "hotkey": compHotkey, + "buttons": compBtns, + } + // 配置项:布尔值 + Component { + id: compBoolean + + ConfigItemComp { + id: boolRoot + property bool checked: true + property bool isInit: false + + // 初始化 + Component.onCompleted: { + isInit = true // 初始化完毕,允许启用动画 + } + + // 更新UI + updateUI: ()=>{ + checked = value() + } + + // 按下 + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + checked = !checked + value(checked) + } + } + + // 开关图标 + CheckBox_ { + id: switchBtn + anchors.right: parent.right + anchors.rightMargin: size_.smallSpacing + anchors.verticalCenter: parent.verticalCenter + checked: boolRoot.checked + enabledAnime: boolRoot.isInit + } + } + } + // 配置项:文本 + Component { + id: compText + + ConfigItemComp { + id: rootText + // 更新UI + updateUI: ()=>{ + textInput.text = value() + } + // 修改值 + function set(t) { + value(t) // 设置值 + } + // 输入框 + Rectangle { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: 1 + width: parent.width*0.5 + color: theme.bgColor + border.width: 2 + border.color: theme.coverColor2 + radius: size_.btnRadius + + TextInput_ { + id: textInput + clip: true + anchors.fill: parent + anchors.leftMargin: parent.border.width + anchors.rightMargin: parent.border.width + onTextChanged: { // 对话框文本改变时设置值 + rootText.set(text) + } + } + } + } + } + // 配置项:数字 + Component { + id: compNumber + + ConfigItemComp { + id: rootNumber + property string errTips: "" + // 更新UI + updateUI: ()=>{ + textInput.text = value() + } + // 修改值 + function set(t) { + let n = check(t) + value(n) // 设置值 + } + // 检查值 + function check(val) { + let n = Number(val); + if (!isNaN(n)) { // 是数字 + if(origin.isInt && !Number.isInteger(n)) { + errTips = qsTr("必须为整数") + } + else { + if(origin.max !== undefined && n > origin.max) { + errTips = qsTr("不能超过")+origin.max + } + else if(origin.min !== undefined && n < origin.min) { + errTips = qsTr("不能低于")+origin.min + } + else + errTips = "" + } + } + else { + errTips = qsTr("必须为数字") + } + if(errTips==="") + return n + return null + } + // 提示信息 + Rectangle { + visible: errTips!=="" + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: textInputContainer.left + anchors.margins: 3 + color: theme.noColor + radius: size_.btnRadius + width: errTipsText.width+size_.smallSpacing*2 + Text_ { + id: errTipsText + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.leftMargin: size_.smallSpacing + color: theme.bgColor + text: errTips + } + } + // 输入框 + Rectangle { + id: textInputContainer + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: 1 + width: parent.width*0.25 + color: theme.bgColor + border.width: 2 + border.color: theme.coverColor2 + radius: size_.btnRadius + + TextInput_ { + id: textInput + clip: true + anchors.left: parent.left + anchors.right: unitText.left + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.leftMargin: parent.border.width + // anchors.rightMargin: parent.border.width + onTextChanged: { // 对话框文本改变时设置值 + rootNumber.set(text) + } + } + // 单位 + Text_ { + id: unitText + visible: origin.unit!==undefined + text: origin.unit===undefined ? "" : origin.unit + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.rightMargin: size_.smallSpacing + verticalAlignment: Text.AlignVCenter // 垂直居中 + color: theme.subTextColor + font.pixelSize: size_.smallText + } + } + } + } + // 配置项:枚举 + Component { + id: compEnum + + ConfigItemComp { + + property var optionsList: [] // 候选列表原型 + // 更新UI + updateUI: ()=>{ + optionsList = origin.optionsList + let model = [] + let index = 0 + const v = value() + for(let i=0, l=optionsList.length; i{ + textInput.text = value() + } + // 导入路径 + function set(path) { + value(path) // 设置值 + } + + Rectangle { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: 1 + width: parent.width*0.5 + color: theme.bgColor + + // 选择按钮 + IconButton { + id: iconButton + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + width: height + icon_: "folder" + onClicked: fileDialog.open() + + FileDialog_ { + id: fileDialog + title: origin.dialogTitle + selectExisting: origin.selectExisting + selectFolder: origin.selectFolder + selectMultiple: false // 始终禁止多选 + nameFilters: origin.nameFilters + folder: shortcuts.desktop + onAccepted: { + if(fileDialog.fileUrls_.length > 0) { + textInput.text = fileDialog.fileUrls_[0] // 设置对话框文本 + } + } + } + } + + // 文本输入框 + Rectangle { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: iconButton.left + anchors.rightMargin: 2 + color: theme.bgColor + border.width: 2 + border.color: theme.coverColor2 + radius: size_.btnRadius + + TextInput_ { + id: textInput + clip: true + anchors.fill: parent + anchors.leftMargin: parent.border.width + anchors.rightMargin: parent.border.width + onTextChanged: { // 对话框文本改变时设置值 + rootFile.set(text) + } + } + } + } + } + } + // 配置项:按钮组 + Component { + id: compBtns + + ConfigItemComp { + + Rectangle { + anchors.fill: btnsRow + color: theme.bgColor + } + Row { + id: btnsRow + anchors.right: parent.right + anchors.rightMargin: size_.smallSpacing + anchors.top: parent.top + anchors.bottom: parent.bottom + spacing: size_.smallSpacing + + Repeater { + model: origin.btnsList + Button_ { + property var info: origin.btnsList[index] + text_: info.text + anchors.top: parent.top + anchors.bottom: parent.bottom + bgColor_: info.bgColorKey?theme[info.bgColorKey]:theme.coverColor1 + textColor_: info.textColorKey?theme[info.textColorKey]:theme.textColor + + onClicked: { + info.onClicked() + } + } + } + } + } + } + // 配置项:热键 + Component { + id: compHotkey + + ConfigItemComp { + id: rootHotkey + property string eventTitle: origin.eventTitle + property string keysName: "" + property int readNum: 0 // 记录更新了几次 + + // 初始化,更新UI + updateUI: ()=>{ + const kn = value() + changeHotkey(kn, false) + } + // 改变快捷键 + function changeHotkey(kn, showMsg=true) { + // 移除相同事件的快捷键 + qmlapp.keyMouse.delHotkey("", eventTitle, 0) + // 取消快捷键 + if(kn === "") { + keysName = "" + value("") + if(showMsg) + qmlapp.popup.simple(qsTr("已取消%1的快捷键。").arg(title), "") + } + // 注册新按键 + else { + const res = qmlapp.keyMouse.addHotkey(kn, eventTitle, 0) + // 成功 + if(res.startsWith("[Success]")) { + keysName = kn + value(kn) + if(showMsg) + qmlapp.popup.simple(qsTr("更新热键成功"), qsTr("%1的快捷键为 %2").arg(title).arg(kn)) + } + // 失败 + else { + keysName = "" + value("") + // 重复注册 + if(res.startsWith("[Warning] Registering same hotkey.")) { + qmlapp.popup.message("", qsTr("%1 快捷键%2已被注册,请尝试另外的按键组合。").arg(title).arg(kn), "warning") + } + else { // 未知原因 + qmlapp.popup.message("", qsTr("%1 快捷键%2无法注册,请尝试另外的按键组合。").arg(title).arg(kn), "error") + } + } + } + } + // 录制开始 + function readHotkey() { + // 展开遮罩 + readNum = 1 + qmlapp.popup.showMask(qsTr("请按下快捷键组合。按【Esc】退出。"), "<>") + // 订阅事件 + qmlapp.pubSub.subscribe("<>", rootHotkey, "readRunning") + qmlapp.pubSub.subscribe("<>", rootHotkey, "readFinish") + // 开始录制 + let res = qmlapp.keyMouse.readHotkey("<>", "<>") + if(res !== "[Success]") { // 开始录制失败 + // 隐藏遮罩 + qmlapp.popup.hideMask("<>") + // 取消订阅事件 + qmlapp.pubSub.unsubscribe("<>", rootHotkey, "readRunning") + qmlapp.pubSub.unsubscribe("<>", rootHotkey, "readFinish") + if(res.startsWith("[Warning] Recording is running.")) // 报错 + qmlapp.popup.message("", qsTr("当前快捷键录制已在进行,不能同时录制!"), "warning") + else + qmlapp.popup.message(qsTr("无法录制快捷键"), res, "error") + } + } + // 录制中的回调 + function readRunning(kn){ + readNum++ // 更新遮罩 + qmlapp.popup.showMask(kn, "<>") + } + // 录制完毕的回调 + function readFinish(kn) { + // 隐藏遮罩 + for(let i=0; i>") + // 取消订阅事件 + qmlapp.pubSub.unsubscribe("<>", rootHotkey, "readRunning") + qmlapp.pubSub.unsubscribe("<>", rootHotkey, "readFinish") + // 改变快捷键 + changeHotkey(kn) + } + + Rectangle { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: 1 + width: parent.width*0.5 + color: theme.bgColor + + IconButton { + id: clearBtn + icon_: "clear" + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: 1 + width: height + onClicked: changeHotkey("") + } + Button_ { + id: hotkeyBtn + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: clearBtn.left + clip: true + textColor_: theme.subTextColor + borderWidth: 2 + borderColor: theme.coverColor2 + text_: keysName + onClicked: readHotkey() + } + } + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Configs/GlobalConfigs.qml b/UmiOCR-data/qt_res/qml/Configs/GlobalConfigs.qml new file mode 100644 index 0000000..f1aa299 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Configs/GlobalConfigs.qml @@ -0,0 +1,488 @@ +// ========================================= +// =============== 全局配置项 =============== +// ========================================= + +import QtQuick 2.15 +import GlobalConfigsConnector 1.0 +import PluginsConnector 1.0 +import "../Configs" +import "../ApiManager" + +Configs { + id: gRoot + category_: "Global" + autoLoad: false + // ========================= 【全局配置项】 ========================= + configDict: { + + // 快捷方式 + "shortcut": { + "title": qsTr("快捷方式"), + "type": "group", + + "desktop": { + "title": qsTr("桌面"), + "default": false, + "onChanged": (newFlag, oldFlag)=>{ + if(oldFlag !== undefined) + return changeShortcut(newFlag, "desktop") + }, + }, + "startMenu": { + "title": qsTr("开始菜单"), + "default": false, + "onChanged": (newFlag, oldFlag)=>{ + if(oldFlag !== undefined) + return changeShortcut(newFlag, "startMenu") + }, + }, + "startup": { + "title": qsTr("开机自启"), + "default": false, + "onChanged": (newFlag, oldFlag)=>{ + if(oldFlag !== undefined) + return changeShortcut(newFlag, "startup") + }, + }, + }, + + // 界面和外观 + "ui": { + "title": qsTr("界面和外观"), + "type": "group", + + "i18n": getI18n, + "theme": { + "title": qsTr("主题"), + "optionsList": theme.manager.getOptionsList(), // 从全局主题管理器中取列表 + "onChanged": (val, old)=>{ + theme.manager.switchTheme(val) + if(old) + qmlapp.popup.simple(qsTr("切换主题"), val) + }, + }, + "fontBtn": { + "title": qsTr("字体"), + "btnsList": [ + { + "text":qsTr("修改字体"), + "onClicked": openFontPanel, + "textColorKey":"specialTextColor", + }, + ], + }, + "fontFamily": { + "default": "Microsoft YaHei", + "onChanged": (val)=>{ theme.fontFamily = val }, + }, + "dataFontFamily": { + "default": "Microsoft YaHei", + "onChanged": (val)=>{ theme.dataFontFamily = val }, + }, + "scale": { + "title": qsTr("界面与文字大小"), + "default": 1, + "optionsList": [ + [0.5, "50%"], + [0.75, "75%"], + [0.9, "90%"], + [1, "100%"], + [1.1, "110%"], + [1.25, "125%"], + [1.5, "150%"], + ], + "onChanged": (val)=>{ + size_.scale = val + }, + }, + "opengl": { + "title": qsTr("渲染器"), + "save": false, + "default": getOpengl(), + "optionsList": [ + ["AA_UseSoftwareOpenGL", qsTr("关闭硬件加速")], + ["AA_UseDesktopOpenGL", "Desktop OpenGL"], + ["AA_UseOpenGLES", "OpenGL ES"], + ], + "toolTip": qsTr("若出现界面闪烁、元素错位等界面异常,尝试切换渲染器或者关闭硬件加速"), + "onChanged": (opt, old)=>{ + old!==undefined && setOpengl(opt) + }, + }, + "disableEffect": { + "title": qsTr("禁用美化效果"), + "default": false, + "toolTip": qsTr("在低配置机器上,禁用动画、阴影等效果可减少部分资源占用"), + "onChanged": (flag)=>{ + qmlapp.enabledEffect = !flag + }, + }, + "imgShowOverlay": { + "title": qsTr("图片预览默认显示叠加层"), + "default": true, + "toolTip": qsTr("默认开启/关闭叠加层显示\n对所有图片预览组件生效"), + } + }, + + // 窗口 + "window": { + "title": qsTr("窗口"), + "type": "group", + + "startupInvisible": { + "title": qsTr("启动时缩小到任务栏"), + "default": false, + "toolTip": qsTr("软件启动时,不弹出主窗口"), + }, + "isMainWindowTop": { + "title": qsTr("窗口置顶"), + "default": false, + "toolTip": qsTr("捷径:窗口左上角图钉"), + "onChanged": (val)=>{ + mainWindowRoot.isMainWindowTop = val + }, + }, + "barIsLock": { + "title": qsTr("锁定标签栏"), + "default": false, + "toolTip": qsTr("捷径:窗口右上角小锁"), + "onChanged": (val)=>{ + qmlapp.tab.barIsLock = val + }, + }, + "closeWin2Hide": { + "title": qsTr("关闭主窗口时"), + "optionsList": [ + [true, qsTr("最小化到系统托盘")], + [false, qsTr("退出应用")], + ], + }, + "hideTrayIcon": { + "title": qsTr("隐藏托盘图标"), + "toolTip": qsTr("若要弹出位于后台的软件窗口,请在文件管理器中重复启动软件。\n若要彻底退出软件,请从任务管理器中结束进程。"), + "default": false, + "onChanged": changeHideTrayIcon, + }, + "simpleNotificationType": utilsDicts.getSimpleNotificationType(true), + "geometry": { // 存放主窗位置大小,字符串格式 "x,y,w,h" + "type": "var", + "default": "", // 如: 300,20,500,300 + }, + "messageMemory": { // 存放“不再弹出”的消息弹窗的mid + "type": "var", + "default": [], // 如: ["aaa", "bbb"] + }, + "doubleLayout": { // 存放 Double__Layout 的 hide 信息 + "type": "var", + "default": {}, // 如: {"ScreenshotOCR1": 0, } + }, + }, + + // 截图 + "screenshot": { + "title": qsTr("截图"), + "type": "group", + + "hideWindow": { + "title": qsTr("截图前隐藏主窗口"), + "toolTip": qsTr("截图前,如果主窗口处于前台,则隐藏主窗口\n将会延时等待主窗口关闭"), + "default": true, + }, + "hideWindowTime": { + "title": qsTr("隐藏等待时间"), + "default": 0.2, + "min": 0, + "unit": qsTr("秒"), + "isInt": false, + "advanced": true, + }, + }, + + // 服务 + "server": { + "title": qsTr("服务"), + "type": "group", + + "enable": { + "title": qsTr("允许HTTP服务"), + "toolTip": qsTr("Umi-OCR依赖HTTP接口进行本机跨进程通信。如果禁用,将无法使用命令行模式、多开检测等功能。"), + "default": true, + "onChanged": (val, old)=>{ + old!==undefined && qmlapp.popup.simple(qsTr("重启软件后生效"), "") + }, + }, + "host": { + "title": qsTr("主机"), + "optionsList": [ + ["127.0.0.1", qsTr("仅本地")+" (127.0.0.1)"], + ["0.0.0.0", qsTr("任何可用地址")], + ], + "onChanged": (val, old)=>{ + if(old!==undefined) { + let msg = val==="0.0.0.0"?qsTr("将允许局域网访问。请开启对应防火墙权限!"):qsTr("将禁止局域网访问。") + qmlapp.popup.simple(qsTr("重启软件后生效"), msg) + } + }, + }, + "port": { + "title": qsTr("端口"), + "isInt": true, + "default": 1224, + "max": 65535, + "min": 1, + // 保存数值,只是用于前端展示。实际端口号由pre_config保存。 + "onChanged": (port, old)=>{ + old!==undefined && setServerPort(port) + }, + }, + }, + + // 日志 + "logs": { + "title": qsTr("日志"), + "type": "group", + + "btns": { + "title": qsTr("操作"), + "btnsList": [ + { + "text":qsTr("打开日志保存目录"), + "onClicked": globalConfigConn.open_logs_dir, + "textColorKey":"specialTextColor", + }, + ], + }, + "saveLogLevel": { + "title": qsTr("保存的日志级别"), + "default": "ERROR", + "optionsList": [ + ["NONE", "Close"], + ["DEBUG", "DEBUG"], + ["INFO", "INFO"], + ["WARNING", "WARNING"], + ["ERROR", "ERROR"], + ["CRITICAL", "CRITICAL"], + ], + "onChanged": (val, old)=>{ + // 调整级别,如果修改失败则不动UI + if(!globalConfigConn.change_save_log_level(val)) + return true + if(val === "NONE") + qmlapp.popup.simple(qsTr("已禁用日志文件保存"), "") + }, + }, + }, + + // OCR接口全局设定 + "ocr": undefined, + + // 开发者工具 + "developer": { + "title": "developer tools", + "type": "group", + "advanced": true, + + "textScale": { + "title": "languageScale (textScale)", + "isInt": false, + "default": 1, + "max": 10, + "min": 0.1, + "save": false, + "onChanged": (val, old)=>{ + if(old!==undefined) + size_.textScale = val + }, + }, + }, + } + + // ========================= 【全局单例,通过 qmlapp.globalConfigs.xxx 访问】 ========================= + + OcrManager { id: ocrManager } // OCR管理器 qmlapp.globalConfigs.ocrManager + UtilsConfigDicts { id: utilsDicts } // 通用配置项 qmlapp.globalConfigs.utilsDicts + GlobalConfigsConnector { id: globalConfigConn } // 全局设置连接器 + PluginsConnector { id: pluginsConnector } // 插件全局设置连接器 + + property alias ocrManager: ocrManager + property alias utilsDicts: utilsDicts + property bool isPortInit: false // 标记端口号是否初始化完毕 + property var fontPanel: undefined // 缓存字体控制面板组件引用 + + Component.onCompleted: { + // 初始化主题 + theme.manager.init() + // 加载插件 + initPlugins() + // 初始化配置项 + reload() + // 检查权限和异常情况 + checkAccess() + // 应用OCR信息 + if(configDict.ocr) + ocrManager.init2() + console.log("GlobalConfig 初始化全局配置完毕!") + // 延迟执行 + Qt.callLater(()=>{ + setQmlToCmd() // 将qml模块字典传入cmd执行模块 + // 启动web服务 + const enab = getValue("server.enable") + if(enab) { + const host = getValue("server.host") + globalConfigConn.runUmiWeb(gRoot, "setRealPort", host) + } + }) + } + + // 检查权限和异常情况 + function checkAccess() { + let msg = globalConfigConn.checkAccess() + if(msg.length > 0) { + msg += "\n" + qsTr("请尝试更换软件路径!") + qmlapp.popup.message(qsTr("配置文件读写异常"), msg, "error") + } + } + + // 初始化插件 + function initPlugins() { + // 初始化插件 + let pluginInfos = pluginsConnector.init() + if(!pluginInfos) return false // 没有加载插件 + // 记录失败信息 + if(pluginInfos.errors && Object.keys(pluginInfos.errors).length > 0) { + const errs = pluginInfos.errors + let msg = "" + for (let key in errs) { + msg += `${key}: ${errs[key]}\n` + } + qmlapp.popup.message(qsTr("插件加载失败"), msg, "error") + } + // 成功的插件 + if(pluginInfos.options) { + // 初始化OCR管理器 + if(pluginInfos.options.ocr) + ocrManager.init1(pluginInfos.options.ocr) + } + } + + // 添加/删除快捷方式 + function changeShortcut(flag, position) { + if(flag) { + const res = globalConfigConn.createShortcut(position) + if(res === "[Success]") { + qmlapp.popup.simple(qsTr("成功添加快捷方式"), "") + } + else { + qmlapp.popup.message(qsTr("添加快捷方式失败"), res, "error") + return true // 阻止变化 + } + } + else { + let res = globalConfigConn.deleteShortcut(position) + if(res > 0) { + qmlapp.popup.simple(qsTr("成功移除 %1 个快捷方式").arg(res), "") + } + else { + qmlapp.popup.message(qsTr("提示"), qsTr("没有找到可移除的快捷方式。"), "warning") + } + } + } + + // 询问重启软件 + function askRebootApp(msg) { + const callback = (flag)=>{ flag&&Qt.quit() } + const argd = {yesText: "Quit Umi-OCR", noText: "Later"} + qmlapp.popup.dialog("", msg, callback, "", argd) + } + + // 初始化i18n参数 + property var getI18n: _getI18n() // 转为静态 + function _getI18n() { + const info = globalConfigConn.i18nGetInfos() + const lang = info[0] + const langDict = info[1] + let optionsList = [] + for(let code in langDict) { + const text = langDict[code][0] + optionsList.push([code, text]) + } + return { + "title": "语言 / Language", + "save": false, + "default": lang, + "optionsList": optionsList, + "onChanged": changeI18n, + } + } + // 改变i18n + function changeI18n(lang, old) { + if(old===undefined) return + const flag = globalConfigConn.i18nSetLanguage(lang) + if(flag) { + const msg = "UI language has been modified. Please reload Umi-OCR to take effect.\n\n已修改界面语言,请重启软件生效。" + askRebootApp(msg) + } + } + + // 获取渲染器 + function getOpengl() { + return globalConfigConn.getOpengl() + } + // 设置渲染器 + function setOpengl(opt) { + globalConfigConn.setOpengl(opt) + const msg = qsTr("渲染器变更 将在重启软件后生效") + askRebootApp(msg) + } + + // 设置服务端口号 + function setServerPort(port) { + if(port===null || port===undefined) { + qmlapp.popup.simple(qsTr("端口号不合法"), "") + return + } + if(isPortInit) { // 用户修改,忽略系统修改 + globalConfigConn.setServerPort(port) + qmlapp.popup.simple(qsTr("重启软件后生效"), qsTr("端口号改为%1").arg(port)) + } + } + // py回调,设置当前实际的端口号 + function setRealPort(port1) { + const port2 = getValue("server.port") + if(port1 !== port2) { + setValue("server.port", port1, true) + if(advanced) { + const msg = qsTr("原端口号%1被占用,\n切换为新端口号%2。\n\n若不想看到此通知,请在全局设置关闭高级模式。").arg(port2).arg(port1) + qmlapp.popup.message("", msg, "") + } + else { + console.log(`原端口号${port1}被占用,\n切换为新端口号${port2}。`) + } + } + isPortInit = true + } + // 将qml模块字典传入cmd执行模块 + function setQmlToCmd() { + const moduleDict = { + "GlobalConfigs": this, + "MainWindow": qmlapp.mainWin, + "TabViewManager": qmlapp.tab, + } + globalConfigConn.setQmlToCmd(moduleDict) + } + + // 打开字体控制板 + function openFontPanel() { + if(fontPanel) { + fontPanel.visible = true + } + } + + // 改变托盘图标显示 + function changeHideTrayIcon(flag, old) { + if(flag && old===false) { + qmlapp.popup.messageMemory("changeHideTrayIcon", "", configDict.window.hideTrayIcon.toolTip) + } + qmlapp.systemTray.visible = !flag + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Configs/Settings_.qml b/UmiOCR-data/qt_res/qml/Configs/Settings_.qml new file mode 100644 index 0000000..e8b4c27 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Configs/Settings_.qml @@ -0,0 +1,9 @@ +// ============================================================== +// =============== 对Settings的封装,提供本地化保存 =============== +// ============================================================== + +import Qt.labs.settings 1.0 + +Settings { + fileName: "./.settings" // 配置文件名 +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Configs/UtilsConfigDicts.qml b/UmiOCR-data/qt_res/qml/Configs/UtilsConfigDicts.qml new file mode 100644 index 0000000..6bedcbf --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Configs/UtilsConfigDicts.qml @@ -0,0 +1,94 @@ +// ============================================= +// =============== 通用的配置字典 =============== +// ============================================= + +import QtQuick 2.15 + +QtObject { + // 任务完成后续操作 + function getPostTaskActions() { + return { + "title": qsTr("任务完成后的操作"), + "type": "group", + + "system": { + "title": qsTr("系统"), + "save": false, // 不保存 + "optionsList": [ + ["", qsTr("无")], + ["shutdown", qsTr("关机")], + ["hibernate", qsTr("休眠")], + ], + "onChanged": (newVal, oldVal)=>{ + // TODO: 禁止非 win32 系统修改选项 + if(UmiAbout.app.system !=="win32" && newVal !== "") { + qmlapp.popup.message("", qsTr("%1 系统暂不支持电源控制!").arg(UmiAbout.app.system), "warning") + return true + } + } + }, + } + } + // 传入 getPostTaskActions 的system值,执行硬件控制操作 + function postTaskHardwareCtrl(system) { + if(system) { + let s = "" + const sysList = getPostTaskActions().system.optionsList + for(let i in sysList) + if(sysList[i][0] === system) { + s = sysList[i][1] + break + } + // 对话框:系统即将关机 继续关机 | 取消关机 + const argd = {yesText: qsTr("继续%1").arg(s), noText: qsTr("取消%1").arg(s)} + const c = (flag)=>{ + if(flag) + qmlapp.utilsConnector.hardwareCtrl(system) + } + qmlapp.popup.dialogCountdown(qsTr("系统即将%1").arg(s), "", c, "", argd) + } + } + + // OCR文本后处理-排版解析 + function getTbpuParser(d=undefined) { + return { + "title": qsTr("排版解析方案"), + "toolTip": qsTr("按什么方式,解析和排序图片中的文字块"), + "default": d, + "optionsList": [ + ["multi_para", qsTr("多栏-按自然段换行")], + ["multi_line", qsTr("多栏-总是换行")], + ["multi_none", qsTr("多栏-无换行")], + ["single_para", qsTr("单栏-按自然段换行")], + ["single_line", qsTr("单栏-总是换行")], + ["single_none", qsTr("单栏-无换行")], + ["single_code", qsTr("单栏-保留缩进")], + ["none", qsTr("不做处理")], + ], + } + } + + // 通知类型 + function getSimpleNotificationType(flag=false) { + let optionsList = [ + ["inside", qsTr("优先内部")], + ["onlyInside", qsTr("只允许内部")], + ["onlyOutside", qsTr("只允许外部")], + ["none", qsTr("禁用所有通知")], + ] + if(!flag) optionsList.unshift(["default", qsTr("跟随全局设定")]) + return { + "title": qsTr("通知弹窗类型"), + "optionsList": optionsList, + "onChanged": (newVal, oldValal)=>{ + let msg = "" + if(oldValal!==undefined) { + for(let i in optionsList) + if(optionsList[i][0]===newVal) + msg = optionsList[i][1] + qmlapp.popup.simple(qsTr("通知类型已更改"), msg, newVal) + } + }, + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/EventBus/PubSub.qml b/UmiOCR-data/qt_res/qml/EventBus/PubSub.qml new file mode 100644 index 0000000..f55192b --- /dev/null +++ b/UmiOCR-data/qt_res/qml/EventBus/PubSub.qml @@ -0,0 +1,26 @@ +// ==================================================== +// =============== 全局事件 发布/订阅机制 =============== +// ==================================================== + +import PubSubConnector 1.0 + +PubSubConnector{ + + // 订阅事件。传入 标题,函数所在Item,函数名 + // subscribe(title, item, funcName) + + // 订阅事件,可额外传入组 + // subscribeGroup(title, item, funcName, groupName) + + // 取消订阅事件 + // unsubscribe(title, item, funcName) + + // 取消订阅整组事件 + // unsubscribeGroup(groupName) + + // 发布事件,传入任意参数 + function publish(title, ...args) { + console.log("qml 发布事件", title, args) + publish_(title, args) + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/ImageManager/ImageManager.qml b/UmiOCR-data/qt_res/qml/ImageManager/ImageManager.qml new file mode 100644 index 0000000..0930213 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/ImageManager/ImageManager.qml @@ -0,0 +1,32 @@ +// ========================================= +// =============== 图片管理器 =============== +// ========================================= + +import QtQuick 2.15 +import ImageConnector 1.0 // 图片连接器 + +Item { + // ========================= 【接口】 ========================= + + // 截图,向回调函数传入裁切后的 clipImgID + readonly property var screenshot: screenshotManager.screenshot + // 重复截图 + readonly property var reScreenshot: screenshotManager.reScreenshot + // 获取指定区域的截图ID + readonly property var getScreenshot: screenshotManager.getScreenshot + // 复制图片 + readonly property var copyImage: imageConnector.copyImage + // 用系统默认应用打开图片 + readonly property var openImage: imageConnector.openImage + // 保存图片 + readonly property var saveImage: imageConnector.saveImage + // 获取剪贴板 + readonly property var getPaste: imageConnector.getPaste + + // =========================================================== + + // 图片连接器 + property QtObject imageConnector: ImageConnector {} + // 截图管理器 + property QtObject screenshotManager: ScreenshotManager {} +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/ImageManager/ScreenshotManager.qml b/UmiOCR-data/qt_res/qml/ImageManager/ScreenshotManager.qml new file mode 100644 index 0000000..006eeb7 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/ImageManager/ScreenshotManager.qml @@ -0,0 +1,214 @@ +// ========================================= +// =============== 截图管理器 =============== +// ========================================= + +import QtQuick 2.15 +import QtQuick.Window 2.15 + +Item { + id: ssWinRoot + // ==================== 【接口】 ==================== + + // 开始一次截图。传入回调函数。 + function screenshot(callback) { + // 重复调用验证 + if(running) { + qmlapp.popup.simple(errorTitle, errorRepeat) + return + } + running = true + // 获取所有屏幕的截图图像 + const grabList = getGrabList() + // 遍历截图列表,收集覆盖窗口属性 argds + let argds = [] + for(let i in grabList) { + const g = grabList[i] // 截图属性 + // 合法性检查 + if(g.imgID.startsWith("[")) { + qmlapp.popup.message(errorTitle, + qsTr("显示器: %1\n错误信息: %2").arg(g.screenName).arg(g.imgID), "error") + callback() + running = false + return + } + const screen = Qt.application.screens[i] // 获取对应编号的屏幕 + if(screen.name !== g.screenName) { + qmlapp.popup.message(errorTitle, + qsTr("屏幕设备名称不相同:\n%1\n%2").arg(screen.name).arg(g.screenName), "error") + callback() + running = false + return + } + const argd = { + imgID: g.imgID, + screenName: screen.name, + screen: screen, // 为Window设定所属屏幕属性 + screenRatio: screen.devicePixelRatio, // 屏幕缩放比 + x: screen.virtualX, + y: screen.virtualY, + width: screen.width, + height: screen.height, + selectionMode: selectMode, + screenshotEnd: ssWinRoot.ssEnd // 关闭函数 + } + console.log("即将传入 ssWinComp 参数:", JSON.stringify(argds)) + argds.push(argd) + } + // 记录回调 + lastCallback = callback + if(winDict === undefined) winDict = {} + // 生成覆盖窗口 + for(let a in argds) { + const obj = ssWinComp.createObject(this, argds[a]) + winDict[argds[a].imgID] = obj + } + // 注册esc事件监听 + qmlapp.pubSub.subscribeGroup("<>", ssWinRoot, "ssEsc", "ssEsc") + } + + // 重复上一次截图区域 + function reScreenshot(callback) { + // 重复调用验证 + if(running) { + qmlapp.popup.simple(errorTitle, errorRepeat) + return + } + running = true + if(!lastClipArgd) { + qmlapp.popup.simple(qsTr("尚未记录截图区域"), "") + callback() + running = false + return + } + // 获取所有屏幕的截图图像 + const grabList = getGrabList() + let errorMsg = "" // 缓存报错信息 + // 在截图列表中,寻找上一次截图所在的屏幕 + for(let i in grabList) { + const g = grabList[i] // 截图属性 + // 合法性检查 + if(g.imgID.startsWith("[")) { + errorMsg = g.imgID // 记录失败,跳过本轮 + continue + } + // 找到对应屏幕 + if(lastClipArgd.screenName === g.screenName) { + // 向py汇报,获取裁剪后的imgID + const clipImgID = imageConn.getClipImgID(g.imgID, + lastClipArgd.clipX, lastClipArgd.clipY, lastClipArgd.clipW, lastClipArgd.clipH) + // 成功 调用回调 + runLastCallback(clipImgID) + running = false + return + } + } + // 失败,未找到相同屏幕 + lastClipArgd = undefined + if(!errorMsg) errorMsg = qsTr("未找到匹配的屏幕") + qmlapp.popup.simple(qsTr("重复截图失败"), errorMsg) + runLastCallback() + running = false + } + + // 【同步】获取指定区域的截图ID,失败返回 "[Error]..." + function getScreenshot(rect, screen=0) { // screen 屏幕编号 + // 无需验证 running + // 获取所有屏幕的截图图像 + const grabList = getGrabList() + // 参数检查 + if(!Number.isInteger(screen) || screen < 0 || screen >= grabList.length) + return `[Error] Invalid screen=${screen}: must be an integer (0~${grabList.length-1})` + if(rect.length != 4) // 不检查内部是否合法,getClipImgID会检查 + return `[Error] Invalid rect=${rect}: must be integers [x,y,w,h]` + const grab = grabList[screen] + const imgID = grab.imgID + if(imgID.startsWith("[")) // 获取截图数据失败 + return imgID + let [x, y, w, h] = rect + // 补充缺省宽高 + if(w <= 0) w = grab.width - x + if(h <= 0) h = grab.height - y + // 返回裁剪后的imgID + return imageConn.getClipImgID(imgID, x, y, w, h) + } + + // ================================================= + + // 获取所有屏幕的截图图像 + function getGrabList() { + /* + 返回列表(不为空),每项为:\n + { + "imgID": 图片ID 或 报错信息 "[Error]开头" , + "screenName": 显示器名称 , + "width": 截图宽度 , + "height": 截图高度 , + } + */ + + // 截图前等待时间 + let wait = 0 + // 需要隐藏主窗口 + if(qmlapp.globalConfigs.getValue("screenshot.hideWindow")) { + // 若主窗口可见,则记录位置,避免linux隐藏→显示主窗口后位置偏移。 + if(qmlapp.mainWin.getVisibility()) + qmlapp.mainWin.saveGeometry() + qmlapp.mainWin.setVisibility(false) + wait = qmlapp.globalConfigs.getValue("screenshot.hideWindowTime") + } + // 获取所有屏幕的截图 + const grabList = imageConn.getScreenshot(wait) + return grabList + } + + // Esc退出截图 + function ssEsc() { + const argd = {clipX: -1, clipY: -1, clipW: -1, clipH: -1} + ssEnd(argd) + } + + // 截图窗口操作完毕的回调 + function ssEnd(argd) { + // 注销esc事件监听 + qmlapp.pubSub.unsubscribeGroup("ssEsc") + // 关闭所有覆盖窗口 + for (let key in winDict) { + winDict[key].destroy() + } + winDict = {} + // 检测是否有效。如果无效,可能是按Esc退出了截图流程 + if(argd.clipX<0 || argd.clipY<0 || argd.clipW<1 || argd.clipH<1 || !argd.imgID) { + runLastCallback() + lastClipArgd = undefined + running = false + return + } + // 记录当前截图信息 + lastClipArgd = argd + // 向py汇报,获取裁剪后的imgID + const clipImgID = imageConn.getClipImgID(argd.imgID, argd.clipX, argd.clipY, argd.clipW, argd.clipH) + // 调用回调 + runLastCallback(clipImgID) + running = false + } + + // 调用上级回调 + function runLastCallback(clipImgID) { + if (lastCallback && typeof lastCallback === "function") { + lastCallback(clipImgID) + } + } + + property string errorTitle: qsTr("截图失败") + property string errorRepeat: qsTr("上次截图操作未结束,不能进行新的截图!") + property bool running: false // 当前是否正在截图 + property var lastClipArgd: undefined // 最后一次截图的信息 + property var lastCallback: undefined // 截图完毕的回调,得到 clipImgID + property var winDict: {} // 存放当前已打开的窗口 + property QtObject imageConn: qmlapp.imageManager.imageConnector + property string selectMode: "drag" + Component { + id: ssWinComp + ScreenshotWindowComp { } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/ImageManager/ScreenshotWindowComp.qml b/UmiOCR-data/qt_res/qml/ImageManager/ScreenshotWindowComp.qml new file mode 100644 index 0000000..e96e0ba --- /dev/null +++ b/UmiOCR-data/qt_res/qml/ImageManager/ScreenshotWindowComp.qml @@ -0,0 +1,276 @@ +// ======================================= +// =============== 截图窗口 =============== +// ======================================= + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import QtGraphicalEffects 1.15 +import "../Widgets" + +Window { + id: win + + property string imgID: "" // 图片id + property string screenName: "" // 显示器名称 + property var screenRatio: 1 // 屏幕缩放比 + property var screenshotEnd // 关闭函数,外部传入 + + // 配置 + property int lineWidth: 1 // 线宽 + property color crossLineColor: "#00f91a" // 十字指示器的颜色 + property color clipBorderColor: "white" // 框选区边框的颜色 + property color darkLayerColor: "#73000000" // 深色背景层的颜色 + + // 鼠标状态, 0 等待 , 1 拖拽中 + property int mouseStatus: 0 + // status==0时为当前鼠标位置,status==1时为拖拽开始位置 + property int mouseX: -1 + property int mouseY: -1 + // 裁切区域的左上角坐标和宽高 + property int clipX: -1 + property int clipY: -1 + property int clipW: -1 + property int clipH: -1 + + property string selectionMode: "drag" // 模式:"drag" 或 "click" + property real firstClickX: -1 // 记录首次点击的X + property real firstClickY: -1 // 记录首次点击的Y + property bool hasFirstClick: false // 是否已完成第一次点击 + + flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint // 无边框+置顶 + + Component.onCompleted: { + image.showImgID(imgID) + visible = true // 窗口可见 + // 窗口模式设置为全屏,避免Linux任务栏排斥窗口位置 + win.visibility = Window.FullScreen + raise() // 弹到最前层 + requestActivate() // 激活窗口 + } + + // 截图完毕,成功为true + function ssEnd(okk) { + visible = false // 先隐藏窗口 + let argd = {} + if(okk) { // 成功 + // 乘以屏幕缩放比 + if(screenRatio !== 1) { + clipX*=screenRatio; clipY*=screenRatio; + clipW*=screenRatio; clipH*=screenRatio; + } + argd = { + screenName: screenName, + imgID: imgID, + clipX: clipX, + clipY: clipY, + clipW: clipW, + clipH: clipH, + } + } + else { + argd = {clipX:-1, clipY:-1, clipW:-1, clipH:-1} + } + // 向父级回报 + win.screenshotEnd(argd) + } + + + // 底层,图片 + Image_ { + id: image + anchors.fill: parent + } + // 深色叠加层 + Rectangle { + id: darkLayer + anchors.fill: parent + color: darkLayerColor + // 遮罩,拖拽时扣除框选区域 + layer.enabled: mouseStatus==1 + layer.effect: OpacityMask { + invert: true // 取反 + maskSource: Item { + width: darkLayer.width + height: darkLayer.height + Rectangle { + x: clipX + y: clipY + width: clipW + height: clipH + } + } + } + } + // 框选区边框 + Rectangle { + visible: mouseStatus==1 + x: clipX + y: clipY + width: clipW + height: clipH + color: "#00000000" + border.width: lineWidth + border.color: clipBorderColor + } + // 十字指示器, mouseStatus==0 时启用 + Item { + anchors.fill: parent + visible: mouseArea.containsMouse && ( + (selectionMode === "drag" && mouseStatus === 0) || + (selectionMode === "click" && mouseStatus === 0) + ) + Rectangle { // 水平 + anchors.left: parent.left + anchors.right: parent.right + color: crossLineColor + height: lineWidth + y: mouseY-lineWidth + } + Rectangle { // 垂直 + anchors.top: parent.top + anchors.bottom: parent.bottom + color: crossLineColor + width: lineWidth + x: mouseX-lineWidth + } + // ✅ 固定点击点的十字线(仅 click 模式第一次点击后) + Rectangle { // 横线 + anchors.left: parent.left + anchors.right: parent.right + color: crossLineColor + height: lineWidth + visible: selectionMode === "click" && hasFirstClick + y: firstClickY - lineWidth + } + + Rectangle { // 竖线 + anchors.top: parent.top + anchors.bottom: parent.bottom + color: crossLineColor + width: lineWidth + visible: selectionMode === "click" && hasFirstClick + x: firstClickX - lineWidth + } + } + // 鼠标触控层 + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton // 捕获左右键 + cursorShape: Qt.CrossCursor // 十字光标 + focus: true // 获取焦点 + + // 按下 + onPressed: { + if (win.selectionMode === "click") { + if (mouse.button === Qt.RightButton) { + win.hasFirstClick = false + ssEnd(false) // 取消截图 + } + // 若首次点击尚未完成,不做拖拽处理 + return + } + if (mouse.button === Qt.RightButton) { + return + } + if(mouseStatus == 0) { + mouseStatus = 1 + win.mouseX = mouse.x + win.mouseY = mouse.y + win.clipX = mouse.x + win.clipY = mouse.y + } + } + // 移动 + onPositionChanged: { + if (win.selectionMode === "click"){ + win.mouseX = mouse.x + win.mouseY = mouse.y + return + } + // 正常移动 + if(mouseStatus == 0) { + win.mouseX = mouse.x + win.mouseY = mouse.y + } + // 拖拽 + else if(mouseStatus == 1) { + // 右 + if(mouse.x > win.mouseX) { + win.clipX = win.mouseX + win.clipW = mouse.x - win.mouseX + if(win.clipX + win.clipW > win.width) // 防右越界 + win.clipW = win.width - win.clipX + } + // 左 + else { + win.clipX = mouse.x + win.clipW = win.mouseX - mouse.x + if(win.clipX < 0) { // 防左越界 + win.clipX = 0 + win.clipW = win.mouseX + } + } + // 下 + if(mouse.y > win.mouseY) { + win.clipY = win.mouseY + win.clipH = mouse.y - win.mouseY + if(win.clipY + win.clipH > win.height) // 防下越界 + win.clipH = win.height - win.clipY + } + // 上 + else { + win.clipY = mouse.y + win.clipH = win.mouseY - mouse.y + if(win.clipY < 0) { // 防上越界 + win.clipY = 0 + win.clipH = win.mouseY + } + } + } + } + // 松开 + onReleased: { + // addnew + if (win.selectionMode === "click") { + // 点击模式,首次单击后不立即结束 + return + } + if (mouse.button === Qt.RightButton) { + ssEnd(false) + return + } + if(mouseStatus == 1) { + ssEnd(true) + } + } + // adnew + // 点击模式使用 onClicked 进行两次点击逻辑 + onClicked: { + if (win.selectionMode !== "click") return + if (mouse.button === Qt.LeftButton) { + if (!win.hasFirstClick) { + // 第一次点击:记录起点 + win.firstClickX = mouse.x; win.firstClickY = mouse.y + win.clipX = win.firstClickX; win.clipY = win.firstClickY + win.clipW = 0; win.clipH = 0 + win.hasFirstClick = true + } else { + // 第二次点击:计算矩形并完成截图 + var x2 = mouse.x, y2 = mouse.y + win.clipX = Math.min(win.firstClickX, x2) + win.clipY = Math.min(win.firstClickY, y2) + win.clipW = Math.abs(x2 - win.firstClickX) + win.clipH = Math.abs(y2 - win.firstClickY) + win.hasFirstClick = false + ssEnd(true) + } + } else if (mouse.button === Qt.RightButton) { + // 右键取消截图 + win.hasFirstClick = false + ssEnd(false) + } + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Main.qml b/UmiOCR-data/qt_res/qml/Main.qml new file mode 100644 index 0000000..430a11f --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Main.qml @@ -0,0 +1,147 @@ +// ===================================== +// =============== 主窗口 =============== +// ===================================== + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import QtQuick.Controls 2.15 +import QtGraphicalEffects 1.15 +import Qt.labs.settings 1.1 +import MissionConnector 1.0 // Python任务连接器 +import KeyMouseConnector 1.0 // 键盘/鼠标连接器 +import UtilsConnector 1.0 // 通用连接器 + +import "Themes" +import "TabView_" +import "Configs" +import "EventBus" +import "Popup_" +import "MainWindow" +import "ImageManager" + +Window { + id: mainWindowRoot + visibility: Window.Hidden // 在 MainWindowManager 中启用可见性 + // 窗口 | 自定义标题栏 | 有标题 | 有系统菜单 | 有最小最大化按钮 | 有关闭按钮 | 根据条件是否置顶 + property bool isMainWindowTop: false + flags: Qt.Window | Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint + | Qt.WindowMinMaxButtonsHint | Qt.WindowCloseButtonHint | (isMainWindowTop?Qt.WindowStaysOnTopHint:0) + + width: 800 + height: 500 + minimumWidth: 300 + minimumHeight: 300 + color: "#00000000" + title: UmiAbout.fullname + + // ========================= 【控制】 ========================= + + // 全局样式,通过 theme 来访问 + property Theme theme: Theme{} + // 全局尺寸,通过 size_ 来访问 + property Size_ size_: Size_{} + + // 全局单例,通过 qmlapp. 来访问 + Item { + id: qmlapp + + // 普通全局单例 + TabViewManager { id: tab } // 标签页逻辑管理器 + ImageManager { id: imageManager } // 图片管理器 + MissionConnector { id: msnConnector } // 任务连接器 + PubSub { id: pubSub } // 全局事件发布/订阅 + KeyMouseConnector { id:keyMouse } // 鼠标/键盘 + UtilsConnector { id:utilsConnector } // 通用连接器 + AsynFilesLoader { id:asynFilesLoader } // 异步文件加载器 + + // 必须先初始化的单例,onCompleted顺序从下往上 + MainWindowManager { id:mainWin; mainWin:mainWindowRoot } // 主窗管理 + SystemTray { id:systemTray } // 系统托盘 + PopupManager { id: popup } // 弹窗管理器 + GlobalConfigs { id: globalConfigs } // 全局设置 + + property alias asynFilesLoader: asynFilesLoader + property alias imageManager: imageManager + property alias globalConfigs: globalConfigs + property alias tab: tab + property alias popup: popup + property alias msnConnector: msnConnector + property alias pubSub: pubSub + property alias keyMouse: keyMouse + property alias utilsConnector: utilsConnector + property alias systemTray: systemTray + property alias mainWin: mainWin + property bool enabledEffect: false // 全局是否启用动画 + + Component.onCompleted: { + // 延时加载标签页 + Qt.callLater(()=>{ + qmlapp.tab.init() + }) + } + } + + onClosing: { // 窗口关闭事件 + close.accepted = false // 阻止原生事件 + mainWin.close() // 调用主窗管理器的关闭 + } + + // ========================= 【布局】 ========================= + + // 主窗口的容器,兼做边框 + Rectangle { + id: mainContainer + anchors.fill: parent + color: "#00000000" + radius: size_.windowRadius // 窗口圆角 + focus: true + + // Esc 关闭窗口 + Keys.enabled: true + Keys.onEscapePressed: { + // 如果设置了关闭窗口后隐藏到托盘区,则关闭窗口 + if(qmlapp.globalConfigs.getValue("window.closeWin2Hide")) { + mainWindowRoot.visibility = Window.Hidden + } + // 否则,最小化窗口,以兼容没有托盘区的平台 + else { + mainWindowRoot.visibility = Window.Minimized + } + // TODO: Deiban 平台测试! + } + + // 为了防止主窗启动不显示时,内容的宽度初始值为0,先让内容挂到固定宽度的Item下 + Item { + width: 800 + height: 500 + property alias mainWidth: mainContainer.width + onMainWidthChanged: { // 主窗恢复显示时,再让内容挂回主窗 + if(mainUI.parent !== mainContainer) + mainUI.parent = mainContainer + } + + // 主窗口的内容 + Rectangle { + id: mainUI + anchors.fill: parent + anchors.margins: 0 // 透明边框宽度 + + color: theme.bgColor // 整个窗口的背景颜色 + radius: size_.windowRadius // 窗口圆角 + + // 标签视图 + TabView_ { } + + // 裁切子元素,并应用圆角 + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: mainUI.width + height: mainUI.height + radius: size_.windowRadius + } + } + } + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/MainWindow/AsynFilesLoader.qml b/UmiOCR-data/qt_res/qml/MainWindow/AsynFilesLoader.qml new file mode 100644 index 0000000..aa7ec43 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/MainWindow/AsynFilesLoader.qml @@ -0,0 +1,49 @@ +// ============================================= +// =============== 异步文件加载器 =============== +// ============================================= +// 用于全局任意模块加载大量文件 + +import QtQuick 2.15 + +Item { + property string tips: qsTr("正在载入 %1 个文件:\n%2") + property real updateTime: 0.2 // 刷新事件时间间隔 + property var callback_: undefined // 缓存最近一次回调函数 + + Component.onCompleted: { + // 订阅事件 + qmlapp.pubSub.subscribeGroup("<>", this, + "fileLoadComplete", "FilesLoader") + qmlapp.pubSub.subscribeGroup("<>", this, + "fileLoadUpdate", "FilesLoader") + } + + function run( + urls, // 初始路径列表 + sufType, // 后缀类型,image / doc + isRecurrence, // 若为True,则递归搜索 + callback // 加载完成后,向此回调函数传入路径列表 + ) { + callback_ = callback + qmlapp.popup.showMask(tips.arg(1).arg(""), "LoadingFiles") + qmlapp.utilsConnector.asynFindFiles( + urls, + sufType, + isRecurrence, + "<>", + "<>", + updateTime + ) + } + // 文件扫描结束,获取合法文件列表 + function fileLoadComplete(paths) { + qmlapp.popup.hideMask("LoadingFiles") + callback_(paths) + callback_ = undefined + } + // 文件扫描更新,刷新提示文本 + function fileLoadUpdate(filesCount, lastPath) { + qmlapp.popup.showMask(tips.arg(filesCount).arg(lastPath), + "LoadingFiles") + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/MainWindow/MainWindowManager.qml b/UmiOCR-data/qt_res/qml/MainWindow/MainWindowManager.qml new file mode 100644 index 0000000..7aaa209 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/MainWindow/MainWindowManager.qml @@ -0,0 +1,160 @@ +// =========================================== +// =============== 主窗口管理器 =============== +// =========================================== + +import QtQuick 2.15 +import QtQuick.Window 2.15 + +Item { + property var mainWin // 主窗口的引用 + property var screens: Qt.application.screens // 屏幕属性的引用 + + property var mScreen: mainWin.screen + property int mx: mainWin.x + property int my: mainWin.y + property int mw: mainWin.width + property int mh: mainWin.height + // 最小宽高 + property int minW: 300 + property int minH: 300 + + // ========================= 【保存量】 ========================= + + // 主窗口属性初始化 + Component.onCompleted: { + loadGeometry(true) // 恢复上次大小位置 + // 启动时可见 + const visi = !qmlapp.globalConfigs.getValue("window.startupInvisible") + setVisibility(visi) + if(!visi) { + qmlapp.popup.simple(qsTr("欢迎使用 Umi-OCR"), qsTr("已启用后台模式,可通过快捷键使用功能。")) + } + } + + // ========================= 【记录窗口位置大小】 ========================= + + + Connections { + target: mainWin + function onClosing() { + saveGeometry() + } + } + // 保存 + function saveGeometry() { + let xywh = [mx, my, mw, mh] + xywh = xywh.join(",") + qmlapp.globalConfigs.setValue("window.geometry", xywh, false, true) + console.log("保存窗口位置", xywh) + } + // 读取。isCheck==true时进行位置安全检查,避免窗口出现在屏幕外 + function loadGeometry(isCheck=false) { + let xywh = qmlapp.globalConfigs.getValue("window.geometry") + xywh = xywh.split(",") + if(xywh.length < 4) { + console.log("未能读取窗口位置", xywh) + return + } + for(let i=0; i<4; i++) + xywh[i] = parseInt(xywh[i]) + let [x, y, w, h] = xywh + // 安全检查,避免窗口出现在屏幕外 + if(isCheck) + [x, y, w, h] = checkGeometry(xywh[0], xywh[1], xywh[2], xywh[3]) + mainWin.x = x + mainWin.y = y + mainWin.width = w + mainWin.height = h + let screenIndex = 0 + for(let i=0, l=Qt.application.screens.length; i= s.virtualX && x <= s.virtualX+s.width + && y >= s.virtualY && y <= s.virtualY+s.height) { + screenIndex = i + break + } + } + mainWin.screen = Qt.application.screens[screenIndex] + console.log("读取窗口位置", x, y, w, h, screenIndex, isCheck) + } + // 检查窗口位置,返回检查后的值 + function checkGeometry(x, y, w, h) { + // 检查宽高 + if(w > mScreen.desktopAvailableWidth) + w = mScreen.desktopAvailableWidth + else if(w < minW) + w = minW + if(h > mScreen.desktopAvailableHeight) + h = mScreen.desktopAvailableHeight + else if(h < minH) + h = minH + // 检查位置 + if(x < mScreen.virtualX) + x = mScreen.virtualX + else if(x > mScreen.virtualX+mScreen.desktopAvailableWidth-w) + x = mScreen.virtualX+mScreen.desktopAvailableWidth-w + if(y < mScreen.virtualY+30) // +30防止标题栏出界 + y = mScreen.virtualY+30 + else if(y > mScreen.virtualY+mScreen.desktopAvailableHeight-h) + y = mScreen.virtualY+mScreen.desktopAvailableHeight-h + return [x, y, w, h] + } + + + // ========================= 【接口】 ========================= + + // 返回主窗口是否可见 + function getVisibility() { + return mainWin.visibility==2||mainWin.visibility==4||mainWin.visibility==5 + } + + // 设置主窗口可见性。 false 隐藏, true 恢复。 + function setVisibility(flag) { + if(flag) { + mainWin.visibility = Window.Windowed // 状态为可见 + mainWin.requestActivate() // 激活窗口 + mainWin.raise() // 弹到顶层 + } + else { + mainWin.visibility = Window.Hidden + } + } + + // 关闭主窗口 + function close() { + // 隐藏 + if(qmlapp.globalConfigs.getValue("window.closeWin2Hide")) { + setVisibility(false) + } + // 关闭 + else { + quit() + } + } + + // 退出主窗口 + function quit() { + saveGeometry() + Qt.quit() + } + + // 检查主窗口初始化屏幕位置,防止出界及过大 + function checkScreen() { + if(mw > mScreen.desktopAvailableWidth) + mw = mScreen.desktopAvailableWidth + if(mh > mScreen.desktopAvailableHeight) + mh = mScreen.desktopAvailableHeight + if(mx < mScreen.virtualX) { + mainWin.x = mScreen.virtualX + } + else if(mx > mScreen.virtualX+mScreen.desktopAvailableWidth-mainWin.width) { + mainWin.x = mScreen.virtualX+mScreen.desktopAvailableWidth-mainWin.width + } + if(my < mScreen.virtualY+30) { + mainWin.y = mScreen.virtualY+30 + } + else if(my > mScreen.virtualY+mScreen.desktopAvailableHeight-mainWin.height) { + mainWin.y = mScreen.virtualY+mScreen.desktopAvailableHeight-mainWin.height + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/MainWindow/Size_.qml b/UmiOCR-data/qt_res/qml/MainWindow/Size_.qml new file mode 100644 index 0000000..9b6cbaf --- /dev/null +++ b/UmiOCR-data/qt_res/qml/MainWindow/Size_.qml @@ -0,0 +1,61 @@ +// =========================================== +// =============== 组件尺寸相关 =============== +// =========================================== + +import QtQuick 2.15 + +Item { + // ========================= 【尺寸】 ========================= + + // 全局缩放值,适配分辨率 + property real scale: 1 + + // 行高 + // 主要文字 + property int line: 16 * scale + // 较小的文字 + property int smallLine: 13 * scale + // 较大的文字 + property int largeLine: 20 * scale + + // 文字缩放值 + property real textScale: 1 // 由下列 languageScale 控制 + // 主要文字大小 + property int text: line * textScale + // 较小的文字大小 + property int smallText: smallLine * textScale + // 较大的文字大小 + property int largeText: largeLine * textScale + + // 窗口圆角 + property real windowRadius: 0 + // 基础圆角 + property real baseRadius: 6 * scale + // 按钮圆角 + property real btnRadius: baseRadius + // 面板圆角 + property real panelRadius: baseRadius * 1.7 + + // 水平标签栏高度 + property real hTabBarHeight: line * 1.8 + + // 常用间距 + property real spacing: 7 * scale + // 小间距 + property real smallSpacing: 4 * scale + + // 语言缩放系数 + // 在相同的行高内,有些语言经过缩放可以表现更好。如英文可以比汉字的字号更小。 + // 通过在翻译文件中定义 languageScale 可以单独修改这种语言的缩放。 + property string languageScale: qsTr("1.0") + + Component.onCompleted: { + const s = parseFloat(languageScale) + if(!isNaN(s)) { + textScale = s + } + else { + console.warn("语言缩放系数无法应用:", languageScale) + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/MainWindow/SystemTray.qml b/UmiOCR-data/qt_res/qml/MainWindow/SystemTray.qml new file mode 100644 index 0000000..efa83bb --- /dev/null +++ b/UmiOCR-data/qt_res/qml/MainWindow/SystemTray.qml @@ -0,0 +1,116 @@ +// ======================================= +// =============== 系统托盘 =============== +// ======================================= + +import QtQuick 2.15 +import QtQml.Models 2.15 +import Qt.labs.platform 1.1 + +SystemTrayIcon { + + // ========================= 【接口】 ========================= + + // 添加一项菜单。如果只传入 eventTitle ,则发送事件。如果传入回调函数 func ,则调用函数,不发送事件。 + function addMenuItem(eventTitle, text, func=undefined) { + // 检查重复 + const index = findMenuEvent(eventTitle) + if(index >= 0) { + console.warn(`注册系统托盘菜单重复! ${eventTitle} - ${text}`) + return + } + const argv = {eventTitle: eventTitle, text_:text, isFunc:func?true:false} + if(func) funcDict[eventTitle] = func // 不能将函数塞进ListModel,故放进单独的函数字典 + menuModel.append(argv) + + } + + // 移除一项菜单 + function delMenuItem(eventTitle) { + console.log(`删除系统托盘菜单 ${eventTitle}`) + const index = findMenuEvent(eventTitle) + if(index < 0) { + console.warn(`删除系统托盘菜单,找不到对应项! ${eventTitle} - ${text}`) + return + } + const argd = menuModel.get(index) + if(argd.isFunc) { // 删除回调函数 + delete funcDict[argd.eventTitle] + } + // 删除菜单项 + menuModel.remove(index) + + } + + // ========================= 【控制】 ========================= + + // 在 menuModel 中寻找 eventTitle 对应的项,返回下标,找不到返回-1 + function findMenuEvent(eventTitle) { + const len = menuModel.count + let index = len-1 + for(; index >= 0; index--) { + const d = menuModel.get(index) + if(d.eventTitle === eventTitle) + break + } + return index + } + + // 点击菜单项 + function menuCall(index) { + const argd = menuModel.get(index) + if(argd.isFunc) { // 执行函数 + funcDict[argd.eventTitle]() + } + else { // 发布事件 + qmlapp.pubSub.publish(argd.eventTitle) + } + } + + // ========================= 【布局】 ========================= + + id: systemTrayRoot + visible: false + icon.source: "../../images/icons/umiocr.ico" + tooltip: "Umi-OCR" + property var funcDict: {} // 存放函数的字典 + Component.onCompleted: funcDict = {} + + onVisibleChanged: { + // 隐藏/显示托盘图标时,重新挂载菜单 + systemTrayRoot.menu = visible ? trayMenu : null + } + + // 右键菜单 + menu: Menu { + id: trayMenu + ListModel{ id:menuModel } // 自定义菜单的模型 + property alias menuModel: menuModel + + // Menu { title: qsTr("所有功能") } + + Instantiator { + model: trayMenu.menuModel + delegate: MenuItem { + text: text_ + onTriggered: systemTrayRoot.menuCall(index) + } + + onObjectAdded: trayMenu.insertItem(index, object) + onObjectRemoved: trayMenu.removeItem(object) + } + + MenuItem { + text: qsTr("打开主窗口") + onTriggered: qmlapp.mainWin.setVisibility(true) + } + MenuItem { + text: qsTr("退出 Umi-OCR") + onTriggered: qmlapp.mainWin.quit() + } + } + + onActivated: { + if(reason == SystemTrayIcon.DoubleClick) + qmlapp.mainWin.setVisibility(true) // 主窗可见 + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Popup_/MaskLayer.qml b/UmiOCR-data/qt_res/qml/Popup_/MaskLayer.qml new file mode 100644 index 0000000..c1e1d7b --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Popup_/MaskLayer.qml @@ -0,0 +1,77 @@ +// ========================================= +// =============== 全屏遮罩层 =============== +// ========================================= + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import "../Widgets" + +Popup { + + // 展示遮罩层。同一个id重复调用时,可刷新显示文本。 + function showMask(msg="", id="") { + if(msgDict === undefined) + msgDict = {} + if(!(id in msgDict)) // 新加入的id + idList.push(id) + msgDict[id] = msg // 刷新消息字典 + message = msg // 刷新消息显示 + visible = true // 开启显示状态 + } + + // 隐藏指定id的遮罩层 + function hideMask(id="") { + if(id in msgDict) { // id已记录,则移除对应项 + delete msgDict[id] + const index = idList.indexOf(id) + if(index !== -1) + idList.splice(index, 1) + } + // 不存在激活的id时,解除显示状态 + if(idList.length === 0) { + visible = false + } + // 否则,寻找上一个激活的id,刷新显示文本 + else { + const lastID = idList[idList.length - 1] + const msg = msgDict[lastID] || "" + message = msg + } + } + + property string message: "" + property var idList: [] // 存放所有已显示的id,有序 + property var msgDict: {} // key为id,value为消息文本 + + visible: false + modal: true // 阻止按键穿透 + closePolicy: Popup.NoAutoClose + parent: Overlay.overlay + anchors.centerIn: parent + property int maxWidth: parent.width - size_.line*10 + width: text.width + size_.spacing*2 + height: text.height + size_.spacing*2 + + // 背景 + background: Rectangle { + visible: message!=="" + color: theme.bgColor + radius: size_.panelRadius + + Text_ { + id: text + anchors.centerIn: parent + text: message + width: textMetrics.width > maxWidth ? maxWidth : implicitWidth // 宽度根据内容自适应,不超过 maxWidth + wrapMode: textMetrics.width > maxWidth ? Text.Wrap : Text.NoWrap + } + } + + // 文本宽度测量器,不可见 + Text_ { // TextMetrics不准确,只能用普通Text + id: textMetrics + visible: false + text: message + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Popup_/MessageBox.qml b/UmiOCR-data/qt_res/qml/Popup_/MessageBox.qml new file mode 100644 index 0000000..88841b7 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Popup_/MessageBox.qml @@ -0,0 +1,204 @@ +// ============================================== +// =============== 带确认的消息盒子 =============== +// ============================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtGraphicalEffects 1.15 // 阴影 + +import "../Widgets" + +Rectangle { + id: msgRoot + + // ========================= 【参数】 ========================= + + // 常规参数 + property string title: "" // 标题 + property string msg: "" // 内容 + property var onClosed: undefined // 关闭函数,外部传入 + property string type: "" // 可选:""默认, "warning"警告, "error"错误 + // 定制参数 + property int shadowWidth: qmlapp.enabledEffect ? size_.spacing*3 : 0 // 边缘阴影宽度 + property string icon: "bell" // 图标 + property string iconColorKey: "specialTextColor" // 图标前景颜色 + property string iconBgColorKey: "coverColor1" // 图标背景颜色 + property var btnsList: [ // 按钮列表 + // text 显示文本, value 点击返回的值, textColor 文本颜色, bgColor 背景颜色 + {"text":qsTr("取消"), "value": false, "textColor": theme.subTextColor, "bgColor": theme.bgColor}, + {"text":qsTr("确定"), "value": true, "textColor": theme.specialTextColor, "bgColor": theme.specialBgColor}, + ] + // 自动类型参数 + Component.onCompleted: { + switch(type) { + case "warning": + icon = "warning" + iconColorKey = "noColor" + if(title==="") + title=qsTr("警告") + break + case "error": + icon = "no" + iconColorKey = "noColor" + if(title==="") + title=qsTr("发生了一点小问题") + break + } + } + + + // =========================================================== + + width: size_.line * 25 + height: childrenRect.height + color: theme.bgColor + radius: qmlapp.enabledEffect ? size_.panelRadius : 0 + + // 列布局 + Column { + anchors.left: parent.left + anchors.right: parent.right + spacing: size_.spacing + // 顶部占位 + Item { + width: 1 + height: 1 + } + // 图标 + Rectangle { + width: size_.line*3 + height: size_.line*3 + anchors.horizontalCenter: parent.horizontalCenter + color: theme[iconBgColorKey] + radius: 99999 + + Icon_ { + width: parent.height*0.6 + height: parent.height*0.6 + anchors.centerIn: parent + color: theme[iconColorKey] + icon: msgRoot.icon + } + } + // 标题 + Text_ { + id: textTitle + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: size_.largeText + visible: title!=="" + text: title + } + // 内容 + Text_ { + id: textMsg + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: size_.line + wrapMode: TextEdit.Wrap // 尽量在单词边界处换行 + horizontalAlignment: Text.AlignHCenter // 水平居中 + visible: msg!=="" + text: msg + } + // 下层小按钮 + Item { + anchors.left: parent.left + anchors.right: parent.right + height: type==="error"?size_.line:1 + // 错误 + Row { + visible: type==="error" + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.rightMargin: size_.spacing + Button_ { + anchors.top: parent.top + anchors.bottom: parent.bottom + text_: qsTr("复制") + textSize: size_.smallText + textColor_: theme.subTextColor + onClicked: { + qmlapp.utilsConnector.copyText(msg) + qmlapp.popup.simple(qsTr("已复制报错信息 %1").arg(msg.length), qsTr("请前往 Issues 页面寻找解答或反馈")) + } + } + Button_ { + anchors.top: parent.top + anchors.bottom: parent.bottom + property string issueUrl: UmiAbout.url.newIssue + text_: qsTr("问题反馈") + toolTip: issueUrl + textSize: size_.smallText + textColor_: theme.subTextColor + onClicked: { + Qt.openUrlExternally(issueUrl) + } + } + } + } + // 底部按钮 + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + height: size_.line*2+size_.spacing*2 + color: theme.coverColor1 + + Row { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.margins: size_.spacing + + property real lrMargin: size_.spacing*4 + spacing: size_.spacing + property int childrenWidth: 10 + Component.onCompleted: { // 计算子级宽度 + const n = btnsList.length + if(n==1){ // 单个按钮,宽度为一半 + width = parent.width / 2 + childrenWidth = width + } + else { // 多个按钮,宽度平均分配 + width = parent.width - lrMargin + childrenWidth = (width - (n - 1) * spacing) / n + } + } + + + Repeater { + model: btnsList + Button_ { + property var info: btnsList[index] + anchors.top: parent.top + anchors.bottom: parent.bottom + width: parent.childrenWidth + text_: info.text + bgColor_: info.bgColor?info.bgColor:theme.bgColor + textColor_: info.textColor?info.textColor:theme.subTextColor + onClicked: { + if(typeof onClosed === "function") + onClosed(info.value) // 调用关闭函数 + } + } + } + } + } + // 内圆角裁切 + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: msgRoot.width + height: msgRoot.height + radius: msgRoot.radius + } + } + } + + // 边缘阴影 + layer.enabled: shadowWidth>0 + layer.effect: DropShadow { + transparentBorder: true + color: theme.coverColor4 + samples: shadowWidth + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Popup_/MessageBoxWin.qml b/UmiOCR-data/qt_res/qml/Popup_/MessageBoxWin.qml new file mode 100644 index 0000000..a7f987d --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Popup_/MessageBoxWin.qml @@ -0,0 +1,271 @@ +// ================================================== +// =============== 外部通知弹窗 =============== +// ================================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Window 2.15 + +import "../Widgets" + +Item { + id: messageRoot + // ========================= 【对外接口】 ========================= + + // 显示一个消息 + function showMessage(title, msg, type) { + const argd = { + title:title, msg:msg, type:type + } + createWin(winMsg, argd) + } + + // 带“不再提示”的消息 + // mid: 唯一标识一个提示 + function showMessageMemory(mid, title, msg, type) { + // 检查全局设置记忆列表中,是否已记录mid + const midList = qmlapp.globalConfigs.getValue("window.messageMemory") + if(midList.includes(mid)) // 已记录则不再弹出 + return + const argd = { + mid: mid, title:title, msg:msg, type:type + } + createWin(winMsgMemory, argd) + } + + // 显示一个双选项对话窗 + function showDialog(title, msg, callback, yesText, noText, type) { + const argd = { + title:title, msg:msg, callback:callback, yesText:yesText, noText:noText, type:type + } + createWin(winDialog, argd) + } + + // 显示一个双选项带倒计时对话窗 + function showDialogCountdown(title, msg, callback, yesText, noText, type, time) { + const argd = { + title:title, msg:msg, callback:callback, yesText:yesText, noText:noText, type:type, time:time + } + createWin(winDialogCountdown, argd) + } + + + // ========================= 【弹窗】 ========================= + + // 主按钮颜色 + function getYesColor(type) { + switch(type) { + case "warning": + case "error": + return theme.noColor + default: + return theme.specialTextColor + } + } + + property var winDict: {} + // 生成一个弹窗,返回生成ID + function createWin(winComponent, argd) { + // 初始化字典 + if(winDict===undefined) winDict={} + // 生成一个id + const winId = (Date.now()+Math.random()).toString() + argd.winId = winId // 添加id + // 生成组件,计入字典 + const obj = winComponent.createObject(this, argd) + winDict[winId] = obj + // 显示遮罩层 + qmlapp.popup.showMask("", winId) + } + // 关闭一个弹窗,传入生成ID + function close(winId) { + if(winDict.hasOwnProperty(winId)) { + winDict[winId].destroy() + } + // 隐藏遮罩层 + qmlapp.popup.hideMask(winId) + } + + // 只有单个确认键的普通消息 + Component { + id: winMsg + + FramelessWindow { + id: win + property string title: "" + property string msg: "" + property string type: "" + property string winId: "" + + visible: true + width: msgComp.width+msgComp.shadowWidth + height: msgComp.height+msgComp.shadowWidth + color: "#00000000" + + // 消息盒组件 + MessageBox { + id: msgComp + anchors.centerIn: parent + title: win.title // 标题 + msg: win.msg // 内容 + type: win.type // 类型 + btnsList: [ // 按钮列表 + {"text": qsTr("确定"), "textColor": theme.specialTextColor, "bgColor": theme.specialBgColor}, + ] + onClosed: win.close // 关闭函数 + } + + function close() { + messageRoot.close(winId) + } + } + } + + // 带“不再提示”的记忆消息 + Component { + id: winMsgMemory + + FramelessWindow { + id: win + property string title: "" + property string msg: "" + property string type: "" + property string winId: "" + property string mid: "" + + visible: true + width: msgComp.width+msgComp.shadowWidth + height: msgComp.height+msgComp.shadowWidth + color: "#00000000" + + // 消息盒组件 + MessageBox { + id: msgComp + anchors.centerIn: parent + title: win.title // 标题 + msg: win.msg // 内容 + type: win.type // 类型 + btnsList: [ // 按钮列表 + {"text": qsTr("不再提示"), "value": true, "textColor": theme.specialTextColor, "bgColor": theme.specialBgColor}, + {"text": qsTr("知道了"), "value": false, "textColor": theme.subTextColor, "bgColor": theme.bgColor}, + ] + onClosed: (value)=>{ + if(value) { + // 将mid添加到全局设置记忆列表中 + let midList = qmlapp.globalConfigs.getValue("window.messageMemory") + midList.push(mid) + qmlapp.globalConfigs.setValue("window.messageMemory", midList) + } + messageRoot.close(winId) // 关闭窗口 + } + } + } + } + + // 有两个键(确认/取消)的对话框 + Component { + id: winDialog + + FramelessWindow { + id: win + property string title: "" + property string msg: "" + property string type: "" + property string winId: "" + property string yesText: "" + property string noText: "" + property var callback // 回调函数 + + visible: true + width: msgComp.width+msgComp.shadowWidth + height: msgComp.height+msgComp.shadowWidth + color: "#00000000" + + MessageBox { + id: msgComp + anchors.centerIn: parent + title: win.title // 标题 + msg: win.msg // 内容 + type: win.type // 类型 + btnsList: [ // 按钮列表 + // 确认 + {"text": yesText, "value": true, "textColor": messageRoot.getYesColor(win.type), "bgColor": theme.specialBgColor}, + // 取消 + {"text": noText, "value": false, "textColor": theme.subTextColor, "bgColor": theme.bgColor}, + ] + onClosed: (value)=>{ + callback(value) + messageRoot.close(winId) // 关闭窗口 + } + } + } + } + + // 带倒计时的双键对话框 + Component { + id: winDialogCountdown + + FramelessWindow { + id: win + property string title: "" + property string msg: "" + property string type: "" + property string winId: "" + property string yesText: "" + property string yesTextTime: "" // 带倒计时的确定文本 + property string noText: "" + property int time: 10000 + property int nowTime: 0 + property int interval: 1000 + property var callback // 回调函数 + + visible: true + width: msgComp.width+msgComp.shadowWidth + height: msgComp.height+msgComp.shadowWidth + color: "#00000000" + + + Component.onCompleted: { + win.yesTextTime = win.yesText+` (${win.time*0.001})` + win.nowTime = win.time + timer.running = true + } + + Timer { + id: timer + interval: win.interval // 间隔 + running: false + repeat: true // 重复执行 + onTriggered: { + win.nowTime -= win.interval + win.yesTextTime = win.yesText+` (${win.nowTime*0.001})` + if(win.nowTime<=0) { + timer.stop() // 停止计时器 + callback(true) // 回调 + messageRoot.close(winId) // 关闭窗口 + return + } + } + } + + MessageBox { + id: msgComp + anchors.centerIn: parent + title: win.title // 标题 + msg: win.msg // 内容 + type: win.type // 类型 + btnsList: [ // 按钮列表 + // 确认 + {"text": yesTextTime, "value": true, "textColor": messageRoot.getYesColor(win.type), "bgColor": theme.specialBgColor}, + // 取消 + {"text": noText, "value": false, "textColor": theme.subTextColor, "bgColor": theme.bgColor}, + ] + onClosed: (value)=>{ + timer.running = false // 停止计时器 + callback(value) + messageRoot.close(winId) // 关闭窗口 + } + } + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Popup_/MessageSimple.qml b/UmiOCR-data/qt_res/qml/Popup_/MessageSimple.qml new file mode 100644 index 0000000..1735de6 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Popup_/MessageSimple.qml @@ -0,0 +1,186 @@ +// ================================================== +// =============== 简单通知弹窗 =============== +// =============== 无确认,计时自动消失 =============== +// ================================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtGraphicalEffects 1.15 // 阴影 +import QtQuick.Window 2.15 + +import "../Widgets" + +Item { + // ========================= 【对外接口】 ========================= + + function show(title, msg, showType) { + if(showType=="default") { + showType = qmlapp.globalConfigs.getValue("window.simpleNotificationType") + } + if(showType=="none") { + return // 不发送 + } + const time = 3000 + // 强制发送外部 + if(showType=="onlyOutside") { + notificationWindow.show(title, msg, time) + } + // 主窗口可见,发送内部 + else if(qmlapp.mainWin.getVisibility()) { + if(showType=="inside" || showType=="onlyInside") { + notificationPopup.show(title, msg, time) + } + } + // 主窗口不可见,发送外部 + else { + if(showType=="inside") { + // 发送外部通知 + notificationWindow.show(title, msg, time) + } + } + } + + // ========================= 【内部模式】 ========================= + + Popup { + id: notificationPopup + + // 显示通知弹窗 + function show(title, msg, time=3000) { + if(opened) { // 已打开,则先关闭 + close() + } + nscPopup.show(title, msg, time) // 传入信息 + open() // 开始 + if(!qmlapp.enabledEffect) { // 无动画时,瞬间出现 + nscPopup.y = showY + } + } + // 隐藏通知弹窗 + function hide() { + close() + } + // 属性 + padding: 0 + property real showY: -nscPopup.height*1.5 + modal: false // 非模态层(不阻挡下方) + parent: Overlay.overlay + x: Math.round((parent.width - width) / 2) + y: Math.round((parent.height - height)) + closePolicy: Popup.NoAutoClose // 不被系统关闭 + // 组件 + MessageSimpleComp { + id: nscPopup + onHided: notificationPopup.hide // 关闭事件 + anchors.horizontalCenter: parent.horizontalCenter + y: notificationPopup.showY + } + // 进入动画 + enter: qmlapp.enabledEffect ? enterAnimePop : null + Transition { + id: enterAnimePop + NumberAnimation { + target: nscPopup + property: "y" + duration: 200 + from: 0 + to: notificationPopup.showY + easing.type: Easing.OutCubic + } + } + // 关闭动画 + exit: qmlapp.enabledEffect ? exitAnimePop : null + Transition { + id: exitAnimePop + NumberAnimation { + target: nscPopup + property: "y" + duration: 200 + from: notificationPopup.showY + to: 0 + easing.type: Easing.InCubic + } + } + } + + // ========================= 【外部模式】 ========================= + + Window { + id: notificationWindow + + /* TODO + 当:主窗口初始隐藏,弹出通知弹窗,隐藏通知弹窗,恢复主窗口。会报错: + Conflicting properties 'visible' and 'visibility' for Window 'notificationWindow' + 似乎不影响使用,待调查。 + */ + + // 显示通知弹窗 + function show(title, msg, time) { + let screenWidth = Screen.width + let screenHeight = Screen.height + x = (screenWidth - width) / 2 // 水平居中 + nscWindow.show(title, msg, time) // 传入信息 + if(qmlapp.enabledEffect) { // 出现动画 + enterAnimeWin.start() + } + else { // 无动画时,y瞬间出现 + y = screenHeight-showY + } + visible = true + visibility = Window.Windowed + } + // 隐藏通知弹窗 + function hide() { + if(qmlapp.enabledEffect) { // 关闭动画 + exitAnimeWin.start() + } + else { // 无动画时,瞬间关闭 + visible = false + visibility = Window.Hidden + } + } + // 属性 + property real showY: nscWindow.height+100 // 显示位置高度 + visible: false + flags: Qt.Popup | Qt.NoDropShadowWindowHint | Qt.WindowStaysOnTopHint // 弹出式,无阴影,置顶 + color: "#00000000" + width: nscWindow.width+nscWindow.shadowWidth // 长宽要加上阴影宽度 + height: nscWindow.height+nscWindow.shadowWidth + Component.onCompleted: y=Screen.height-showY // 初始y值,防止theme未加载导致第一次调用异常 + MessageSimpleComp { + id: nscWindow + anchors.centerIn: parent + onHided: notificationWindow.hide // 关闭事件 + } + // 进入动画 + ParallelAnimation { + id: enterAnimeWin + running: false + NumberAnimation { + target: notificationWindow + property: "y" + duration: 200 + from: Screen.height + to: Screen.height-notificationWindow.showY + easing.type: Easing.OutCubic + } + } + // 关闭动画 + ParallelAnimation { + id: exitAnimeWin + running: false + onStopped: { + notificationWindow.visible = false + notificationWindow.visibility = Window.Hidden + } + NumberAnimation { + target: notificationWindow + property: "y" + duration: 200 + from: Screen.height-notificationWindow.showY + to: Screen.height + easing.type: Easing.InCubic + } + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Popup_/MessageSimpleComp.qml b/UmiOCR-data/qt_res/qml/Popup_/MessageSimpleComp.qml new file mode 100644 index 0000000..54c18de --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Popup_/MessageSimpleComp.qml @@ -0,0 +1,142 @@ +// ============================================= +// =============== 简单消息的界面 =============== +// ============================================= + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtGraphicalEffects 1.15 // 阴影 + +import "../Widgets" + +Rectangle { + id: spMsg + + property var onHided: undefined // 关闭函数,外部传入 + function show(title, msg, time=5000, icon="bell") { + textTitle.text = title + textMsg.text = msg + nowTime = 0 + allTime = time>0?time:1 // 防止除0 + timerProgressBar.percent = 0 + iconComp.icon = icon // 图标 + timer.start() // 启动计时器 + } + + property int allTime: 1 // 总时间 + property int nowTime: 0 // 当前时间 + property int interval: 10 // 间隔刷新 + property int shadowWidth: qmlapp.enabledEffect ? size_.spacing*3 : 0 // 边缘阴影宽度 + Timer { + id: timer + interval: spMsg.interval // 间隔 + running: false + repeat: true // 重复执行 + onTriggered: { + spMsg.nowTime += spMsg.interval + timerProgressBar.percent = spMsg.nowTime/spMsg.allTime + if(spMsg.nowTime>=spMsg.allTime) { + timer.stop() // 停止计时器 + if(typeof onHided === "function") + onHided() // 调用关闭函数 + return + } + } + } + + width: size_.line * 20 + height: textTitle.height + textMsg.height+size_.spacing*2 + color: theme.specialBgColor + radius: qmlapp.enabledEffect ? size_.panelRadius : 0 + // 内容组件 + Item { + anchors.fill: parent + anchors.margins: size_.spacing + // 倒计时 ▽按钮 + HProgressBar { + id: timerProgressBar + anchors.top: parent.top + anchors.right: parent.right + height: size_.smallLine + width: size_.smallLine*2 + color: theme.coverColor1 + highlightColor: theme.coverColor2 + radius: size_.btnRadius + percent: 0 + // 鼠标悬浮背景 + Rectangle { + id: btnHoverBg + visible: false + anchors.fill: parent + color: theme.coverColor2 + radius: size_.btnRadius + } + // 下箭头图标 + Icon_ { + anchors.fill: parent + icon: "down" + color: theme.bgColor + } + } + // 标题图标 + Icon_ { + id: iconComp + anchors.top: parent.top + anchors.left: parent.left + color: theme.specialTextColor + height: size_.line + width: size_.line + } + // 标题文字 + Text_ { + id: textTitle + anchors.top: parent.top + anchors.left: iconComp.right + anchors.leftMargin: size_.line*0.5 + height: size_.line + verticalAlignment: Text.AlignVCenter // 垂直居中 + } + // 内容文字 + Text_ { + id: textMsg + anchors.top: textTitle.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: size_.smallSpacing + color: theme.subTextColor + font.pixelSize: size_.smallText + wrapMode: TextEdit.Wrap // 尽量在单词边界处换行 + maximumLineCount: 2 // 限制显示两行 + height: text=="" ? 0:undefined // 无文字时高为0,有文字时自动高度 + + } + } + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + btnHoverBg.visible = true + } + onExited: { + btnHoverBg.visible = false + } + function mouseClicked() { + timer.stop() // 停止计时器 + if(typeof onHided === "function") + onHided() // 调用关闭函数 + } + // 单击隐藏弹窗 + onClicked: mouseClicked() + // 双击弹出主窗,隐藏弹窗 + onDoubleClicked: { + qmlapp.mainWin.setVisibility(true) // 主窗可见 + mouseClicked() + } + } + // 边缘阴影 + layer.enabled: shadowWidth>0 + layer.effect: DropShadow { + transparentBorder: true + color: theme.coverColor4 + samples: shadowWidth + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Popup_/PopupManager.qml b/UmiOCR-data/qt_res/qml/Popup_/PopupManager.qml new file mode 100644 index 0000000..9a73807 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Popup_/PopupManager.qml @@ -0,0 +1,73 @@ +// ========================================= +// =============== 弹窗管理器 =============== +// ========================================= + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtGraphicalEffects 1.15 // 阴影 + +import "../Widgets" + +Item { + property string gYesText: qsTr("确认") // 全局确定文本 + property string gNoText: qsTr("取消") // 全局取消文本 + + // ========================= 【对外接口】 ========================= + + // 显示简单通知,无需确认,计时自动消失 + function simple(title, msg, showType="default") { + messageSimple.show(title, msg, showType) + } + + // 显示带确认的通知弹窗 + // type可选: ""默认, "warning"警告, "error"错误 + function message(title, msg, type="") { + messageWin.showMessage(title, msg, type) + } + + // 带“不再提示”的通知弹窗 + // mid: 唯一标识一个提示 + function messageMemory(mid, title, msg, type="") { + messageWin.showMessageMemory(mid, title, msg, type) + } + + // 双选项对话窗(确定|取消)。需要传入回调函数,返回true/false + // argd: {"yesText":"确定", "noText": "取消"} + function dialog(title, msg, callback, type="", argd={}) { + let yesText=gYesText, noText=gNoText + if(argd.hasOwnProperty("yesText")) + yesText = argd.yesText + if(argd.hasOwnProperty("noText")) + noText = argd.noText + messageWin.showDialog(title, msg, callback, yesText, noText, type) + } + + // 双选项对话窗,带倒计时,结束后调用确定按钮 + // argd: {"yesText":"确定", "noText": "取消", "time": 10} + function dialogCountdown(title, msg, callback, type="", argd={}) { + let yesText=gYesText, noText=gNoText, time=10000 + if(argd.hasOwnProperty("yesText")) + yesText = argd.yesText + if(argd.hasOwnProperty("noText")) + noText = argd.noText + if(argd.hasOwnProperty("time")) + time = argd.time + messageWin.showDialogCountdown(title, msg, callback, yesText, noText, type, time) + } + + // 展示遮罩层 + function showMask(msg="", id="") { + maskLayer.showMask(msg, id) + } + + // 隐藏指定id的遮罩层 + function hideMask(id="") { + maskLayer.hideMask(id) + } + + // ========================= 【内部】 ========================= + + MessageBoxWin{ id: messageWin } + MessageSimple { id: messageSimple } + MaskLayer{ id: maskLayer } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/TabBar_/BarManager.qml b/UmiOCR-data/qt_res/qml/TabBar_/BarManager.qml new file mode 100644 index 0000000..3ac9e15 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabBar_/BarManager.qml @@ -0,0 +1,105 @@ +// ====================================================== +// =============== 标签栏的复制器的逻辑管理 =============== +// ====================================================== + +/* 页面表现(布局、动画)应该在继承的子类中编写 + + + delegate: // 子类中填入按钮组件,属性必须包含: + title: title_ // 标题 + checked: checked_ // 是否选中 + width: 宽 + height: 高 +*/ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +Repeater { + + // ========================= 【属性与变量】 ========================= + + model: ListModel{} // 标签元素列表,初始为空 + property bool isLock: false + + + Component.onCompleted: { + qmlapp.tab.bar = this // 逆向连接引用 + } + // ========================= 【增删改查】 ========================= + + // 增: 在 index 处,插入一个标题为 title 的标签。 + function addTab(index, title){ // index=-1 代表尾部插入 + if(index<0) index=model.count // 尾部添加 + else if(index>model.count){ + console.error("【Error】添加标签失败:下标"+index+"超出范围"+model.count+"!") + return + } + + model.insert(index, { + "title_": title, + "checked_": false + }) + } + + // 删: 在 index 处,删除该标签。 + function delTab(index){ + if(!isIndex(index, "【Error】删除标签失败:")) + return + model.remove(index) // 删除按钮 + } + + // 改: 在 index 处,重设标签的 title 。 + function changeTab(index, title){ + if(!isIndex(index, "【Error】重命名标签失败:")) + return + model.set(index, {"title_": title}) + } + + // 改: 选中 index 标签。 + function showTab(index){ + if(!isIndex(index, "【Error】选中标签失败:")) + return + for(let i=0; i=model.count){ + if(msg) + console.error(msg+"下标"+index+"超出范围"+(model.count-1)+"!") + return false + } + return true + } + + // ========================= 【创建和删除事件】 ========================= + + // 创建新标签时 + onItemAdded: { + resetIndex() // 重设序号 + } + + onItemRemoved: { + resetIndex() // 重设序号 + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/TabBar_/HTabBar.qml b/UmiOCR-data/qt_res/qml/TabBar_/HTabBar.qml new file mode 100644 index 0000000..8acfee4 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabBar_/HTabBar.qml @@ -0,0 +1,256 @@ +// ========================================= +// =============== 水平标签栏 =============== +// ========================================= + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import "../Widgets" + +RowLayout { + id: hTabBarLayout + anchors.fill: parent + spacing: 0 + + // 标签栏控制(左,置顶按钮) + Item { + width: size_.hTabBarHeight + Layout.fillHeight: true + // 图钉按钮 + Button { + checkable: true + checked: mainWindowRoot.isMainWindowTop + onCheckedChanged: { // 双向绑定锁定标记 + mainWindowRoot.isMainWindowTop = checked + qmlapp.globalConfigs.setValue("window.isMainWindowTop", checked, true) + } + anchors.fill: parent + anchors.margins: 4 + + contentItem: Icon_ { + icon: "pin" + anchors.fill: parent + color: parent.checked ? theme.bgColor : theme.textColor + } + + background: Rectangle { + anchors.fill: parent + radius: size_.btnRadius + color: parent.checked ? theme.coverColor4 : ( + parent.hovered ? theme.coverColor2 : "#00000000" + ) + } + + ToolTip_ { + visible: parent.hovered + text: qsTr("窗口置顶") + } + } + } + + // 标签栏本体(中) + Rectangle { + id: hTabBarMain + Layout.fillWidth: true + Layout.fillHeight: true + color: "#00000000" + + property int tabWidth: 200 // 标签当前宽度 + + // 方法:重设标签按钮宽度 + function resetTabBtnWidth() { + let w = hTabBarMain.width + if(!qmlapp.tab.barIsLock) w -= tabBarControl.width // 无锁定时,减去+按钮宽度 + w = w / barManager.model.count + tabWidth = Math.min(w, size_.line * 8) + } + onWidthChanged: resetTabBtnWidth() // 监听标签栏总宽度变化 + // 监听改变锁定,重设宽度 + property bool isLock: qmlapp.tab.barIsLock + onIsLockChanged: { + hTabBarMain.resetTabBtnWidth() + } + + MouseArea { // 点击标签栏空余位置,都是添加新标签 + anchors.fill: parent + onClicked: { + if(!qmlapp.tab.barIsLock) + qmlapp.tab.addNavi() // 添加导航页 + } + } + + // Rectangle { // 标签按钮下方的阴影 + // anchors.bottom: parent.bottom + // width: parent.width + // height: size_.hTabBarHeight * 0.5 + // gradient: Gradient { + // GradientStop { position: 0.0; color: "#00000000" } + // GradientStop { position: 1.0; color: theme.coverColor2 } + // } + // } + + Rectangle { // 拖拽时的位置指示器 + id: dragIndicator + visible: false + width: parent.tabWidth + height: size_.hTabBarHeight + gradient: Gradient { // 水平渐变 + orientation: Gradient.Horizontal + GradientStop { position: 1.0; color: "#00000000" } + GradientStop { position: 0.0; color: theme.coverColor3 } + } + } + + // 水平标签栏行布局 + Row { + id: hTabBarMainRow + spacing: -1 // 给负的间隔,是为了让选中标签能覆盖左右两边标签的竖线 + + // ===== 标签按钮组 ===== + BarManager { + id: barManager + // 标签元素模板 + delegate: TabButton_ { + title: title_ // 标题 + checked: checked_ // 初始时是否选中 + index: index_ // 初始位置 + width: hTabBarMain.tabWidth + } + + // 事件:创建新标签时(与父类的槽同时生效) + onItemAdded: { + // 链接表现相关的槽函数 + item.dragStart.connect(dragStart) + item.dragFinish.connect(dragFinish) + item.dragMoving.connect(dragMoving) + } + + // 事件:按钮数量变化 + onCountChanged: hTabBarMain.resetTabBtnWidth() + + // ========================= 【拖拽相关】 ========================= + + property var intervalList: [] // 记录按钮位置区间的列表 + property var originalPosList: [] // 记录按钮初始位置的列表 + property int originalX // 记录本轮拖拽前,被拖拽按钮原本的位置 + function dragStart(index){ // 方法:开始拖拽 + // 重新记录当前所有按钮的位置 + originalX = itemAt(index).x + intervalList = [-Infinity] // 下限:负无穷 + originalPosList = [itemAt(0).x] + for(let i=1, c=model.count; i < c; i++){ // 按钮位置区间 + const it = itemAt(i) + intervalList.push(it.x) + originalPosList.push(it.x) + } + intervalList.push(Infinity) // 上限:负无穷 + dragIndicator.visible = true + + } + function btnDragIndex(index){ // 函数:返回当前index应该所处的序号 + const dragItem = itemAt(index) + const x = dragItem.x + Math.round(dragItem.width/2) // 被拖动按钮的中心位置 + let go = 0 // 应该拖放到的位置 + for(const c=intervalList.length-1; go < c; go++){ + if(x >= intervalList[go] && x <= intervalList[go+1]){ + break; + } + } + return go; + } + function dragMoving(index, x){ // 方法:拖拽移动 + let go = btnDragIndex(index) // 应该拖放到的序号 + dragIndicator.x = originalPosList[go] + } + function dragFinish(index){ // 方法:结束拖拽 + dragIndicator.visible = false + let go = btnDragIndex(index) // 应该拖放到的序号 + if(index !== go){ // 需要移动 + // model.move(index, go, 1) + qmlapp.tab.moveTabPage(index, go) + } else { // 无需移动,则回到原位 + itemAt(index).x = originalX + } + resetIndex() + } + } + + // 元素:控制按钮 + Rectangle{ + id: tabBarControl + color: "#00000000" + width: size_.hTabBarHeight + height: size_.hTabBarHeight + visible: !qmlapp.tab.barIsLock + + // 添加“+”按钮 + IconButton { + anchors.fill: parent + anchors.margins: 4 + icon_: "add" + color: theme.textColor + onClicked: { + qmlapp.tab.addNavi() // 添加导航页 + } + } + } + + // 动画 + add: Transition { // 添加子项 + enabled: qmlapp.enabledEffect + NumberAnimation { + properties: "opacity, scale" // 透明度和大小从小到大 + from: 0; to: 1.0 + easing.type: Easing.OutBack // 缓动:超出反弹 + duration: 300 + } + } + move: Transition { // 移动子项 + enabled: qmlapp.enabledEffect + NumberAnimation { + properties: "x,y" + easing.type: Easing.OutBack + duration: 300 + } + } + } + } + + // 标签栏控制(右,锁定按钮) + Item{ + width: size_.hTabBarHeight + Layout.fillHeight: true + + // 锁定“🔒︎”按钮 + Button { + checkable: true + checked: qmlapp.tab.barIsLock + onCheckedChanged: { // 双向绑定锁定标记 + qmlapp.tab.barIsLock = checked + qmlapp.globalConfigs.setValue("window.barIsLock", checked, true) + } + anchors.fill: parent + anchors.margins: 4 + + contentItem: Icon_ { + icon: "lock" + anchors.fill: parent + color: parent.checked ? theme.bgColor : theme.textColor + } + + background: Rectangle { + anchors.fill: parent + radius: size_.btnRadius + color: parent.checked ? theme.coverColor4 : ( + parent.hovered ? theme.coverColor2 : "#00000000" + ) + } + + ToolTip_ { + visible: parent.hovered + text: qsTr("锁定标签栏") + } + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/TabBar_/TabButton_.qml b/UmiOCR-data/qt_res/qml/TabBar_/TabButton_.qml new file mode 100644 index 0000000..3c3dec4 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabBar_/TabButton_.qml @@ -0,0 +1,171 @@ +// ================================================== +// =============== 水平标签栏的标签按钮 =============== +// ================================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtGraphicalEffects 1.15 // 阴影 + +import "../Widgets" + +Button { + id: btn + + // 设定值 + property string title: "Unknown TabBtn" // 显示的标题 + property int index: -1 // 在标签栏中的序号 + + // 默认值 + height: size_.hTabBarHeight + checkable: false // 手动控制 + z: checked? 10 : 0 // 选中模式下弹到顶层 + // 信号 + signal dragStart(int index) // 开始拖拽的信号 + signal dragFinish(int index) // 结束拖拽的信号 + signal dragMoving(int index, int x) // 拖拽移动的信号 + + // 按钮前景 + contentItem: Item { + anchors.fill: parent + + RowLayout { + anchors.fill: parent + + // TODO: 图标 + Item { + width: size_.line*0.2 // 先占位 + height: btn.height // 适应整个按钮的高度 + } + + // 标题 + Text_ { + text: title // 外部传入的title + + elide: Text.ElideRight // 隐藏超出宽度 + Layout.fillWidth: true // 填充宽度 + height: btn.height // 适应整个按钮的高度 + color: (btn.hovered || btn.checked)?theme.textColor:theme.subTextColor + font.bold: btn.checked + } + + // 关闭按钮 + IconButton { + // 未锁定,且主按钮悬停或选中时才显示 + visible: !qmlapp.tab.barIsLock && (btn.hovered || btn.checked) + Layout.alignment: Qt.AlignRight + Layout.rightMargin: size_.hTabBarHeight * 0.2 + + property real size: size_.hTabBarHeight * 0.7 + implicitWidth: size + implicitHeight: size + bgColor_: "#00000000" + + icon_: "no" + + onClicked: { + qmlapp.tab.closeTabPage(index) + } + } + } + } + + // 按钮背景 + background: Rectangle { + anchors.fill: parent + + color: parent.checked ? theme.bgColor : ( + parent.hovered ? theme.coverColor1 : "#00000000" + ) + + // 侧边小条 + Rectangle{ + visible: !parent.parent.checked + height: size_.line + width: 1 + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + color: theme.coverColor4 + } + + // 边缘阴影 + layer.enabled: parent.checked + layer.effect: DropShadow { + transparentBorder: true + color: theme.coverColor3 + samples: size_.hTabBarHeight + } + + // 点击和拖拽处理 + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.MiddleButton + + // 拖拽 + drag.target: qmlapp.tab.barIsLock ? undefined : parent.parent // 动态启用、禁用拖拽 + drag.axis: Drag.XAxis // 只能沿X轴 + drag.threshold: 50 // 起始阈值 + property bool dragActive: drag.active // 动态记录拖拽状态 + property int dragX: parent.parent.x // 动态记录拖拽时整体的位置 + + onPressed: { // 左键按下,切换焦点 + if(mouse.button === Qt.LeftButton) { + qmlapp.tab.showTabPage(index) + } + } + onClicked: { // 中键点击,删除标签 + if(mouse.button === Qt.MiddleButton && !qmlapp.tab.barIsLock) { + qmlapp.tab.closeTabPage(index) + } + } + onDragActiveChanged: { + if(drag.active) { // 拖拽开始 + parent.opacity = 0.6 + parent.parent.y += parent.parent.height / 2 + dragStart(index) + } else { // 拖拽结束 + parent.opacity = 1 + parent.parent.y -= parent.parent.height / 2 + dragFinish(index) + } + } + onDragXChanged: { + if(drag.active) { + dragMoving(index, dragX) + } + } + } + } + + + // 选中时的放大动画 + property bool enabledAni: false // true是允许动画 + property bool runAni: false + Timer { // 计时器,保证初始化的一段时间内不允许动画 + running: true + interval: 300 + onTriggered: enabledAni=true + } + onCheckedChanged: { + if(enabledAni) runAni = checked + } + SequentialAnimation{ // 串行动画 + running: qmlapp.enabledEffect && runAni + // 动画1:放大 + NumberAnimation{ + target: btn + property: "scale" + to: 1.2 + duration: 80 + easing.type: Easing.OutCubic + } + // 动画2:缩小 + NumberAnimation{ + target: btn + property: "scale" + to: 1 + duration: 150 + easing.type: Easing.InCubic + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/TabPages/About/About.qml b/UmiOCR-data/qt_res/qml/TabPages/About/About.qml new file mode 100644 index 0000000..59370af --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabPages/About/About.qml @@ -0,0 +1,137 @@ +// =================================================== +// =============== 功能页:关于/检查更新 =============== +// =================================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import ".." +import "../../Widgets" + +TabPage { + id: tabPage + + Panel{ + anchors.fill: parent + anchors.margins: size_.line + + ScrollView { + anchors.fill: parent + anchors.margins: size_.spacing + anchors.leftMargin: size_.line * 2 + anchors.rightMargin: size_.line * 2 + contentWidth: width + clip: true + + Column { + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: size_.spacing * 2 + spacing: size_.spacing + clip: true + + // ==================== 标题 ==================== + + Image { + anchors.left: parent.left + anchors.right: parent.right + height: size_.line * 10 + fillMode: Image.PreserveAspectFit + source: "../../../images/Umi-OCR_logo_full.png" + } + Text_ { + anchors.horizontalCenter: parent.horizontalCenter + text: qsTr("开源、免费的离线OCR软件") + } + SplitLine {} + + // ==================== 软件 / 项目信息 ==================== + + Text_ {text: qsTr("当前版本") + " • " + UmiAbout.version.string} + UrlList { + title: qsTr("项目链接") + urlList: [ + {text:qsTr("官方网站"), url:UmiAbout.url.home}, + {text:qsTr("插件拓展"), url:UmiAbout.url.plugins}, + {text:qsTr("问题反馈"), url:UmiAbout.url.issue}, + ] + } + UrlList { + title: qsTr("发布地址") + urlList: [ + {text:"Github", url:"https://github.com/hiroi-sora/Umi-OCR/releases/latest"}, + {text:"Source Forge", url:"https://sourceforge.net/projects/umi-ocr"}, + {text:"Lanzou (蓝奏云)", url:"https://hiroi-sora.lanzoul.com/s/umi-ocr"}, + ] + } + UrlList { + title: qsTr("许可协议") + urlList: [ + {text:UmiAbout.license.type, url:UmiAbout.license.url}, + ] + } + SplitLine {} + + // ==================== 开发者 ==================== + + UrlList { + title: qsTr("作者") + urlList: (() => { + let as = UmiAbout.authors, t = [] + for(const i in as) + t.push({ text: as[i].name, url: as[i].url, }) + return t + })() + } + Text_ {text: qsTr("译者")} + Repeater { + model: UmiAbout.localization + + UrlList { + anchors.leftMargin: size_.line * 2 + textSize: size_.smallText + title: UmiAbout.localization[index].language + urlList: (() => { + let as = UmiAbout.localization[index].translators, t = [] + for(const i in as) + t.push({ text: as[i].name, url: as[i].url, }) + return t + })() + } + } + SplitLine {} + + // ==================== 系统 / Debug信息 ==================== + + Row { + anchors.left: parent.left + Text_ { + font.pixelSize: size_.smallText + height: size_.smallLine + size_.spacing*2 + verticalAlignment: Text.AlignVCenter + text: qsTr("运行环境信息(如需请求协助,请提供给开发者)") + } + Button_ { + height: size_.smallLine + size_.spacing*2 + textSize: size_.smallText + text_: qsTr("复制") + textColor_: theme.yesColor + onClicked: { + const info = JSON.stringify(UmiAbout.app, null, 2) + qmlapp.utilsConnector.copyText(info) + } + } + } + Text_ { + anchors.left: parent.left + anchors.leftMargin: size_.line * 2 + wrapMode: Text.WordWrap + font.pixelSize: size_.smallText + text: JSON.stringify(UmiAbout.app, null, 2) + } + + Item {width: 1; height: size_.line} + } + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/TabPages/About/SplitLine.qml b/UmiOCR-data/qt_res/qml/TabPages/About/SplitLine.qml new file mode 100644 index 0000000..aa11715 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabPages/About/SplitLine.qml @@ -0,0 +1,17 @@ +// 分割线 + +import QtQuick 2.15 + +Item { + anchors.left: parent.left + anchors.right: parent.right + height: size_.line * 2 + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + height: 2 + color: theme.coverColor2 + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/TabPages/About/UrlButton.qml b/UmiOCR-data/qt_res/qml/TabPages/About/UrlButton.qml new file mode 100644 index 0000000..84698bf --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabPages/About/UrlButton.qml @@ -0,0 +1,35 @@ +// 网页链接按钮 + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import "../../Widgets" + +Button_ { + id: btn + property string url: "" + // toolTip: url + height: size_.text + size_.spacing * 2 + bgHoverColor_: theme.coverColor1 + + contentItem: Text_ { + text: btn.text_ + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: theme.specialTextColor + + // 手动绘制下划线,减少抖动现象。不使用 font.underline + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.bottomMargin: -2 + height: 1 + color: theme.specialTextColor + } + } + + onClicked: { + Qt.openUrlExternally(url) + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/TabPages/About/UrlList.qml b/UmiOCR-data/qt_res/qml/TabPages/About/UrlList.qml new file mode 100644 index 0000000..6c4d3df --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabPages/About/UrlList.qml @@ -0,0 +1,45 @@ +// 链接水平列表 + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import ".." +import "../../Widgets" + +Item { + id: uRoot + property string title: "" + property var urlList: [] // {"text", "url"} + property int textSize: size_.text + + anchors.left: parent.left + anchors.right: parent.right + height: flow.height + + Text_ { + id: lText + anchors.left: parent.left + anchors.top: parent.top + height: textSize + size_.spacing * 2 + verticalAlignment: Text.AlignVCenter + text: title + " • " + font.pixelSize: textSize + } + + Flow { + id: flow + anchors.left: lText.right + anchors.right: parent.right + spacing: 0 + Repeater { + model: urlList + + UrlButton { + height: textSize + size_.spacing * 2 + text_: urlList[index].text + url: urlList[index].url + textSize: uRoot.textSize + } + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/TabPages/BatchDOC/BatchDOC.qml b/UmiOCR-data/qt_res/qml/TabPages/BatchDOC/BatchDOC.qml new file mode 100644 index 0000000..d100fb9 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabPages/BatchDOC/BatchDOC.qml @@ -0,0 +1,312 @@ +// ================================================== +// =============== 功能页:批量文档处理 =============== +// ================================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import ".." +import "../../Widgets" +import "../../Widgets/ResultLayout" +import "../../Widgets/IgnoreArea" + +/* 文档参数: + path 路径 + pages 页数显示 + state 状态显示 + page_count 总页数 + range_start 范围开始 + range_end 范围结束 + is_encrypted 需要密码 + is_authenticate 密码正确 + password 密码 +*/ + +TabPage { + id: tabPage + + // ========================= 【逻辑】 ========================= + + property string msnID: "" // 当前任务ID + + // 异步加载一批文档路径 + function addDocs(paths) { + if(paths.length <= 0) return + const isRecurrence = configsComp.getValue("mission.recurrence") + qmlapp.asynFilesLoader.run(paths,"doc",isRecurrence,onAddDocs) + } + // 完毕后,将合法表格加入表格 + function onAddDocs(docs) { + if(docs.length <= 0) return + let encryptedCount = 0 + for(let i in docs) { + const info = docs[i] + filesTableView.add({ + // 显示:路径,状态,页范围 + path: info.path, + pages: `${info.page_count}`, // 如果范围为整本,只显示总页数。否则显示 起始-结束 + state: info.is_encrypted ? qsTr("加密") : "" , + // 数据 + page_count: info.page_count, + range_start: 1, + range_end: info.page_count, + is_encrypted: info.is_encrypted, // 有密码 + is_authenticate: !info.is_encrypted, // 已解密(密码正确) + password: "", + }) + if(info.is_encrypted) encryptedCount++ + } + if(encryptedCount > 0) { + qmlapp.popup.simple(qsTr("%1个加密文档").arg(encryptedCount), + qsTr("请点击文件名填写密码")) + } + } + + // 运行文档任务 + function docStart() { + const fileCount = filesTableView.rowCount + if(fileCount <= 0) { + ctrlPanel.stopFinished() + return + } + // 获取信息 + const docs = filesTableView.getColumnsValues([ + "path","range_start", "range_end", "page_count", "is_encrypted", "is_authenticate", "password"]) + // 第1次遍历:检查密码填写 + for(let i = 0; i < fileCount; i++) { + const d = docs[i] + if(d.is_encrypted && !d.is_authenticate) { + qmlapp.popup.message(qsTr("文档已加密"), qsTr("【%1】\n请点击文档名,设置密码").arg(d.path), "warning") + ctrlPanel.stopFinished() + return + } + } + // 第2次遍历:刷新信息 + let allPages = 0 // 页总数 + for(let i = 0; i < fileCount; i++) { + const d = docs[i] + allPages += d.range_end - d.range_start + 1 + filesTableView.setProperty(d.path, "state", qsTr("排队")) + } + // 若tabPanel面板的下标没有变化过,则切换到记录页 + if(tabPanel.indexChangeNum < 2) + tabPanel.currentIndex = 1 + // 任务进度 开始计时 + ctrlPanel.runFinished(allPages) + // 提交任务 + const argd = configsComp.getValueDict() + tabPage.callPy("msnDocs", docs, argd) + } + + // 停止文档任务 + function docStop() { + tabPage.callPy("msnStop") + // 刷新表格,清空未执行的任务的状态 + let msnLength = filesTableView.rowCount + for(let i = 0; i < msnLength; i++) { + const row = filesTableView.get(i) + const s = row.state + if(s.length > 0 && s[0] !== "√" && s[0] !== "×") { + filesTableView.setProperty(i, "state", "") + } + } + ctrlPanel.stopFinished() + } + + // 文件表格中单击文档 + function onClickDoc(index) { + if(ctrlPanel.state_ !== "stop") return + const info = filesTableView.get(index) + if(Object.keys(info).length > 0) + previewDoc.show(info) + } + + // 关闭页面 + function closePage() { + if(ctrlPanel.state_ !== "stop") { + const argd = { yesText: qsTr("依然关闭") } + const callback = (flag)=>{ + if(flag) { + docStop() + delPage() + } + } + qmlapp.popup.dialog("", qsTr("任务正在进行中。\n要结束任务并关闭页面吗?"), callback, "warning", argd) + } + else { + delPage() + } + } + + // ========================= 【python调用qml】 ========================= + + // 准备开始处理一个文档 + function onDocStart(path) { + // 刷新表格显示 + const d = filesTableView.get(path) + let state = `0/${d.range_end - d.range_start + 1}` + filesTableView.setProperty(path, "state", state) + } + + // 获取一个文档的一页的结果 + function onDocGet(path, page, res) { + // 刷新单个文档的信息 + const d = filesTableView.get(path) + const state = `${page - d.range_start + 1}/${d.range_end - d.range_start + 1}` + filesTableView.setProperty(path, "state", state) + // 提取文字,添加到结果表格 + const title = path2name(path) + res.title = `${title} - ${page}` + resultsTableView.addOcrResult(res) + ctrlPanel.msnStep(1) + } + + // 一个文档处理完毕。 isAll==true 时所有文档处理完毕。 + function onDocEnd(path, msg, isAll) { + const errTitle = qsTr("文档识别异常") + // 成功结束 + if(msg.startsWith("[Success]")) { + filesTableView.setProperty(path, "state", "√") + msg = "" + } + // 单个文档任务失败,总体未结束 + else if(!isAll) { + filesTableView.setProperty(path, "state", "× "+ qsTr("失败")) + qmlapp.popup.simple(errTitle, msg) + } + // 所有文档处理完毕 + if(isAll) { + const simpleType = configsComp.getValue("other.simpleNotificationType") + qmlapp.popup.simple(qsTr("批量识别完成"), "", simpleType) + if(msg) // 如果有异常,则弹窗 + qmlapp.popup.message(errTitle, msg, "error") + ctrlPanel.stopFinished() + // 任务完成后续操作 + qmlapp.globalConfigs.utilsDicts.postTaskHardwareCtrl( + configsComp.getValue("postTaskActions.system") + ) + } + } + + // 路径转文件名 + function path2name(path) { + const parts = path.split("/") + return parts[parts.length - 1] + } + + // ========================= 【布局】 ========================= + + // 配置 + configsComp: BatchDOCConfigs {} + + // 主区域:可切换双栏面板 + DoubleSwitchableLayout { + id: doubleLayout + saveKey: "BatchDOC_1" + anchors.fill: parent + + // 面板A:控制板+文件表格 + itemA: Panel { + anchors.fill: parent + + // 上方控制板 + MissionCtrlPanel { + id: ctrlPanel + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: size_.spacing + height: size_.line * 2 + + onRunClicked: tabPage.docStart() + onPauseClicked: { + tabPage.callPy("msnPause") + pauseFinished() + } + onResumeClicked: { + tabPage.callPy("msnResume") + resumeFinished() + } + onStopClicked: tabPage.docStop() + } + + // 下方文件表格 + FilesTableView { + id: filesTableView + anchors.top: ctrlPanel.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: size_.spacing + anchors.topMargin: size_.smallSpacing + headers: [ + {key: "path", title: qsTr("文档"), left: true, display: path2name, + btn: true, onClicked:onClickDoc}, + {key: "state", title: qsTr("状态"), btn: true, onClicked:onClickDoc}, + {key: "pages", title: qsTr("页数"), btn: true, onClicked:onClickDoc}, + ] + openBtnText: qsTr("打开文档") + clearBtnText: qsTr("清空") + defaultTips: qsTr("拖入文档或文件夹") + fileDialogTitle: qsTr("请选择文档") + fileDialogNameFilters: [qsTr("文档")+" (*.pdf *.xps *.epub *.mobi *.fb2 *.cbz)"] + isLock: ctrlPanel.state_ !== "stop" + onAddPaths: { + tabPage.addDocs(paths) + } + } + } + // 面板B:文字输出 & 设置 + itemB: Panel { + anchors.fill: parent + + // 配置项控制板 + TabPanel { + id: tabPanel + anchors.fill: parent + anchors.margins: size_.spacing + isMenuTop: doubleLayout.isRow // 左右布局时,菜单在顶部;上下布局时菜单在底部 + menuHeight: size_.line * 2 + + // 结果面板 + ResultsTableView { + id: resultsTableView + anchors.fill: parent + visible: false + } + + tabsModel: [ + { + "key": "configs", + "title": qsTr("设置"), + "component": configsComp.panelComponent, + }, + { + "key": "ocrResult", + "title": qsTr("记录"), + "component": resultsTableView, + }, + ] + } + } + } + + // 鼠标拖入文档 + DropArea_ { + id: "addDocsDropArea" + anchors.fill: parent + callback: tabPage.addDocs + } + + // 预览面板 + PreviewDoc { + id: previewDoc + anchors.fill: parent + configsComp: tabPage.configsComp + updateInfo: (path, info) => { + let infoA = filesTableView.get(path) + Object.assign(infoA, info) + filesTableView.set(path, infoA) + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/TabPages/BatchDOC/BatchDOCConfigs.qml b/UmiOCR-data/qt_res/qml/TabPages/BatchDOC/BatchDOCConfigs.qml new file mode 100644 index 0000000..a4af160 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabPages/BatchDOC/BatchDOCConfigs.qml @@ -0,0 +1,149 @@ +// ============================================== +// =============== 批量PDF的配置项 =============== +// ============================================== + +import QtQuick 2.15 +import "../../Configs" + +Configs { + category_: "BatchPDF" + signal clickIgnoreArea() // 打开忽略区域 + + configDict: { + // OCR参数 + "ocr": qmlapp.globalConfigs.ocrManager.deploy(this, "ocr"), + + // 后处理 + "tbpu": { + "title": qsTr("OCR文本后处理"), + "type": "group", + + "parser": qmlapp.globalConfigs.utilsDicts.getTbpuParser(), + "btns": { + "title": "👈"+qsTr("点击表格,可设置更多内容"), + "btnsList": [], + }, + "ignoreArea": { + "type": "var", + "save": false, + }, + "ignoreRangeStart": { // 忽略区域范围 + "default": 1, + "save": false, + }, + "ignoreRangeEnd": { + "default": -1, + "save": false, + }, + }, + + // 文档参数 + "doc": { + "title": qsTr("文档处理"), + "type": "group", + + "extractionMode": { + "title": qsTr("内容提取模式"), + "toolTip": qsTr("若一页文档既存在图片又存在文本,如何进行处理"), + "optionsList": [ + ["mixed", qsTr("混合OCR/原文本")], + ["fullPage", qsTr("整页强制OCR")], + ["imageOnly", qsTr("仅OCR图片")], + ["textOnly", qsTr("仅拷贝原有文本")], + ], + }, + }, + + // 任务参数 + "mission": { + "title": qsTr("批量任务"), + "type": "group", + + "recurrence": { + "title": qsTr("递归读取子文件夹"), + "toolTip": qsTr("导入文件夹时,导入子文件夹中全部文档"), + "default": false, + }, + "dirType": { + "title": qsTr("保存到"), + "optionsList": [ + ["source", qsTr("文档原目录")], + ["specify", qsTr("指定目录")], + ], + }, + "dir": { + "title": qsTr("指定目录"), + "toolTip": qsTr("必须先指定“保存到指定目录”才生效"), + "type": "file", + "selectExisting": true, // 选择现有 + "selectFolder": true, // 选择文件夹 + "dialogTitle": qsTr("OCR结果保存目录"), + }, + "fileNameFormat": { + "title": qsTr("文件名格式"), + "toolTip": qsTr("无需填写拓展名。支持插入以下占位符:\n%date 日期时间\n%name 原文档名\n%range 识别页数范围。只有识别页数小于总页数时才会显示。\n举例:[OCR]_%name%range_%date\n生成:[OCR]_文档A(p2-10)_20230901_1213.txt\n添加占位符可以避免旧文件被新文件覆盖。"), + "default": "[OCR]_%name%range_%date", + "advanced": true, + }, + "datetimeFormat": { + "title": qsTr("日期时间格式"), + "toolTip": qsTr("文件名中 %date 的日期格式。支持插入以下占位符:\n%Y 年、 %m 月、 %d 日、 %H 小时、 \n%M 分钟、 %S 秒 、 %unix 时间戳 \n举例:%Y年%m月%d日_%H-%M\n生成:2023年09月01日_12-13.txt"), + "default": "%Y%m%d_%H%M", + "advanced": true, + }, + + "filesType": { + "title": qsTr("保存文件类型"), + "type": "group", + "enabledFold": true, + "fold": false, + + "pdfLayered": { + "title": qsTr("layered.pdf 双层可搜索文档"), + "toolTip": qsTr("保留原有图片,叠加一层透明文字,可以搜索和复制"), + "default": true, + }, + "pdfOneLayer": { + "title": qsTr("text.pdf 单层纯文本文档"), + "toolTip": qsTr("创建空白PDF文档,只写入识别文字,不含图片"), + "default": false, + }, + "txt": { + "title": qsTr("txt 标准格式"), + "toolTip": qsTr("含识别文字和页数信息"), + "default": false, + }, + "txtPlain": { + "title": qsTr("p.txt 纯文字格式"), + "toolTip": qsTr("输出所有识别文字"), + "default": false, + }, + "csv": { + "title": qsTr("csv 表格文件(Excel)"), + "toolTip": qsTr("将页数信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。"), + "default": false, + }, + "jsonl": { + "title": qsTr("jsonl 原始信息"), + "toolTip": qsTr("每行为一条json数据,便于第三方程序读取操作"), + "default": false, + }, + }, + + "ignoreBlank": { + "title": qsTr("忽略空白页"), + "toolTip": qsTr("若某一页没有文字或识别失败,也不会输出错误提示信息"), + "default": true, + }, + }, + + // 任务完成后续操作 + "postTaskActions": qmlapp.globalConfigs.utilsDicts.getPostTaskActions(), + + "other": { + "title": qsTr("其它"), + "type": "group", + "simpleNotificationType": qmlapp.globalConfigs.utilsDicts.getSimpleNotificationType() + }, + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/TabPages/BatchDOC/PreviewDoc.qml b/UmiOCR-data/qt_res/qml/TabPages/BatchDOC/PreviewDoc.qml new file mode 100644 index 0000000..8c93c4b --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabPages/BatchDOC/PreviewDoc.qml @@ -0,0 +1,431 @@ +// =========================================== +// =============== 文档预览面板 =============== +// =========================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import DocPreviewConnector 1.0 + +import "../../Widgets" +import "../../Widgets/IgnoreArea" + +ModalLayer { + id: pRoot + property var updateInfo // 更新信息函数 + property var configsComp: undefined // 设置组件 + + property string previewPath: "" + property string password: "" + property bool isEncrypted: false // 已加密 + property bool isAuthenticate: false // 密码正确 + property int previewPage: -1 + property int pageCount: -1 + property int rangeStart: -1 + property int rangeEnd: -1 + property int ignoreRangeStart: 1 + property int ignoreRangeEnd: -1 + property bool previewOCR: false // 是否预览OCR + property bool ocrRunning: false // 是否预览OCR正在执行 + + // 展示文档 + // info: path, page_count, range_start, range_end, is_encrypted, password, is_authenticate + function show(info) { + imgViewer.clear() + visible = true + previewPath = info.path + pageCount = info.page_count + previewPage = info.range_start + rangeStart = info.range_start + rangeEnd = info.range_end + password = info.password + isEncrypted = info.is_encrypted + isAuthenticate = info.is_authenticate + // 读取忽略区域设置 + let initArea = configsComp.getValue("tbpu.ignoreArea") + if(initArea && initArea.length>0) { + // 读取设置,反格式化 + let ig1 = [] + for(let i=0,l=initArea.length; i pageCount) rangeStart = pageCount + if(rangeEnd < rangeStart) rangeEnd = rangeStart + if(rangeEnd > pageCount) rangeEnd = pageCount + let pages_ = `${pageCount}` // 如果范围为整本,只显示总页数。否则显示 起始-结束 + if(rangeEnd - rangeStart + 1 < pageCount) + pages_ = `${rangeStart}-${rangeEnd}` + if(updateInfo) { + updateInfo(previewPath, { + pages: pages_, + state: isAuthenticate ? "" : qsTr("加密"), + range_start: rangeStart, + range_end: rangeEnd, + password: password, + is_authenticate: isAuthenticate, + }) + } + // 更新忽略区域 + if(imgViewer.ig1Boxes.length > 0) { + // 格式化,存入设置 + let ig1 = [] + for(let i=0,l=imgViewer.ig1Boxes.length; i 0 && to <= pageCount) { + previewPage = to + toPreview() + } + } + Keys.onLeftPressed: changePage(-1, -1) // 上一页 + Keys.onUpPressed: changePage(-1, -1) + Keys.onRightPressed: changePage(-1, 1) // 下一页 + Keys.onDownPressed: changePage(-1, 1) + + // 预览一页文档 + function toPreview() { + if(!previewPath) return + if(previewPage < 1) previewPage = 1 + if(previewPage > pageCount) previewPage = pageCount + prevConn.preview(previewPath, previewPage, password) + if(previewOCR) { // 预览OCR + ocrRunning = true + const argd = configsComp.getValueDict() + argd["tbpu.parser"] = "None" // 去除排版解析 + prevConn.ocr(previewPath, previewPage, password, argd) + } + } + // 预览连接器 + DocPreviewConnector { + id: prevConn + // 图片渲染的回调 + onPreviewImg: function(imgID) { + const title = qsTr("打开文档失败") + if(imgID === "[Warning] is_encrypted") { + qmlapp.popup.simple(title, qsTr("请填写正确的密码")) + isAuthenticate = false + } + else if(imgID.startsWith("[Error]")) { + qmlapp.popup.message(title, imgID, "error") + } + else { + imgViewer.showImgID(imgID) + if(!isAuthenticate) { + qmlapp.popup.simple(qsTr("密码正确"), password) + isAuthenticate = true + } + } + } + // ocr预览的回调 + onPreviewOcr: function(info) { + let path = info[0], page = info[1], res = info[2] + if(res.code!=100&&res.code!=101) { // 遇到异常 + qmlapp.popup.message(qsTr("文档预览异常"), res.data, "error") + return + } + if(path != previewPath || page != previewPage) { + console.warn("文档OCR预览回调不匹配") + return + } + ocrRunning = false + imgViewer.showTextBoxes(res) + } + } + + property string ignoreTips: qsTr("忽略区域说明:\n右键拖拽,绘制矩形区域,包含在区域内的文字框将被忽略。可用于排除水印、页眉页脚。\n范围允许填写负数,表示倒数第x页。如-1表示最后一页,-2表示倒数第2页。\n忽略区域的设置对所有文档生效。") + + contentItem: DoubleRowLayout { + anchors.fill: parent + initSplitterX: size_.line * 13 + // 左:控制面板 + leftItem: Panel { + anchors.fill: parent + + ScrollView { + anchors.fill: parent + contentWidth: width // 内容宽度 + clip: true // 溢出隐藏 + + Column { + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: size_.spacing + spacing: size_.smallSpacing + // ===== 文件名 ===== + Text_ { + text: previewPath + anchors.left: parent.left + anchors.right: parent.right + wrapMode: TextEdit.WrapAnywhere // 任意换行 + maximumLineCount: 4 // 限制行数 + color: theme.subTextColor + font.pixelSize: size_.smallText + } + // ===== 密码 ===== + Row { + visible: isEncrypted && !isAuthenticate // 已加密,未填密码,才显示 + spacing: size_.spacing + height: size_.line + size_.spacing * 2 + Text_ { + color: theme.noColor + anchors.verticalCenter: parent.verticalCenter + text: qsTr("密码:") + } + TextField_ { + width: size_.line * 6 + anchors.top: parent.top + anchors.bottom: parent.bottom + text: password + onTextChanged: password = text + } + IconButton { + anchors.top: parent.top + anchors.bottom: parent.bottom + width: height + icon_: "yes" + onClicked: toPreview() + } + } + // ===== 控制项 ===== + Column { + visible: !isEncrypted || isAuthenticate + spacing: size_.smallSpacing + anchors.left: parent.left + anchors.right: parent.right + // ===== 页数 ===== + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + height: 1 + color: theme.coverColor4 + } + Row { + spacing: size_.line + height: size_.line + Text_ { + text: qsTr("预览页面") + anchors.verticalCenter: parent.verticalCenter + } + CheckButton { + anchors.verticalCenter: parent.verticalCenter + height: size_.line + enabledAnime: true + checked: previewOCR + textColor_: theme.subTextColor + onCheckedChanged: { + if(!previewOCR&&checked) { + previewOCR = true + toPreview() + } + else { + previewOCR = ocrRunning = false + } + } + text_: "OCR" + toolTip: qsTr("预览PDF时,是否预览OCR结果") + } + } + Row { + spacing: size_.spacing + height: size_.line + size_.smallSpacing * 2 + Button_ { + anchors.top: parent.top + anchors.bottom: parent.bottom + text_: "<" + onClicked: changePage(0, -1) + } + Button_ { + anchors.top: parent.top + anchors.bottom: parent.bottom + text_: ">" + onClicked: changePage(0, 1) + } + TextField_ { + width: size_.line * 3 + anchors.top: parent.top + anchors.bottom: parent.bottom + validator: IntValidator{bottom: 1; top: pageCount;} + text: previewPage + onTextChanged: changePage(text) + } + Text_ { + anchors.verticalCenter: parent.verticalCenter + text: "/ "+pageCount + } + } + // ===== OCR范围 ===== + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + height: 1 + color: theme.coverColor4 + } + Text_ { + text: qsTr("OCR页数") + } + Row { + height: size_.line + size_.smallSpacing * 2 + spacing: size_.spacing + Text_ { + font.pixelSize: size_.smallText + anchors.verticalCenter: parent.verticalCenter + text: qsTr("范围") + } + TextField_ { + width: size_.line * 3 + anchors.top: parent.top + anchors.bottom: parent.bottom + validator: IntValidator{bottom: -pageCount; top: pageCount;} + text: rangeStart + onTextChanged: { + if(text !== "" && text !== "-") rangeStart = text + } + } + Text_ { + anchors.verticalCenter: parent.verticalCenter + text: "-" + } + TextField_ { + width: size_.line * 3 + anchors.top: parent.top + anchors.bottom: parent.bottom + validator: IntValidator{bottom: -pageCount; top: pageCount;} + text: rangeEnd + onTextChanged: { + if(text !== "" && text !== "-") rangeEnd = text + } + } + } + // ===== 忽略区域 ===== + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + height: 1 + color: theme.coverColor4 + } + Text_ { + text: qsTr("忽略区域页数(全局)") + } + Row { + height: size_.line + size_.smallSpacing * 2 + spacing: size_.spacing + Text_ { + font.pixelSize: size_.smallText + anchors.verticalCenter: parent.verticalCenter + text: qsTr("范围") + } + TextField_ { + width: size_.line * 3 + anchors.top: parent.top + anchors.bottom: parent.bottom + validator: IntValidator {} + text: ignoreRangeStart + onTextChanged: { + if(text !== "" && text !== "-") ignoreRangeStart = text + } + } + Text_ { + anchors.verticalCenter: parent.verticalCenter + text: "-" + } + TextField_ { + width: size_.line * 3 + anchors.top: parent.top + anchors.bottom: parent.bottom + validator: IntValidator {} + text: ignoreRangeEnd + onTextChanged: { + if(text !== "" && text !== "-") ignoreRangeEnd = text + } + } + } + Row { + spacing: size_.spacing + height: size_.smallLine + size_.smallSpacing * 2 + Button_ { + anchors.verticalCenter: parent.verticalCenter + height: size_.smallLine + size_.smallSpacing + bgColor_: theme.coverColor1 + text_: qsTr("撤销") + onClicked: imgViewer.revokeIg() + textSize: size_.smallText + } + Button_ { + anchors.verticalCenter: parent.verticalCenter + height: size_.smallLine + size_.smallSpacing + bgColor_: theme.coverColor1 + textColor_: theme.noColor + text_: qsTr("清空") + onClicked: imgViewer.clearIg() + textSize: size_.smallText + } + } + Text_ { + text: ignoreTips + color: theme.subTextColor + font.pixelSize: size_.smallText + anchors.left: parent.left + anchors.right: parent.right + wrapMode: TextEdit.Wrap + } + } + } + } + } + // 右:图片查看面板 + rightItem: ImageWithIgnore { + id: imgViewer + anchors.fill: parent + // 加载中 动态图标 + Loading { + visible: ocrRunning + anchors.centerIn: parent + } + } + } +} diff --git a/UmiOCR-data/qt_res/qml/TabPages/BatchOCR/BatchOCR.qml b/UmiOCR-data/qt_res/qml/TabPages/BatchOCR/BatchOCR.qml new file mode 100644 index 0000000..bd88ee0 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabPages/BatchOCR/BatchOCR.qml @@ -0,0 +1,312 @@ +// ============================================== +// =============== 功能页:批量OCR =============== +// ============================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import ".." +import "../../Widgets" +import "../../Widgets/ResultLayout" +import "../../Widgets/IgnoreArea" + +TabPage { + id: tabPage + + // ========================= 【逻辑】 ========================= + + property int errorNum: 0 // 异常的任务个数 + property string msnID: "" // 当前任务ID + + Component.onCompleted: { + } + + // 异步加载一批图像路径 + function addImages(paths) { + if(ctrlPanel.state_ !== "stop") return + if(paths.length <= 0) return + const isRecurrence = configsComp.getValue("mission.recurrence") + qmlapp.asynFilesLoader.run(paths,"image",isRecurrence,onAddImages) + } + // 完毕后,将合法路径加入表格 + function onAddImages(paths) { + for(let i in paths) { + filesTableView.add({ path: paths[i], time: "", state: "" }) + } + } + + // 运行OCR + function ocrStart() { + let msnLength = filesTableView.rowCount + if(msnLength <= 0) { + ctrlPanel.stopFinished() + return + } + // 刷新表格 + for(let i = 0; i < msnLength; i++) { + filesTableView.set(i, { time: "", state: qsTr("排队") }) + } + // 刷新计数 + errorNum = 0 // 异常任务个数 + // 开始运行 + const paths = filesTableView.getColumnsValue("path") + const argd = configsComp.getValueDict() + msnID = tabPage.callPy("msnPaths", paths, argd) + // 若tabPanel面板的下标没有变化过,则切换到记录页 + if(tabPanel.indexChangeNum < 2) + tabPanel.currentIndex = 1 + ctrlPanel.runFinished(msnLength) + } + + // 停止OCR + function ocrStop() { + _ocrStop() + tabPage.callPy("msnStop") + ctrlPanel.stopFinished() + } + + function _ocrStop() { + msnID = "" // 清除任务ID + // 刷新表格,清空未执行的任务的状态 + let msnLength = filesTableView.rowCount + for(let i = 0; i < msnLength; i++) { + const row = filesTableView.get(i) + if(row.time === "") { + filesTableView.setProperty(i, "state", "") + } + } + } + + // 关闭页面 + function closePage() { + if(ctrlPanel.state_ !== "stop") { + const argd = { yesText: qsTr("依然关闭") } + const callback = (flag)=>{ + if(flag) { + ocrStop() + delPage() + } + } + qmlapp.popup.dialog("", qsTr("任务正在进行中。\n要结束任务并关闭页面吗?"), callback, "warning", argd) + } + else { + delPage() + } + } + + // 预览 + function msnPreview(path) { + const argd = configsComp.getValueDict() + tabPage.callPy("msnPreview", path, argd) + } + + // ========================= 【python调用qml】 ========================= + + // 准备开始一个任务 + function onOcrReady(path) { + // 刷新表格显示 + filesTableView.setProperty(path, "state", qsTr("处理")) + } + + // 获取一个OCR的返回值 + function onOcrGet(path, res) { + const time = res.time.toFixed(2) + let state = "" + switch(res.code){ + case 100: + state = "√ "+res.score.toFixed(2);break + case 101: + state = "√ ---- ";break + default: + state = "× "+res.code + errorNum++ // 异常任务数量+1 + break + } + // 刷新表格显示 + filesTableView.set(path, { "time": time, "state": state }) + // 提取文字,添加到结果表格 + res.title = res.fileName + resultsTableView.addOcrResult(res) + ctrlPanel.msnStep(1) // 任务计数器步进 + } + + // 任务队列完毕 + function onOcrEnd(msg, thisMsnID) { + // msg: [Success] [Warning] [Error] + if(msnID !== thisMsnID) { // 返回的任务ID不等于前端展示的任务ID,则不处理 + return + } + _ocrStop() + // 任务成功 + if(msg.startsWith("[Success]")) { + let errMsg = "" + if(errorNum > 0) { // 有异常任务 + errMsg = qsTr("%1 张图片识别失败!").arg(errorNum) + } + const simpleType = configsComp.getValue("other.simpleNotificationType") + qmlapp.popup.simple(qsTr("批量识别完成"), errMsg, simpleType) + // 任务完成后续操作 + qmlapp.globalConfigs.utilsDicts.postTaskHardwareCtrl( + configsComp.getValue("postTaskActions.system") + ) + } + // 任务失败 + else if(msg.startsWith("[Error]")) { + qmlapp.popup.message(qsTr("批量识别任务异常"), msg, "error") + } + ctrlPanel.stopFinished() + } + + // 预览 + function onPreview(path, res) { + ignoreArea.getPreview(res, path, "") + } + // 路径转文件名 + function path2name(path) { + const parts = path.split("/") + return parts[parts.length - 1] + } + // 文件表格中单击路径 + function onClickPath(index) { + let info = filesTableView.get(index) + let fileName = path2name(info.path) + let res = resultsTableView.getResult(fileName) + let data = undefined, text = undefined + if(res) { + if(res.source) + data = JSON.parse(res.source) + if(res.resText) + text = res.resText + } + previewImage.show(info.path, data, text) + } + + // ========================= 【布局】 ========================= + + // 配置 + configsComp: BatchOCRConfigs { + // 点按钮打开忽略区域 + onClickIgnoreArea: { + if(filesTableView.rowCount > 0) { + const path = filesTableView.get(0).path + console.log("打开路径", path) + ignoreArea.showPath(path) + } + else { + ignoreArea.show() + } + } + } + // 主区域:可切换双栏面板 + DoubleSwitchableLayout { + id: doubleLayout + saveKey: "BatchOCR_1" + anchors.fill: parent + + // 面板A:控制板+文件表格 + itemA: Panel { + anchors.fill: parent + + // 上方控制板 + MissionCtrlPanel { + id: ctrlPanel + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: size_.spacing + height: size_.line * 2 + + onRunClicked: tabPage.ocrStart() + onPauseClicked: { + tabPage.callPy("msnPause") + pauseFinished() + } + onResumeClicked: { + tabPage.callPy("msnResume") + resumeFinished() + } + onStopClicked: tabPage.ocrStop() + } + + // 下方文件表格 + FilesTableView { + id: filesTableView + anchors.top: ctrlPanel.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: size_.spacing + anchors.topMargin: size_.smallSpacing + headers: [ + {key: "path", title: qsTr("图片"), left: true, display:path2name, + btn: true, onClicked:onClickPath}, + {key: "time", title: qsTr("耗时"), }, + {key: "state", title: qsTr("状态"), }, + ] + openBtnText: qsTr("打开图片") + clearBtnText: qsTr("清空") + defaultTips: qsTr("拖入图片或文件夹") + fileDialogTitle: qsTr("请选择图片") + fileDialogNameFilters: [qsTr("图片")+" (*.jpg *.jpe *.jpeg *.jfif *.png *.webp *.bmp *.tif *.tiff)"] + isLock: ctrlPanel.state_ !== "stop" + onAddPaths: { + tabPage.addImages(paths) + } + } + } + // 面板B:文字输出 & 设置 + itemB: Panel { + anchors.fill: parent + + // 配置项控制板 + TabPanel { + id: tabPanel + anchors.fill: parent + anchors.margins: size_.spacing + isMenuTop: doubleLayout.isRow // 左右布局时,菜单在顶部;上下布局时菜单在底部 + menuHeight: size_.line * 2 + + // 结果面板 + ResultsTableView { + id: resultsTableView + anchors.fill: parent + visible: false + } + + tabsModel: [ + { + "key": "configs", + "title": qsTr("设置"), + "component": configsComp.panelComponent, + }, + { + "key": "ocrResult", + "title": qsTr("记录"), + "component": resultsTableView, + }, + ] + } + } + } + + // 鼠标拖入图片 + DropArea_ { + anchors.fill: parent + callback: tabPage.addImages + } + + // 预览面板 + PreviewImage { + id: previewImage + anchors.fill: parent + } + + // 忽略区域编辑器 + IgnoreArea { + id: ignoreArea + anchors.fill: parent + pathPreview: msnPreview + configsComp: tabPage.configsComp + configKey: "tbpu.ignoreArea" + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/TabPages/BatchOCR/BatchOCRConfigs.qml b/UmiOCR-data/qt_res/qml/TabPages/BatchOCR/BatchOCRConfigs.qml new file mode 100644 index 0000000..2908d97 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabPages/BatchOCR/BatchOCRConfigs.qml @@ -0,0 +1,136 @@ +// ============================================== +// =============== 批量OCR的配置项 =============== +// ============================================== + +import QtQuick 2.15 +import "../../Configs" + +Configs { + category_: "BatchOCR" + signal clickIgnoreArea() // 打开忽略区域 + + configDict: { + // OCR参数 + "ocr": qmlapp.globalConfigs.ocrManager.deploy(this, "ocr"), + + // 后处理 + "tbpu": { + "title": qsTr("OCR文本后处理"), + "type": "group", + + "parser": qmlapp.globalConfigs.utilsDicts.getTbpuParser(), + "btns": { + "title": qsTr("忽略区域"), + "btnsList": [ + {"text":qsTr("进入设置"), "onClicked": clickIgnoreArea}, + ], + }, + "ignoreArea": { + "type": "var", + "save": false, + }, + }, + + // 任务参数 + "mission": { + "title": qsTr("批量任务"), + "type": "group", + + "recurrence": { + "title": qsTr("递归读取子文件夹"), + "toolTip": qsTr("导入文件夹时,导入子文件夹中全部图片"), + "default": false, + }, + "dirType": { + "title": qsTr("保存到"), + "optionsList": [ + ["source", qsTr("图片原目录")], + ["specify", qsTr("指定目录")], + ], + }, + "dir": { + "title": qsTr("指定目录"), + "toolTip": qsTr("必须先指定“保存到指定目录”才生效"), + "type": "file", + "selectExisting": true, // 选择现有 + "selectFolder": true, // 选择文件夹 + "dialogTitle": qsTr("OCR结果保存目录"), + }, + "fileNameFormat": { + "title": qsTr("文件名格式"), + "toolTip": qsTr("无需填写拓展名。支持插入以下占位符:\n%date 日期时间\n%name 原文件夹名/文件名\n举例:[OCR]_%name_%date\n生成:[OCR]_我的图片_2023-09-01_12-13.txt\n添加占位符可以避免旧文件被新文件覆盖。"), + "default": "[OCR]_%name_%date", + "advanced": true, // 高级选项 + }, + "datetimeFormat": { + "title": qsTr("日期时间格式"), + "toolTip": qsTr("文件名中 %date 的日期格式。支持插入以下占位符:\n%Y 年、 %m 月、 %d 日、 %H 小时、 \n%M 分钟、 %S 秒 、 %unix 时间戳 \n举例:%Y年%m月%d日_%H-%M\n生成:2023年09月01日_12-13.txt"), + "default": "%Y%m%d_%H%M", + "advanced": true, // 高级选项 + }, + + "filesType": { + "title": qsTr("保存文件类型"), + "type": "group", + "enabledFold": true, + "fold": false, + + "txt": { + "title": qsTr("txt 标准格式"), + "toolTip": qsTr("含原图片文件名和识别文字"), + "default": true, + }, + "txtPlain": { + "title": qsTr("p.txt 纯文字格式"), + "toolTip": qsTr("仅输出识别文字,不含图片标题"), + "default": false, + }, + "txtIndividual": { + "title": qsTr("txt 单独文件"), + "toolTip": qsTr("对每张图片,生成同名txt文件,仅输出识别文字"), + "default": false, + }, + "md": { + "title": qsTr("md 图文混排"), + "toolTip": qsTr("Markdown图文混排格式,可用Markdown阅读器浏览文件"), + "default": false, + }, + "csv": { + "title": qsTr("csv 表格文件(Excel)"), + "toolTip": qsTr("将图片信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。"), + "default": false, + }, + "jsonl": { + "title": qsTr("jsonl 原始信息"), + "toolTip": qsTr("每行为一条json数据,便于第三方程序读取操作"), + "default": false, + }, + }, + + "ignoreBlank": { + "title": qsTr("输出忽略空白图片"), + "toolTip": qsTr("若图片没有文字或识别失败,也不会输出错误提示信息"), + "default": true, + }, + }, + + // 任务完成后续操作 + "postTaskActions": qmlapp.globalConfigs.utilsDicts.getPostTaskActions(), + + "other": { + "title": qsTr("其它"), + "type": "group", + + "simpleNotificationType": qmlapp.globalConfigs.utilsDicts.getSimpleNotificationType() + }, + } +} + + +/* +输出文件类型 + .txt 标准格式 + .txt 纯文本格式 + .txt 多个独立文件 + .jsonl 原始信息 +*/ \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/TabPages/BatchOCR/PreviewImage.qml b/UmiOCR-data/qt_res/qml/TabPages/BatchOCR/PreviewImage.qml new file mode 100644 index 0000000..f44c36a --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabPages/BatchOCR/PreviewImage.qml @@ -0,0 +1,145 @@ +// =========================================== +// =============== 图片预览面板 =============== +// =========================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import "../../Widgets" +import "../../Widgets/ImageViewer" + +ModalLayer { + id: pRoot + closeText: "" + + // 展示图片/文本 + function show(path, data, text) { + visible = true + imageText.showPath(path) + if(data) { + console.log("展示data", data) + imageText.showTextBoxes(data) + } + if(text) { + textEdit.text = text + } + else { + textEdit.text = "" + } + } + + + contentItem: DoubleRowLayout { + anchors.fill: parent + initSplitterX: 0.7 + leftItem: Panel { + anchors.fill: parent + clip: true + // 顶部栏 + Item { + id: leftTop + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: size_.smallSpacing + height: size_.line + size_.smallSpacing + // 靠右 + Row { + id: leftTopR + anchors.top: parent.top + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.rightMargin: size_.spacing + spacing: size_.smallSpacing + + // 显示文字 + CheckButton { + anchors.top: parent.top + anchors.bottom: parent.bottom + text_: qsTr("文字") + toolTip: qsTr("在图片上叠加显示识别文字\n可在全局设置中设为默认关闭") + checked: imageText.showOverlay + enabledAnime: true + onCheckedChanged: imageText.showOverlay = checked + } + + IconButtonBar { + anchors.top: parent.top + anchors.bottom: parent.bottom + btnList: [ + { + icon: "menu", + onClicked: imageText.popupMenu, + toolTip: tr("右键菜单"), + }, + { + icon: "save", + onClicked: imageText.saveImage, + toolTip: tr("保存图片"), + }, + { + icon: "open_image", + onClicked: imageText.openImage, + toolTip: tr("用默认应用打开图片"), + }, + { + icon: "full_screen", + onClicked: imageText.imageFullFit, + toolTip: tr("图片大小:适应窗口"), + }, + { + icon: "one_to_one", + onClicked: imageText.imageScaleAddSub, + toolTip: tr("图片大小:实际"), + }, + ] + } + // 百分比显示 + Text_ { + anchors.top: parent.top + anchors.bottom: parent.bottom + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignRight + text: (imageText.scale*100).toFixed(0) + "%" + color: theme.subTextColor + width: size_.line * 2.5 + } + } + } + // 图片预览区域 + ImageWithText { + id: imageText + anchors.top: leftTop.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: size_.spacing + anchors.topMargin: size_.smallSpacing + } + } + rightItem: Panel { + anchors.fill: parent + Rectangle { + anchors.fill: parent + anchors.margins: size_.spacing + color: theme.bgColor + border.width: 1 + border.color: theme.coverColor4 + + ScrollView { + id: textView + anchors.fill: parent + anchors.leftMargin: size_.spacing + anchors.rightMargin: size_.spacing + contentWidth: width // 内容宽度 + clip: true // 溢出隐藏 + + TextEdit_ { + id: textEdit + width: textView.width + } + } + } + } + } +} diff --git a/UmiOCR-data/qt_res/qml/TabPages/GlobalConfigsPage/FontPanel.qml b/UmiOCR-data/qt_res/qml/TabPages/GlobalConfigsPage/FontPanel.qml new file mode 100644 index 0000000..548adff --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabPages/GlobalConfigsPage/FontPanel.qml @@ -0,0 +1,228 @@ +// =========================================== +// =============== 字体修改面板 =============== +// =========================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import "../../Widgets" + +ModalLayer { + id: fRoot + property var fontsList: [] + + // 主要UI文字字体,内容可控,可以用裁切的ttf + property string fontFamily: "" + // 数据显示文字字体,内容不可控,用兼容性好的系统字体 + property string dataFontFamily: "" + // 不可加载的字体 + property var illegalFonts: ["", "Terminal", "System", "Small Fonts", "Script", "Roman", "MS Serif", "MS Sans Serif", "Modern", "Fixedsys"] + + function setFontFamily(f) { + fontFamily = f + qmlapp.globalConfigs.setValue("ui.fontFamily", f) + } + function setDataFontFamily(f) { + dataFontFamily = f + qmlapp.globalConfigs.setValue("ui.dataFontFamily", f) + } + + Component.onCompleted: { + // 将此组件的引用传入全局设置 + qmlapp.globalConfigs.fontPanel = this + fontFamily = qmlapp.globalConfigs.getValue("ui.fontFamily") + dataFontFamily = qmlapp.globalConfigs.getValue("ui.dataFontFamily") + } + + contentItem: Item { + id: content + anchors.fill: parent + } + Loader { + id: panelLoader + asynchronous: true + sourceComponent: com + active: fRoot.visible + } + Component { + id: com + DoubleRowLayout { + parent: content + anchors.fill: parent + initSplitterX: 0.5 + + Component.onCompleted: { + // 获取字体列表,过滤出以中文字符开头的字体 + let fList = Qt.fontFamilies() + let newList = fList.filter(function(str) { + return /^[\u4e00-\u9fa5]/.test(str); + }) + // 补充剩余字体 + for(let i in fList) { + if(illegalFonts.includes(fList[i])) + continue + if(!newList.includes(fList[i])) + newList.push(fList[i]) + } + // 将当前选中的移到最前面 + const i1 = newList.indexOf(dataFontFamily) + if (i1 > -1) { + newList.splice(i1, 1) + newList.unshift(dataFontFamily) + } + const i2 = newList.indexOf(fontFamily) + if (i2 > -1 && i2 !== i1) { + newList.splice(i2, 1) + newList.unshift(fontFamily) + } + fRoot.fontsList = newList + } + + leftItem: Panel { + anchors.fill: parent + clip: true + + Row { + id: leftTop + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: size_.spacing + anchors.topMargin: 0 + anchors.rightMargin: size_.spacing * 3 + spacing: size_.spacing + height: size_.line * 2 + + // Text_ { + // anchors.top: parent.top + // anchors.bottom: parent.bottom + // verticalAlignment: Text.AlignVCenter + // text: qsTr("设置为:") + // } + Text_ { + anchors.top: parent.top + anchors.bottom: parent.bottom + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + width: size_.line * 3 + text: qsTr("界面") + } + Text_ { + anchors.top: parent.top + anchors.bottom: parent.bottom + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + width: size_.line * 3 + text: qsTr("内容") + font.family: theme.dataFontFamily + } + } + Panel { + anchors.top: leftTop.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: size_.spacing + anchors.topMargin: 0 + color: theme.bgColor + + TableView { + id: leftTable + anchors.fill: parent + anchors.margins: size_.spacing + clip: true + model: fRoot.fontsList + contentWidth: width // 内容宽度 + rowSpacing: size_.spacing // 行间隔 + flickableDirection: Flickable.VerticalFlick // 只允许垂直滚动 + columnWidthProvider: ()=>leftTable.width + + delegate: Rectangle { + height: size_.line * 2 + implicitWidth: 100 + implicitHeight: height + width: leftTable.width + color: fontMouseArea.containsMouse?theme.coverColor2:"#00000000" + + Text_ { + text: modelData + anchors.fill: parent + anchors.leftMargin: size_.spacing + verticalAlignment: Text.AlignVCenter + font.family: modelData + } + MouseArea { + id: fontMouseArea + anchors.fill: parent + hoverEnabled: true + } + // 右边,内容字体 + IconButton { + id: btn2 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: size_.smallSpacing + color: theme.yesColor + bgColor_: theme.coverColor1 + width: size_.line * 3 + borderWidth: 1 + borderColor: theme.specialTextColor + bgHoverColor_: theme.coverColor3 + icon_: dataFontFamily===modelData?"yes":"" + onClicked: setDataFontFamily(modelData) + } + // 左边,界面字体 + IconButton { + id: btn1 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: btn2.left + anchors.margins: size_.smallSpacing + anchors.rightMargin: size_.spacing + color: theme.yesColor + bgColor_: theme.coverColor1 + width: size_.line * 3 + borderWidth: 1 + borderColor: theme.specialTextColor + bgHoverColor_: theme.coverColor3 + icon_: fontFamily===modelData?"yes":"" + onClicked: setFontFamily(modelData) + } + } + } + } + } + rightItem: Panel { + anchors.fill: parent + + Item { + anchors.fill: parent + anchors.margins: size_.spacing * 3 + + Column { + anchors.fill: parent + spacing: size_.line + + Text_ { + anchors.left: parent.left + anchors.right: parent.right + wrapMode: Text.Wrap + font.family: fontFamily + text: qsTr("界面字体:\n软件中大部分UI的字体。") + } + + Text_ { + anchors.left: parent.left + anchors.right: parent.right + wrapMode: Text.Wrap + font.family: dataFontFamily + text: qsTr("内容字体:\n识别结果内容的字体。") + } + + } + } + } + } + } + +} diff --git a/UmiOCR-data/qt_res/qml/TabPages/GlobalConfigsPage/GlobalConfigsPage.qml b/UmiOCR-data/qt_res/qml/TabPages/GlobalConfigsPage/GlobalConfigsPage.qml new file mode 100644 index 0000000..56011da --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabPages/GlobalConfigsPage/GlobalConfigsPage.qml @@ -0,0 +1,93 @@ +// ============================================== +// =============== 功能页:全局设置 =============== +// ============================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import ".." +import "../../Widgets" + +TabPage { + id: tabPage + property QtObject confComp: qmlapp.globalConfigs.panelComponent + property var groupList: [] + onShowPage: { + groupList = confComp.getGroupList() + } + + Component.onCompleted: { + const tips = qsTr("1. 关于快捷键、OCR语言等选项,请在各个功能页中进行设置。\n2. 勾选左下角的“高级”按钮,可以显示更多进阶选项。") + qmlapp.popup.messageMemory("globalConfigsKey", qsTr("小贴士"), tips) + } + + DoubleRowLayout { + anchors.fill: parent + initSplitterX: size_.line * 15 + + // 左面板:设置标题列表 + leftItem: Panel { + anchors.fill: parent + // 上:标题栏 + Item { + id: leftTop + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: leftBottom.top + anchors.margins: size_.spacing + ScrollView { + id: scrollView + anchors.fill: parent + anchors.margins: size_.spacing + clip: true + Column { + anchors.fill: parent + Repeater { + model: groupList + Button_ { + visible: !modelData.advanced || confComp.advanced + text_: modelData.title + width: scrollView.width + height: size_.line * 2.5 + onClicked: { + confComp.scrollToGroup(index) + } + } + } + } + } + } + // 下:控制按钮栏 + Item { + id: leftBottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: size_.line * 2 + anchors.margins: size_.spacing + } + } + + // 右面板:设置面板 + rightItem: Panel { + anchors.fill: parent + Item { + anchors.fill: parent + anchors.margins: size_.spacing + Component.onCompleted: { // 将全局设置UI的父级重定向过来 + // 就算本页面删除,全局UI也不会被删,只会丢失父级 + confComp.parent = this + confComp.ctrlBar.parent = leftBottom + confComp.ctrlBar.anchors.fill = leftBottom + } + } + } + } + + // 字体设置面板 + FontPanel { + anchors.fill: parent + z: 10 + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/TabPages/Navigation/Navigation.qml b/UmiOCR-data/qt_res/qml/TabPages/Navigation/Navigation.qml new file mode 100644 index 0000000..cc2cfcd --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabPages/Navigation/Navigation.qml @@ -0,0 +1,135 @@ +// ================================================ +// =============== 导航页(新标签页) =============== +// ================================================ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import ".." +import "../.." +import "../../Widgets" + +TabPage { + + // =============== 逻辑 =============== + + id: naviPage + // 页面信息存储 + ListModel { id: pageModel } + // 动态变化的简介文本 + property string introText: "" + // 初始简介(欢迎词) + property string welcomeText: `# `+qsTr("欢迎使用 Umi-OCR")+` + +## 👈 `+qsTr("请选择功能页")+` + + + + +`+qsTr("当前版本")+` • ${UmiAbout.version.string} + +`+qsTr("项目链接")+` • [`+qsTr("官方网站")+`](${UmiAbout.url.home}) [`+qsTr("插件拓展")+`](${UmiAbout.url.plugins}) [`+qsTr("问题反馈")+`](${UmiAbout.url.issue}) + +` + + // 初始化数据 + Component.onCompleted: initData() + function initData() { + introText = welcomeText + pageModel.clear() + const f = qmlapp.tab.infoList + // 遍历所有文件信息(排除第一项自己) + for(let i=1,c=f.length; i=0; i--){ + const info = infoList[i] + if(!info.url) { + info.url = `${info.key}/${info.key}.qml` + } + } + } + + // 创建一个页面的组件类 comp + function getComp(infoIndex) { + const info = infoList[infoIndex] + if(info.comp) return info.comp + const url = info.url + const comp = Qt.createComponent(url) + if (comp.status === Component.Ready) { // 加载成功 + infoList[infoIndex].comp = comp + return comp + } else{ // 加载失败 + if (comp.status === Component.Error) { // 加载失败,提取错误信息 + let str = comp.errorString() + const last = str.lastIndexOf(":") + if(last < 0) last = -1 + str = str.substring(last+1).replace("\n","") + console.error(`【Error】加载页面文件失败:【${url}】${str}`) + } + else{ + console.error(`【Error】加载页面文件异常:【${url}】`) + } + return undefined + } + } + + // 创建并返回一个 infoList[infoIndex] 页面。 + function newPage(infoIndex){ + const info = infoList[infoIndex] + // 实例化逻辑控制器 + let ctrlKey = "" + if(info.needController){ + ctrlKey = connector.addPage(info.key) + if(!ctrlKey){ + console.error("【Error】添加页面失败:组件["+info.key+"]创建控制器失败!") + return undefined + } + } + else { // 新增一个不带控制器的简单页 + ctrlKey = connector.addSimplePage(info.key) + } + // 检查组件 + let comp = getComp(infoIndex) + if(!comp){ + console.error("【Error】添加页面失败:组件["+info.key+"]的comp无法创建!") + return undefined + } + // 实例化页面,挂到巢下,写入自身参数 + const obj = comp.createObject(pagesNest, { + z: -1, visible: false, + ctrlKey: ctrlKey, // Python控制器key + connector: connector, // Python控制器对象 + }) + // 收集并返回页面对象信息 + const dic = { + obj: obj, + info: info, + infoIndex: infoIndex, + ctrlKey: ctrlKey + } + // 向控制器传入页面对象 + connector.setPageQmlObj(ctrlKey, obj) + return dic + } + + // 增: 在 pageList 的 index 处,插入一个 infoList[infoIndex] 页面。 + function addPage(index, infoIndex){ // index=-1 代表尾部插入 + // 列表添加 + const dic = newPage(infoIndex) + if(dic == undefined){ + return false + } + pageList.splice(index, 0, dic) // 列表添加 + return true + } + + // 增改: 在 pageList 的 index 处,删除该页面,改为 infoIndex 页。 + function changePage(index, infoIndex){ + const page = pageList[index] + // 删除旧页的python逻辑控制器 + const flag = connector.delPage(page.ctrlKey) + if(!flag){ + console.error("【Warning】删除页面失败:控制器["+page.ctrlKey+"]删除失败!") + // return false // 暂时不管控制器删除失败 + } + const dic = newPage(infoIndex) + if(dic == undefined){ + return false + } + page.obj.destroy() // 旧页对象删除 + pageList[index] = dic // 替换新页 + return true + } + + // 删: 在 pageList 的 index 处,发送关闭指令。 + function closePage(index){ + pageList[index].obj.closePage() + } + + // 删: 在 pageList 的 index 处,删除该页面。 + function delPage(index){ + const page = pageList[index] + // 删除python逻辑控制器 + const flag = connector.delPage(page.ctrlKey) + if(!flag){ + console.error("【Warning】删除页面失败:控制器["+page.ctrlKey+"]删除失败!") + // return false // 暂时不管控制器删除失败 + } + page.obj.destroy() // 页对象删除 + pageList.splice(index, 1) // 列表删除 + return true + } + + // 改: 展示 index 页。 + function showPage(index){ + // 遍历,将展示的页面设为可视状态,其他页面设为非可视状态 + for(let i in pageList){ + if(i==index){ + pageList[i].obj.z = 0 + pageList[i].obj.visible = true + pageList[i].obj.showPage() + }else{ + pageList[i].obj.z = -1 + pageList[i].obj.visible = false + } + } + } + + // 改: 将一个原本在 index 的页移到 go 处。 + function movePage(index, go){ + var x = pageList.splice(index, 1)[0] // 删除 + pageList.splice(go, 0, x) // 添加 + } + + // 查: 传入下标 index 列表 list 报错内容前缀 msg ,返回下标是否合法。 + function isIndex(index, list, msg=""){ + if(index<0 || index>=list.length){ + if(msg) + console.error(msg+"下标"+index+"超出范围"+(pageList.length-1)+"!") + return false + } + return true + } + + // ========================= 【辅助元素】 ========================= + + // 页巢,作为已生成的页组件对象的父级。可挂载到可视节点下来展示。 + Item { + id: pagesNest + anchors.fill: parent + } + property var pagesNest: pagesNest +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/TabPages/QRCode/QRCode.qml b/UmiOCR-data/qt_res/qml/TabPages/QRCode/QRCode.qml new file mode 100644 index 0000000..848eb5a --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabPages/QRCode/QRCode.qml @@ -0,0 +1,464 @@ +// ============================================== +// =============== 功能页:二维码 =============== +// ============================================== + +import QtQuick 2.15 + +import ".." +import "../../Widgets" +import "../../Widgets/ResultLayout" +import "../../Widgets/ImageViewer" + +TabPage { + id: tabPage + // 配置 + configsComp: QRCodeConfigs { + // 修改配置信号触发后,延迟一个事件循环,重新生成二维码图片 + onReBarcode: Qt.callLater(reWriteBarcode) + } + + // ========================= 【逻辑】 ========================= + + // 开始截图 + function screenshot() { + qmlapp.imageManager.screenshot(screenshotEnd) + } + // 截图完毕 + function screenshotEnd(clipID) { + popMainWindow() + if(!clipID) { + return + } + const configDict = configsComp.getValueDict() + tabPage.callPy("scanImgID", clipID, configDict) + qmlapp.tab.showTabPageObj(tabPage) // 切换标签页 + } + + // 开始粘贴 + function paste() { + popMainWindow() + const res = qmlapp.imageManager.getPaste() + if(res.error) { + qmlapp.popup.simple(qsTr("获取剪贴板异常"), res.error) + return + } + if(res.text) { + qmlapp.popup.simple(qsTr("剪贴板中为文本"), res.text) + return + } + qmlapp.tab.showTabPageObj(tabPage) // 切换标签页 + if(res.imgID) { // 图片 + imageText.showImgID(res.imgID) + const configDict = configsComp.getValueDict() + tabPage.callPy("scanImgID", res.imgID, configDict) + } + else if(res.paths) { // 地址 + scanPaths(res.paths) + } + } + + // 异步加载一批图像路径 + function scanPaths(paths) { + qmlapp.asynFilesLoader.run(paths,"image",false,onScanPaths) + } + // 完毕后,对合法路径进行扫码 + function onScanPaths(paths) { + if(!paths || paths.length < 1) { + qmlapp.popup.simple(qsTr("无有效图片"), "") + return + } + const configDict = configsComp.getValueDict() + const simpleType = configDict["other.simpleNotificationType"] + qmlapp.popup.simple(qsTr("导入%1条图片路径").arg(paths.length), "", simpleType) + imageText.showPath(paths[0]) + tabPage.callPy("scanPaths", paths, configDict) + } + + // 弹出主窗口 + function popMainWindow() { + // 若主窗口已经可见,则不处理 + if(qmlapp.mainWin.getVisibility()) + return + // 等一回合再弹,防止与收回截图窗口相冲突 + if(configsComp.getValue("action.popMainWindow")) { + Qt.callLater(()=>{ + qmlapp.mainWin.loadGeometry(false) + qmlapp.mainWin.setVisibility(true) + }) + } + } + + // 生成二维码 + function writeBarcode(text) { + if(!text || text.length===0) + return + setRunning(true) + const configDict = configsComp.getValueDict() + const format = configDict["writeBarcode.format"] + const w = configDict["writeBarcode.width"] + const h = configDict["writeBarcode.height"] + const quiet_zone = configDict["writeBarcode.quiet_zone"] + const ec_level = configDict["writeBarcode.ec_level"] + const imgID = tabPage.callPy("writeBarcode", text, format, w, h, quiet_zone, ec_level) + setRunning(false) + if(imgID.startsWith("[Error]") || imgID.startsWith("[Warning]")) { + if(imgID.startsWith("[Error] [")) { + const msg = qsTr("参数有误,或输入内容不合规定。请参照报错指示修改:") +"\n"+ imgID + qmlapp.popup.message(qsTr("生成二维码失败"), msg, "error") + } + else { + qmlapp.popup.message(qsTr("生成二维码失败"), imgID, "error") + } + return + } + imageText.showImgID(imgID) + } + // 立刻重新生成二维码图片 + function reWriteBarcode() { + writeBarcode(writeEdit.text) + } + + + // ========================= 【python调用qml】 ========================= + + // 获取一个扫码的返回值 + function onQRCodeGet(res, imgID="", imgPath="") { + // 添加到结果 + if(imgID) // 图片类型 + imageText.showImgID(imgID) + else if(imgPath) // 地址类型 + imageText.showPath(imgPath) + // 路径转文件名 + const parts = imgPath.split("/") + res.title = parts[parts.length - 1] + imageText.showTextBoxes(res) + const resText = resultsTableView.addOcrResult(res) + // 若tabPanel面板的下标没有变化过,则切换到记录页 + if(tabPanel.indexChangeNum < 2) + tabPanel.currentIndex = 1 + // 复制到剪贴板 + const copy = configsComp.getValue("action.copy") + if(copy && resText!="") + qmlapp.utilsConnector.copyText(resText) + // 弹出通知 + showSimple(res, resText, copy) + // 升起主窗口 + popMainWindow() + } + + // 任务完成后发送通知 + function showSimple(res, resText, isCopy) { + // 获取弹窗类型 + let simpleType = configsComp.getValue("other.simpleNotificationType") + if(simpleType==="default") { + simpleType = qmlapp.globalConfigs.getValue("window.simpleNotificationType") + } + const code = res.code + const time = res.time.toFixed(2) + let title = "" + resText = resText.replace(/\n/g, " ") // 换行符替换空格 + if(code === 100 || code === 101) { // 成功时,不发送内部弹窗 + if(simpleType==="inside" || simpleType==="onlyInside") + if(qmlapp.mainWin.getVisibility()) + return + } + if(code === 100) { + if(isCopy) title = qsTr("已复制到剪贴板") + else title = qsTr("识图完成") + } + else if(code === 101) { + title = qsTr("无文字") + resText = "" + } + else { + title = qsTr("识别失败") + } + title += ` - ${time}s` + qmlapp.popup.simple(title, resText, simpleType) + } + + // 设置运行状态 + function setRunning(flag) { + running = flag + } + + // ========================= 【事件管理】 ========================= + + Component.onCompleted: { + eventSub() // 订阅事件 + } + // 关闭页面 + function closePage() { + eventUnsub() + delPage() + } + // 订阅事件 + function eventSub() { + qmlapp.pubSub.subscribeGroup("<>", this, "screenshot", ctrlKey) + qmlapp.pubSub.subscribeGroup("<>", this, "paste", ctrlKey) + qmlapp.systemTray.addMenuItem("<>", qsTr("扫描二维码"), screenshot) + } + // 取消订阅事件 + function eventUnsub() { + qmlapp.systemTray.delMenuItem("<>") + qmlapp.pubSub.unsubscribeGroup(ctrlKey) + } + + // ========================= 【布局】 ========================= + property bool running: false + // 主区域:可切换双栏面板 + DoubleSwitchableLayout { + id: doubleLayout + saveKey: "QRCode_1" + anchors.fill: parent + + // 面板A:图像展示 + itemA: Panel { + anchors.fill: parent + clip: true + // 顶部控制栏 + Item { + id: dLeftTop + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: size_.spacing + height: size_.line * 1.5 + // 靠右 + Row { + id: dLeftTopR + anchors.top: parent.top + anchors.right: parent.right + anchors.bottom: parent.bottom + spacing: size_.smallSpacing + + IconButtonBar { + anchors.top: parent.top + anchors.bottom: parent.bottom + btnList: [ + { + icon: "menu", + onClicked: imageText.popupMenu, + toolTip: tr("右键菜单"), + }, + { + icon: "save", + onClicked: imageText.saveImage, + toolTip: tr("保存图片"), + }, + { + icon: "full_screen", + onClicked: imageText.imageFullFit, + toolTip: tr("图片大小:适应窗口"), + }, + { + icon: "one_to_one", + onClicked: imageText.imageScaleAddSub, + toolTip: tr("图片大小:实际"), + }, + ] + } + // 百分比显示 + Text_ { + anchors.top: parent.top + anchors.bottom: parent.bottom + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignRight + text: (imageText.scale*100).toFixed(0) + "%" + color: theme.subTextColor + width: size_.line * 2.5 + } + } + // 靠左 + Rectangle { // 背景 + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: dLeftTopL.width + color: theme.bgColor + Rectangle { + anchors.fill: parent + color: theme.coverColor1 + } + } + Row { + id: dLeftTopL + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + spacing: size_.smallSpacing + + IconButtonBar { + anchors.top: parent.top + anchors.bottom: parent.bottom + btnList: [ + { + icon: "screenshot", + onClicked: tabPage.screenshot, + color: theme.textColor, + bgColor: theme.bgColor, + text: tr("截图"), + toolTip: tr("屏幕截图"), + }, + { + icon: "paste", + onClicked: tabPage.paste, + color: theme.textColor, + bgColor: theme.bgColor, + text: tr("粘贴"), + toolTip: tr("粘贴图片"), + }, + ] + } + } + } + // 图片预览区域 + ImageWithText { + id: imageText + anchors.top: dLeftTop.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: size_.spacing + anchors.topMargin: size_.smallSpacing + + // 加载中 动态图标 + Loading { + text: "Running" + visible: running + anchors.centerIn: parent + } + // 提示 + DefaultTips { + visibleFlag: running + anchors.fill: parent + tips: qsTr("截图、拖入或粘贴二维码图片") + } + } + } + + // 面板B:结果 + itemB: Panel { + anchors.fill: parent + + TabPanel { + id: tabPanel + anchors.fill: parent + anchors.margins: size_.spacing + isMenuTop: doubleLayout.isRow // 左右布局时,菜单在顶部;上下布局时菜单在底部 + menuHeight: size_.line * 1.5 + + // 结果面板 + ResultsTableView { + id: resultsTableView + anchors.fill: parent + visible: false + } + + //生成面板 + Item { + id: writePanel + anchors.fill: parent + visible: false + Item { + id: writePanelTop + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: size_.line * 1.5 + + Button_ { + id: writePanelBtn1 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + text_: qsTr("设置") + onClicked: { + tabPanel.currentIndex = 0 // 转到设置面板 + configsComp.panelComponent.scrollToGroup(3) // 滚动到生成设置 + } + } + Row { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + CheckButton { + anchors.top: parent.top + anchors.bottom: parent.bottom + text_: qsTr("自动刷新") + toolTip: qsTr("修改文字后,自动生成二维码/条形码") + visible: writePanelTop.width > writePanelBtn1.width+writePanelBtn2.width+this.width + textColor_: theme.textColor + checked: writeEdit.autoUpdate + enabledAnime: true + onCheckedChanged: { + writeEdit.autoUpdate = checked + } + } + Button_ { + id: writePanelBtn2 + anchors.top: parent.top + anchors.bottom: parent.bottom + text_: qsTr("刷新") + toolTip: qsTr("生成二维码/条形码") + onClicked: reWriteBarcode() + } + } + } + Rectangle { + anchors.top: writePanelTop.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.topMargin: size_.smallSpacing + color: theme.bgColor + border.width: 1 + border.color: theme.coverColor4 + TextEdit_ { + id: writeEdit + anchors.fill: parent + anchors.margins: size_.spacing + // 自动刷新 + property bool autoUpdate: true + // 文字输入改变时,等待一段时间,自动刷新 + Timer { + id: writeEditTimer + interval: 500 // 0.5 秒 + repeat: false + onTriggered: reWriteBarcode() + } + onTextChanged: { + if(autoUpdate) // 重启计时器 + writeEditTimer.restart() + } + } + } + } + + tabsModel: [ + { + "key": "configs", + "title": qsTr("设置"), + "component": configsComp.panelComponent, + }, + { + "key": "ocrResult", + "title": qsTr("记录"), + "component": resultsTableView, + }, + { + "key": "writePanel", + "title": qsTr("生成"), + "component": writePanel, + }, + ] + } + } + } + + // 鼠标拖入图片 + DropArea_ { + anchors.fill: parent + callback: tabPage.scanPaths + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/TabPages/QRCode/QRCodeConfigs.qml b/UmiOCR-data/qt_res/qml/TabPages/QRCode/QRCodeConfigs.qml new file mode 100644 index 0000000..5dbedf9 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabPages/QRCode/QRCodeConfigs.qml @@ -0,0 +1,181 @@ +// ============================================== +// =============== 截图OCR的配置项 =============== +// ============================================== + +import QtQuick 2.15 +import "../../Configs" + +Configs { + category_: "QRCode" + + signal reBarcode() // 重新生成二维码的信号 + + configDict: { + + "hotkey": { + "title": qsTr("快捷键"), + "type": "group", + + "screenshot": { + "title": qsTr("屏幕截图"), + "type": "hotkey", + "default": "", // 默认热键 + "eventTitle": "<>", // 触发事件标题 + }, + "paste": { + "title": qsTr("粘贴图片"), + "type": "hotkey", + "default": "", + "eventTitle": "<>", + }, + }, + + "preprocessing": { + "title": qsTr("预处理(一般无需改动)"), + "type": "group", + "advanced": true, + + "median_filter_size": { + "title": qsTr("中值滤波"), + "toolTip": qsTr("对图像进行平滑处理\n>0 时生效。可填1~9的奇数(不允许偶数)"), + "isInt": true, + "default": 0, + "max": 10, + "min": 0, + }, + "sharpness_factor": { + "title": qsTr("锐度增强"), + "toolTip": qsTr("增加图像的锐度\n>0.1 时生效。可填0.1~10的小数"), + "isInt": false, + "default": 0, + "max": 10, + "min": 0, + }, + "contrast_factor": { + "title": qsTr("对比度增强"), + "toolTip": qsTr("增加图像的对比度\n>0.1 时生效。可填0.1~10的小数"), + "isInt": false, + "default": 0, + "max": 10, + "min": 0, + }, + "grayscale": { + "title": qsTr("转为灰度"), + "toolTip": qsTr("将图像像素转为灰度"), + "default": false, + }, + "threshold": { + "title": qsTr("二值化"), + "toolTip": qsTr("将图像像素转为纯黑和纯白\n启用了灰度,且二值化 >-1 时生效。可填0~255的整数"), + "isInt": true, + "default": -1, + "max": 255, + "min": -1, + }, + }, + + "action": { + "title": qsTr("扫码后的操作"), + "type": "group", + + "copy": { + "title": qsTr("复制结果"), + "default": false, + }, + "popMainWindow": { + "title": qsTr("弹出主窗口"), + "toolTip": qsTr("识图后,如果主窗口最小化或处于后台,则弹到前台"), + "default": true, + }, + }, + + "writeBarcode": { + "title": qsTr("生成二维码/条形码"), + "type": "group", + + "format": { + "title": qsTr("类型"), + "toolTip": qsTr("默认二维码:")+"QRCode", + "default": "QRCode", + "optionsList": [ + ["Aztec", "Aztec"], + ["Codabar", "Codabar"], + ["Code128", "Code128"], + ["Code39", "Code39"], + ["Code93", "Code93"], + ["DataBar", "DataBar"], + ["DataBarExpanded", "DataBarExpanded"], + ["DataMatrix", "DataMatrix"], + ["EAN13", "EAN13"], + ["EAN8", "EAN8"], + ["ITF", "ITF"], + ["LinearCodes", "LinearCodes"], + ["MatrixCodes", "MatrixCodes"], + ["MaxiCode", "MaxiCode"], + ["MicroQRCode", "MicroQRCode"], + ["PDF417", "PDF417"], + ["QRCode", "QRCode"], + ["UPCA", "UPCA"], + ["UPCE", "UPCE"], + ], + "onChanged": (newFlag, oldFlag)=>{ + if(oldFlag !== undefined) reBarcode() + }, + }, + "width": { + "title": qsTr("宽度"), + "toolTip": qsTr("填0:自动选择"), + "isInt": true, + "default": 256, + "min": 0, + "unit": qsTr("像素"), + "onChanged": (newFlag, oldFlag)=>{ + if(oldFlag !== undefined) reBarcode() + }, + }, + "height": { + "title": qsTr("高度"), + "toolTip": qsTr("填0:自动选择"), + "isInt": true, + "default": 256, + "min": 0, + "unit": qsTr("像素"), + "onChanged": (newFlag, oldFlag)=>{ + if(oldFlag !== undefined) reBarcode() + }, + }, + "quiet_zone": { + "title": qsTr("边缘空白"), + "toolTip": qsTr("填-1:自动选择"), + "isInt": true, + "default": -1, + "min": -1, + "unit": qsTr("像素"), + "onChanged": (newFlag, oldFlag)=>{ + if(oldFlag !== undefined) reBarcode() + }, + }, + "ec_level": { + "title": qsTr("纠错等级"), + "toolTip": qsTr("仅适用于:")+"Aztec, PDF417, QRCode", + "optionsList": [ + [-1, qsTr("自动")], + [1, "7%"], + [0, "15%"], + [3, "25%"], + [2, "30%"], + ], + "onChanged": (newFlag, oldFlag)=>{ + if(oldFlag !== undefined) reBarcode() + }, + }, + }, + + "other": { + "title": qsTr("其它"), + "type": "group", + + "simpleNotificationType": qmlapp.globalConfigs.utilsDicts.getSimpleNotificationType() + }, + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/TabPages/ScreenshotOCR/ScreenshotOCR.qml b/UmiOCR-data/qt_res/qml/TabPages/ScreenshotOCR/ScreenshotOCR.qml new file mode 100644 index 0000000..4485467 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabPages/ScreenshotOCR/ScreenshotOCR.qml @@ -0,0 +1,511 @@ +// ============================================== +// =============== 功能页:截图OCR =============== +// ============================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import ".." +import "../../Widgets" +import "../../Widgets/ResultLayout" +import "../../Widgets/ImageViewer" + +TabPage { + id: tabPage + // 配置 + configsComp: ScreenshotOcrConfigs {} + property string msnState: "none" // OCR任务状态, none run + + // ========================= 【逻辑】 ========================= + + // 重复截图 + function reScreenshot() { + qmlapp.imageManager.reScreenshot(screenshotEnd) + } + + // 开始截图 + function screenshot() { + qmlapp.imageManager.screenshot(screenshotEnd) + } + + // 截图完毕 + function screenshotEnd(clipID) { + popMainWindow() + if(!clipID) { // 截图取消 + tabPage.callPy("ocrImgID", undefined, undefined) + return + } + const configDict = configsComp.getValueDict() + tabPage.callPy("ocrImgID", clipID, configDict) + qmlapp.tab.showTabPageObj(tabPage) // 切换标签页 + imageText.showImgID(clipID) // 展示图片 + } + + // 指定区域截图。rect=[x,y,w,h] screen=屏幕编号 返回"[Success]"为成功 + function autoScreenshot(rect, screen) { + // 获取截图 + const clipID = qmlapp.imageManager.getScreenshot(rect, screen) + if(!clipID) { + tabPage.callPy("ocrImgID", "[Error] Unknow", undefined) + return + } + if(clipID.startsWith("[")) { + tabPage.callPy("ocrImgID", clipID, undefined) + return + } + // 进行识别 + const configDict = configsComp.getValueDict() + tabPage.callPy("ocrImgID", clipID, configDict) + } + + // 开始粘贴 + function paste() { + popMainWindow() + const res = qmlapp.imageManager.getPaste() + if(res.error) { + const t = qsTr("获取剪贴板异常") + qmlapp.popup.simple(t, res.error) + tabPage.callPy("ocrImgID", `[Error] ${t} ${res.error}`, undefined) + return + } + if(res.text) { + const t = qsTr("剪贴板中为文本") + qmlapp.popup.simple(t, res.text) + tabPage.callPy("ocrImgID", `[Warning] ${t}`, undefined) + return + } + qmlapp.tab.showTabPageObj(tabPage) // 切换标签页 + if(res.imgID) { // 图片 + imageText.showImgID(res.imgID) + const configDict = configsComp.getValueDict() + tabPage.callPy("ocrImgID", res.imgID, configDict) + } + else if(res.paths) { // 地址 + ocrPaths(res.paths) + } + } + + // 异步扫描一批图像路径 + function ocrPaths(paths) { + qmlapp.asynFilesLoader.run(paths,"image",false,onAddImages) + } + // 完毕后,对合法路径进行OCR + function onAddImages(paths) { + if(!paths || paths.length < 1) { + qmlapp.popup.simple(qsTr("无有效图片"), "") + return + } + const configDict = configsComp.getValueDict() + const simpleType = configDict["other.simpleNotificationType"] + qmlapp.popup.simple(qsTr("导入%1条图片路径").arg(paths.length), "", simpleType) + imageText.showPath(paths[0]) + tabPage.callPy("ocrPaths", paths, configDict) + } + + // 停止所有任务 + function msnStop() { + tabPage.callPy("msnStop") + } + + // 关闭页面 + function closePage() { + if(msnState !== "none") { + const argd = {yesText: qsTr("依然关闭")} + const callback = (flag)=>{ + if(flag) { + msnStop() + eventUnsub() + delPage() + } + } + qmlapp.popup.dialog("", qsTr("任务正在进行中。\n要结束任务并关闭页面吗?"), callback, "warning", argd) + } + else { + eventUnsub() + delPage() + } + } + + // 弹出主窗口 + function popMainWindow() { + // 若主窗口已经可见,则不处理 + if(qmlapp.mainWin.getVisibility()) + return + // 等一回合再弹,防止与收回截图窗口相冲突 + if(configsComp.getValue("action.popMainWindow")) { + Qt.callLater(()=>{ + qmlapp.mainWin.loadGeometry(false) + qmlapp.mainWin.setVisibility(true) + }) + } + } + + // ========================= 【事件管理】 ========================= + + Component.onCompleted: { + eventSub() // 订阅事件 + } + // 订阅事件 + function eventSub() { + qmlapp.pubSub.subscribeGroup("<>", this, "reScreenshot", ctrlKey) + qmlapp.pubSub.subscribeGroup("<>", this, "screenshot", ctrlKey) + qmlapp.pubSub.subscribeGroup("<>", this, "paste", ctrlKey) + qmlapp.systemTray.addMenuItem("<>", qsTr("屏幕功能"), screenshot) + qmlapp.systemTray.addMenuItem("<>", qsTr("粘贴图片"), paste) + } + // 取消订阅事件 + function eventUnsub() { + qmlapp.pubSub.unsubscribeGroup(ctrlKey) + qmlapp.systemTray.delMenuItem("<>") + qmlapp.systemTray.delMenuItem("<>") + } + + // ========================= 【python调用qml】 ========================= + + // 设置任务状态 + function setMsnState(flag) { + msnState = flag + } + + // 获取一个OCR的返回值 + function onOcrGet(res, imgID="", imgPath="") { + // 添加到结果 + const resText = resultsTableView.addOcrResult(res) + if(imgID) // 图片类型 + imageText.showImgID(imgID) + else if(imgPath) // 地址类型 + imageText.showPath(imgPath) + imageText.showTextBoxes(res) + // 若tabPanel面板的下标没有变化过,则切换到记录页 + if(tabPanel.indexChangeNum < 2) + tabPanel.currentIndex = 1 + // 复制到剪贴板 + const copy = configsComp.getValue("action.copy") + if(copy && resText!="") + qmlapp.utilsConnector.copyText(resText) + // 弹出通知 + showSimple(res, resText, copy) + // 升起主窗口 + popMainWindow() + } + + // 一组OCR任务完毕 + function onOcrEnd(msg) { + if(msg.startsWith("[Error]")) { + qmlapp.popup.message(qsTr("截图识别任务异常"), msg, "error") + } + } + + // ========================= 【后处理】 ========================= + + // 任务完成后发送通知 + function showSimple(res, resText, isCopy) { + // 获取弹窗类型 + let simpleType = configsComp.getValue("other.simpleNotificationType") + if(simpleType==="default") { + simpleType = qmlapp.globalConfigs.getValue("window.simpleNotificationType") + } + const code = res.code + const time = res.time.toFixed(2) + let title = "" + resText = resText.replace(/\n/g, " ") // 换行符替换空格 + if(code === 100 || code === 101) { // 成功时,不发送内部弹窗 + if(simpleType==="inside" || simpleType==="onlyInside") + if(qmlapp.mainWin.getVisibility()) + return + } + if(code === 100) { + if(isCopy) title = qsTr("已复制到剪贴板") + else title = qsTr("识图完成") + } + else if(code === 101) { + title = qsTr("无文字") + resText = "" + } + else { + title = qsTr("识别失败") + } + title += ` - ${time}s` + qmlapp.popup.simple(title, resText, simpleType) + } + + // ========================= 【布局】 ========================= + + // 左侧栏。主区域为左右双栏且左栏隐藏时,才显示左侧栏。 + Item { + id: leftCtrlPanel + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + // 展示条件 + visible: doubleLayout.isRow && doubleLayout.hideAB === 1 + anchors.leftMargin: visible ? size_.smallSpacing : 0 + width: visible ? size_.line * 1.5 : 0 + + + Menu { + id: modeMenu + parent: Qt.application.activeWindow + width: 60 + + MenuItem { + text: qsTr("拖动截屏") + onTriggered: qmlapp.imageManager.screenshotManager.selectMode = "drag" + } + MenuItem { + text: qsTr("点击截屏") + onTriggered: qmlapp.imageManager.screenshotManager.selectMode = "click" + } + } + + + Column { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: -size_.smallSpacing // 负间距,增加空间利用 + spacing: size_.spacing + + Item { + anchors.left: parent.left + anchors.right: parent.right + height: width + } + + IconButton { + anchors.left: parent.left + anchors.right: parent.right + height: width + icon_: "screenshot" + color: theme.textColor + toolTip: qsTr("屏幕截图") + onClicked: tabPage.screenshot() + } + IconButton { + anchors.left: parent.left + anchors.right: parent.right + height: width + icon_: "paste" + color: theme.textColor + toolTip: qsTr("粘贴图片") + onClicked: tabPage.paste() + } + IconButton { + visible: msnState==="run" + anchors.left: parent.left + anchors.right: parent.right + height: width + icon_: "stop" + color: theme.noColor + toolTip: qsTr("停止任务") + onClicked: tabPage.msnStop() + } + } + } + + // 主区域:可切换双栏面板 + DoubleSwitchableLayout { + id: doubleLayout + saveKey: "ScreenshotOCR_1" + anchors.left: leftCtrlPanel.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + + // 面板A:图像展示 + itemA: Panel { + anchors.fill: parent + clip: true + // 顶部控制栏 + Item { + id: dLeftTop + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: size_.spacing + anchors.bottomMargin: 0 + height: size_.line * 1.5 + clip: true + + // 靠右 + Row { + id: dLeftTopR + anchors.top: parent.top + anchors.right: parent.right + anchors.bottom: parent.bottom + spacing: size_.smallSpacing + + // 显示文字 + CheckButton { + anchors.top: parent.top + anchors.bottom: parent.bottom + text_: qsTr("文字") + toolTip: qsTr("在图片上叠加显示识别文字\n可在全局设置中设为默认关闭") + checked: imageText.showOverlay + enabledAnime: true + onCheckedChanged: imageText.showOverlay = checked + } + IconButtonBar { + anchors.top: parent.top + anchors.bottom: parent.bottom + btnList: [ + { + icon: "menu", + onClicked: imageText.popupMenu, + toolTip: tr("右键菜单"), + }, + { + icon: "save", + onClicked: imageText.saveImage, + toolTip: tr("保存图片"), + }, + { + icon: "full_screen", + onClicked: imageText.imageFullFit, + toolTip: tr("图片大小:适应窗口"), + }, + { + icon: "one_to_one", + onClicked: imageText.imageScaleAddSub, + toolTip: tr("图片大小:实际"), + }, + ] + } + // 百分比显示 + Text_ { + anchors.top: parent.top + anchors.bottom: parent.bottom + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignRight + text: (imageText.scale*100).toFixed(0) + "%" + color: theme.subTextColor + width: size_.line * 2.5 + } + } + + // 靠左 + Rectangle { // 背景 + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: dLeftTopL.width + color: theme.bgColor + Rectangle { + anchors.fill: parent + color: theme.coverColor1 + } + } + Row { // 按钮栏 + id: dLeftTopL + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + spacing: size_.smallSpacing + + IconButtonBar { + anchors.top: parent.top + anchors.bottom: parent.bottom + btnList: [ + { icon: "camera-rear", + onClicked: function() { + const pos = mapToItem(null, 0, height - 25) + modeMenu.popup(pos) + }, + color: theme.textColor, + bgColor: theme.bgColor, + toolTip: tr("截图模式"), + }, + { + icon: "screenshot", + onClicked: tabPage.screenshot, + color: theme.textColor, + bgColor: theme.bgColor, + text: tr("截图"), + toolTip: tr("屏幕截图"), + }, + { + icon: "paste", + onClicked: tabPage.paste, + color: theme.textColor, + bgColor: theme.bgColor, + text: tr("粘贴"), + toolTip: tr("粘贴图片"), + }, + ] + } + // 停止任务 + Button_ { + visible: msnState==="run" + anchors.top: parent.top + anchors.bottom: parent.bottom + text_: qsTr("停止任务") + textColor_: theme.noColor + onClicked: tabPage.msnStop() + } + } + } + // 图片预览区域 + ImageWithText { + id: imageText + anchors.top: dLeftTop.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: size_.spacing + anchors.topMargin: size_.smallSpacing + + // 加载中 动态图标 + Loading { + visible: msnState==="run" + anchors.centerIn: parent + } + + // 提示 + DefaultTips { + visibleFlag: msnState + anchors.fill: parent + tips: qsTr("截图、拖入或粘贴图片") + } + } + } + + // 面板B:结果 + itemB: Panel { + anchors.fill: parent + + TabPanel { + id: tabPanel + anchors.fill: parent + anchors.margins: size_.spacing + isMenuTop: doubleLayout.isRow // 左右布局时,菜单在顶部;上下布局时菜单在底部 + menuHeight: size_.line * 1.5 + + // 结果面板 + ResultsTableView { + id: resultsTableView + anchors.fill: parent + visible: false + } + + tabsModel: [ + { + "key": "configs", + "title": qsTr("设置"), + "component": configsComp.panelComponent, + }, + { + "key": "ocrResult", + "title": qsTr("记录"), + "component": resultsTableView, + }, + ] + } + } + } + + // 鼠标拖入图片 + DropArea_ { + anchors.fill: parent + callback: tabPage.ocrPaths + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/TabPages/ScreenshotOCR/ScreenshotOcrConfigs.qml b/UmiOCR-data/qt_res/qml/TabPages/ScreenshotOCR/ScreenshotOcrConfigs.qml new file mode 100644 index 0000000..dbcbc13 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabPages/ScreenshotOCR/ScreenshotOcrConfigs.qml @@ -0,0 +1,73 @@ +// ============================================== +// =============== 截图OCR的配置项 =============== +// ============================================== + +import QtQuick 2.15 +import "../../Configs" + +Configs { + category_: "ScreenshotOCR" + + configDict: { + // OCR参数 + "ocr": qmlapp.globalConfigs.ocrManager.deploy(this, "ocr"), + + // 后处理 + "tbpu": { + "title": qsTr("OCR文本后处理"), + "type": "group", + + "parser": qmlapp.globalConfigs.utilsDicts.getTbpuParser(), + }, + + "hotkey": { + "title": qsTr("快捷键"), + "type": "group", + + "screenshot": { + "title": qsTr("屏幕截图"), + "type": "hotkey", + // 默认热键 + "default": UmiAbout.app.system==="win32" ? + "win+alt+c" : "alt+c", + "eventTitle": "<>", // 触发事件标题 + }, + "paste": { + "title": qsTr("粘贴图片"), + "type": "hotkey", + "default": UmiAbout.app.system==="win32" ? + "win+alt+v" : "alt+v", + "eventTitle": "<>", + }, + "reScreenshot": { + "title": qsTr("重复截图"), + "toolTip": qsTr("重新截取上一次截图的范围"), + "type": "hotkey", + "default": "", + "eventTitle": "<>", + }, + }, + + "action": { + "title": qsTr("识图后的操作"), + "type": "group", + + "copy": { + "title": qsTr("复制结果"), + "default": false, + }, + "popMainWindow": { + "title": qsTr("弹出主窗口"), + "toolTip": qsTr("识图后,如果主窗口最小化或处于后台,则弹到前台"), + "default": true, + }, + }, + + "other": { + "title": qsTr("其它"), + "type": "group", + + "simpleNotificationType": qmlapp.globalConfigs.utilsDicts.getSimpleNotificationType() + }, + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/TabPages/TabPage.qml b/UmiOCR-data/qt_res/qml/TabPages/TabPage.qml new file mode 100644 index 0000000..e530d79 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabPages/TabPage.qml @@ -0,0 +1,58 @@ +// =========================================== +// =============== 标签页的基类 =============== +// =========================================== + +import QtQuick 2.15 + +Item { + anchors.fill: parent + + property string ctrlKey // Python连接器的key + property var connector // Python连接器的引用 + property var configsComp: undefined // 该页面的配置组件 + signal showPage // 页面展示时的信号,用 onShowPage 监听 + + // ========================= 【页面控制】 ========================= + + // 关闭页面。子类重载后可先向用户弹窗询问,再调用 delPage() + function closePage() { + delPage() + } + // 销毁页面 + function delPage() { + const index = qmlapp.tab.getTabPageIndex(this) // 获取当前页下标 + qmlapp.tab.delTabPage(index) // 销毁页面 + } + // 调用Python连接器的func方法名,传入任意个数的args作为参数 + function callPy(funcName, ...args) { + return connector.callPy(ctrlKey, funcName, args) + } + // 获取配置项值字典 + function getValueDict() { + // 控制组件存在,且有方法getValueDict + if (typeof configsComp === "object" && typeof configsComp.getValueDict === "function") { + return configsComp.getValueDict() + } + console.error("返回空配置项字典") + return {} + } + // 获取原始值字典 + function getOriginDict() { + // 控制组件存在,且有方法getValueDict + if (typeof configsComp === "object" && typeof configsComp.getValueDict === "function") { + return configsComp.getOriginDict() + } + console.error("返回空原始值字典") + return {} + } + // 设置配置项值 + function setValue(key, val) { + // 控制组件存在,且有方法getValueDict + if (typeof configsComp === "object" && typeof configsComp.setValue === "function") { + configsComp.setValue(key, val, true) + return + } + console.error("设置配置项失败", key, val) + } + +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/TabView_/TabViewManager.qml b/UmiOCR-data/qt_res/qml/TabView_/TabViewManager.qml new file mode 100644 index 0000000..d066daf --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabView_/TabViewManager.qml @@ -0,0 +1,218 @@ +// ==================================================== +// =============== 标签页整体的逻辑管理器 =============== +// ==================================================== + + +import QtQuick 2.15 +import "../TabPages" +import "../Configs" + +Item { + id: tabViewManager + // ========================= 【子管理器】 ========================= + + PagesManager{ id: page } // 页管理器 + property alias page: page + property var bar: undefined // 栏管理器 + // 连接别名 + property alias infoList: page.infoList + property alias pageList: page.pageList + // 控制属性 + property bool barIsLock: false // 栏是否已锁定 + property int showPageIndex: -1 // 当前展示的标签页序号 + property var openPageList: [] // 当前已打开的的标签页列表,存放URL,用于记录历史操作 + + // 持久化存储 + Settings_ { + id: settings + category: "TabPageManager" // 自定义类别名称 + property alias openPageList: tabViewManager.openPageList + property alias showPageIndex: tabViewManager.showPageIndex + property bool refresh: false // 用于刷新 + function save(){ // 手动刷新 + refresh=!refresh + } + } + + // ========================= 【初始化】 ========================= + + function init() { // 需要延迟加载,全局初始化完毕后执行 + page.initListUrl() // 页管理器初始化文件 + // 提取所有info的url + const urlList = [] + for(let i in infoList){ + urlList.push(infoList[i].url) + } + // 从历史记录中搜索仍存在的项 + const newPage = [] + const newIndex = [] + for(let i in openPageList){ + const urlIndex = urlList.indexOf(openPageList[i]) + if(urlIndex == -1){ + console.error("【Error】上次url历史中的 "+openPageList[i]+" 已经丢失!") + } + else if(!page.getComp(urlIndex)){ + console.error("【Error】上次url历史中的 "+openPageList[i]+" 页面组件类未初始化!") + } else { + newPage.push(openPageList[i]) + newIndex.push(urlIndex) + } + } + openPageList = [] // 清空当前页面记录,重新添加 + // 初始为空,默认添加导航页 + if(newPage.length == 0){ + addNavi() + } + else { + let showPageI = showPageIndex // 另起一变量记录showPageIndex,以免受addTabPage的影响。 + // 初始不为空,添加初始页 + for(let i in newIndex){ + addTabPage(i, newIndex[i]) + } + // 初始选中 + if(!isIndex(showPageI, pageList, "【Warning】初始选中页面失败:")) + showPageI = 0 + showTabPage(showPageI) + } + settings.save() + console.log("TabViewManager 初始化页面完毕!") + } + + // ========================= 【增删改查】 ========================= + + // 增: 在 index 处,插入一个 infoList[infoIndex] 标签页。 + function addTabPage(index, infoIndex){ // index=-1 代表尾部插入 + if(index<0) index = pageList.length // 尾部添加 + else if(index>pageList.length){ + console.error("【Error】添加标签页失败:页面下标"+index+"超出范围"+pageList.length+"!") + return + } + if(!isIndex(infoIndex, infoList, "【Error】添加标签页失败:信息")) + return + if(!page.addPage(index, infoIndex)) return + bar.addTab(index, infoList[infoIndex].title) + openPageList.splice(index, 0, infoList[infoIndex].url) + if(showPageIndex >= index) { // 若选中页在被添加页之前 + showPageIndex++ // 选中页序号后移 + } + settings.save() + } + + // 增: 在最后,添加一个导航页,并选中该页 + function addNavi(){ + addTabPage(-1, 0) // 添加页 + showTabPage(-1) // 选中页 + } + + // 删:关闭页面(点击关闭按钮),不立刻删除 + function closeTabPage(index){ + page.closePage(index) + } + + // 删: 在 index 处,删除该标签页。 + function delTabPage(index){ + if(!isIndex(index, pageList, "【Error】删除标签页失败:页面")) + return + page.delPage(index) + bar.delTab(index) + openPageList.splice(index, 1) + if(showPageIndex > index) { // 若选中页在被删除页之后 + showPageIndex-- // 选中页序号前移 + } + else if(showPageIndex == index){ // 若选中页就是被删除页 + if(pageList.length == 0){ // 已经没页了,就补充一个导航页 + addNavi() + } else if (index == pageList.length){ // 原本在队尾,则展示当前队尾页 + showTabPage(-1) + } else { // 原本不在队尾,则展示原本的后一个页 + showTabPage(index) + } + } + settings.save() + } + + // 改: 将 index 处的标签页,改为 infoIndex 页。 + function changeTabPage(index, infoIndex){ + if(!isIndex(index, pageList, "【Error】更改标签页失败:页面")) + return + if(!isIndex(infoIndex, infoList, "【Error】更改标签页失败:信息")) + return + // 检查pageList中是否已有目标页面,有则展示该页 + for(let i in pageList) { + if(pageList[i].infoIndex===infoIndex) { + showTabPage(i) + return + } + } + // 尚无目标页,则删除原页,新增目标页 + if(!page.changePage(index, infoIndex)) return + bar.changeTab(index, infoList[infoIndex].title) + openPageList[index] = infoList[infoIndex].url + settings.save() // 改动列表项,需要手动触发保存 + if(showPageIndex == index) // 若原本是选中的,则改页后要再选中一次 + showTabPage(index) + } + + // 改: 展示 index 页。index=-1 代表尾部 + function showTabPage(index){ + if(index<0) index = pageList.length-1 + if(!isIndex(index, pageList, "【Error】展示页面失败:页面")) + return + showPageIndex = index + page.showPage(index) + bar.showTab(index) + } + + // 改: 展示 obj 页。 + function showTabPageObj(obj){ + const index = getTabPageIndex(obj) + if(index < 0) { + return // 未找到对应页下标 + } + showTabPage(index) + } + + // 改: 将一个原本在 index 的页移到 go 处。 + function moveTabPage(index, go){ + if(!isIndex(index, pageList, "【Error】移动页面失败:起点")) + return + if(!isIndex(go, pageList, "【Error】移动页面失败:终点")) + return + page.movePage(index, go) + bar.moveTab(index, go) + if(showPageIndex == index) // 若原本是选中的,则改页后要同步选中位置 + showPageIndex = go + for(let i in pageList){ // 记录新URL顺序 + openPageList[i] = pageList[i].info.url + } + settings.save() + } + + // 查: 传入页面对象 obj ,返回该对象的下标。 + function getTabPageIndex(obj){ + for(let i in pageList){ + if(pageList[i].obj===obj) + return i + } + return -1 + } + + // 查: 传入下标 index 列表 list 报错内容前缀 msg ,返回下标是否合法。 + function isIndex(index, list, msg=""){ + if(index<0 || index>=list.length){ + if(msg) + console.error(msg+"下标"+index+"超出范围"+(pageList.length-1)+"!") + return false + } + return true + } + + // 查: 返回 infoList + function getInfoList() { + return infoList + } + // 查: 返回 pageList + function getPageList() { + return pageList + } +} diff --git a/UmiOCR-data/qt_res/qml/TabView_/TabView_.qml b/UmiOCR-data/qt_res/qml/TabView_/TabView_.qml new file mode 100644 index 0000000..10aa0e3 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/TabView_/TabView_.qml @@ -0,0 +1,38 @@ +// ============================================================= +// =============== 水平标签组件(即标签按钮位于顶部) ============= +// ============================================================= + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import "../TabBar_" + +Rectangle { + + anchors.fill: parent + + // 标签页容器 + Rectangle { + anchors.top: topBar.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + color: theme.bgColor + + Component.onCompleted: { + qmlapp.tab.page.pagesNest.parent = this + } + } + + // 标签栏容器 + Rectangle { + id: topBar + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: size_.hTabBarHeight + color: theme.tabBarColor + clip: true + + HTabBar { } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Themes/Theme.qml b/UmiOCR-data/qt_res/qml/Themes/Theme.qml new file mode 100644 index 0000000..f868e4f --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Themes/Theme.qml @@ -0,0 +1,133 @@ +// ========================================= +// =============== 样式的基类 =============== +// ========================================= + +import QtQuick 2.15 + +Item { + // 主题名称 + property string themeTitle: "Unknow theme" + + // ========================= 【颜色】 ========================= + + property color tabBarColor // 标签栏 + property color bgColor // 背景 + property color textColor // 主要文字 + property color subTextColor // 次要文字 + property color yesColor // 允许、成功 + property color noColor // 禁止、失败 + property color specialBgColor // 特殊背景,弹窗确认按钮 + property color specialTextColor // 特殊前景,文字、图标 + // 叠加层颜色,半透明,从浅到深 + property color coverColor1 // 大部分需要突出的背景 + property color coverColor2 // 按钮悬停 + property color coverColor3 // 阴影 + property color coverColor4 // 按钮按下 + + // 必要的键 + property var keys: [ + "themeTitle", + "tabBarColor", + "bgColor", + "textColor", + "subTextColor", + "yesColor", + "noColor", + "specialBgColor", + "specialTextColor", + "coverColor1", + "coverColor2", + "coverColor3", + "coverColor4", + ] + // 默认主题 / 当前读入的主题配置 + property var all: { + // 默认主题 + "Default Light": { + "themeTitle": qsTr("珍珠白"), + "tabBarColor": "#F3F3F3", + "bgColor": "#FFF", + "textColor": "#000", + "subTextColor": "#555", + "yesColor": "#00CC00", + "noColor": "#FF0000", + "specialBgColor": "#FCF9BE", + "specialTextColor": "#C58940", + "coverColor1": "#11000000", + "coverColor2": "#22000000", + "coverColor3": "#33000000", + "coverColor4": "#55000000", + }, + "Default Dark": { + "themeTitle": qsTr("云墨黑"), + "tabBarColor": "#4A4A4A", + "bgColor": "#444", + "textColor": "#FFF", + "subTextColor": "#AAA", + "yesColor": "#6EFC39", + "noColor": "#FF2E2E", + "specialBgColor": "#005c99", + "specialTextColor": "#00BFFF", + "coverColor1": "#22FFFFFF", + "coverColor2": "#33FFFFFF", + "coverColor3": "#44FFFFFF", + "coverColor4": "#55FFFFFF", + }, + // 抄: https://github.com/altercation/solarized + "Solarized Light": { + "themeTitle": "Solarized Light", + "tabBarColor": "#d9d2c2", + "bgColor": "#fdf6e3", + "textColor": "#586e75", + "subTextColor": "#839496", + "yesColor": "#48985d", + "noColor": "#e51d09", + "specialBgColor": "#FCF9BE", + "specialTextColor": "#C58940", + "coverColor1": "#11000000", + "coverColor2": "#22000000", + "coverColor3": "#33000000", + "coverColor4": "#55000000" + }, + "Solarized Dark": { + "themeTitle": "Solarized Dark", + "tabBarColor": "#004052", + "bgColor": "#002b36", + "textColor": "#93a1a1", + "subTextColor": "#657b83", + "yesColor": "#6EFC39", + "noColor": "#f14c4c", + "specialBgColor": "#00517D", + "specialTextColor": "#00BFFF", + "coverColor1": "#19FFFFFF", + "coverColor2": "#29FFFFFF", + "coverColor3": "#44FFFFFF", + "coverColor4": "#55FFFFFF" + }, + // 抄: https://github.com/Fndroid/clash_for_windows_pkg + "Cyberpunk": { + "themeTitle": qsTr("赛博朋克"), + "tabBarColor": "#084A5A", + "bgColor": "#136377", + "textColor": "#FCEC0C", + "subTextColor": "#CF9F0F", + "yesColor": "#6EFC39", + "noColor": "#FF5E5E", + "specialBgColor": "#00517D", + "specialTextColor": "#00BFFF", + "coverColor1": "#33000000", + "coverColor2": "#29FFFFFF", + "coverColor3": "#44FFFFFF", + "coverColor4": "#55FFFFFF" + } + } + // 主题控制器 + property ThemeManager manager: ThemeManager{} + + // ========================= 【字体】 ========================= + + // 主要UI文字字体,内容可控,可以用裁切的ttf + property string fontFamily: "Microsoft YaHei" + // 数据显示文字字体,内容不可控,用兼容性好的系统字体 + property string dataFontFamily: "Microsoft YaHei" +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Themes/ThemeManager.qml b/UmiOCR-data/qt_res/qml/Themes/ThemeManager.qml new file mode 100644 index 0000000..d86e63b --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Themes/ThemeManager.qml @@ -0,0 +1,106 @@ +// ================================================ +// =============== 主题切换的逻辑管理 =============== +// ================================================ + +import QtQuick 2.15 +import ThemeConnector 1.0 + +Item { + // ==================== 【接口】 ==================== + + // 初始化 + function init() { + loadAll() // 加载主题 + console.log("主题管理器初始化完毕!", this) + } + + // 从配置文件中加载 theme.all + function loadAll() { + const tstr = tConn.loadThemeStr() // 文件读字符串 + let f = strToAll(tstr) // 字符串写入all + if(!f) { // 写入失败,则初始化配置文件 + console.log("初始化主题配置文件") + saveAll() + } + } + + // 将 theme.all写入配置文件 + function saveAll() { + const tstr = allToStr() + tConn.saveThemeStr(tstr) + } + + // 切换主题 + function switchTheme(th) { + if(!theme.all.hasOwnProperty(th)) { + console.warn("切换主题失败,theme.all 不存在主题", k) + return + } + const target = theme.all[th] + for(const k of theme.keys) { + theme[k] = target[k] + } + } + + // 获取主题列表 + function getOptionsList() { + let optList = [] + for(let k in theme.all) { + optList.push([k, theme.all[k].themeTitle]) + } + return optList + } + + // ========================================================= + + // 连接器 + ThemeConnector{id: tConn} + + // 从json字符串加载主题配置,写入 theme.all 。返回是否成功 + function strToAll(tstr) { + if(!tstr) + return false + try { + // 加载并检查主题字典 + let all = JSON.parse(tstr) + for(let k in all) { + if(!checkThemeDict(all[k])) { + console.warn("加载单个主题失败:", k) + delete all[k] + continue + } + // 覆盖加载名称 + if(theme.all.hasOwnProperty(k)) { + all[k].themeTitle = theme.all[k].themeTitle + } + } + if(Object.keys(all).length === 0) { + return false + } + theme.all = all + return true + } catch (error) { + console.warn("解析主题JSON字符串时出现异常:", error) + } + return false + } + + // 将 theme.all 转为json字符串 + function allToStr() { + return JSON.stringify(theme.all, null, " ") + } + + // 检测单个主题字典的合法性 + function checkThemeDict(td) { + const tdKeys = Object.keys(td); + if (tdKeys.length !== theme.keys.length) { + return false // 长度不符 + } + for (let k of theme.keys) { + if (!tdKeys.includes(k)) + return false // 缺键 + } + return true + } + +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/Button_.qml b/UmiOCR-data/qt_res/qml/Widgets/Button_.qml new file mode 100644 index 0000000..8a288bd --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/Button_.qml @@ -0,0 +1,76 @@ +// ============================================== +// =============== 标准按钮外观样式 =============== +// ============================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +Button { + id: btn + // ========================= 【可调参数】 ========================= + property string text_: "" + property string toolTip: "" // 鼠标悬停提示 + property bool bold_: false + property int textSize: size_.text + property color textColor_: theme.textColor + + property color bgColor_: "#00000000" + property color bgPressColor_: theme.coverColor4 + property color bgHoverColor_: theme.coverColor2 + + property int borderWidth: 0 + property color borderColor: theme.coverColor3 + property real radius: size_.btnRadius + + Component.onCompleted: { + // 如果设定了提示,则加载提示组件 + if(btn.toolTip){ + toolTipLoader.sourceComponent = toolTip + } + } + + contentItem: Text_ { + text: btn.text_ + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pixelSize: btn.textSize + color: btn.textColor_ + font.bold: btn.bold_ + } + + background: Rectangle { + anchors.fill: parent + radius: btn.radius + color: parent.pressed ? parent.bgPressColor_ : ( + parent.hovered ? parent.bgHoverColor_ : parent.bgColor_ + ) + border.width: borderWidth + border.color: borderColor + } + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + // 穿透 + onPressed: { + mouse.accepted = false + } + onReleased: { + mouse.accepted = false + } + } + // 提示 + Component { + id: toolTip + ToolTip_ { + visible: btn.hovered + text: btn.toolTip + } + } + // ToolTip_ 的动态加载器 + Loader { + id: toolTipLoader + anchors.fill: parent + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/CheckBox_.qml b/UmiOCR-data/qt_res/qml/Widgets/CheckBox_.qml new file mode 100644 index 0000000..2a07f56 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/CheckBox_.qml @@ -0,0 +1,49 @@ +// ======================================================== +// =============== 复选框样式,不提供点击事件 =============== +// ======================================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +Rectangle { + property bool checked: false // 选中/非选中 + property bool enabledAnime: false // true 启用动画 + + height: size_.line + width: size_.line*2 + clip: true + color: theme.bgColor + radius: size_.btnRadius + border.width: 2 + border.color: theme.coverColor4 + + // 关闭:- + Icon_ { + anchors.fill: parent + anchors.margins: 3 + icon: "dash" + color: theme.noColor + } + + // 启用:√ + Rectangle { + id: enableIcon + x: checked ? 0 : width*-1.1 + height: parent.height + width: parent.width + color: theme.yesColor + radius: size_.btnRadius + Icon_ { + anchors.fill: parent + icon: "yes" + color: theme.bgColor + } + Behavior on x { // 位移动画 + enabled: qmlapp.enabledEffect && enabledAnime + NumberAnimation { + duration: 200 + easing.type: Easing.OutCirc + } + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/CheckButton.qml b/UmiOCR-data/qt_res/qml/Widgets/CheckButton.qml new file mode 100644 index 0000000..0139919 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/CheckButton.qml @@ -0,0 +1,42 @@ +// ====================================================== +// =============== 复选框按钮,提供点击事件 =============== +// ====================================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +Button_ { + id: btn + property bool enabledAnime: false + property real margins_: size_.smallSpacing + checkable: true + checked: false + width: checkBox.width + textComp.width + btn.margins_ * 3 + + contentItem: Row { + anchors.fill: parent + anchors.leftMargin: btn.margins_ + spacing: btn.margins_ + + // 复选框 + CheckBox_ { + id: checkBox + anchors.verticalCenter: parent.verticalCenter + width: size_.line + checked: btn.checked + enabledAnime: btn.enabledAnime + } + + // 文字 + Text_ { + id: textComp + anchors.verticalCenter: parent.verticalCenter + text: btn.text_ + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pixelSize: btn.textSize + color: btn.textColor_ + font.bold: btn.bold_ + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/DefaultTips.qml b/UmiOCR-data/qt_res/qml/Widgets/DefaultTips.qml new file mode 100644 index 0000000..d92d05c --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/DefaultTips.qml @@ -0,0 +1,28 @@ +// ======================================= +// =============== 默认提示 =============== +// ======================================= + +import QtQuick 2.15 + +// 提示 +Item { + property string tips: "" // 提示文本 + property var visibleFlag // 只要该变量改变,就永久隐藏该组件 + property bool first: true + + onVisibleFlagChanged: { + if(first) first = false + else visible = false + } + + Text_ { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: size_.spacing + wrapMode: TextEdit.Wrap + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: tips + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/DoubleColumnLayout.qml b/UmiOCR-data/qt_res/qml/Widgets/DoubleColumnLayout.qml new file mode 100644 index 0000000..a49be89 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/DoubleColumnLayout.qml @@ -0,0 +1,272 @@ +// =========================================== +// =============== 上下双栏布局 =============== +// =========================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +Item { + + // ========================= 【可调参数】 ========================= + + property QtObject topItem // 上元素 + property QtObject bottomItem // 下元素 + property real hideHeight: 40 // 一个栏小于该值时隐藏 + property real initSplitterY: 0.5 // 分割线初始位置。>1时为像素,0~1为比例。 + property string saveKey: "" // 如果非空,则缓存 hideTB 参数。 + property real margins: size_.spacing // 边缘空白 + property bool isShowSplitView: true // 是否展示分栏按钮 + signal switchView() // 更改分栏布局信号 + + // 只读信息 + property int hideTB: 0 // 0为不隐藏,1为隐藏上边,2为隐藏下边 + + // =============================================================== + id: doubleCC + // 上下元素变化时,挂到容器下 + onTopItemChanged: topItem.parent = topContainer + onBottomItemChanged: bottomItem.parent = bottomContainer + + Item { + id: doubleColumn + anchors.fill: parent + anchors.margins: parent.margins + + property alias hideHeight: doubleCC.hideHeight + property alias splitterY: splitter.y // 分割线当前位置 + property bool isInitialized: false // 当前是否初始化完毕 + Component.onCompleted: { // 初始化分割线位置 + if(parent.initSplitterY <= 0) + parent.initSplitterY = 0.5 // 默认值0.5 + Qt.callLater(() => { // 延迟一个事件循环,再进行位置初始化 + isInitialized = true // 标记初始化完成 + let hideFlag = 0 + if(doubleCC.saveKey) { // 取hide缓存 + const layoutDict = qmlapp.globalConfigs.getValue("window.doubleLayout") + const f = layoutDict[doubleCC.saveKey] + if(Number.isInteger(f)) hideFlag = f // 合法检查 + } + toTB(hideFlag) + }) + } + property int bottomMax: height - splitter.height // 下边缘位置 + + function setHideTB(h) { + if(doubleCC.hideTB === h) return + doubleCC.hideTB = h + // 缓存状态 + if(doubleCC.saveKey) { + let layoutDict = qmlapp.globalConfigs.getValue("window.doubleLayout") + layoutDict[doubleCC.saveKey] = doubleCC.hideTB + qmlapp.globalConfigs.setValue("window.doubleLayout", layoutDict) + } + } + // 检查上下隐藏 + function toHide(isHeightChanged = false){ + if(!isInitialized) return // 防止初始化完成之前自动触发 + if(isHeightChanged && doubleCC.hideTB === 2) { // 总体高改变时下吸附 + splitterY = height - splitter.height + return + } + if(splitterY+splitter.height > (height - hideHeight)){ // 隐藏下边 + topContainer.visible = true + bottomContainer.visible = false + setHideTB(2) + splitterY = height - splitter.height + } + else if(splitterY < hideHeight){ // 隐藏上边 + topContainer.visible = false + bottomContainer.visible = true + setHideTB(1) + splitterY = 0 + } + else{ + topContainer.visible = true + bottomContainer.visible = true + setHideTB(0) + } + + } + // 去到上下。flag: 0-初始 1-上 2-下 3-切换分栏信号 + function toTB(flag) { + if(flag === 3) { + switchView() + return + } + if(flag === 0) + toInitPosition() + else if(flag === 1) + splitterY = hideHeight-1 + else if(flag === 2) + splitterY = height-splitter.height-hideHeight+1 + toHide() + } + // 去到初始位置 + function toInitPosition() { + if(parent.initSplitterY >= 0 && parent.initSplitterY <= 1) + splitterY = height * parent.initSplitterY + else + splitterY = parent.initSplitterY + } + // 拖拽分割线,或者调整整体宽度,都会触发检查隐藏 + onSplitterYChanged: toHide() + onHeightChanged: toHide(true) + + // 上容器 + Item{ + id: topContainer + anchors.bottom: splitter.top + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + onVisibleChanged: { + topItem.parent = visible?topContainer:hideContainer + } + } + + // 中间拖动条 + Item{ + id: splitter + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: size_.spacing + anchors.rightMargin: size_.spacing + height: size_.spacing + y: 0 // 位置可变换 + z: 1 + property bool isVisible: splitterMouseArea.containsMouse || btnsMouseArea.containsMouse || splitterMouseArea.drag.active || doubleCC.hideTB!==0 + + // 分割线 拖拽、悬停 + MouseArea { + id: splitterMouseArea + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.right: parent.right + // 平常高为分隔栏高度,按下拖拽时高度增加防止鼠标出界 + height: pressed ? doubleCC.height : parent.height + hoverEnabled: true // 鼠标悬停时,分割线颜色变深 + cursorShape: Qt.SizeVerCursor // 鼠标指针为双箭头 + // 拖拽 + drag.target: splitter + drag.axis: Drag.YAxis + drag.minimumY: 0 + drag.maximumY: doubleColumn.bottomMax + drag.smoothed: false // 无阈值,一拖就动 + } + // 分割线 视觉展示 + Rectangle { + id: splitterShow + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.right: parent.right + height: size_.spacing * 0.3 + radius: height + visible: splitterMouseArea.containsMouse || splitterMouseArea.drag.active || doubleCC.hideTB!==0 + color: splitterMouseArea.pressed ? theme.coverColor4 : theme.coverColor2 + } + + // 控制按钮 点击 + MouseArea { + id: btnsMouseArea + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + height: containsMouse ? size_.line * 2 : parent.height + width: size_.line * ((doubleCC.hideTB===0 ? 6 : 4) + (doubleCC.isShowSplitView ? 2 : 0)) + property int selectIndex: -1 + onExited: selectIndex = -1 + onPositionChanged: { + if(doubleCC.hideTB===0) { + if(mouse.x < size_.line * 2) + selectIndex = 1 + else if(mouse.x < size_.line * 4) + selectIndex = 2 + else if(mouse.x < size_.line * 6) + selectIndex = 0 + else + selectIndex = 3 + } + else { + if(mouse.x < size_.line * 2) + selectIndex = doubleCC.hideTB===1 ? 2 : 1 + else if(mouse.x < size_.line * 4) + selectIndex = 0 + else + selectIndex = 3 + } + } + onClicked: doubleColumn.toTB(selectIndex) + + // 控制按钮 视觉 + Rectangle { + color: theme.specialBgColor + visible: (splitterMouseArea.containsMouse || btnsMouseArea.containsMouse) && !splitterMouseArea.drag.active + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.right: parent.right + height: size_.line * 2 + radius: size_.panelRadius + + Row { + height: parent.height + Icon_ { + visible: doubleCC.hideTB!==1 + width: parent.height + height: width + color: btnsMouseArea.selectIndex===1 ? theme.textColor:theme.specialTextColor + icon: "arrow_to_left" + rotation: 90 + } + Icon_ { + visible: doubleCC.hideTB!==2 + width: parent.height + height: width + color: btnsMouseArea.selectIndex===2 ? theme.textColor:theme.specialTextColor + icon: "arrow_to_left" + rotation: 270 + } + Icon_ { + width: parent.height + height: width + color: btnsMouseArea.selectIndex===0 ? theme.textColor:theme.specialTextColor + icon: "arrow_to_center" + rotation: 90 + } + // 转为左右分栏 + Icon_ { + visible: doubleCC.isShowSplitView + width: parent.height + height: width + color: btnsMouseArea.selectIndex===3 ? theme.textColor:theme.specialTextColor + icon: "split_view" + } + } + } + } + + } + + // 下容器 + Item{ + id: bottomContainer + anchors.top: splitter.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + onVisibleChanged: { + bottomItem.parent = visible?bottomContainer:hideContainer + } + } + + // 隐藏容器 + Item { + id: hideContainer + visible: false + height: Math.max(doubleCC.height, 400) + anchors.left: parent.left + anchors.right: parent.right + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/DoubleRowLayout.qml b/UmiOCR-data/qt_res/qml/Widgets/DoubleRowLayout.qml new file mode 100644 index 0000000..885c140 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/DoubleRowLayout.qml @@ -0,0 +1,271 @@ +// =========================================== +// =============== 左右双栏布局 =============== +// =========================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +Item { + + // ========================= 【可调参数】 ========================= + + property QtObject leftItem // 左元素 + property QtObject rightItem // 右元素 + property real hideWidth: 80 // 一个栏小于该值时隐藏 + property real initSplitterX: 0.5 // 分割线初始位置。>1时为像素,0~1为比例。 + property string saveKey: "" // 如果非空,则缓存 hideLR 参数。 + property real margins: size_.spacing // 边缘空白 + property bool isShowSplitView: false // 是否展示分栏按钮 + signal switchView() // 更改分栏布局信号 + + // 只读信息 + property int hideLR: 0 // 0为不隐藏,1为隐藏左边,2为隐藏右边 + + // =============================================================== + id: doubleCC + // 左右元素变化时,挂到容器下 + onLeftItemChanged: leftItem.parent = leftContainer + onRightItemChanged: rightItem.parent = rightContainer + + Item { + id: doubleColumn + anchors.fill: parent + anchors.margins: parent.margins + + property alias hideWidth: doubleCC.hideWidth + property alias splitterX: splitter.x // 分割线当前位置 + property bool isInitialized: false // 当前是否初始化完毕 + Component.onCompleted: { // 初始化分割线位置 + if(parent.initSplitterX <= 0) + parent.initSplitterX = 0.5 // 默认值0.5 + Qt.callLater(() => { // 延迟一个事件循环,再进行位置初始化 + isInitialized = true // 标记初始化完成 + let hideFlag = 0 + if(doubleCC.saveKey) { // 取hide缓存 + const layoutDict = qmlapp.globalConfigs.getValue("window.doubleLayout") + const f = layoutDict[doubleCC.saveKey] + if(Number.isInteger(f)) hideFlag = f // 合法检查 + } + toLR(hideFlag) + }) + } + property int rightMax: width - splitter.width // 右边缘位置 + + function setHideLR(h) { + if(doubleCC.hideLR === h) return + doubleCC.hideLR = h + // 缓存状态 + if(doubleCC.saveKey) { + let layoutDict = qmlapp.globalConfigs.getValue("window.doubleLayout") + layoutDict[doubleCC.saveKey] = doubleCC.hideLR + qmlapp.globalConfigs.setValue("window.doubleLayout", layoutDict) + } + } + // 检查左右隐藏 + function toHide(isWidthChanged = false){ + if(!isInitialized) return // 防止初始化完成之前自动触发 + if(isWidthChanged && doubleCC.hideLR === 2) { // 总体宽度改变时右吸附 + splitterX = width - splitter.width + return + } + if(splitterX+splitter.width > (width - hideWidth)){ // 隐藏右边 + leftContainer.visible = true + rightContainer.visible = false + setHideLR(2) + splitterX = width - splitter.width + } + else if(splitterX < hideWidth){ // 隐藏左边 + leftContainer.visible = false + rightContainer.visible = true + setHideLR(1) + splitterX = 0 + } + else{ + leftContainer.visible = true + rightContainer.visible = true + setHideLR(0) + } + + } + // 去到左右。flag: 0-初始 1-左 2-右 3-切换分栏信号 + function toLR(flag) { + if(flag === 3) { + switchView() + return + } + if(flag === 0) + toInitPosition() + else if(flag === 1) + splitterX = hideWidth-1 + else if(flag === 2) + splitterX = width-splitter.width-hideWidth+1 + toHide() + } + // 去到初始位置 + function toInitPosition() { + if(parent.initSplitterX >= 0 && parent.initSplitterX <= 1) + splitterX = width * parent.initSplitterX - size_.spacing * 2 + else + splitterX = parent.initSplitterX + } + // 拖拽分割线,或者调整整体宽度,都会触发检查隐藏 + onSplitterXChanged: toHide() + onWidthChanged: toHide(true) + // 左容器 + Item{ + id: leftContainer + anchors.right: splitter.left + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + onVisibleChanged: { + leftItem.parent = visible?leftContainer:hideContainer + } + } + + // 中间拖动条 + Item{ + id: splitter + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.topMargin: size_.spacing + anchors.bottomMargin: size_.spacing + width: size_.spacing + x: 0 // 位置可变换 + z: 1 + property bool isVisible: splitterMouseArea.containsMouse || btnsMouseArea.containsMouse || splitterMouseArea.drag.active || doubleCC.hideLR!==0 + + // 分割线 拖拽、悬停 + MouseArea { + id: splitterMouseArea + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.bottom: btnsMouseArea.top + // 平常宽度为分隔栏宽度,按下拖拽时宽度增加防止鼠标出界 + width: pressed ? doubleCC.width : parent.width + hoverEnabled: true // 鼠标悬停时,分割线颜色变深 + cursorShape: Qt.SizeHorCursor // 鼠标指针为双箭头 + // 拖拽 + drag.target: splitter + drag.axis: Drag.XAxis + drag.minimumX: 0 + drag.maximumX: doubleColumn.rightMax + drag.smoothed: false // 无阈值,一拖就动 + + } + // 分割线 视觉展示 + Rectangle { + id: splitterShow + visible: splitter.isVisible + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.bottom: parent.bottom + width: size_.spacing * 0.3 + radius: width + color: splitterMouseArea.pressed ? theme.coverColor4 : theme.coverColor2 + } + + // 控制按钮 点击 + MouseArea { + id: btnsMouseArea + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + width: containsMouse ? size_.line * 2 : parent.width + height: size_.line * ((doubleCC.hideLR===0 ? 6 : 4) + (doubleCC.isShowSplitView ? 2 : 0)) + property int selectIndex: -1 + onExited: selectIndex = -1 + onPositionChanged: { + if(doubleCC.hideLR===0) { + if(mouse.y < size_.line * 2) + selectIndex = 1 + else if(mouse.y < size_.line * 4) + selectIndex = 2 + else if(mouse.y < size_.line * 6) + selectIndex = 0 + else + selectIndex = 3 + } + else { + if(mouse.y < size_.line * 2) + selectIndex = doubleCC.hideLR===1 ? 2 : 1 + else if(mouse.y < size_.line * 4) + selectIndex = 0 + else + selectIndex = 3 + } + } + onClicked: doubleColumn.toLR(selectIndex) + + // 控制按钮 视觉 + Rectangle { + color: theme.specialBgColor + visible: (splitterMouseArea.containsMouse || btnsMouseArea.containsMouse) && !splitterMouseArea.drag.active + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.bottom: parent.bottom + width: size_.line * 2 + radius: size_.panelRadius + + Column { + width: parent.width + Icon_ { + visible: doubleCC.hideLR!==1 + width: parent.width + height: width + color: btnsMouseArea.selectIndex===1 ? theme.textColor:theme.specialTextColor + icon: "arrow_to_left" + } + Icon_ { + visible: doubleCC.hideLR!==2 + width: parent.width + height: width + color: btnsMouseArea.selectIndex===2 ? theme.textColor:theme.specialTextColor + icon: "arrow_to_left" + mirror: true + } + Icon_ { + width: parent.width + height: width + color: btnsMouseArea.selectIndex===0 ? theme.textColor:theme.specialTextColor + icon: "arrow_to_center" + } + // 转为上下分栏 + Icon_ { + visible: doubleCC.isShowSplitView + width: parent.width + height: width + color: btnsMouseArea.selectIndex===3 ? theme.textColor:theme.specialTextColor + icon: "split_view" + rotation: 90 + } + } + } + } + + } + + // 右容器 + Item{ + id: rightContainer + anchors.left: splitter.right + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + onVisibleChanged: { + rightItem.parent = visible?rightContainer:hideContainer + } + } + + // 隐藏容器 + Item { + id: hideContainer + visible: false + width: Math.max(doubleCC.width, 400) + anchors.top: parent.top + anchors.bottom: parent.bottom + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/DoubleSwitchableLayout.qml b/UmiOCR-data/qt_res/qml/Widgets/DoubleSwitchableLayout.qml new file mode 100644 index 0000000..c79324c --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/DoubleSwitchableLayout.qml @@ -0,0 +1,83 @@ +// ===================================================== +// =============== 可切换左右/上下双栏布局 =============== +// ===================================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +Item { + + // ========================= 【可调参数】 ========================= + + property QtObject itemA // 第一子组件,左或上 + property QtObject itemB // 第二子组件,右或下 + property bool isRow: true // t为左右布局,f为上下 + property string saveKey: "" // 如果非空,则缓存 isRow 和布局的 hide 参数。 + // 只读:0为不隐藏,1为隐藏第一子组件,2为隐藏第二子组件 + property int hideAB: 0 + + id: doubleLayout + + Component.onCompleted: { + // 取 isRow 缓存 + Qt.callLater(() => { + if(doubleLayout.saveKey) { + const layoutDict = qmlapp.globalConfigs.getValue("window.doubleLayout") + const f = layoutDict[doubleLayout.saveKey] + if(f === true || f === false) isRow = f // bool类型合法性检查 + } + }) + } + + // 切换布局 + function toSwitchView(toRow) { // isRow 要变为的值 + isRow = toRow + hideAB = doubleLayout.isRow ? rowLayout.hideLR : columnLayout.hideTB + // 缓存 isRow + if(doubleLayout.saveKey) { + let layoutDict = qmlapp.globalConfigs.getValue("window.doubleLayout") + layoutDict[doubleLayout.saveKey] = toRow + qmlapp.globalConfigs.setValue("window.doubleLayout", layoutDict) + } + } + + // 左右布局 + DoubleRowLayout { + id: rowLayout + anchors.fill: parent + visible: doubleLayout.isRow + leftItem: doubleLayout.isRow ? itemA : placeholderA + rightItem: doubleLayout.isRow ? itemB : placeholderB + isShowSplitView: true + saveKey: doubleLayout.saveKey==="" ? "" : doubleLayout.saveKey+"row" + onSwitchView: toSwitchView(false) + onHideLRChanged: { + if(doubleLayout.isRow) doubleLayout.hideAB = hideLR + } + } + // 上下布局 + DoubleColumnLayout { + id: columnLayout + anchors.fill: parent + visible: !doubleLayout.isRow + topItem: doubleLayout.isRow ? placeholderA : itemA + bottomItem: doubleLayout.isRow ? placeholderB : itemB + isShowSplitView: true + saveKey: doubleLayout.saveKey==="" ? "" : doubleLayout.saveKey+"col" + onSwitchView: toSwitchView(true) + onHideTBChanged: { + if(!doubleLayout.isRow) doubleLayout.hideAB = hideTB + } + } + + // 临时占位用子组件 + Item { + id: placeholderA + visible: false + } + Item { + id: placeholderB + visible: false + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/DropArea_.qml b/UmiOCR-data/qt_res/qml/Widgets/DropArea_.qml new file mode 100644 index 0000000..54a45a8 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/DropArea_.qml @@ -0,0 +1,29 @@ +// ============================================= +// =============== 拖入图片的区域 =============== +// ============================================= + +import QtQuick 2.15 + +// 鼠标拖入图片 +DropArea { + property var callback: undefined + property bool enable: true // 是否启用 + property string tips: qsTr("松手放入文件") + onEntered: { + if(!enable) return + qmlapp.popup.showMask(tips, "DropImage") + } + onExited: { + if(!enable) return + qmlapp.popup.hideMask("DropImage") + } + onDropped: { + if(!enable) return + qmlapp.popup.hideMask("DropImage") + if(drop.hasUrls){ + var urls = qmlapp.utilsConnector.QUrl2String(drop.urls) + if(urls.length > 0 && callback) + callback(urls) + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/FileDialog_.qml b/UmiOCR-data/qt_res/qml/Widgets/FileDialog_.qml new file mode 100644 index 0000000..bbc3a3d --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/FileDialog_.qml @@ -0,0 +1,13 @@ +// ========================================= +// =============== 文件选择窗 =============== +// ========================================= + +import QtQuick.Dialogs 1.3 + +FileDialog { + property var fileUrls_: [] // 缓存处理好的 fileUrls + + onAccepted: { + fileUrls_ = qmlapp.utilsConnector.QUrl2String(fileUrls) + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/FilesTableView.qml b/UmiOCR-data/qt_res/qml/Widgets/FilesTableView.qml new file mode 100644 index 0000000..83ad250 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/FilesTableView.qml @@ -0,0 +1,429 @@ +// =============== 文件表格面板 =============== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import Qt.labs.qmlmodels 1.0 // 表格 +import QtGraphicalEffects 1.15 // 子元素圆角 +import QtQuick.Dialogs 1.3 // 文件对话框 + +Item { + id: fTableRoot + + // ========================= 【定义】 ========================= + + // 表头。定义每一列。 + property var headers: [ + // 第一列也作为总key(tk),不允许重复。 + {key: "path", title: "文件", }, + {key: "time", title: "耗时", }, + {key: "state", title: "状态", }, + // 可选项: + // btn: true 启用按钮 + // onClicked: 单击函数 + // left: true 左对齐 + // display: 显示函数,输入value,返回显示文本 + ] + property string openBtnText: "选择文件" + property string clearBtnText: "清空" + property string defaultTips: "拖入或选择文件" + property string fileDialogTitle: "请选择文件" + property var fileDialogNameFilters: ["文件 (*.jpg *.jpe *.jpeg *.jfif *.png *.webp *.bmp *.tif *.tiff)"] + property int spacing: size_.smallLine // 表项水平间隔 + property int minWidth0: size_.smallLine * 5 // 第0列最小宽度 + property bool isLock: false // 是否锁定UI操作 + + + // ========================= 【调用接口】 ========================= + + // 增:添加一项。 row:字典,key在headers中,如 { "path" "time" "state" } + // ik:可以是表格行index(int),也可以是总key(string) + function add(row, ik=-1) { + const key = row[headerKey] + if(key in dataDict) { + console.warn(`add: ${key} 已在dataDict中!`) + return false + } + if(ik === -1 || ik === rowCount) { + dataDict[key] = rowCount + dataModel.append(row) + } + else { + const i = ik2i(ik) + if(i < 0) { + console.warn(`add: ik ${ik} ${i} < 0 !`) + return false + } + dataDict[key] = i + dataModel.insert(i, row) + } + updateWidth() + return true + } + // 删:删除一项 + function del(ik) { + const i = ik2i(ik) + if(i < 0) { + console.warn(`del: ik ${ik} ${i} < 0 !`) + return false + } + const key = dataModel.get(i)[headerKey] + delete dataDict[key] + dataModel.remove(i) + return true + } + // 删:清空 + function clear() { + dataModel.clear() + dataDict = {} + } + // 改:属性字典 + function set(ik, columnDict) { + const i = ik2i(ik) + if(i < 0) { + console.warn(`set: ik ${ik} ${i} < 0 !`) + return false + } + dataModel.set(i, columnDict) + updateWidth() + return true + } + // 改:单个属性 + function setProperty(ik, columnKey, value) { + const i = ik2i(ik) + if(i < 0) { + console.warn(`setProperty: ik ${ik} ${i} < 0 !`) + return false + } + dataModel.setProperty(i, columnKey, value) + updateWidth() + return true + } + // 查:ik转index。返回-1表示失败。 + function ik2i(ik) { + if (typeof ik === "number") { + if(ik >= 0 && ik < rowCount) + return ik + } else if (typeof ik === "string") { + if(ik in dataDict) + return dataDict[ik] + } + return -1 + } + // 查:获取单个行的字典 + function get(ik) { + const i = ik2i(ik) + if(i < 0) { + console.warn(`get: ik ${ik} ${i} < 0 !`) + return {} + } + return dataModel.get(i) + } + // 查:获取key列的所有数据,返回每项为value + function getColumnsValue(key) { + let list = [] + for(let y = 0; y < rowCount; y++) { + list.push( dataModel.get(y)[key] ) + } + return list + } + // 查:获取多个列的数据,返回每项为字典 + function getColumnsValues(keys=[]) { + let list = [] + if(keys.length > 0) { + for(let y = 0; y < rowCount; y++) { + const data = dataModel.get(y) + const d = {} + for(let i in keys) + d[keys[i]] = data[keys[i]] + list.push(d) + } + } + else { + for(let y = 0; y < rowCount; y++) + list.push(dataModel.get(y)) + } + return list + } + + // 定义信号 + signal addPaths(var paths) // 添加文件的信号 + signal click(var info) // 点击条目的信号 + + Component.onCompleted: { + dataDict = {} + columnCount = headers.length + for(let i=0; i ws[i]) ws[i] = maxWidth + } + // 表体 + for(let y in tableView.items) { + const repeater = tableView.items[y].repeater + for(let x = 1; x < columnCount; x++) { + let maxWidth = repeater.itemAt(x).maxWidth + fTableRoot.spacing*2 + if(maxWidth > ws[x]) ws[x] = maxWidth + } + } + // 赋值 / 计算第0列宽度 + let w0 = tableArea.width + for(let i = 1; i < columnCount; i++) { + headerModel.setProperty(i, "width", ws[i]) + w0 -= ws[i] + } + // 更新第0列宽度 + updateWidth0(w0) + } + // 更新第0列宽度 + function updateWidth0(w0 = -1) { + if(headerModel.count <= 0) return + if(w0 < 0) { + w0 = tableArea.width + for(let i = 1; i < columnCount; i++) + w0 -= headerModel.get(i).width + } + w0 += columnCount-10 // 避让右侧滚动条空间 + if(w0 < minWidth0) w0 = minWidth0 + headerModel.setProperty(0, "width", w0) + } + + // ========================= 【布局】 ========================= + + // 表格区域 + Rectangle { + id: tableArea + anchors.fill: parent + color: theme.bgColor + + Item { + id: tableContainer + anchors.fill: parent + + // 上方操控版 + Item { + id: tableTopPanel + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: size_.line * 2 + + // 左打开图片按钮 + IconTextButton { + id: openBtn + visible: parent.width > openBtn.width + clearBtn.width // 容器宽度过小时隐藏 + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.margins: size_.smallSpacing * 0.5 + icon_: "folder" + text_: openBtnText + + onClicked: { + if(isLock) return + fileDialog.open() + } + } + + // 右清空按钮 + IconTextButton { + id: clearBtn + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.margins: size_.smallSpacing * 0.5 + icon_: "clear" + text_: clearBtnText + + onClicked: { + if(isLock) return + fTableRoot.clear() + } + } + } + + // 提示 + DefaultTips { + visibleFlag: fTableRoot.rowCount + anchors.top: tableTopPanel.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + tips: defaultTips + } + + // 表头 + Item { + id: tableHeaderContainer + visible: fTableRoot.rowCount > 0 + anchors.top: tableTopPanel.bottom + anchors.left: parent.left + anchors.right: parent.right + height: size_.line * 1.5 + onWidthChanged: updateWidth0() + + Row { + anchors.fill: parent + spacing: -1 + Repeater { + model: headerModel + id: headerRepeater + Rectangle { + width: model.width + anchors.top: parent.top + anchors.bottom: parent.bottom + color: theme.bgColor + border.width: 1 + border.color: theme.coverColor2 + property alias maxWidth: hText.width + Text_ { + id: hText + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + verticalAlignment: Text.AlignVCenter // 垂直居中 + font.pixelSize: size_.smallText + text: model.title + } + } + } + } + } + // 表体 + TableView { + id: tableView + anchors.top: tableHeaderContainer.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + flickableDirection: Flickable.VerticalFlick // 只允许垂直滚动 + boundsBehavior: Flickable.StopAtBounds // 禁止flick过冲。不影响滚轮滚动的过冲 + model: dataModel + clip: true + property var items: tableView.children[0].children + rowSpacing: -1 + delegate: Item { + Component.onCompleted: updateWidth() + TableView.onReused: updateWidth() + implicitHeight: size_.smallLine * 1.5 + implicitWidth: 1 + property int rowIndex: index + property var rowModel: model + property alias repeater: repeater + Row { + anchors.fill: parent + spacing: -1 + Repeater { + id: repeater + model: headerModel + Rectangle { + width: model.width + anchors.top: parent.top + anchors.bottom: parent.bottom + color: theme.bgColor + border.width: 1 + border.color: theme.coverColor2 + property alias maxWidth: hText.width + property int columnIndex: index + property string columnKey: model.key + property var header: headers[columnIndex] + clip: true + Button_ { + visible: header.btn?true:false + anchors.fill: parent + radius: 0 + onClicked: { + if(header.onClicked) { + header.onClicked(rowIndex) + } + } + } + Text_ { + id: hText + property bool isLeft: headers[columnIndex].left?true:false + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: isLeft? parent.left : undefined + anchors.leftMargin: fTableRoot.spacing * 0.5 + anchors.horizontalCenter: isLeft? undefined : parent.horizontalCenter + verticalAlignment: Text.AlignVCenter // 垂直居中 + font.pixelSize: size_.smallText + color: (columnKey != "state"|| typeof rowModel.state != "string" || rowModel.state.length == 0) ? theme.subTextColor : + (rowModel.state.startsWith("×") ? theme.noColor : (rowModel.state.startsWith("√") ? theme.yesColor : theme.subTextColor)) + text: header.display ? header.display(rowModel[columnKey]) : rowModel[columnKey] + } + } + } + } + } + // 滚动条 + ScrollBar.vertical: ScrollBar { } + } + } + + // 内圆角裁切 + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: tableContainer.width + height: tableContainer.height + radius: size_.btnRadius + } + } + } + + // 文件选择对话框 + // QT-5.15.2 会报错:“Model size of -225 is less than 0”,不影响使用。 + // QT-5.15.5 修复了这个Bug,但是PySide2尚未更新到这个版本号。只能先忍忍了 + // https://bugreports.qt.io/browse/QTBUG-92444 + FileDialog_ { + id: fileDialog + title: fileDialogTitle + nameFilters: fileDialogNameFilters + folder: shortcuts.pictures + selectMultiple: true // 多选 + onAccepted: { + addPaths(fileDialog.fileUrls_) + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/FramelessWindow.qml b/UmiOCR-data/qt_res/qml/Widgets/FramelessWindow.qml new file mode 100644 index 0000000..d73c8b8 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/FramelessWindow.qml @@ -0,0 +1,27 @@ +// ================================================== +// =============== 无边框窗口,支持拖拽 =============== +// ================================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Window 2.15 + +Window { + id: win + flags: Qt.FramelessWindowHint // 无边框 + + // 鼠标拖拽移动窗口 + MouseArea { + anchors.fill: parent + property int ox: 0 + property int oy: 0 + onPressed: { + ox = mouse.x + oy = mouse.y + } + onPositionChanged: { + win.setX(win.x+mouse.x-ox) + win.setY(win.y+mouse.y-oy) + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/HProgressBar.qml b/UmiOCR-data/qt_res/qml/Widgets/HProgressBar.qml new file mode 100644 index 0000000..1ae865b --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/HProgressBar.qml @@ -0,0 +1,32 @@ +// ========================================= +// =============== 水平进度条 =============== +// ========================================= + +import QtQuick 2.15 +import QtGraphicalEffects 1.15 + +Rectangle { + id: progressBarRoot + property real percent: 0 // 百分比,0~1 + property color highlightColor: theme.yesColor // 高亮颜色 + color: theme.coverColor1 // 背景颜色 + radius: size_.btnRadius // 整体圆角 + Rectangle { + width: percent * parent.width // 宽度动态计算 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + radius: parent.radius + color: parent.highlightColor + } + + // 内圆角裁切 + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: progressBarRoot.width + height: progressBarRoot.height + radius: progressBarRoot.radius + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/IconButton.qml b/UmiOCR-data/qt_res/qml/Widgets/IconButton.qml new file mode 100644 index 0000000..19c3174 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/IconButton.qml @@ -0,0 +1,20 @@ +// ======================================= +// =============== 图标按钮 =============== +// ======================================= +// 宽高可外部定义 + +import QtQuick 2.15 + +Button_ { + property string icon_: "" + property color color: theme.subTextColor + property real margins: Math.min(height * 0.1, size_.spacing) + + contentItem: Icon_ { + anchors.fill: parent + anchors.margins: parent.margins + + icon: icon_ + color: parent.color + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/IconButtonBar.qml b/UmiOCR-data/qt_res/qml/Widgets/IconButtonBar.qml new file mode 100644 index 0000000..845e9cb --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/IconButtonBar.qml @@ -0,0 +1,57 @@ +// ========================================= +// =============== 图标按钮栏 =============== +// ========================================= + +import QtQuick 2.15 + + +Row { + id: bRoot + /* btnList 每一项: { + // 必填 + "icon": "screenshot", // 图标 + // 选填 + "onClicked": func, // 回调函数 + "toolTip": "提示,支持", + "text": "显示文本", + "color": theme.subTextColor, + "bgColor": theme.subTextColor, + } */ + property var btnList: [] + + // 常用按钮的翻译 + function tr(text) { + return trTable[text] + } + property var trTable: { + "右键菜单": qsTr("右键菜单"), + "保存图片": qsTr("保存图片"), + "图片大小:适应窗口": qsTr("图片大小:适应窗口"), + "图片大小:实际": qsTr("图片大小:实际"), + + "截图模式": qsTr("截图模式"), + "截图": qsTr("截图"), + "粘贴": qsTr("粘贴"), + "屏幕截图": qsTr("屏幕截图"), + "粘贴图片": qsTr("粘贴图片"), + } + + + spacing: size_.smallSpacing + Repeater { + model: bRoot.btnList + + IconTextButton { + anchors.top: bRoot.top + anchors.bottom: bRoot.bottom + // width: height + icon_: modelData.icon + color: modelData.color || theme.subTextColor + bgColor_: modelData.bgColor || "#00000000" + text_: modelData.text || "" + toolTip: modelData.toolTip || "" + // 函数类型onClicked只能用 [index] 访问 + onClicked: btnList[index].onClicked ? btnList[index].onClicked() : undefined + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/IconTextButton.qml b/UmiOCR-data/qt_res/qml/Widgets/IconTextButton.qml new file mode 100644 index 0000000..bfa371e --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/IconTextButton.qml @@ -0,0 +1,40 @@ +// ============================================== +// =============== 图标+文字的按钮 =============== +// ============================================== + +import QtQuick 2.15 + +Button_ { + id: btn + property string icon_: "" + property string text_: "" + property color color: theme.subTextColor + width: size_.line + size_.smallSpacing * 2 + (text_ ? btnText.width + size_.smallSpacing * 0.5 : 0) + implicitWidth: width + + contentItem: Item { + anchors.fill: parent + Icon_ { + id: btnIcon + icon: icon_ + height: size_.line + width: size_.line + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: size_.smallSpacing + color: btn.color + } + Text_ { + id: btnText + anchors.left: btnIcon.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.leftMargin: size_.smallSpacing * 0.5 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: text_ + width: text_ ? undefined : 0 + color: btn.color + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/Icon_.qml b/UmiOCR-data/qt_res/qml/Widgets/Icon_.qml new file mode 100644 index 0000000..b2f29d3 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/Icon_.qml @@ -0,0 +1,30 @@ +import QtQuick 2.0 +import QtGraphicalEffects 1.15 // 改颜色 + + +Item { + // ========================= 【设定值】 ========================= + + property string icon: "" + property color color: theme.subTextColor + property bool mirror: false // 水平翻转 + + // ============================================================= + id: iconRoot + Image { + id: image + anchors.fill: parent + source: icon ? `../../images/icons/${icon}.svg` : "" + fillMode: Image.PreserveAspectFit // 均匀缩放 + visible: false // 关闭原图显示,只显示填充颜色 + mirror: iconRoot.mirror + } + // 填充颜色 + ColorOverlay{ + id: overlay + anchors.fill: parent + source: image + color: parent.color + cached: true + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/IgnoreArea/IgnoreArea.qml b/UmiOCR-data/qt_res/qml/Widgets/IgnoreArea/IgnoreArea.qml new file mode 100644 index 0000000..47a1b77 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/IgnoreArea/IgnoreArea.qml @@ -0,0 +1,191 @@ +// ============================================= +// =============== 忽略区域编辑器 =============== +// ============================================= + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import ".." + +ModalLayer { + id: iRoot + property string previewPath: "" // 图片预览路径 + property string previewType: "" // 图片预览路径 + property bool running: false + property var configsComp: undefined // 设置组件 + property string configKey: "" // 设置组件中忽略区域的key + + // 显示面板 + function show() { + running = false + iRoot.visible = true + let initArea = configsComp.getValue(configKey) + if(initArea && initArea.length>0) { + // 读取设置,反格式化 + let ig1 = [] + for(let i=0,l=initArea.length; i 0) { + // 格式化,存入设置 + let ig1 = [] + for(let i=0,l=imageWithIgnore.ig1Boxes.length; i { + textBoxes = [] // 清空旧文本块 + } + // 展示文本块 + function showTextBoxes(res) { + beforeShow() + // 提取文本框 + if(res.code == 100 && res.data.length > 0) { + let tbs = [] + for(let i in res.data) { + const d = res.data[i] + const info = { + x: d.box[0][0], + y: d.box[0][1], + width: d.box[2][0] - d.box[0][0], + height: d.box[2][1] - d.box[0][1], + text: d.text, + } + tbs.push(info) + } + textBoxes = tbs + } + } + // 显示/隐藏叠加层 + function switchOverlay() { + showOverlay = !showOverlay + } + + // 撤销 + function revokeIg() { + let bs = ig1Boxes + if(bs.length > 0) + bs.pop() + ig1Boxes = bs + } + // 清空 + function clearIg() { + ig1Boxes = [] + } + + property var textBoxes: [] // 文本块列表 + property var ig1Boxes: [] // 忽略区域1 + + // 文本框叠加层 + overlayLayer: Item { + id: oRoot + anchors.fill: parent + visible: showOverlay + property real borderWidth: Math.max(1, 1/iRoot.scale) + + // 文本块 + Repeater { + model: textBoxes + Rectangle { + x: modelData.x + y: modelData.y + width: modelData.width + height: modelData.height + color: "#00000000" + border.width: oRoot.borderWidth + border.color: "#000" + Rectangle { + anchors.fill: parent + anchors.margins: oRoot.borderWidth + color: "#22000000" + border.width: oRoot.borderWidth + border.color: "#FFF" + } + } + } + + // 忽略区域1 + Repeater { + model: ig1Boxes + Rectangle { + x: modelData.x + y: modelData.y + width: modelData.width + height: modelData.height + color: "#99000000" + border.width: oRoot.borderWidth + border.color: "#FFFF00" + } + } + } + // 十字线与当前选区 + Item { + id: cross + anchors.fill: parent + clip: true + property int min: -2147480000 + property int px: cross.min // 十字线 + property int py: cross.min + property int ax: cross.min // 当前选区 + property int ay: cross.min + property int aw: cross.min + property int ah: cross.min + Rectangle { + visible: y > cross.min + color: crossLineColor + anchors.left: parent.left + anchors.right: parent.right + y: cross.py + height: lineWidth + } + Rectangle { + visible: x > cross.min + color: crossLineColor + anchors.top: parent.top + anchors.bottom: parent.bottom + x: cross.px + width: lineWidth + } + Rectangle { + visible: x>cross.min && y>cross.min + x: cross.ax + y: cross.ay + width: cross.aw + height: cross.ah + border.width: lineWidth + border.color: crossLineColor + color: "#00000000" + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + // 拖拽起始和结束的坐标 + property int startX: -1 + property int startY: -1 + property int endX: -1 + property int endY: -1 + acceptedButtons: Qt.LeftButton | Qt.RightButton + + // 添加选区框 + function addSelectBox() { + let bs = ig1Boxes + let x1 = cross.ax, y1 = cross.ay, x2 = x1 + cross.aw, y2 = y1 + cross.ah + cross.ax = cross.ay = cross.aw = cross.ah = cross.min + const p1 = oRoot.mapFromItem(mouseArea, x1, y1) + const p2 = oRoot.mapFromItem(mouseArea, x2, y2) + x1 = p1.x; y1 = p1.y; x2 = p2.x; y2 = p2.y; + bs.push({ + "x": x1, + "y": y1, + "width": x2 - x1, + "height": y2 - y1, + }) + ig1Boxes = bs + } + // 按下 + onPressed: { + cross.px = cross.py = cross.min + if(iRoot.imageSW===0 || iRoot.imageSH===0) { + startX = startY = endX = endY = -1 + return + } + if (mouse.button === Qt.LeftButton) { + mouse.accepted = false + } + else if (mouse.button === Qt.RightButton) { + startX = mouseX + startY = mouseY + endX = endY = 0 + } + } + // 移动 + onPositionChanged: { + if(iRoot.imageSW===0 || iRoot.imageSH===0) { + return + } + endX = mouseX + endY = mouseY + if(pressed) { + cross.px = cross.py = cross.min + // 更新选区框 + if(startX < endX) { + cross.ax = startX + cross.aw = endX - startX + } + else { + cross.ax = endX + cross.aw = startX - endX + } + if(startY < endY) { + cross.ay = startY + cross.ah = endY - startY + } + else { + cross.ay = endY + cross.ah = startY - endY + } + } + else { + cross.px = mouseX + cross.py = mouseY + } + } + // 抬起 + onReleased: { + if(iRoot.imageSW===0 || iRoot.imageSH===0) { + return + } + cross.px = endX = mouseX + cross.py = endY = mouseY + mouseArea.addSelectBox() + startX = startY = endX = endY = -1 + } + // 离开 + onExited: { + cross.px=cross.py=cross.ax=cross.ay=cross.min + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/ImageViewer/ImageScale.qml b/UmiOCR-data/qt_res/qml/Widgets/ImageViewer/ImageScale.qml new file mode 100644 index 0000000..f82c561 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/ImageViewer/ImageScale.qml @@ -0,0 +1,229 @@ +// ================================================== +// =============== 可缩放的图片预览组件 =============== +// ================================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import ".." + +Rectangle { + id: iRoot + // ========================= 【接口】 ========================= + + // 可设置 + property real scaleMax: 2.0 // 比例上下限 + property real scaleMin: 0.1 // 比例上下限 + property QtObject overlayLayer // 图片叠加层 + property var border: bRect.border // 边框 + // 只读 + property alias showImage: showImage // 图片组件 + property real scale: 1.0 // 图片缩放比例 + property int imageSW: 0 // 图片原始宽高 + property int imageSH: 0 + // 子类重写 + property var beforeShow: undefined // 展示图片之前执行的操作 + + // 设置图片源,展示一张图片 + function setSource(source) { + if(source) { + // 特殊字符#替换为%23 + if(source.startsWith("file:///") && source.includes("#")) + source = source.replace(new RegExp("#", "g"), "%23"); + showImage.source = source // 设置源 + } + else + showImage.source = "" + } + + // 传入路径,展示图片 + function showPath(path) { + if(beforeShow) beforeShow() + showImage.showPath(path) + } + + // 传入imgID,展示图片 + function showImgID(imgID) { + if(beforeShow) beforeShow() + showImage.showImgID(imgID) + } + + // 清空展示 + function clear() { + if(beforeShow) beforeShow() + // showImage.clear() + showImage.source = "" + imageSW = imageSH = 0 + } + + // 复制当前图片 + function copyImage() { + if(showImage.source == "") return + const res = qmlapp.imageManager.copyImage(showImage.source) + if(res === "[Success]") + qmlapp.popup.simple(qsTr("复制图片"), "") + else + qmlapp.popup.simple(qsTr("复制图片失败"), res) + } + + // 用系统默认应用打开图片 + function openImage() { + if(showImage.source == "") return + const res = qmlapp.imageManager.openImage(showImage.source) + if(res === "[Success]") + qmlapp.popup.simple(qsTr("打开图片"), "") + else + qmlapp.popup.simple(qsTr("打开图片失败"), res) + } + + // 保存当前图片 + function saveImage() { + if(showImage.source == "") return + saveDialog.open() + } + + FileDialog_ { + id: saveDialog + title: qsTr("保存图片") + selectExisting: false + selectFolder: false + folder: shortcuts.desktop // 默认放桌面 + nameFilters: ["*.png", "*.jpg"] + onAccepted: { + if(!fileUrl) { + console.log("文件对话框:未选择任何文件") + return + } + let filePath = fileUrl + const res = qmlapp.imageManager.saveImage(showImage.source, filePath) + if(res.startsWith("[Success]")) + qmlapp.popup.simple(qsTr("保存图片"), res) + else + qmlapp.popup.simple(qsTr("保存图片失败"), res) + } + } + + // ========================= 【处理】 ========================= + + Component.onCompleted: { + // 叠加层挂父级 + if(overlayLayer && overlayLayer.hasOwnProperty("parent")) + overlayLayer.parent = showImage + } + + + // 图片组件的状态改变 + function imageStatusChanged(s) { + // 已就绪 + if(s == Image.Ready) { + imageSW = showImage.sourceSize.width // 记录图片原始宽高 + imageSH = showImage.sourceSize.height + imageFullFit() // 初始大小 + } + else { + imageSW = imageSH = 0 + iRoot.scale = 1 + } + } + + // 缩放,传入 flag>0 放大, <0 缩小 ,0回归100%。以相框中心为锚点。 + function imageScaleAddSub(flag=0, step=0.1) { + if(showImage.status != Image.Ready) return + // 计算缩放比例 + let s = 1.0 // flag==0 时复原 + if (flag > 0) { // 放大 + s = (iRoot.scale + step).toFixed(1) + // 禁止大于上限 或 图片填满大小(裁切) + const max = Math.max(flickable.width/imageSW, flickable.height/imageSH, scaleMax) + if(s > max) s = max + } + else if(flag < 0) { // 缩小 + s = (iRoot.scale - step).toFixed(1) + // 禁止小于下限 或 图片填满大小(不裁切) + const min = Math.min(flickable.width/imageSW, flickable.height/imageSH, scaleMin) + if(s < min) s = min + } + + // 目标锚点 + let gx = -flickable.width/2 + let gy = -flickable.height/2 + // 目标锚点在图片中的原比例 + let s1x = (flickable.contentX-gx)/showImageContainer.width + let s1y = (flickable.contentY-gy)/showImageContainer.height + // 目标锚点在图片中的新比例,及差值 + iRoot.scale = s // 更新缩放 + let s2x = (flickable.contentX-gx)/showImageContainer.width + let s2y = (flickable.contentY-gy)/showImageContainer.height + let sx = s2x-s1x + let sy = s2y-s1y + // 实际长度差值 + let lx = sx*showImageContainer.width + let ly = sy*showImageContainer.height + // 偏移 + flickable.contentX -= lx + flickable.contentY -= ly + } + + // 图片填满组件,不裁切 + function imageFullFit() { + if(showImage.source == "" || imageSW <= 0) return + iRoot.scale = Math.min(flickable.width/imageSW, flickable.height/imageSH) + // 图片中心对齐相框 + flickable.contentY = - (flickable.height - showImageContainer.height)/2 + flickable.contentX = - (flickable.width - showImageContainer.width)/2 + } + + // ======================== 【布局】 ========================= + color: theme.bgColor + + + // 滑动区域,显示图片,监听左键拖拽 + Flickable { + id: flickable + anchors.fill: parent + contentWidth: showImageContainer.width + contentHeight: showImageContainer.height + clip: true + + // 图片容器,大小不小于滑动区域 + Item { + id: showImageContainer + width: Math.max( imageSW * iRoot.scale , flickable.width ) + height: Math.max( imageSH * iRoot.scale , flickable.height ) + Image_ { + id: showImage + anchors.centerIn: parent + scale: iRoot.scale + onStatusChanged: imageStatusChanged(status) + } + } + + // 滚动条 + ScrollBar.vertical: ScrollBar { } + ScrollBar.horizontal: ScrollBar { } + } + + // 监听滚轮缩放 + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + // 滚轮缩放 + onWheel: { + if (wheel.angleDelta.y > 0) { + imageScaleAddSub(1) // 放大 + } + else { + imageScaleAddSub(-1) // 缩小 + } + } + } + + // 边框 + Rectangle { + id: bRect + anchors.fill: parent + color: "#00000000" + border.width: 1 + border.color: theme.coverColor4 + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/ImageViewer/ImageWithText.qml b/UmiOCR-data/qt_res/qml/Widgets/ImageViewer/ImageWithText.qml new file mode 100644 index 0000000..90ef0d1 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/ImageViewer/ImageWithText.qml @@ -0,0 +1,300 @@ +// ========================================================== +// =============== 可显示OCR文本的增强Image组件 =============== +// ========================================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import ".." + +ImageScale { + id: iRoot + property bool showOverlay: true // 显示叠加层 + + Component.onCompleted: { + // 默认显示/关闭叠加层 + showOverlay = qmlapp.globalConfigs.getValue("ui.imgShowOverlay") + } + + beforeShow: () => { + mouseArea.initIndex() // 清空选字参数 + textBoxes = [] // 清空旧文本块 + } + + // 展示文本块 + function showTextBoxes(res) { + beforeShow() + // 提取文本框 + if(res.code == 100 && res.data.length > 0) { + let tbs = [] + for(let i in res.data) { + const d = res.data[i] + const info = { + x: d.box[0][0], + y: d.box[0][1], + width: d.box[2][0] - d.box[0][0], + height: d.box[2][1] - d.box[0][1], + text: d.text, + end: d.end || "", // 行尾间隔符 + } + tbs.push(info) + } + textBoxes = tbs + } + } + + // 弹出菜单 + function popupMenu() { + selectMenu.popup() + } + // 显示/隐藏叠加层 + function switchOverlay() { + showOverlay = !showOverlay + if(!showOverlay) + mouseArea.cursorShape = Qt.OpenHandCursor + } + + property var textBoxes: [] // 文本块列表 + + // 文本块叠加层 + overlayLayer: Item { + id: oRoot + anchors.fill: parent + visible: showOverlay + + Repeater { + id: textBoxRepeater + model: textBoxes + TextBox { + text: modelData.text + x: modelData.x + y: modelData.y + end: modelData.end // 结尾间隔符 + + Component.onCompleted: { + width = modelData.width + height = modelData.height + resetSize() // 自适应字体和组件大小 + textBoxes[index].width = width // 记录修改后的组件大小 + textBoxes[index].height = height + textBoxes[index].obj = this + } + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + property int startIndex: -1 // 拖拽开始时,文本框序号 + property int startTextIndex: -1 // 拖拽开始时,字符序号 + property int endIndex: -1 // 拖拽结束时,文本框序号 + property int endTextIndex: -1 // 拖拽结束时,字符序号 + property int selectUpdate: 0 + acceptedButtons: Qt.LeftButton | Qt.RightButton + + // 清除index + function initIndex() { + startIndex = startTextIndex = endIndex = endTextIndex = -1 + } + // 检测当前鼠标点,位于哪一个tb内 + function mouseInTextBox() { + const localPoint = oRoot.mapFromItem(mouseArea, mouseX, mouseY) + const x = localPoint.x, y = localPoint.y + for (let i=0,l=textBoxes.length; i= rect.x && x <= rect.x + rect.width && + y >= rect.y && y <= rect.y + rect.height) { + return i + } + } + return -1 + } + // 检测当前鼠标点,在index tb内的哪一个字符处 + function mouseInTextIndex(index) { + return textBoxes[index].obj.where(mouseArea, mouseX, mouseY) + } + // 获取Index正确顺序。返回: [li 起始块, lt 起始块选区左侧, ri 结束块, rt 结束块选区右侧] + function getIndexes() { + let li, lt, ri, rt + if(startIndex < endIndex) { + li=startIndex; lt=startTextIndex; ri=endIndex; rt=endTextIndex; + } + else if(startIndex > endIndex) { + li=endIndex; lt=endTextIndex; ri=startIndex; rt=startTextIndex; + } + else { + li = ri = startIndex + if(startTextIndex < endTextIndex) { + lt=startTextIndex; rt=endTextIndex; + } + else if(startTextIndex > endTextIndex) { + lt=endTextIndex; rt=startTextIndex; + } + else { // 单击,未选中 + lt = rt = -1 + } + } + return [li, lt, ri, rt] + } + // 根据 Index 的参数,选择对应文本。 + function selectIndex() { + const [li, lt, ri, rt] = getIndexes() + // 遍历每个文本框数据 + for (let i = 0, l=textBoxes.length; i < l; i++) { + const tEdit = textBoxes[i].obj.textEdit + if( li<0 || ri<0 || i
  • ri ) { // 未被选中 + tEdit.deselect() + } + else if(i === li && i === ri) { // 单个块 + if(lt === rt) // 无有效选中 + tEdit.deselect() + else + tEdit.select(lt, rt) + } + else if(i === li) { // 多个块的起始 + const len = textBoxes[i].text.length + tEdit.select(lt, len) + } + else if(i === ri) { // 多个块的结束 + tEdit.select(0, rt) + } + else { // 多个块的中间 + tEdit.selectAll(0, rt) + } + } + } + // 全选 + function selectAll() { + const l = textBoxes.length + if(l === 0) return + startIndex = startTextIndex = 0 + endIndex = l-1 + endTextIndex = textBoxes[endIndex].text.length + selectIndex() + } + // 复制已选中的内容 + function selectCopy() { + let [li, lt, ri, rt] = getIndexes() + // 没有有效选中,则复制全部 + if(li<0 || ri<0 || (li===ri && lt===rt)) { + selectAllCopy() + return + } + let copyText = "" + // 选中单个文本块 + if(li === ri) { + copyText = textBoxes[li].text.substring(lt, rt) + } + // 选中多个块,则遍历多个块,提取各自的文本 + else { + for(let i = li; i <= ri; i++) { + const text = textBoxes[i].text + const end = textBoxes[i].end + if(i === li) // 多个块的起始 + copyText = text.substring(lt) + end + else if(i === ri) // 多个块的结束 + copyText += text.substring(0, rt) + else // 多个块的中间 + copyText += text + end + } + } + if(copyText && copyText.length > 0) { + qmlapp.utilsConnector.copyText(copyText) + qmlapp.popup.simple(qsTr("图片:复制%1字").arg(copyText.length), "") + } + else { + qmlapp.popup.simple(qsTr("图片:无选中文字"), "") + } + } + // 复制所有 + function selectAllCopy() { + let copyText = "" + for (let i = 0, l=textBoxes.length; i < l; i++) { + copyText += textBoxes[i].text + if(i < l-1) copyText += textBoxes[i].end + } + qmlapp.utilsConnector.copyText(copyText) + qmlapp.popup.simple(qsTr("图片:复制全部%1字").arg(copyText.length), "") + selectAll() + } + + // 按下 + onPressed: { + mouseArea.forceActiveFocus() + if (mouse.button === Qt.RightButton) { + selectMenu.popup() + return + } + if(!showOverlay) { + mouse.accepted = false + return + } + initIndex() + const tbi = mouseInTextBox() + cursorShape = tbi < 0 ? Qt.ClosedHandCursor : Qt.IBeamCursor + if(tbi >= 0) { // 选择文本 + startIndex = tbi + startTextIndex = mouseInTextIndex(tbi) + } + else { + mouse.accepted = false + } + } + // 移动 + onPositionChanged: { + if(!showOverlay) return + const tbi = mouseInTextBox() + if(pressed) { // 拖拽中 + if(tbi >= 0) { // 选择文本 + endIndex = tbi + endTextIndex = mouseInTextIndex(tbi) + selectIndex() + } + } + else { // 悬停中 + cursorShape = tbi < 0 ? Qt.OpenHandCursor : Qt.IBeamCursor + } + } + // 抬起 + onReleased: { + if(!showOverlay) return + if (mouse.button === Qt.RightButton) return + const tbi = mouseInTextBox() + cursorShape = tbi < 0 ? Qt.OpenHandCursor : Qt.IBeamCursor + if(startIndex === -1) return + if(tbi >= 0) { + endIndex = tbi + endTextIndex = mouseInTextIndex(tbi) + } + selectIndex() + } + // 菜单 + Menu_ { + id: selectMenu + menuList: [ + [mouseArea.selectCopy, qsTr("复制  (Ctrl+C)")], + [mouseArea.selectAll, qsTr("全选  (Ctrl+A)")], + [iRoot.copyImage, qsTr("复制图片(Ctrl+X)")], + [iRoot.saveImage, qsTr("保存图片(Ctrl+S)")], + [iRoot.switchOverlay, qsTr("显示/隐藏文字(Tab)")], + [iRoot.openImage, qsTr("用默认应用打开图片")], + [iRoot.clear, qsTr("删除图片(Ctrl+D)"), "noColor"], + ] + } + // 按键事件 + Keys.onPressed: { + if (event.modifiers & Qt.ControlModifier) { + event.key===Qt.Key_A && selectAll() + event.key===Qt.Key_C && selectCopy() + event.key===Qt.Key_X && iRoot.copyImage() + event.key===Qt.Key_S && iRoot.saveImage() + event.key===Qt.Key_D && iRoot.clear() + } + if (event.key === Qt.Key_Tab) { + iRoot.switchOverlay() + Qt.callLater(mouseArea.forceActiveFocus) + } + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/ImageViewer/TextBox.qml b/UmiOCR-data/qt_res/qml/Widgets/ImageViewer/TextBox.qml new file mode 100644 index 0000000..3dbaac4 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/ImageViewer/TextBox.qml @@ -0,0 +1,87 @@ +// ============================================= +// =============== 单个文本块组件 =============== +// ============================================= + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import ".." + +Item { + id: tRoot + property string text: "" + + // 颜色固定,不受主题影响 + property color bgColor: "#99000000" + property color textColor: "#FFFFFF" + + // 外部接口,重设字体和组件大小 + property var resetSize: textEdit.resetFontSize + property alias textEdit: textEdit // 文本组件 + property string end: "" // 结尾间隔符 + + // 传入一个相对于item的坐标,返回该坐标的文本序号。 + function where(item, mx, my) { + const textPoint = textEdit.mapFromItem(item, mx, my) + const textPos = textEdit.positionAt(textPoint.x, textPoint.y) + return textPos + } + + // 背景 + Rectangle { + id: bgRect + anchors.fill: parent + color: bgColor + } + // 结尾标识 + Icon_ { + icon: "line_feed" + visible: end == "\n" // 标记换行符 + anchors.left: parent.right + anchors.bottom: parent.bottom + anchors.leftMargin: 1 + height: Math.min(tRoot.height * 0.8 ,tRoot.width * 0.8 , size_.line * 2) + width: height + color: theme.subTextColor + } + // 文本 + TextEdit_ { + id: textEdit + anchors.fill: parent + color: textColor + readOnly: true // 只读 + selectByMouse: false // 禁止选择文本 + selectByKeyboard: false + persistentSelection: true // 丢失焦点时,保留选区 + font.pixelSize: 10 // 初始:10像素 + + // 重设字体大小,以适合组件大小 + function resetFontSize() { + text = tRoot.text + // 初次调整,利用初始文字面积与容器面积的比值,计算字体大小 + let s = 1 + if(contentWidth>0 && contentHeight>0) + s = (width * height) / (contentWidth * contentHeight) + let ps = font.pixelSize * Math.sqrt(s) + font.pixelSize = ps + // 二次调整:如果文本比容器高出至少半行,则减小字体大小,直到不高于容器 + if(contentHeight >= height+(ps*0.5)) { + // 为了保持性能,限定调整的最大次数 + for(let i=0; i<10 && contentHeight > height; i++) { + font.pixelSize-- + } + } + // 二次调整:如果当前只有一行,则优化字间距 + if(lineCount === 1 && text.length > 0) { + const s = (tRoot.width - contentWidth) / text.length + if(s > 0) { + font.letterSpacing = s + } + } + // 优化整体宽高 + if(contentWidth > tRoot.width) + tRoot.width = contentWidth + if(contentHeight > tRoot.height) + tRoot.height = contentHeight + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/Image_.qml b/UmiOCR-data/qt_res/qml/Widgets/Image_.qml new file mode 100644 index 0000000..6c13676 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/Image_.qml @@ -0,0 +1,31 @@ +// ============================================== +// =============== 增强的Image组件 =============== +// ============================================== + +import QtQuick 2.15 + +Image { + + // 组件ID + readonly property string compID: this.toString() + + // 传入路径,展示图片 + function showPath(path) { + path = encodeURIComponent(path) // URL编码 + source = `file:///${path}` + } + + // 传入imgID,展示图片 + function showImgID(imgID) { + source = `image://pixmapprovider/${compID}/${imgID}` + } + + // 清空展示,并释放缓存 + function clear() { + source = `image://pixmapprovider/${compID}` + } + + Component.onDestruction: { + clear() + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/Loading.qml b/UmiOCR-data/qt_res/qml/Widgets/Loading.qml new file mode 100644 index 0000000..e11341f --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/Loading.qml @@ -0,0 +1,200 @@ +// ============================================== +// =============== 加载中 动态图标 =============== +// ============================================== + +import QtQuick 2.15 +import QtGraphicalEffects 1.15 // 阴影 + +Item { + id: lRoot + property string text: "OCR..." + property real time: 700 // 动画间隔时间,毫秒 + width: size_.smallText * 6 + height: width + clip: true + property bool running: false // 是否运行动画 + + onVisibleChanged: { + if(visible) { + image.init() + cRoot.init() + running = qmlapp.enabledEffect // 启动动画 + } + else { + running = false // 停止动画 + } + } + + // 主体 + Panel { + anchors.fill: parent + color: theme.bgColor + + // 图标 + Item { + anchors.fill: parent + anchors.margins: size_.spacing * 2 + anchors.topMargin: size_.spacing + Image { + id: image + source: "../../images/icons/dango_right.png" + mipmap: true // 启用高质量缩放 + fillMode: Image.Stretch + property real p1x + property real p1y + property real pw + property real ph + property real sw + property real sh + property real p1w + property real p1h + property real p2x + property real p2y + property real p2w + property real p2h + property real pt: lRoot.time * 0.5 + // 预计算变形动画参数 + Component.onCompleted: { + // 父组件大小 + pw=parent.width, ph=parent.height + // 图片原始大小 + sw=sourceSize.width, sh=sourceSize.height + // 最高点 + p1h=ph*0.9, p1w=p1h*(sw/sh)*0.9, p1y=0, p1x=(pw-p1w)*0.5 + // 最低点 + p2h=p1h*0.9, p2w=p1h*(sw/sh)*1.1, p2x=(pw-p2w)*0.5, p2y=ph-p2h + } + // 恢复初始状态 + function init() { + if(qmlapp.enabledEffect) { + x=p2x, y=p2y + width=p2w, height=p2h + } + else { + height=ph, width=ph*(sw/sh) + y=0, x=(pw-width)*0.5 + } + } + // 串行动画 + SequentialAnimation{ + id: animation + running: lRoot.running + loops: Animation.Infinite + ParallelAnimation { + NumberAnimation{ target:image; property:"x"; to:image.p1x; duration:image.pt; easing.type:Easing.OutCubic} + NumberAnimation{ target:image; property:"y"; to:image.p1y; duration:image.pt; easing.type:Easing.OutCubic} + NumberAnimation{ target:image; property:"width"; to:image.p1w; duration:image.pt; easing.type:Easing.OutCubic} + NumberAnimation{ target:image; property:"height"; to:image.p1h; duration:image.pt; easing.type:Easing.OutCubic} + } + ParallelAnimation { + NumberAnimation{ target:image; property:"x"; to:image.p2x; duration:image.pt; easing.type:Easing.InCubic} + NumberAnimation{ target:image; property:"y"; to:image.p2y; duration:image.pt; easing.type:Easing.InCubic} + NumberAnimation{ target:image; property:"width"; to:image.p2w; duration:image.pt; easing.type:Easing.InCubic} + NumberAnimation{ target:image; property:"height"; to:image.p2h; duration:image.pt; easing.type:Easing.InCubic} + } + } + } + } + + // 下方文字动画 + Item { + id: cRoot + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + // anchors.margins: size_.smallSpacing + height: size_.smallText + property var chars: [] + property int time: lRoot.time + + Component.onCompleted: { + let l = [] + for(let i in lRoot.text) + l.push(lRoot.text[i]) + chars = l + textTimer.max = l.length + } + // 恢复初始状态 + function init() { + textTimer.now = 0 + } + Timer { + id: textTimer + property int now: 0 + property int max: 0 + interval: cRoot.time + running: lRoot.visible + repeat: true + onTriggered: { + if(now===max-1) + now = 0 + else + now++ + } + } + + Row { + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + spacing: size_.smallText * 0.1 + + Repeater { + model: cRoot.chars + Text_ { + id: charT + property bool charShow: textTimer.now===index + font.pixelSize: size_.smallText + text: modelData + color: theme.specialTextColor + property real minOpa: 0.5 + opacity: minOpa + property real sp: size_.smallSpacing + property real spMax: size_.smallSpacing * 1.5 + anchors.bottom: parent.bottom + anchors.bottomMargin: sp + height: size_.smallText + + onCharShowChanged: { + if(lRoot.running) { + if(charShow) caShow.running = true + else caHide.running = true + } + else { + anchors.bottomMargin = charShow ? spMax : sp + opacity = charShow ? 1 : minOpa + } + } + ParallelAnimation { + id: caShow + running: false + NumberAnimation { + target:charT; properties: "anchors.bottomMargin"; + from: charT.sp; to: charT.spMax; duration:cRoot.time * 0.5; + easing.type:Easing.OutCubic; + } + NumberAnimation { + target:charT; properties: "opacity"; + from: minOpa; to: 1; duration:cRoot.time * 0.5; + easing.type:Easing.OutCubic; + } + } + ParallelAnimation { + id: caHide + running: false + NumberAnimation { + target:charT; properties: "anchors.bottomMargin"; + from: charT.spMax; to: charT.sp; + easing.type:Easing.InQuart; duration:cRoot.time; + } + NumberAnimation { + target:charT; properties: "opacity"; + from: 1; to: minOpa; duration:cRoot.time; + easing.type:Easing.InQuart; + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/MarkdownView.qml b/UmiOCR-data/qt_res/qml/Widgets/MarkdownView.qml new file mode 100644 index 0000000..ae038fd --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/MarkdownView.qml @@ -0,0 +1,72 @@ +// =============================================== +// =============== Markdown渲染视图 =============== +// =============================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +ScrollView { + id: mdView + contentWidth: width // 内容宽度 + clip: true // 溢出隐藏 + property string text: "" // 要显示的文本 + property var textFormat: TextEdit.MarkdownText + // 内部 + property var regexes: undefined // 正则 + + // 初始化 + function init() { + // 初始化 字符串替换正则 + const replacements = [ + // 双换行 + ["\n\n", "\n\n  \n\n"], + // Markdown 链接转 HTML 链接 + ["!?(\\[(.*?)\\]\\((.*?)\\))", (match, p1, p2, p3, offset, string) => { + // 如果匹配的字符串以感叹号开头,就返回原字符串,不做替换 + if (match.startsWith('!')) return match + // 否则,进行替换 + return `${p2}` + }], + ] + regexes = replacements.map(([search, replacement]) => ({ + regex: new RegExp(search, "g"), replacement + })) + } + // 将标准MD格式转化为qml支持渲染的格式 + function getQmlText() { + let t = text + // 正则替换 + if(!regexes) init() + regexes.forEach(({ regex, replacement }) => { + t = t.replace(regex, replacement) + }) + return t + } + // 外部文本刷新时,更新内部文本 + onTextChanged: { + textEdit.text = getQmlText() + } + + TextEdit_ { + id: textEdit + text: "" + width: mdView.width // 与内容宽度相同 + textFormat: mdView.textFormat // md格式 + readOnly: true // 只读 + + // 点击链接 link + onLinkActivated: { + const argd = { + yesText: qsTr("打开网页"), + } + const callback = (flag)=>{ + if(flag) + Qt.openUrlExternally(link) + } + qmlapp.popup.dialog(qsTr("链接"), link, callback, "", argd) + } + onLinkHovered: {} // 链接悬停,空事件,自动改变鼠标光标 + } +} + +// https://doc.qt.io/qt-5.15/qml-qtquick-textedit.html \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/Menu_.qml b/UmiOCR-data/qt_res/qml/Widgets/Menu_.qml new file mode 100644 index 0000000..dbc2935 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/Menu_.qml @@ -0,0 +1,52 @@ +// =========================================== +// =============== 右键弹出菜单 =============== +// =========================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + + +Menu { + id: mRoot + property var menuList: [] + // [ + // [func1, "菜单项1"], + // [func2, "菜单项2", "noColor"], + // ] + + contentItem: Rectangle { + id: mRect + color: theme.specialBgColor + border.width: 1 + border.color: theme.specialTextColor + Column { + anchors.fill: parent + Repeater { + model: menuList + Button_ { + anchors.left: parent.left + anchors.right: parent.right + radius: 0 + height: size_.line + size_.smallSpacing * 2 + contentItem: Text_ { + text: modelData[1] + color: modelData.length>2 ? theme[modelData[2]] : theme.textColor + verticalAlignment: Text.AlignVCenter + Component.onCompleted: { + let w = width + size_.spacing * 2 + if(w > mRoot.width) { + mRoot.width = w + } + } + } + + onClicked: { + const func = menuList[index][0] + func && func() + mRoot.close() + } + } + } + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/MissionCtrlPanel.qml b/UmiOCR-data/qt_res/qml/Widgets/MissionCtrlPanel.qml new file mode 100644 index 0000000..9a621bb --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/MissionCtrlPanel.qml @@ -0,0 +1,277 @@ +// ============================================== +// =============== 任务进度控制面板 =============== +// ============================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +Item { + id: missionCtrl + // 信号 + signal runClicked // 点击 运行 + signal pauseClicked // 点击 暂停 + signal resumeClicked // 点击 恢复 + signal stopClicked // 点击 停止 + // 通知回调 + function runFinished(allNum) { // 任务开始,传入总任务数 + state_ = "run" + isWaiting = false + timer.start() // 新开始计时 + msnNowNum = 0 // 刷新任务计数 + msnAllNum = allNum + timer.updateNumber() // 刷新任务计数文本 + } + function pauseFinished() { // 暂停 + state_ = "pause" + isWaiting = false + timer.pause() // 暂停计时 + timer.updateNumber() // 刷新任务计数文本 + } + function resumeFinished() { // 恢复 + state_ = "run" + isWaiting = false + timer.resume() // 恢复计时 + timer.updateNumber() // 刷新任务计数文本 + } + function stopFinished() { // 停止 + state_ = "stop" + isWaiting = false + timer.pause() // 暂停计时 + timer.updateNumber() // 刷新任务计数文本 + } + function msnStep(step=1) { // 任务计数步进 + msnNowNum += step + if(msnNowNum > msnAllNum) msnNowNum = msnAllNum + timer.updateNumber() // 刷新任务计数文本 + } + + // 任务状态 + property string state_: "stop" // 当前状态, stop run pause + property bool isWaiting: false // 等待回调中 + // 任务计数 + property int msnAllNum: 0 // 总任务数 + property int msnNowNum: 0 // 当前已完成任务数 + clip: true + + // 右:开始/暂停/停止按钮 + Item { + id: ctrlRight + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + // 右栏宽度 + property real width_: size_.line * 8 + width: width_ + + // 开始/暂停/恢复 按钮 + Item { + id: btn1 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + width: qmlapp.enabledEffect?ctrlRight.width_:( + state_==="stop"?ctrlRight.width_:ctrlRight.width_*0.5) + + Button_ { + visible: state_ === "stop" && !isWaiting + anchors.fill: parent + bold_: true + bgColor_: theme.coverColor1 + bgHoverColor_: theme.coverColor2 + text_: qsTr("开始任务") + onClicked: { + if(isWaiting) return + isWaiting = true + Qt.callLater(runClicked) + } + } + IconButton { + visible: state_ !== "stop" || isWaiting + anchors.fill: parent + color: theme.textColor + bgColor_: theme.coverColor1 + bgHoverColor_: theme.coverColor2 + margins: size_.spacing + icon_: isWaiting?"wait":(state_==="run" ? "pause" : "run") + toolTip: (state_==="run" ? qsTr("暂停任务\n暂停后可以待机或休眠。\n但是关机或退出软件,将会丢弃任务内容。") : + qsTr("继续任务")) + onClicked: { + if(isWaiting) return + isWaiting = true + switch(state_) { + case "run": Qt.callLater(pauseClicked); return; + case "pause": Qt.callLater(resumeClicked); return; + } + } + } + + // 动画 + PropertyAnimation on width { // 折叠一半 + running: qmlapp.enabledEffect && state_!=="stop" + to: ctrlRight.width_*0.5 + duration: 80 + easing.type: Easing.InCubic + } + PropertyAnimation on width { // 展开全部 + running: qmlapp.enabledEffect && state_==="stop" + to: ctrlRight.width_ + duration: 80 + easing.type: Easing.OutCubic + } + } + // 停止 按钮 + IconButton { + id: btn2 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.left: btn1.right + anchors.leftMargin: size_.smallSpacing + visible: width > 1 + color: theme.noColor + bgColor_: theme.coverColor1 + bgHoverColor_: theme.coverColor2 + margins: size_.spacing + icon_: isWaiting?"wait":"stop" + toolTip: qsTr("终止任务\n放弃未完成的内容。") + onClicked: { + if(isWaiting) return + isWaiting = true + Qt.callLater(stopClicked) + } + } + } + // 左:文字/进度条 + Item { + id: ctrlLeft + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: ctrlRight.left + anchors.rightMargin: size_.spacing + Item { + id: ctrlLeftTop + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: size_.line + + Row { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + spacing: size_.line + + // 计时器、任务计数器 + Text_ { + id: timer + text: `${strState} ${strTime} ${strNumber}` + color: timer_.running?theme.textColor:(missionCtrl.msnNowNum 0) + missionProgress.percent = missionCtrl.msnNowNum/missionCtrl.msnAllNum + } + + property real startStamp // 开始时间戳,秒 + property real pauseStartStamp // 本次暂停开始的时间戳,秒 + property real pauseTime // 暂停时长,秒 + property real runTime // 运行时长,秒 + property bool isPause: false // 是否正在暂停 + Timer { + id: timer_ + interval: 100 // 更新间隔 + repeat: true + running: false + onTriggered: { + // 刷新运行时长 + const timestamp = timer.getTimestamp() + timer.runTime = timestamp-timer.startStamp-timer.pauseTime + timer.updateTime() + } + } + } + } + } + + HProgressBar { + id: missionProgress + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottomMargin: size_.line * 0.1 + height: size_.line * 0.5 + highlightColor: missionCtrl.state_==="run"?theme.yesColor:theme.coverColor4 + color: theme.bgColor + percent: 0 + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/ModalLayer.qml b/UmiOCR-data/qt_res/qml/Widgets/ModalLayer.qml new file mode 100644 index 0000000..0e6ee31 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/ModalLayer.qml @@ -0,0 +1,99 @@ +// =========================================== +// =============== 模态弹窗基类 =============== +// =========================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtGraphicalEffects 1.0 + +Rectangle { + id: mRoot + visible: false + color: theme.coverColor4 + property alias contentItem: mPanel.data + property string closeText: qsTr("保存并返回") // 右上角关闭提示文本 + + onVisibleChanged: { + focus = visible + if(!visible) { + // 当父组件隐藏,导致子组件连锁隐藏时,显式将visible设为false, + // 以避免父组件显示时 子组件也连锁显示,导致的重加载bug。 + visible = false + // 主标签页容器重新取得焦点,让主窗口继续接收键盘事件 + mainContainer.focus = true + } + } + // Esc 退出 + focus: false + Keys.enabled: true + Keys.onEscapePressed: mRoot.visible = false + Component.onCompleted: { + if(!closeText) + closeText = qsTr("返回") + closeText += " (Esc)" + } + + // 模糊效果 + // GaussianBlur { + // anchors.fill: parent + // source: mRoot.parent + // visible: qmlapp.enabledEffect + // cached: true + // radius: 8 + // samples: 16 + // } + // 底层鼠标事件 + MouseArea { + id: bottomArea + anchors.fill: parent + onWheel: {} // 拦截滚轮事件 + hoverEnabled: true // 拦截悬停事件 + onClicked: mRoot.visible = false // 单击关闭面板 + cursorShape: Qt.PointingHandCursor + } + property bool inBottom: bottomArea.containsMouse + // 关闭提示 + Item { + anchors.top: parent.top + anchors.right: parent.right + anchors.rightMargin: size_.line * 2 + anchors.topMargin: size_.line * 0.25 + Rectangle { + id: closeIconRect + anchors.right: parent.right + width: size_.line * 1.5 + height: size_.line * 1.5 + radius: size_.btnRadius + color: theme.bgColor + Icon_ { + anchors.fill: parent + color: theme.noColor + icon: "no" + } + } + Text_ { + visible: closeText + anchors.right: closeIconRect.left + anchors.rightMargin: size_.spacing + anchors.verticalCenter: closeIconRect.verticalCenter + text: closeText + color: theme.bgColor + font.pixelSize: size_.smallText + } + } + + // 内容面板 + Panel { + id: mPanel + anchors.fill: parent + anchors.margins: size_.line * 2 + color: theme.bgColor + // 面板拦截鼠标事件 + MouseArea { + id: panelArea + anchors.fill: parent + // hoverEnabled: true + onClicked: {} + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/MouseAreaBackgroud.qml b/UmiOCR-data/qt_res/qml/Widgets/MouseAreaBackgroud.qml new file mode 100644 index 0000000..fff73c1 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/MouseAreaBackgroud.qml @@ -0,0 +1,39 @@ +// =============================================== +// =============== 鼠标区域 悬停背景 =============== +// =============================================== + +import QtQuick 2.15 + +MouseArea { + property color color_: theme.coverColor1 // 悬停颜色 + property real radius_: size_.btnRadius // 圆角 + property bool hovered: false + + anchors.fill: parent + hoverEnabled: true + onEntered: { + bgRectangle.visible = true + hovered = true + } + onExited: { + bgRectangle.visible = false + hovered = false + } + // 事件穿透 + onClicked: { + mouse.accepted = false + } + onPressed: { + mouse.accepted = false + } + onReleased: { + mouse.accepted = false + } + Rectangle { + id: bgRectangle + visible: false + anchors.fill: parent + color: color_ + radius: radius_ + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/Panel.qml b/UmiOCR-data/qt_res/qml/Widgets/Panel.qml new file mode 100644 index 0000000..d43d638 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/Panel.qml @@ -0,0 +1,10 @@ +// ============================================== +// =============== 标准面板外观样式 =============== +// ============================================== + +import QtQuick 2.15 + +Rectangle { + color: theme.coverColor1 + radius: size_.panelRadius +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/ResultLayout/ResultTextContainer.qml b/UmiOCR-data/qt_res/qml/Widgets/ResultLayout/ResultTextContainer.qml new file mode 100644 index 0000000..73305e2 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/ResultLayout/ResultTextContainer.qml @@ -0,0 +1,176 @@ +// ======================================= +// =============== 结果文本 =============== +// ======================================= + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import "../" + +Item { + id: resultRoot + + property string status_: "" // 状态, text / noText / error + property alias textLeft: textLeft_.text + property string textRight: "" + property alias textMain: textMain_.text + property alias activeFocus_: textMain_.activeFocus // 输入框焦点 + property int index_ + // 选取文字 + property int selectL: -1 + property int selectR: -1 + property int selectUpdate: 0 // 只要有变化,就刷新选中 + // 外部函数 + property var copy: undefined // 复制选中 + property var copyAll: undefined // 复制全部 + property var selectSingle: undefined // 选中单个文本框 + property var selectAll: undefined // 所有文本框全选 + property var selectDel: undefined // 删除单个 + property var selectAllDel: undefined // 清空 + + // 传入一个相对于item的坐标,返回该坐标位于this组件的什么位置。 + // undefined:不在组件中 | -1:顶部信息栏 | 0~N:所在字符的下标 + function where(item, mx, my) { + const localPoint = this.mapFromItem(item, mx, my) + if(!this.contains(localPoint)) { + return undefined + } + if(resultTop.contains(localPoint)) { + return -1 + } + else { + const textPoint = textMain_.mapFromItem(item, mx, my) + const textPos = textMain_.positionAt(textPoint.x, textPoint.y) + return textPos + } + } + // 将光标移到指定位置并激活焦点。 + function focus(pos=-1) { + if(pos >= 0 && textMain_.cursorPosition !== pos) { + textMain_.cursorPosition = pos + } + if(!textMain_.activeFocus) { + textMain_.forceActiveFocus() // 获取焦点 + } + } + function toUpdateSelect() { + if(selectL<0 || selectR<0) + textMain_.deselect() + else + textMain_.select(selectL, selectR) + } + onSelectUpdateChanged: toUpdateSelect() + TableView.onReused: toUpdateSelect() + Component.onCompleted: toUpdateSelect() + + + // 高度适应子组件 + implicitHeight: resultTop.height+resultBottom.height+size_.smallSpacing + height: resultTop.height+resultBottom.height+size_.smallSpacing + property var onTextHeightChanged // 当文字输入导致高度改变时,调用的函数 + + onHeightChanged: { // 高度改变时,通知父级 + // 必须文本框获得焦点时才触发 + if(textMain_.activeFocus && (typeof onTextHeightChanged === "function")) + onTextHeightChanged() + } + + // 顶部信息 + Item { + id: resultTop + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: size_.smallSpacing + anchors.rightMargin: size_.smallSpacing + height: size_.smallLine + size_.spacing*2 + + // 图片名称 + Text_ { + id: textLeft_ + anchors.left: parent.left + anchors.right: textRight_.left + anchors.bottom: parent.bottom + anchors.rightMargin: size_.spacing + anchors.bottomMargin: size_.smallSpacing + color: theme.subTextColor + font.pixelSize: size_.smallText + font.family: theme.dataFontFamily + clip: true + elide: Text.ElideLeft + } + // 日期时间 + Text_ { + id: textRight_ + anchors.right: btnRight.left + anchors.bottom: parent.bottom + anchors.bottomMargin: size_.smallSpacing + color: theme.subTextColor + font.pixelSize: size_.smallText + text: textRight + " | " + } + // 复制按钮 + Text_ { + id: btnRight + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.bottomMargin: size_.smallSpacing + color: theme.specialTextColor + font.pixelSize: size_.smallText + text: qsTr("复制") + } + } + + // 下方主要文字内容 + Rectangle { + id: resultBottom + color: theme.bgColor + anchors.top: resultTop.bottom + anchors.left: parent.left + anchors.right: parent.right + // anchors.topMargin: size_.smallSpacing + radius: size_.baseRadius + height: textMain_.height + + TextEdit_ { + id: textMain_ + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: size_.smallSpacing + anchors.rightMargin: size_.smallSpacing + readOnly: false // 可编辑 + persistentSelection: true // 丢失焦点时,保留选区 + color: status_==="error"? theme.noColor:theme.textColor + + // 按键事件。响应并拦截:单双击 Ctrl+C ,双击 Ctrl+A + property int keyDoubleTime: 300 // 双击毫秒 + property int lastUpTime: -1 // 上次按键抬起的时间戳。需要截取后8位以免int放不下 + property int lastKey: -1 // 上次按键的键值 + property var listeningKeys: [Qt.Key_A, Qt.Key_C, Qt.Key_D] + Keys.onPressed: { + if (event.modifiers & Qt.ControlModifier) { + if (listeningKeys.includes(event.key)) { + event.accepted = true // 拦截按键 + const t = Date.now() & 0xFFFFFFFF + // 双击 + if(t - lastUpTime <= keyDoubleTime && lastKey==event.key) { + event.key===Qt.Key_A && resultRoot.selectAll && resultRoot.selectAll() + event.key===Qt.Key_C && resultRoot.copyAll && resultRoot.copyAll() + event.key===Qt.Key_D && resultRoot.selectAllDel && resultRoot.selectAllDel() + } + else { // 单击 + event.key===Qt.Key_A && resultRoot.selectSingle && resultRoot.selectSingle() + event.key===Qt.Key_C && resultRoot.copy && resultRoot.copy() + } + } + } + } + Keys.onReleased: { + if (listeningKeys.includes(event.key)) { + lastUpTime = Date.now() & 0xFFFFFFFF + lastKey = event.key + } + } + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/ResultLayout/ResultsTableView.qml b/UmiOCR-data/qt_res/qml/Widgets/ResultLayout/ResultsTableView.qml new file mode 100644 index 0000000..728f6a5 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/ResultLayout/ResultsTableView.qml @@ -0,0 +1,530 @@ +// =========================================== +// =============== 结果面板布局 =============== +// =========================================== + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import "../" + +Item { + ListModel { id: resultsModel } // OCR结果模型 + + // ========================= 【对外接口】 ========================= + + property alias ctrlBar: ctrlBar // 控制栏的引用 + + // 添加一条OCR结果。元素: + // timestamp 时间戳,秒为单位 + // title 左边显示标题,可选 + // code 结果代码, data 结果内容 + // 返回结果字符串 + function addOcrResult(res) { + // 提取并转换结果时间 + let date = new Date(res.timestamp * 1000) // 时间戳转日期对象 + let year = date.getFullYear() + let month = ('0' + (date.getMonth() + 1)).slice(-2) + let day = ('0' + date.getDate()).slice(-2) + let hours = ('0' + date.getHours()).slice(-2) + let minutes = ('0' + date.getMinutes()).slice(-2) + let seconds = ('0' + date.getSeconds()).slice(-2) + // let dateTimeString = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` + let dateTimeString = `${hours}:${minutes}` + // 提取结果文本和状态 + let status_ = "" + let resText = "" + switch(res.code){ + case 100: // 成功 + status_ = "text" + const l = res.data.length + for(let i=0; i 0) { + const t2 = res.score.toFixed(2) + res.title += " | "+qsTr("置信度 %1").arg(t2) + } + } + // 添加到列表模型 + resultsModel.append({ + "status__": status_, + "title": res.title, + "datetime": dateTimeString, + "resText": resText, + "timestamp": res.timestamp, + "selectL_": -1, + "selectR_": -1, + "selectUpdate_": 0, + "source": JSON.stringify(res), // 保存原始数据 + }) + // 自动滚动 + if(autoToBottom) { + tableView.toBottom() + } + return resText + } + + // 搜索一个结果。可传入 title 或 timestamp + function getResult(title="", timestamp=-1) { + for (let i = 0, l=resultsModel.count; i < l; i++) { + let item = resultsModel.get(i); + if (item.title === title || item.timestamp === timestamp) { + return item + } + } + return undefined + } + + // ========================= 【布局】 ========================= + + anchors.fill: parent + clip: true // 溢出隐藏 + property bool autoToBottom: true // 自动滚动到底部 + property real scrollBarWidth: size_.spacing * 1.5 // 滚动条区域宽度 + + // 内容滚动组件 + TableView { + id: tableView + anchors.fill: parent + anchors.rightMargin: scrollBarWidth + rowSpacing: size_.smallSpacing // 行间隔 + contentWidth: parent.width // 内容宽度 + model: resultsModel // 模型 + flickableDirection: Flickable.VerticalFlick // 只允许垂直滚动 + boundsBehavior: Flickable.StopAtBounds // 禁止flick过冲。不影响滚轮滚动的过冲 + + // ==================== 【滚动和视觉相关】 ==================== + // 滚动到底部 + function toBottom() { + bottomTimer.running = true + } + Timer { + id: bottomTimer + interval: 100 + running: false + repeat: true // 重复执行 + onTriggered: { + // 已滚动到底部 + if(scrollBar.position >= (1 - scrollBar.size)) { + bottomTimer.running = false + tableView.returnToBounds() // 确保未越界 + } + // 未滚动到底部,重复将滚动条拉到底 + else { + scrollBar.position = (1 - scrollBar.size) + } + } + } + // 宽度设定函数 + columnWidthProvider: (column)=>{ + if(column == 0){ // 第一列宽度,变化值 + return tableView.width + } + } + onWidthChanged: { // 组件宽度变化时重设列宽 + Qt.callLater(()=>{ // 延迟调用 + tableView.forceLayout() + }) + } + // ==================== 【元素】 ==================== + delegate: ResultTextContainer { + status_: status__ + textLeft: title + textRight: datetime + textMain: resText + selectL: selectL_ + selectR: selectR_ + selectUpdate: selectUpdate_ + index_: index + onTextHeightChanged: tableView.forceLayout // 文字高度改变时重设列宽 + onTextMainChanged: { + // Bug!!!!!!!!!! + /* + 以下代码的本意是:当用户修改输入框文本时,将修改后的文本同步到 resultsModel 中。 + 这样,当 TableView 动态加载文本框时,可以恢复用户编辑过的内容。 + 但是, onTextChanged 信号有个致命问题:不仅会在手动编辑文本时触发此信号, + 当程序修改文本(如动态加载文本框时的赋值)甚至文本样式改变(比如选中一段文本,使其 + 背景色变化)都会触发此信号。 + 这就有几率触发一些bug,尤其是 TableView 动态加载时,可能一个物理输入框组件轮换展示 + 不同的逻辑内容,切换这些逻辑内容时触发 onTextChanged ,误将一个逻辑内容写入另外一个 + resultsModel 槽位,导致吞掉另一个逻辑内容。 + 本质上,这是由于 TextEdit 组件没有 textEdited 信号。如果有,那就不会产生上述误判了。 + 相关: + https://forum.qt.io/topic/143841/textedited-signal-for-textarea-textedit/5 + https://bugreports.qt.io/browse/QTBUG-103718 + https://codereview.qt-project.org/c/qt/qtdeclarative/+/606008 + 上述链接指出,qt新版本或dev分支已为 TextEdit 补上 textEdited 信号。 + 但是,pyside2暂未更新此版本。 + 本项目作为临时措施,在下文用一些先验条件来判断当前的修改是否有可能由用户发起, + 以此降低误判的概率。 + */ + if(!activeFocus_) return // 临时措施:排除没有焦点的文本修改 + if(resText===textMain) return // 临时措施:排除文本内容无变化的修改 + + resultsModel.setProperty(index, "resText", textMain) // 文字改变时写入列表 + } + copy: tableMouseArea.selectCopy + copyAll: tableMouseArea.selectAllCopy + selectAll: tableMouseArea.selectAll + selectSingle: tableMouseArea.selectSingle + selectDel: tableMouseArea.selectDel + selectAllDel: tableMouseArea.selectAllDel + } + // 滚动条 + ScrollBar.vertical: scrollBar + } + // ==================== 【跨文本框选取】 ==================== + MouseArea { + id: tableMouseArea + z: 10 + anchors.fill: parent + anchors.rightMargin: scrollBarWidth + acceptedButtons: Qt.LeftButton | Qt.RightButton + property var tableChi: tableView.children[0].children + hoverEnabled: true + property int startIndex: -1 // 拖拽开始时,文本框序号 + property int startTextIndex: -1 // 拖拽开始时,字符序号 + property int endIndex: -1 // 拖拽结束时,文本框序号 + property int endTextIndex: -1 // 拖拽结束时,字符序号 + property int selectUpdate: 0 + // 查询鼠标坐标位于哪个表格组件内 ,位于什么地方 + // 若 outside==true:则允许鼠标跑到组件以外,会计算鼠标离组件内最近的点。 + function getWhere(outside = false) { + let mx=mouseX, my=mouseY + if(outside) { // 允许出界时,将出界的x回归到界内 + if(mx < 0) mx = 0 + if(mx > tableMouseArea.width) mx = tableMouseArea.width + } + for(let i in tableChi) { + const c = tableChi[i] + let f = c.where(this, mx, my) + if(outside && f===-1) f = 0 // 允许出界时,将标题栏视为首字符区域 + if(f !== undefined) { + return { + "obj": c, + "index": c.index_, // 文本框序号 + "where": f, // undefined:不在组件中 | -1:顶部信息栏 | 0~N:所在字符的下标 + } + } + } + return undefined + } + // 获取Index正确顺序。返回: [li 起始块, lt 起始块选区左侧, ri 结束块, rt 结束块选区右侧] + function getIndexes() { + let li, lt, ri, rt + if(startIndex < endIndex) { + li=startIndex; lt=startTextIndex; ri=endIndex; rt=endTextIndex; + } + else if(startIndex > endIndex) { + li=endIndex; lt=endTextIndex; ri=startIndex; rt=startTextIndex; + } + else { + li = ri = startIndex + if(startTextIndex < endTextIndex) { + lt=startTextIndex; rt=endTextIndex; + } + else if(startTextIndex > endTextIndex) { + lt=endTextIndex; rt=startTextIndex; + } + else { // 单击,未选中 + lt = rt = -1 + } + } + return [li, lt, ri, rt] + } + // 重设 Index + function initIndexes() { + startIndex=startTextIndex=endIndex=endTextIndex=-1 + } + function selectUpdateAdd() { + selectUpdate++ + if(selectUpdate > 10000) selectUpdate = 0 + } + // 根据 Index 的参数,设置每个文本框中被划选的文本。 + function selectIndex() { + const [li, lt, ri, rt] = getIndexes() + selectUpdateAdd() // 刷新标记步进 + // 遍历每个文本框数据 + for (let i = 0, l = resultsModel.count; i < l; i++) { + let item = resultsModel.get(i) // 缓存改变之前的数据 + // 未被选中 + if( li<0 || ri<0 || i
  • ri ) { + item.selectL_ = -1 + item.selectR_ = -1 + } + // 被选中 + else { + const len = item.resText.length // 当前块的文本长度 + if(i === li && i === ri) { // 单个块,选中 lt~rt + item.selectL_ = lt + item.selectR_ = Math.min(rt, len) + } + else if(i === li) { // 起始块,选中 lt~末尾 + item.selectL_ = lt + item.selectR_ = len + } + else if(i === ri) { // 结束块,选中 开头~rt + item.selectL_ = 0 + item.selectR_ = Math.min(rt, len) + } + else { // 中间块,选中 开头~末尾 + item.selectL_ = 0 + item.selectR_ = len + } + } + item.selectUpdate_ = selectUpdate // 写入刷新标记 + resultsModel.set(i, item) + } + } + // 选中单个文本框 + function selectSingle() { + if(endIndex < 0) return + let item = resultsModel.get(endIndex) + startTextIndex = 0 + endTextIndex = item.resText.length + startIndex = endIndex + selectIndex() + } + // 全选 + function selectAll() { + if(resultsModel.count === 0) return + startIndex = startTextIndex = 0 + endIndex = resultsModel.count-1 + endTextIndex = resultsModel.get(endIndex).resText.length + selectIndex() + } + // 复制已选中的内容,或当前块的所有文本 + function selectCopy() { + // 若当前没有选中的内容,但指针放在一个块上,则选择该块 + if(startIndex>=0 && startIndex===endIndex && startTextIndex===endTextIndex) { + selectSingle() + } + let [li, lt, ri, rt] = getIndexes() + let copyText = "" + // 选中单个文本块 + if(li === ri) { + const item = resultsModel.get(li) + if(item.resText) { + copyText = item.resText.substring(lt, rt) + } + } + // 选中多个块,则遍历多个块,提取各自的文本 + else { + for(let i = li; i <= ri; i++) { + const item = resultsModel.get(i) + if(item.resText) { + const text = item.resText + const len = text.length + if(i === li) // 多个块的起始 + copyText = text.substring(lt) + "\n" + else if(i === ri) // 多个块的结束 + copyText += text.substring(0, rt) + else // 多个块的中间 + copyText += text + "\n" + } + } + } + if(copyText && copyText.length > 0) { + qmlapp.utilsConnector.copyText(copyText) + qmlapp.popup.simple(qsTr("记录:复制%1字").arg(copyText.length), "") + } + else { + qmlapp.popup.simple(qsTr("记录:无选中文字"), "") + } + } + // 复制所有 + function selectAllCopy() { + let copyText = "" + for (let i = 0, l=resultsModel.count; i < l; i++) { + let item = resultsModel.get(i) + if(item.resText) { + copyText += item.resText + if(i < l-1) copyText += "\n" + } + } + qmlapp.utilsConnector.copyText(copyText) + qmlapp.popup.simple(qsTr("记录:复制全部%1字").arg(copyText.length), "") + selectAll() + } + // 删除选中的文本框 + function selectDel() { + const [li, lt, ri, rt] = getIndexes() + if(li < 0 || ri < 0) return + const l = ri-li+1 + resultsModel.remove(li, l) + initIndexes() // 重设 Index + qmlapp.popup.simple(qsTr("删除%1条记录").arg(l), "") + } + // 删除全部 + function selectAllDel() { + resultsModel.clear() + initIndexes() // 重设 Index + qmlapp.popup.simple(qsTr("清空记录"), "") + } + // 按下 + onPressed: { + if (mouse.button === Qt.RightButton) { + selectMenu.popup() + return + } + const info = getWhere() + if(info === undefined) // 无效区域 + initIndexes() + else if(info.where === -1) { // 标题栏区域 + endIndex = startIndex = info.index + startTextIndex = endTextIndex = -1 + selectCopy() + initIndexes() // 复制完后,清空index记录 + } + else if(info.where >= 0) { // 文本区域 + selectUpdateAdd() + // 移除现有的所有选区 + for (let i = 0, l=resultsModel.count; i < l; i++) { + let element = resultsModel.get(i) + element.selectL_ = -1 + element.selectR_ = -1 + element.selectUpdate_ = selectUpdate + resultsModel.set(i, element); // 替换元素,触发一次更新 + } + endIndex = startIndex = info.index + endTextIndex = startTextIndex = info.where + info.obj.focus(info.where) // 放置光标 & 赋予焦点 + } + } + // 移动 + onPositionChanged: { + const info = getWhere(pressed) + // 根据所在区域,调整光标 + if(info===undefined) { + tableMouseArea.cursorShape = Qt.ArrowCursor + return + } + if(info.where >= 0) tableMouseArea.cursorShape = Qt.IBeamCursor + else if(info.where >= -1) tableMouseArea.cursorShape = Qt.PointingHandCursor + else tableMouseArea.cursorShape = Qt.ArrowCursor + // 拖拽中 + if(pressed) { + if(startIndex===startTextIndex && startIndex===-1) + return + endIndex = info.index + endTextIndex = info.where + selectIndex() + } + } + // 抬起 + onReleased: { + if (mouse.button === Qt.RightButton) { + return + } + const info = getWhere() + if(info===undefined || info.where<0) { + if(startIndex >= 0) + selectIndex() + return + } + endIndex = info.index + endTextIndex = info.where + selectIndex() // 选中 + if(startIndex===endIndex && startTextIndex===endTextIndex) { + info.obj.focus(info.where) // 单击移动光标 + } + else { + info.obj.focus(-1) // 激活焦点 + } + } + // 菜单 + Menu_ { + id: selectMenu + menuList: [ + [tableMouseArea.selectCopy, qsTr("复制    (Ctrl+C)")], + [tableMouseArea.selectAllCopy, qsTr("复制全部  (Ctrl+C 双击)")], + [tableMouseArea.selectSingle, qsTr("选中单个  (Ctrl+A)")], + [tableMouseArea.selectAll, qsTr("选中全部记录(Ctrl+A 双击)")], + [tableMouseArea.selectDel, qsTr("删除选中记录"), "noColor"], + [tableMouseArea.selectAllDel, qsTr("清空全部记录(Ctrl+D 双击)"), "noColor"], + ] + } + } + // ==================== 【滚动条】 ==================== + ScrollBar { + id:scrollBar + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + width: scrollBarWidth + } + + // ==================== 【外置控制栏】 ==================== + Item { + id: ctrlBar + height: size_.line*1.5 + anchors.left: parent.left + anchors.right: parent.right + + Row { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + + CheckButton { + anchors.top: parent.top + anchors.bottom: parent.bottom + text_: qsTr("滚动") + toolTip: qsTr("自动滚动到底部") + textColor_: autoToBottom ? theme.textColor : theme.subTextColor + checked: autoToBottom + enabledAnime: true + onCheckedChanged: { + autoToBottom = checked + if(checked) { + tableView.toBottom() + } + else { + bottomTimer.running = false + } + } + } + // 菜单 + IconButton { + anchors.top: parent.top + anchors.bottom: parent.bottom + width: height + icon_: "menu" + color: theme.textColor + onClicked: selectMenu.popup() + toolTip: qsTr("右键菜单") + } + } + } + + // 测试 + // Button_ { + // anchors.top: parent.top + // anchors.left: parent.left + // z:100 + // bgColor_: "red" + // text_: "Test" + // onClicked: { + // let t = "\n" + // for (let i = 0, l=resultsModel.count; i < l; i++) { + // let item = resultsModel.get(i); + // t += "\n "+i+" "+item.resText + // } + // console.log("resultsModel: ", resultsModel.count, t) + // } + // } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/TabPanel.qml b/UmiOCR-data/qt_res/qml/Widgets/TabPanel.qml new file mode 100644 index 0000000..eb6ddfe --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/TabPanel.qml @@ -0,0 +1,203 @@ +// ============================================= +// =============== 重叠选项卡面板 =============== +// ============================================= + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtGraphicalEffects 1.15 + +Item { + property alias currentIndex: bar.currentIndex // 当前下标 + property int indexChangeNum: 0 // 下标变化次数 + property bool isMenuTop: true // t时控制菜单在顶部,f时在底部 + property real menuHeight: size_.line * 2 // 菜单栏高度 + + onCurrentIndexChanged: indexChangeNum++ + + // 模型选项卡 + /* 每一项: + { "key": 标识, + "title": 标题, + "component": 选项卡组件 + 选项卡组件中可有一个属性ctrlBar,指向一个控制栏子组件。这个子组件将会父级移动到选项卡的控制栏。 + } */ + property var tabsModel: [] + clip: true + id: tabPanel + + function updateMenuTop() { + // 菜单在顶部模式 + if(isMenuTop) { + menuContainer.anchors.top = tabPanel.top + menuContainer.anchors.bottom = undefined + menuContainer.height = menuHeight + swipeView.anchors.top = menuContainer.bottom + swipeView.anchors.bottom = tabPanel.bottom + swipeView.anchors.topMargin = size_.smallSpacing + swipeView.anchors.bottomMargin = 0 + } + // 菜单在底部模式 + else { + menuContainer.anchors.top = undefined + menuContainer.anchors.bottom = tabPanel.bottom + menuContainer.height = menuHeight + swipeView.anchors.top = tabPanel.top + swipeView.anchors.bottom = menuContainer.top + swipeView.anchors.topMargin = 0 + swipeView.anchors.bottomMargin = size_.smallSpacing + } + + } + onIsMenuTopChanged: updateMenuTop() + Component.onCompleted: updateMenuTop() + + // 下方 选项页 + SwipeView { + id: swipeView + anchors.left: parent.left + anchors.right: parent.right + currentIndex: bar.currentIndex + interactive: false // 禁止直接滑动视图本身 + Component.onCompleted: { + if(!qmlapp.enabledEffect) // 关闭动画 + contentItem.highlightMoveDuration = 0 + } + + Repeater { + model: tabsModel + + Item { + visible: SwipeView.isCurrentItem + Component.onCompleted: { + modelData.component.parent = this + modelData.component.visible = true + } + } + } + } + + // 选项栏 + Item { + id: menuContainer + anchors.left: parent.left + anchors.right: parent.right + // 在 updateMenuTop 中重设高度 + + Rectangle { // 背景色 + anchors.fill: parent + color: theme.bgColor + Rectangle { + anchors.fill: parent + color: theme.coverColor1 + } + } + + // 左:选项栏 + TabBar { + id: bar + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + + background: Rectangle { + color: theme.bgColor + } + + Repeater { + model: tabsModel + + TabButton { + property string text_: modelData.title + anchors.top: parent.top + anchors.bottom: parent.bottom + checkable: true + width: contentText.contentWidth + size_.line*2 + + contentItem: Text_ { + id: contentText + text: parent.text_ + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: parent.checked ? theme.textColor : theme.subTextColor + font.bold: parent.checked + } + background: MouseAreaBackgroud { + anchors.fill: parent + radius_: 0 + cursorShape: Qt.PointingHandCursor + + Rectangle { + anchors.fill: parent + color: parent.parent.checked ? theme.coverColor3 : theme.coverColor2 + } + } + + // 选中的动画 + property bool runAni: false + onCheckedChanged: { + runAni = checked + } + SequentialAnimation{ // 串行动画 + running: qmlapp.enabledEffect && runAni + // 动画1:放大 + NumberAnimation{ + target: contentText + property: "scale" + to: 1.3 + duration: 80 + easing.type: Easing.OutCubic + } + // 动画2:缩小 + NumberAnimation{ + target: contentText + property: "scale" + to: 1 + duration: 150 + easing.type: Easing.InCubic + } + } + } + } + + // 内圆角裁切 + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + radius: size_.btnRadius + width: bar.width + height: bar.height + } + } + } + // 右:每个卡的控制栏 + SwipeView { + id: ctrlSwipeView + anchors.left: bar.right + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + clip: true + + currentIndex: bar.currentIndex + interactive: false // 禁止直接滑动视图本身 + Component.onCompleted:{ + if(!qmlapp.enabledEffect) // 关闭动画 + contentItem.highlightMoveDuration = 0 + } + + Repeater { + model: tabsModel + + Item { // 控制栏子组件父级重定向 + visible: SwipeView.isCurrentItem + Component.onCompleted: { + if(modelData.component.ctrlBar) { + modelData.component.ctrlBar.parent = this + modelData.component.ctrlBar.anchors.fill = this + } + } + } + } + } + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/TextEdit_.qml b/UmiOCR-data/qt_res/qml/Widgets/TextEdit_.qml new file mode 100644 index 0000000..c260717 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/TextEdit_.qml @@ -0,0 +1,19 @@ +// ================================================ +// =============== 标准文本编辑框样式 =============== +// ================================================ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +TextEdit { + wrapMode: TextEdit.Wrap // 尽量在单词边界处换行 + textFormat: TextEdit.PlainText // 纯文本格式 + readOnly: false // 可编辑 + selectByMouse: true // 允许鼠标选择文本 + selectByKeyboard: true // 允许键盘选择文本 + color: theme.textColor // 文本颜色 + selectedTextColor: theme.specialBgColor // 选区文字色 + selectionColor: theme.specialTextColor // 选区背景色 + font.pixelSize: size_.text + font.family: theme.dataFontFamily +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/TextField_.qml b/UmiOCR-data/qt_res/qml/Widgets/TextField_.qml new file mode 100644 index 0000000..3dabeef --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/TextField_.qml @@ -0,0 +1,35 @@ +// ============================================= +// =============== 单行输入框样式 =============== +// ============================================= + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +TextField { + color: theme.subTextColor // 默认颜色 + font.pixelSize: size_.text // 默认大小 + font.family: theme.dataFontFamily // 默认字体 + property color borderColor: theme.coverColor3 + // 外边距 + leftInset: 0 + rightInset: 0 + topInset: 0 + bottomInset: 0 + // 内边距 + leftPadding: 4 + rightPadding: 4 + topPadding: 0 + bottomPadding: 0 + + verticalAlignment: Text.AlignVCenter // 垂直居中 + focus: true // 键盘聚焦 + selectByMouse: true // 允许鼠标选中 + + background: Rectangle { + color: theme.bgColor + radius: size_.btnRadius + + border.width: 1 + border.color: borderColor + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/TextInput_.qml b/UmiOCR-data/qt_res/qml/Widgets/TextInput_.qml new file mode 100644 index 0000000..760cbec --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/TextInput_.qml @@ -0,0 +1,22 @@ +// ================================================ +// =============== 标准文字编辑框样式 =============== +// ================================================ + +import QtQuick 2.15 + +TextInput { + color: theme.subTextColor // 默认颜色 + font.pixelSize: size_.text // 默认大小 + font.family: theme.dataFontFamily // 默认字体 + leftPadding: 4 + rightPadding: 4 + verticalAlignment: Text.AlignVCenter // 垂直居中 + focus: true // 键盘聚焦 + selectByMouse: true // 允许鼠标选中 + property alias hoverColor: mouseAreaBackgroud.color_ // 鼠标悬浮时的颜色 + + MouseAreaBackgroud { + id: mouseAreaBackgroud + cursorShape: Qt.IBeamCursor + } +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/Text_.qml b/UmiOCR-data/qt_res/qml/Widgets/Text_.qml new file mode 100644 index 0000000..7f86554 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/Text_.qml @@ -0,0 +1,14 @@ +// =========================================== +// =============== 标准文字样式 =============== +// =========================================== + +import QtQuick 2.15 + +Text { + // 默认颜色 + color: theme.textColor + // 默认大小 + font.pixelSize: size_.text + // 默认字体 + font.family: theme.fontFamily +} \ No newline at end of file diff --git a/UmiOCR-data/qt_res/qml/Widgets/ToolTip_.qml b/UmiOCR-data/qt_res/qml/Widgets/ToolTip_.qml new file mode 100644 index 0000000..d32b767 --- /dev/null +++ b/UmiOCR-data/qt_res/qml/Widgets/ToolTip_.qml @@ -0,0 +1,36 @@ +// ========================================= +// =============== 悬浮提示窗 =============== +// ========================================= + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + + +ToolTip { + id: rootToolTip + + delay: 500 + timeout: 0 // 不自动关闭 + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + onEntered: { // 遮挡鼠标时,下移 + rootToolTip.y += mouseY + size_.line + } + } + + contentItem: Text { // 前景文字 + text: rootToolTip.text + font.family: theme.fontFamily + font.pixelSize: size_.smallText + color: theme.textColor + } + + background: Rectangle { // 背景矩形 + color: theme.bgColor + border.color: theme.coverColor4 + radius: size_.btnRadius + } +} diff --git a/dev-tools/i18n/Qt5Core.dll b/dev-tools/i18n/Qt5Core.dll new file mode 100644 index 0000000..0de9728 Binary files /dev/null and b/dev-tools/i18n/Qt5Core.dll differ diff --git a/dev-tools/i18n/Qt5Gui.dll b/dev-tools/i18n/Qt5Gui.dll new file mode 100644 index 0000000..ebb89c3 Binary files /dev/null and b/dev-tools/i18n/Qt5Gui.dll differ diff --git a/dev-tools/i18n/Qt5PrintSupport.dll b/dev-tools/i18n/Qt5PrintSupport.dll new file mode 100644 index 0000000..06e32a5 Binary files /dev/null and b/dev-tools/i18n/Qt5PrintSupport.dll differ diff --git a/dev-tools/i18n/Qt5Widgets.dll b/dev-tools/i18n/Qt5Widgets.dll new file mode 100644 index 0000000..67ba11c Binary files /dev/null and b/dev-tools/i18n/Qt5Widgets.dll differ diff --git a/dev-tools/i18n/Qt5Xml.dll b/dev-tools/i18n/Qt5Xml.dll new file mode 100644 index 0000000..f6e2ca8 Binary files /dev/null and b/dev-tools/i18n/Qt5Xml.dll differ diff --git a/dev-tools/i18n/README.md b/dev-tools/i18n/README.md new file mode 100644 index 0000000..2da59f4 --- /dev/null +++ b/dev-tools/i18n/README.md @@ -0,0 +1,31 @@ +## 参与本地化翻译(译者) + +前往 Weblate 在线翻译平台,注册账户,或用Github账户登录: + +https://hosted.weblate.org/engage/umi-ocr/ + +您可以补充、订正现有语言的翻译,或创建新的语言。 + +## 维护本地化文件(开发者) + +### 从源代码生成或更新翻译文件.ts + +1. 运行 `lupdate_all.py` +2. 提交更改,等待 Weblate 平台更新 + +### 从翻译文件.ts 生成二进制翻译包.qm 并载入软件 + +1. 运行 `lrelease_all.py` +2. 在 `/release` 目录下,找到所有 .qm 后缀的文件,剪贴到 `Umi-OCR/UmiOCR-data/i18n` + +## 翻译插件界面 + +Umi-OCR 中的插件(如引擎组件)使用另一套轻量翻译机制。 + +简单而言: +1. 打开 [插件仓库](https://github.com/hiroi-sora/Umi-OCR_plugins) ,找到某个插件对应的目录。 +2. 目录下往往有 `i18n.csv` 这个文件。用Excel或WPS打开它。如果打开乱码,网上有很多解决方法。 +3. 在Excel中编辑表格。 +4. 保存回csv文件。 +5. 确保 `i18n.csv` 存储为 `utf-8` 编码。可以用VsCode打开该文件并转换编码。 +6. 向插件仓库提交PR。 \ No newline at end of file diff --git a/dev-tools/i18n/convert_ts_txt.py b/dev-tools/i18n/convert_ts_txt.py new file mode 100644 index 0000000..8b23ee0 --- /dev/null +++ b/dev-tools/i18n/convert_ts_txt.py @@ -0,0 +1,32 @@ +import xml.etree.ElementTree as ET +import sys + +inFile = sys.argv[1] +outFile = inFile[:-2] + "txt" + +# 加载XML文件 +tree = ET.parse(inFile) + +# 获取根元素 +root = tree.getroot() + +# 遍历页面 +data = [] +count = 0 +for page in root: + for message in page: + if message.tag == "name": + data.append([]) + else: + for item in message: + if item.tag == "source": + source = item.text + data[-1].append({"source": source}) + count += 1 + print(count) +s = "" +for p in data: + for d in p: + s += d["source"].replace("\n", r"\n") + "\n" # 换行转\n +with open(outFile, "w", encoding="utf-8") as f: + f.write(s) diff --git a/dev-tools/i18n/convert_txt_ts.py b/dev-tools/i18n/convert_txt_ts.py new file mode 100644 index 0000000..491db33 --- /dev/null +++ b/dev-tools/i18n/convert_txt_ts.py @@ -0,0 +1,27 @@ +import xml.etree.ElementTree as ET +import sys + +inFile = sys.argv[1] +outFile = inFile + ".ts" +sourceFile = inFile[:-3] + "ts" + +tree = ET.parse(sourceFile) +txt = open(inFile, encoding="utf-8").read() +txt = txt.split("\n") +tranIndex = 0 + +root = tree.getroot() + +for page in root: + for message in page: + if message.tag == "name": + pass + else: + for item in message: + if item.tag == "translation": + t = txt[tranIndex].replace(r"\n", "\n") # \n转换行 + item.text = t + # if item.attrib.get("type"): + # item.attrib.pop("type") + tranIndex += 1 +tree.write(outFile) diff --git a/dev-tools/i18n/linguist.exe b/dev-tools/i18n/linguist.exe new file mode 100644 index 0000000..5b97572 Binary files /dev/null and b/dev-tools/i18n/linguist.exe differ diff --git a/dev-tools/i18n/lrelease.exe b/dev-tools/i18n/lrelease.exe new file mode 100644 index 0000000..ed214a5 Binary files /dev/null and b/dev-tools/i18n/lrelease.exe differ diff --git a/dev-tools/i18n/lrelease_all.py b/dev-tools/i18n/lrelease_all.py new file mode 100644 index 0000000..5b69d43 --- /dev/null +++ b/dev-tools/i18n/lrelease_all.py @@ -0,0 +1,16 @@ +# 将所有翻译文件转为二进制 + +import os + +ts_files = [] +for root, dirs, files in os.walk("release"): + for file in files: + if file.endswith(".ts"): + ts_files.append(file) + +print(ts_files) + +for t in ts_files: + cmd = f'''lrelease.exe \ +"release/{t}"''' + os.system(cmd) diff --git a/dev-tools/i18n/lupdate.exe b/dev-tools/i18n/lupdate.exe new file mode 100644 index 0000000..59846d3 Binary files /dev/null and b/dev-tools/i18n/lupdate.exe differ diff --git a/dev-tools/i18n/lupdate_all.py b/dev-tools/i18n/lupdate_all.py new file mode 100644 index 0000000..be52569 --- /dev/null +++ b/dev-tools/i18n/lupdate_all.py @@ -0,0 +1,107 @@ +# 刷新所有翻译文本文件 .ts +# git push 后,在线编辑: +# https://hosted.weblate.org/projects/umi-ocr/ui/ + +import os +import sys +import xml.etree.ElementTree as ET + +LangList = [ + "en_US", # 英语(美国) | English + "zh_TW", # 中文(繁体) | 繁體中文 + "ja_JP", # 日语 | 日本語 + "fr_FR", # 法语 | Français + "pt", # 葡萄牙语 | Português + "ru_RU", # 俄语 | Русский + "uz", # 乌兹别克语 + "vi", # 越南语 + "ta", # 泰米尔语 | தமிழ் + # "nb_NO", + # "it_IT", + # "es_ES", + # "de_DE", + # "ko_KR", + # "pt_BR", +] + +working = os.path.dirname(os.path.abspath(__file__)) # 工作目录 +os.chdir(working) + +# 1. 扫描qml文件,生成 en_US.ts +for l in LangList: + cmd = f'''lupdate.exe \ +"../../UmiOCR-data/qt_res/qml" \ +-recursive \ +-no-obsolete \ +-source-language "zh_CN" \ +-target-language "{l}" \ +-ts "release/{l}.ts"''' + os.system(cmd) + +sys.exit() # Py翻译的部分 待定 + + +# 2. 扫描py文件,生成 en_US_2.ts +def find_files(path, end=".py"): + path = os.path.abspath(path) + files_list = [] + for root, dirs, files in os.walk(path): + # if "__pycache__" + for file in files: + if file.endswith(end): + f = os.path.join(root, file) + f = os.path.relpath(f, os.path.dirname(__file__)) + files_list.append(f) # 使用相对路径,避免父路径的编码问题 + return files_list + + +input_files = find_files("../../UmiOCR-data/py_src") +input_str = "\\\n".join(input_files) +output_str = "release/" + "_2.ts\\\nrelease/".join(LangList) + "_2.ts" +temp_file = "temp.pro" # 临时指令文件,用完删除 +pro_str = f""" +CODECFORTR = UTF-8 +CODECFORSRC = UTF-8 +SOURCES = {input_str} +TRANSLATIONS = {output_str} +""" +with open(temp_file, "w", encoding="utf-8") as f: + f.write(pro_str) + +cmd = f"pyside2-lupdate.exe {temp_file}" +os.system(cmd) +print(output_str) +os.remove(temp_file) + +# 3. 合并 en_US.ts 、 en_US_2.ts +for l in LangList: + path_1 = f"release/{l}.ts" + path_2 = f"release/{l}_2.ts" + with open(path_2, "r", encoding="utf-8") as file: + lines_2 = file.readlines() + # py部分 无需翻译 + if len(lines_2) < 10: + print(f"无需合并:{path_1}") + continue + # py部分 需合并 + # 解析xml + tree_1 = ET.parse(path_1) + root_1 = tree_1.getroot() + root_2 = ET.parse(path_2).getroot() + context_2 = root_2.findall("context") + # 建立一个字典,存放所有 context + context_dict = {c.find("name").text: c for c in root_1.findall("context")} + # 遍历2,追加进字典 + # # 将2的context并入1 + for c2 in root_2.findall("context"): + name = c2.find("name").text + if name in context_dict: + c1 = context_dict[name] + for message_2 in c2.findall("message"): + c1.append(message_2) + else: + root_1.append(c2) # TODO: 正确性未验证 + # TODO: 保存时检查缩进格式 + tree_1.write(path_1, encoding="utf-8", xml_declaration=True) + print(f"合并:{path_1}") + os.remove(path_2) diff --git a/dev-tools/i18n/plugins/platforms/qwindows.dll b/dev-tools/i18n/plugins/platforms/qwindows.dll new file mode 100644 index 0000000..0d08fdd Binary files /dev/null and b/dev-tools/i18n/plugins/platforms/qwindows.dll differ diff --git a/dev-tools/i18n/plugins/styles/qwindowsvistastyle.dll b/dev-tools/i18n/plugins/styles/qwindowsvistastyle.dll new file mode 100644 index 0000000..9d5512d Binary files /dev/null and b/dev-tools/i18n/plugins/styles/qwindowsvistastyle.dll differ diff --git a/dev-tools/i18n/plugins_tr.py b/dev-tools/i18n/plugins_tr.py new file mode 100644 index 0000000..b51bfbd --- /dev/null +++ b/dev-tools/i18n/plugins_tr.py @@ -0,0 +1,33 @@ +import re +import csv +import sys + +inFile = sys.argv[1] +outFile = "待翻译.csv" + + +def extract_strings_from_file(file_path): + pattern = r'tr\(["\']([^"\']*)["\']\)' # 正则表达式模式 + strings = [] + + with open(file_path, "r", encoding="utf-8") as file: + content = file.read() + matches = re.findall(pattern, content) # 使用正则表达式查找匹配项 + + for match in matches: + strings.append(match) + + return strings + + +tr_strs = extract_strings_from_file(inFile) +lines = [["key", "en_US"]] +for i in tr_strs: + lines.append([i, ""]) + +with open(outFile, "w", encoding="utf-8", newline="") as f: + writer = csv.writer(f) + for i in lines: + writer.writerow(i) + +print("插件翻译完成:\n", inFile) diff --git a/dev-tools/i18n/pyside2-lupdate.exe b/dev-tools/i18n/pyside2-lupdate.exe new file mode 100644 index 0000000..e1cf977 Binary files /dev/null and b/dev-tools/i18n/pyside2-lupdate.exe differ diff --git a/dev-tools/i18n/release/ar.ts b/dev-tools/i18n/release/ar.ts new file mode 100644 index 0000000..3fd7f53 --- /dev/null +++ b/dev-tools/i18n/release/ar.ts @@ -0,0 +1,2482 @@ + + + + + About + + + 开源、免费的离线OCR软件 + + + + + 当前版本 + + + + + 项目链接 + + + + + 官方网站 + + + + + 插件拓展 + + + + + 问题反馈 + + + + + 发布地址 + + + + + 许可协议 + + + + + 作者 + + + + + 译者 + + + + + 运行环境信息(如需请求协助,请提供给开发者) + + + + + 复制 + + + + + AsynFilesLoader + + + 正在载入 %1 个文件: +%2 + + + + + BatchDOC + + + 加密 + + + + + %1个加密文档 + + + + + 请点击文件名填写密码 + + + + + 文档已加密 + + + + + 【%1】 +请点击文档名,设置密码 + + + + + 失败 + + + + + 排队 + + + + + 依然关闭 + + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + + + + + + 文档 + + + + + 状态 + + + + + 打开文档 + + + + + 拖入文档或文件夹 + + + + + 清空 + + + + + 文档识别异常 + + + + + 批量识别完成 + + + + + 页数 + + + + + 请选择文档 + + + + + 设置 + + + + + 记录 + + + + + BatchDOCConfigs + + + OCR文本后处理 + + + + + 文档处理 + + + + + 内容提取模式 + + + + + 若一页文档既存在图片又存在文本,如何进行处理 + + + + + 混合OCR/原文本 + + + + + 整页强制OCR + + + + + 仅OCR图片 + + + + + 仅拷贝原有文本 + + + + + 批量任务 + + + + + 保存到 + + + + + 文档原目录 + + + + + + 指定目录 + + + + + 必须先指定“保存到指定目录”才生效 + + + + + OCR结果保存目录 + + + + + 文件名格式 + + + + + 日期时间格式 + + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + + + + + 保存文件类型 + + + + + layered.pdf 双层可搜索文档 + + + + + text.pdf 单层纯文本文档 + + + + + 创建空白PDF文档,只写入识别文字,不含图片 + + + + + txt 标准格式 + + + + + 含识别文字和页数信息 + + + + + p.txt 纯文字格式 + + + + + 输出所有识别文字 + + + + + csv 表格文件(Excel) + + + + + 将页数信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + + + + + jsonl 原始信息 + + + + + 每行为一条json数据,便于第三方程序读取操作 + + + + + 忽略空白页 + + + + + 若某一页没有文字或识别失败,也不会输出错误提示信息 + + + + + 递归读取子文件夹 + + + + + 点击表格,可设置更多内容 + + + + + 导入文件夹时,导入子文件夹中全部文档 + + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文档名 +%range 识别页数范围。只有识别页数小于总页数时才会显示。 +举例:[OCR]_%name%range_%date +生成:[OCR]_文档A(p2-10)_20230901_1213.txt +添加占位符可以避免旧文件被新文件覆盖。 + + + + + 保留原有图片,叠加一层透明文字,可以搜索和复制 + + + + + 其它 + + + + + BatchOCR + + + 依然关闭 + + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + + + + + 排队 + + + + + 处理 + + + + + %1 张图片识别失败! + + + + + 批量识别完成 + + + + + 批量识别任务异常 + + + + + + 图片 + + + + + 耗时 + + + + + 状态 + + + + + 打开图片 + + + + + 拖入图片或文件夹 + + + + + 清空 + + + + + 请选择图片 + + + + + 设置 + + + + + 记录 + + + + + BatchOCRConfigs + + + OCR文本后处理 + + + + + 忽略区域 + + + + + 进入设置 + + + + + 批量任务 + + + + + 保存到 + + + + + 图片原目录 + + + + + + 指定目录 + + + + + 必须先指定“保存到指定目录”才生效 + + + + + OCR结果保存目录 + + + + + 文件名格式 + + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文件夹名/文件名 +举例:[OCR]_%name_%date +生成:[OCR]_我的图片_2023-09-01_12-13.txt +添加占位符可以避免旧文件被新文件覆盖。 + + + + + 日期时间格式 + + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + + + + + 保存文件类型 + + + + + txt 标准格式 + + + + + 含原图片文件名和识别文字 + + + + + p.txt 纯文字格式 + + + + + 仅输出识别文字,不含图片标题 + + + + + txt 单独文件 + + + + + 对每张图片,生成同名txt文件,仅输出识别文字 + + + + + md 图文混排 + + + + + Markdown图文混排格式,可用Markdown阅读器浏览文件 + + + + + csv 表格文件(Excel) + + + + + 将图片信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + + + + + jsonl 原始信息 + + + + + 每行为一条json数据,便于第三方程序读取操作 + + + + + 输出忽略空白图片 + + + + + 若图片没有文字或识别失败,也不会输出错误提示信息 + + + + + 递归读取子文件夹 + + + + + 导入文件夹时,导入子文件夹中全部图片 + + + + + 其它 + + + + + Configs + + + %1 处理配置项异常: +%2枚举列表为空。 + + + + + 重置 + + + + + 重置本页上的设定 + + + + + 重置设定 + + + + + 要重置本页的设定吗? + + + + + 高级 + + + + + 显示更多高级选项。它们标有 * 号。 +请谨慎修改高级选项。 + + + + + 展开 + + + + + 折叠 + + + + + 必须为整数 + + + + + 不能超过 + + + + + 不能低于 + + + + + 必须为数字 + + + + + 已取消%1的快捷键。 + + + + + 更新热键成功 + + + + + %1的快捷键为 %2 + + + + + %1 快捷键%2已被注册,请尝试另外的按键组合。 + + + + + %1 快捷键%2无法注册,请尝试另外的按键组合。 + + + + + 请按下快捷键组合。按【Esc】退出。 + + + + + 当前快捷键录制已在进行,不能同时录制! + + + + + 无法录制快捷键 + + + + + DropArea_ + + + 松手放入文件 + + + + + FontPanel + + + 界面 + + + + + 内容 + + + + + 界面字体: +软件中大部分UI的字体。 + + + + + 内容字体: +识别结果内容的字体。 + + + + + GlobalConfigs + + + 快捷方式 + + + + + 桌面 + + + + + 开始菜单 + + + + + 开机自启 + + + + + 界面和外观 + + + + + 主题 + + + + + 切换主题 + + + + + 字体 + + + + + 修改字体 + + + + + 界面与文字大小 + + + + + 渲染器 + + + + + 关闭硬件加速 + + + + + 若出现界面闪烁、元素错位等界面异常,尝试切换渲染器或者关闭硬件加速 + + + + + 禁用美化效果 + + + + + 在低配置机器上,禁用动画、阴影等效果可减少部分资源占用 + + + + + 图片预览默认显示叠加层 + + + + + 默认开启/关闭叠加层显示 +对所有图片预览组件生效 + + + + + 窗口 + + + + + 启动时缩小到任务栏 + + + + + 软件启动时,不弹出主窗口 + + + + + 窗口置顶 + + + + + 捷径:窗口左上角图钉 + + + + + 锁定标签栏 + + + + + 捷径:窗口右上角小锁 + + + + + 关闭主窗口时 + + + + + 最小化到系统托盘 + + + + + 退出应用 + + + + + 隐藏托盘图标 + + + + + 若要弹出位于后台的软件窗口,请在文件管理器中重复启动软件。 +若要彻底退出软件,请从任务管理器中结束进程。 + + + + + 截图 + + + + + 截图前隐藏主窗口 + + + + + 截图前,如果主窗口处于前台,则隐藏主窗口 +将会延时等待主窗口关闭 + + + + + 隐藏等待时间 + + + + + 秒 + + + + + 服务 + + + + + 允许HTTP服务 + + + + + Umi-OCR依赖HTTP接口进行本机跨进程通信。如果禁用,将无法使用命令行模式、多开检测等功能。 + + + + + + + 重启软件后生效 + + + + + 主机 + + + + + 仅本地 + + + + + 任何可用地址 + + + + + 将允许局域网访问。请开启对应防火墙权限! + + + + + 将禁止局域网访问。 + + + + + 端口 + + + + + 日志 + + + + + 操作 + + + + + 打开日志保存目录 + + + + + 保存的日志级别 + + + + + 已禁用日志文件保存 + + + + + 请尝试更换软件路径! + + + + + 配置文件读写异常 + + + + + 插件加载失败 + + + + + 成功添加快捷方式 + + + + + 添加快捷方式失败 + + + + + 成功移除 %1 个快捷方式 + + + + + 提示 + + + + + 没有找到可移除的快捷方式。 + + + + + 渲染器变更 将在重启软件后生效 + + + + + 端口号不合法 + + + + + 端口号改为%1 + + + + + 原端口号%1被占用, +切换为新端口号%2。 + +若不想看到此通知,请在全局设置关闭高级模式。 + + + + + GlobalConfigsPage + + + 1. 关于快捷键、OCR语言等选项,请在各个功能页中进行设置。 +2. 勾选左下角的“高级”按钮,可以显示更多进阶选项。 + + + + + 小贴士 + + + + + HTabBar + + + 窗口置顶 + + + + + 锁定标签栏 + + + + + IconButtonBar + + + 右键菜单 + + + + + 保存图片 + + + + + 图片大小:适应窗口 + + + + + 图片大小:实际 + + + + + 截图 + + + + + 粘贴 + + + + + 屏幕截图 + + + + + 粘贴图片 + + + + + IgnoreArea + + + 拖入本地图片:OCR预览 +滚轮:缩放 +左键:拖拽 +右键:绘制忽略区域 + +可绘制一个或多个忽略区域矩形框。在执行批量OCR时,完全位于忽略区域内的文本块将被排除。 +比如批量处理影视截图时,可在右上角水印处添加忽略区域,避免输出水印文本。 + + + + + 图像尺寸: + + + + + 区域数量: + + + + + 撤销 + + + + + 清空 + + + + + ImageScale + + + 复制图片 + + + + + 复制图片失败 + + + + + 打开图片 + + + + + 打开图片失败 + + + + + + 保存图片 + + + + + 保存图片失败 + + + + + ImageWithText + + + 图片:复制%1字 + + + + + 图片:无选中文字 + + + + + 图片:复制全部%1字 + + + + + 复制  (Ctrl+C) + + + + + 全选  (Ctrl+A) + + + + + 复制图片(Ctrl+X) + + + + + 保存图片(Ctrl+S) + + + + + 显示/隐藏文字(Tab) + + + + + 用默认应用打开图片 + + + + + 删除图片(Ctrl+D) + + + + + MainWindowManager + + + 欢迎使用 Umi-OCR + + + + + 已启用后台模式,可通过快捷键使用功能。 + + + + + MarkdownView + + + 打开网页 + + + + + 链接 + + + + + MessageBox + + + 取消 + + + + + 确定 + + + + + 警告 + + + + + 发生了一点小问题 + + + + + 复制 + + + + + 已复制报错信息 %1 + + + + + 请前往 Issues 页面寻找解答或反馈 + + + + + 问题反馈 + + + + + MessageBoxWin + + + 确定 + + + + + 不再提示 + + + + + 知道了 + + + + + MissionCtrlPanel + + + 开始任务 + + + + + 暂停任务 +暂停后可以待机或休眠。 +但是关机或退出软件,将会丢弃任务内容。 + + + + + 继续任务 + + + + + 终止任务 +放弃未完成的内容。 + + + + + 正在运行 + + + + + 正在保存 + + + + + 已暂停 + + + + + 任务停止 + + + + + 任务完成 + + + + + ModalLayer + + + 保存并返回 + + + + + 返回 + + + + + Navigation + + + 欢迎使用 Umi-OCR + + + + + 请选择功能页 + + + + + 当前版本 + + + + + 项目链接 + + + + + 官方网站 + + + + + 插件拓展 + + + + + 问题反馈 + + + + + 功能页 + + + + + 请打开对应标签页(如批量OCR、批量文档),再拖入文件。 + + + + + OcrManager + + + 文字识别 + + + + + 操作 + + + + + + 强制终止任务 + + + + + 应用修改 + + + + + 当前接口 + + + + + 没有可用的 OCR 插件。 + + + + + OCR API 列表中不存在%1 + + + + + 当前已有%1组任务队列、共%2个任务正在执行。您可【强制终止任务】后修改API。 + + + + + 无法修改 文字识别接口设置 + + + + + 文字识别接口应用成功 + + + + + 当前API为【%1】 + + + + + 文字识别接口应用失败 + + + + + 当前没有运行中的任务 + + + + + 当前已有%1组任务队列、共%2个任务正在执行。 +要强制终止全部任务吗? + + + + + PagesManager + + + 新标签页 + + + + + 截图OCR + + + + + # 截图OCR + +屏幕截图,快捷转文字。也支持粘贴图片。 + + + + + # 全局设置 + +调节全局设置项,对所有页面生效。 + + + + + 批量OCR + + + + + # 批量OCR + +导入本地图片或文件夹,批量转换文字。 + +支持格式: + + + + + 批量文档 + + + + + # 批量文档识别 + +批量导入文档,提取文字,或生成双层可搜索PDF。 + +支持格式: + + + + + 二维码 + + + + + # 二维码 + +识别或生成二维码、条形码。 + +支持协议: + + + + + 全局设置 + + + + + 关于 + + + + + # 关于 + + + + + PopupManager + + + 确认 + + + + + 取消 + + + + + PreviewDoc + + + 加密 + + + + + 打开文档失败 + + + + + 请填写正确的密码 + + + + + 密码正确 + + + + + 文档预览异常 + + + + + 忽略区域说明: +右键拖拽,绘制矩形区域,包含在区域内的文字框将被忽略。可用于排除水印、页眉页脚。 +范围允许填写负数,表示倒数第x页。如-1表示最后一页,-2表示倒数第2页。 +忽略区域的设置对所有文档生效。 + + + + + 密码: + + + + + 预览页面 + + + + + 预览PDF时,是否预览OCR结果 + + + + + OCR页数 + + + + + 忽略区域页数(全局) + + + + + + 范围 + + + + + 撤销 + + + + + 清空 + + + + + PreviewImage + + + 文字 + + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + + + + + QRCode + + + 获取剪贴板异常 + + + + + 剪贴板中为文本 + + + + + 无有效图片 + + + + + 导入%1条图片路径 + + + + + 参数有误,或输入内容不合规定。请参照报错指示修改: + + + + + + 生成二维码失败 + + + + + 已复制到剪贴板 + + + + + 识图完成 + + + + + 无文字 + + + + + 识别失败 + + + + + 扫描二维码 + + + + + 截图、拖入或粘贴二维码图片 + + + + + + 设置 + + + + + 自动刷新 + + + + + 修改文字后,自动生成二维码/条形码 + + + + + 刷新 + + + + + 生成二维码/条形码 + + + + + 记录 + + + + + 生成 + + + + + QRCodeConfigs + + + 快捷键 + + + + + 屏幕截图 + + + + + 粘贴图片 + + + + + 预处理(一般无需改动) + + + + + 中值滤波 + + + + + 对图像进行平滑处理 +>0 时生效。可填1~9的奇数(不允许偶数) + + + + + 锐度增强 + + + + + 增加图像的锐度 +>0.1 时生效。可填0.1~10的小数 + + + + + 对比度增强 + + + + + 增加图像的对比度 +>0.1 时生效。可填0.1~10的小数 + + + + + 转为灰度 + + + + + 将图像像素转为灰度 + + + + + 二值化 + + + + + 将图像像素转为纯黑和纯白 +启用了灰度,且二值化 >-1 时生效。可填0~255的整数 + + + + + 扫码后的操作 + + + + + 复制结果 + + + + + 弹出主窗口 + + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + + + + + 生成二维码/条形码 + + + + + 类型 + + + + + 默认二维码: + + + + + 宽度 + + + + + + 填0:自动选择 + + + + + + + 像素 + + + + + 高度 + + + + + 边缘空白 + + + + + 填-1:自动选择 + + + + + 纠错等级 + + + + + 仅适用于: + + + + + 自动 + + + + + 其它 + + + + + ResultTextContainer + + + 复制 + + + + + ResultsTableView + + + 异常状态码:%1 +异常信息:%2 + + + + + 耗时 %1 + + + + + 置信度 %1 + + + + + 记录:复制%1字 + + + + + 记录:无选中文字 + + + + + 记录:复制全部%1字 + + + + + 删除%1条记录 + + + + + 清空记录 + + + + + 复制    (Ctrl+C) + + + + + 复制全部  (Ctrl+C 双击) + + + + + 选中单个  (Ctrl+A) + + + + + 选中全部记录(Ctrl+A 双击) + + + + + 删除选中记录 + + + + + 清空全部记录(Ctrl+D 双击) + + + + + 滚动 + + + + + 自动滚动到底部 + + + + + 右键菜单 + + + + + ScreenshotManager + + + 截图失败 + + + + + 上次截图操作未结束,不能进行新的截图! + + + + + 尚未记录截图区域 + + + + + 显示器: %1 +错误信息: %2 + + + + + 重复截图失败 + + + + + 未找到匹配的屏幕 + + + + + 屏幕设备名称不相同: +%1 +%2 + + + + + ScreenshotOCR + + + 获取剪贴板异常 + + + + + 剪贴板中为文本 + + + + + 无有效图片 + + + + + 导入%1条图片路径 + + + + + 依然关闭 + + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + + + + + + 屏幕截图 + + + + + + 粘贴图片 + + + + + 截图识别任务异常 + + + + + 已复制到剪贴板 + + + + + 识图完成 + + + + + 无文字 + + + + + 识别失败 + + + + + + 停止任务 + + + + + 文字 + + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + + + + + 截图、拖入或粘贴图片 + + + + + 设置 + + + + + 记录 + + + + + ScreenshotOcrConfigs + + + OCR文本后处理 + + + + + 快捷键 + + + + + 屏幕截图 + + + + + 粘贴图片 + + + + + 重复截图 + + + + + 重新截取上一次截图的范围 + + + + + 识图后的操作 + + + + + 复制结果 + + + + + 弹出主窗口 + + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + + + + + 其它 + + + + + Size_ + + + 1.0 + + + + + SystemTray + + + 打开主窗口 + + + + + 退出 Umi-OCR + + + + + Theme + + + 珍珠白 + + + + + 云墨黑 + + + + + 赛博朋克 + + + + + UtilsConfigDicts + + + 任务完成后的操作 + + + + + 系统 + + + + + 无 + + + + + 关机 + + + + + 休眠 + + + + + %1 系统暂不支持电源控制! + + + + + 继续%1 + + + + + 取消%1 + + + + + 系统即将%1 + + + + + 排版解析方案 + + + + + 按什么方式,解析和排序图片中的文字块 + + + + + 多栏-按自然段换行 + + + + + 多栏-总是换行 + + + + + 多栏-无换行 + + + + + 单栏-按自然段换行 + + + + + 单栏-总是换行 + + + + + 单栏-无换行 + + + + + 单栏-保留缩进 + + + + + 不做处理 + + + + + 优先内部 + + + + + 只允许内部 + + + + + 只允许外部 + + + + + 禁用所有通知 + + + + + 跟随全局设定 + + + + + 通知弹窗类型 + + + + + 通知类型已更改 + + + + diff --git a/dev-tools/i18n/release/en_US.ts b/dev-tools/i18n/release/en_US.ts new file mode 100644 index 0000000..89ad8bb --- /dev/null +++ b/dev-tools/i18n/release/en_US.ts @@ -0,0 +1,2556 @@ + + + + + About + + + 开源、免费的离线OCR软件 + An opensource, free offline OCR software + + + + 当前版本 + Current version + + + + 项目链接 + Project link + + + + 官方网站 + Official website + + + + 插件拓展 + Plugins extension + + + + 问题反馈 + Issue a problem + + + + 发布地址 + Release webpage + + + + 许可协议 + License + + + + 作者 + Author + + + + 译者 + Translators + + + + 运行环境信息(如需请求协助,请提供给开发者) + Runtime environment (If you need assistance, please provide this to the developer) + + + + 复制 + Copy + + + + AsynFilesLoader + + + 正在载入 %1 个文件: +%2 + Loading %1 file(s): +%2 + + + + BatchDOC + + + 加密 + Encrypted + + + + %1个加密文档 + %1 encrypted file + + + + 请点击文件名填写密码 + Please click on the filename and fill in password + + + + 文档已加密 + File has been encrypted + + + + 【%1】 +请点击文档名,设置密码 + 【%1】 +Please click on the filename and reset your password + + + + 失败 + Failed + + + + 排队 + In queue + + + + 依然关闭 + Confirm close + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + Task in progress. +Do you want to end the task and close the page? + + + + + 文档 + File + + + + 状态 + Status + + + + 打开文档 + Open documents + + + + 拖入文档或文件夹 + Drag document or folder in + + + + 清空 + Clear + + + + 文档识别异常 + Document recognition error + + + + 批量识别完成 + Batch recognition completed + + + + 页数 + Contents + + + + 请选择文档 + Please select a file + + + + 设置 + Settings + + + + 记录 + History + + + + BatchDOCConfigs + + + OCR文本后处理 + OCR text post-processing + + + + 文档处理 + document processing + + + + 内容提取模式 + Content extraction mode + + + + 若一页文档既存在图片又存在文本,如何进行处理 + If a document page contains both images and text, how should it be processed + + + + 混合OCR/原文本 + Mixed OCR/Original Text + + + + 整页强制OCR + Force OCR the whole page + + + + 仅OCR图片 + Only OCR the pictures in documents + + + + 仅拷贝原有文本 + Only copy the original text + + + + 批量任务 + Batch task + + + + 保存到 + Save to + + + + 文档原目录 + Original Document Directory + + + + + 指定目录 + Specified directory + + + + 必须先指定“保存到指定目录”才生效 + "Save to specified directory" must be specified first for it to take effect + + + + OCR结果保存目录 + OCR result output directory + + + + 文件名格式 + File name format + + + + 日期时间格式 + Date and time format + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + Date format for %date in the file name. The following placeholders are supported: +%Y year, %m month, %d day, %H hour, +%M minute, %S second, %unix timestamp +Example: %Y-%m-%d_%H-%M +Generated: 2023-09-01_12-13.txt + + + + 保存文件类型 + Output file format + + + + layered.pdf 双层可搜索文档 + layered.pdf Double-layer Searchable Document + + + + text.pdf 单层纯文本文档 + text.pdf Single-layer text-only document + + + + 创建空白PDF文档,只写入识别文字,不含图片 + Create a blank PDF document, write only recognized text, no images included + + + + txt 标准格式 + txt Standard format + + + + 含识别文字和页数信息 + Contains Recognized Text and Page Number Information + + + + p.txt 纯文字格式 + p.txt Plain text format + + + + 输出所有识别文字 + Output All Recognized Text + + + + csv 表格文件(Excel) + csv Table file (Excel) + + + + 将页数信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + Write page number information and recognized content into a CSV table file. It can be opened with Excel and saved as an xlsx format. + + + + jsonl 原始信息 + jsonl Raw information + + + + 每行为一条json数据,便于第三方程序读取操作 + Each line is a json structured data, convenient for third-party program to read + + + + 忽略空白页 + Ignore blank pages + + + + 若某一页没有文字或识别失败,也不会输出错误提示信息 + If a page contains no text or fails to be recognized, no error message will be output + + + + 递归读取子文件夹 + Recursively read subfolders + + + + 点击表格,可设置更多内容 + clicking on tables, which could provide additional content + + + + 导入文件夹时,导入子文件夹中全部文档 + When importing a folder, import all documents in the subfolders as well + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文档名 +%range 识别页数范围。只有识别页数小于总页数时才会显示。 +举例:[OCR]_%name%range_%date +生成:[OCR]_文档A(p2-10)_20230901_1213.txt +添加占位符可以避免旧文件被新文件覆盖。 + No need to fill in the extension name. The following placeholders are supported: +%date Date and time +%name Original folder name/file name +%range Page range. Only displayed when the OCR page count is less than the total page count. +Example: [OCR]_%name%range_%date +Generated: [OCR]_Document(p2-10)_2023-09-01_12-13.txt +Adding placeholders can prevent old files from being overwritten by new files. + + + + 保留原有图片,叠加一层透明文字,可以搜索和复制 + Preserve the original images, overlay a layer of transparent text that can be searched and copied + + + + 其它 + Others + + + + BatchOCR + + + 依然关闭 + Confirm + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + Task in progress. +Do you want to end the task and close the page? + + + + 排队 + In queue + + + + 处理 + Processing + + + + %1 张图片识别失败! + %1 image(s) failed to recognize! + + + + 批量识别完成 + Batch recognition completed + + + + 批量识别任务异常 + Batch recognition task exception + + + + + 图片 + Image + + + + 耗时 + Time elapsed + + + + 状态 + Status + + + + 打开图片 + Open Image + + + + 拖入图片或文件夹 + Drop in picture or folder + + + + 清空 + Clear + + + + 请选择图片 + Please select an image + + + + 设置 + Settings + + + + 记录 + History + + + + BatchOCRConfigs + + + OCR文本后处理 + OCR text post-processing + + + + 忽略区域 + Ignore region + + + + 进入设置 + Open settings + + + + 批量任务 + Batch task + + + + 保存到 + Save to + + + + 图片原目录 + Original directory + + + + + 指定目录 + Specified directory + + + + 必须先指定“保存到指定目录”才生效 + "Save to specified directory" must be specified first for it to take effect + + + + OCR结果保存目录 + OCR result output directory + + + + 文件名格式 + File name format + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文件夹名/文件名 +举例:[OCR]_%name_%date +生成:[OCR]_我的图片_2023-09-01_12-13.txt +添加占位符可以避免旧文件被新文件覆盖。 + No need to fill in the extension name. The following placeholders are supported: +%date Date and time +%name Original folder name/file name +Example: [OCR]_%name_%date +Generated: [OCR]_MyImage_2023-09-01_12-13.txt +Adding placeholders can prevent old files from being overwritten by new files. + + + + 日期时间格式 + Date and time format + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + Date format for %date in the file name. The following placeholders are supported: +%Y year, %m month, %d day, %H hour, +%M minute, %S second, %unix timestamp +Example: %Y-%m-%d_%H-%M +Generated: 2023-09-01_12-13.txt + + + + 保存文件类型 + Output file format + + + + txt 标准格式 + txt Standard format + + + + 含原图片文件名和识别文字 + Includes original image file name and recognized text + + + + p.txt 纯文字格式 + p.txt Plain text format + + + + 仅输出识别文字,不含图片标题 + Outputs only the recognized text without the image title + + + + txt 单独文件 + txt Separated file + + + + 对每张图片,生成同名txt文件,仅输出识别文字 + Generates a txt file of the same name for each image, outputs only the recognized text + + + + md 图文混排 + md Mixture of text and image + + + + Markdown图文混排格式,可用Markdown阅读器浏览文件 + Markdown mixture of text and image format, can be previewed with a Markdown reader + + + + csv 表格文件(Excel) + csv Table file (Excel) + + + + 将图片信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + Writes image information and recognition content to a csv table file. Can be converted to xlsx format using Excel. + + + + jsonl 原始信息 + jsonl Raw information + + + + 每行为一条json数据,便于第三方程序读取操作 + Each line is a json structured data, convenient for third-party program to read + + + + 输出忽略空白图片 + Ignore blank images in output + + + + 若图片没有文字或识别失败,也不会输出错误提示信息 + If the image has no text or recognition fails, no error prompt message will be generated + + + + 递归读取子文件夹 + Recursively read subfolders + + + + 导入文件夹时,导入子文件夹中全部图片 + When importing a folder, import all images in the subfolders + + + + 其它 + Others + + + + Configs + + + %1 处理配置项异常: +%2枚举列表为空。 + %1 Processing configuration item exception: +%2 enumeration list is empty. + + + + 重置 + Reset + + + + 重置本页上的设定 + Reset settings on this page + + + + 重置设定 + Reset settings + + + + 要重置本页的设定吗? + Do you want to reset the settings on this page? + + + + 高级 + Advanced + + + + 显示更多高级选项。它们标有 * 号。 +请谨慎修改高级选项。 + Show more advanced options. They are marked with an asterisk (*). +Please be cautious when modifying advanced options. + + + + 展开 + Expand + + + + 折叠 + Collapse + + + + 必须为整数 + Must be an integer + + + + 不能超过 + Cannot exceed + + + + 不能低于 + Cannot be lower than + + + + 必须为数字 + Must be a number + + + + 已取消%1的快捷键。 + Hotkey for %1 canceled. + + + + 更新热键成功 + Hotkey updated successfully + + + + %1的快捷键为 %2 + Hotkey for %1 is %2 + + + + %1 快捷键%2已被注册,请尝试另外的按键组合。 + Hotkey %2 for %1 is already registered, please try another key combination. + + + + %1 快捷键%2无法注册,请尝试另外的按键组合。 + Hotkey %2 for %1 cannot be registered, please try another key combination. + + + + 请按下快捷键组合。按【Esc】退出。 + Press the key combination. Press [Esc] to exit. + + + + 当前快捷键录制已在进行,不能同时录制! + Hotkey recording is already in progress, cannot record simultaneously! + + + + 无法录制快捷键 + Cannot record hotkey + + + + DropArea_ + + + 松手放入文件 + Release the mouse to drop the file + + + + FontPanel + + + 界面 + Interface + + + + 内容 + Content + + + + 界面字体: +软件中大部分UI的字体。 + Interface font: +The font for most of the UI in the software. + + + + 内容字体: +识别结果内容的字体。 + Content font: +The font for the recognition result. + + + + GlobalConfigs + + + 快捷方式 + Shortcut + + + + 桌面 + Desktop + + + + 开始菜单 + Start menu + + + + 开机自启 + Launch on startup + + + + 界面和外观 + Interface and appearance + + + + 主题 + Theme + + + + 切换主题 + Switch theme + + + + 字体 + Font + + + + 修改字体 + Modify font + + + + 界面与文字大小 + Interface and font size + + + + 渲染器 + Renderer + + + + 关闭硬件加速 + Disable hardware acceleration + + + + 若出现界面闪烁、元素错位等界面异常,尝试切换渲染器或者关闭硬件加速 + If there are interface flickering, misalignment, or other interface anomalies, try switching the renderer or disabling hardware acceleration + + + + 禁用美化效果 + Disable visual effects + + + + 在低配置机器上,禁用动画、阴影等效果可减少部分资源占用 + On low-end machines, disabling animations, shadows, and other effects can reduce resource consumption + + + + 图片预览默认显示叠加层 + Default image preview shows overlay layer + + + + 默认开启/关闭叠加层显示 +对所有图片预览组件生效 + Default to enable/disable overlay layer display +Applies to all image preview components + + + + 窗口 + Window + + + + 启动时缩小到任务栏 + Minimize to taskbar on startup + + + + 软件启动时,不弹出主窗口 + Silent starts + + + + 窗口置顶 + Window always on top + + + + 捷径:窗口左上角图钉 + Shortcut: Pin icon in the top left corner of the window + + + + 锁定标签栏 + Lock tab bar + + + + 捷径:窗口右上角小锁 + Shortcut: Lock icon in the top right corner of the window + + + + 关闭主窗口时 + Action when closing the main window + + + + 最小化到系统托盘 + Minimize to system tray + + + + 退出应用 + Exit the application + + + + 隐藏托盘图标 + Hide the tray icon + + + + 若要弹出位于后台的软件窗口,请在文件管理器中重复启动软件。 +若要彻底退出软件,请从任务管理器中结束进程。 + To bring a software window in the background to the foreground, restart the software in the file manager. +To completely exit the software, end the process from the task manager. + + + + 截图 + Screenshot + + + + 截图前隐藏主窗口 + Hide main window before taking a screenshot + + + + 截图前,如果主窗口处于前台,则隐藏主窗口 +将会延时等待主窗口关闭 + If the main window is in the foreground before taking a screenshot, hide the main window +It will wait for the main window to close before taking the screenshot + + + + 隐藏等待时间 + Hide waiting time + + + + 秒 + seconds + + + + 服务 + Service + + + + 允许HTTP服务 + Allow HTTP service + + + + Umi-OCR依赖HTTP接口进行本机跨进程通信。如果禁用,将无法使用命令行模式、多开检测等功能。 + Umi-OCR relies on HTTP interface for inter-process communication on the local machine. If disabled, features like command-line mode and multi-instance detection will not work. + + + + + + 重启软件后生效 + Takes effect after restarting the software + + + + 主机 + Host + + + + 仅本地 + Local only + + + + 任何可用地址 + Any available address + + + + 将允许局域网访问。请开启对应防火墙权限! + Allows LAN access. Please enable corresponding firewall permissions! + + + + 将禁止局域网访问。 + Disables LAN access. + + + + 端口 + Port + + + + 日志 + Log + + + + 操作 + Action + + + + 打开日志保存目录 + Open log file directory + + + + 保存的日志级别 + Minimum Log Level to Save + + + + 已禁用日志文件保存 + Log file saving is disabled + + + + 请尝试更换软件路径! + Please try changing the software path! + + + + 配置文件读写异常 + Configuration file read/write exception + + + + 插件加载失败 + Plugin loading failure + + + + 成功添加快捷方式 + Shortcut added successfully + + + + 添加快捷方式失败 + Failed to add shortcut + + + + 成功移除 %1 个快捷方式 + Successfully removed %1 shortcuts + + + + 提示 + Tip + + + + 没有找到可移除的快捷方式。 + No removable shortcuts found. + + + + 渲染器变更 将在重启软件后生效 + Renderer change will take effect after restarting the software + + + + 端口号不合法 + Invalid port number + + + + 端口号改为%1 + The port number has been changed to %1 + + + + 原端口号%1被占用, +切换为新端口号%2。 + +若不想看到此通知,请在全局设置关闭高级模式。 + The original port %1 is occupied, +switched to new port %2. + +If you don't want to see this notification, disable advanced mode in global settings. + + + + GlobalConfigsPage + + + 1. 关于快捷键、OCR语言等选项,请在各个功能页中进行设置。 +2. 勾选左下角的“高级”按钮,可以显示更多进阶选项。 + 1. For options related to shortcuts, OCR languages, and others, please configure them on the respective feature pages. +2. Checking the "Advanced" button in the lower left corner will reveal more advanced settings. + + + + 小贴士 + Tips + + + + HTabBar + + + 窗口置顶 + Window always on top + + + + 锁定标签栏 + Lock tab bar + + + + IconButtonBar + + + 右键菜单 + Right-click menu + + + + 保存图片 + Save Image + + + + 图片大小:适应窗口 + Image size: Fit to window + + + + 图片大小:实际 + Image size: Actual + + + + 截图 + Screenshot + + + + 粘贴 + Paste + + + + 屏幕截图 + Screenshot + + + + 粘贴图片 + Paste image + + + + IgnoreArea + + + 拖入本地图片:OCR预览 +滚轮:缩放 +左键:拖拽 +右键:绘制忽略区域 + +可绘制一个或多个忽略区域矩形框。在执行批量OCR时,完全位于忽略区域内的文本块将被排除。 +比如批量处理影视截图时,可在右上角水印处添加忽略区域,避免输出水印文本。 + Drag and drop local image: OCR preview +Scroll wheel: Zoom +Left click: Drag +Right click: Draw an ignore region + +You can draw one or more rectangles as ignore regions. When performing batch OCR, text blocks completely within the ignore region will be excluded. +For example, when processing screenshots of watermarked images, you can add an ignore region at the watermark in the upper right corner to avoid outputting watermark text. + + + + 图像尺寸: + Image size: + + + + 区域数量: + Number of regions: + + + + 撤销 + Undo + + + + 清空 + Clear + + + + ImageScale + + + 复制图片 + Copy image + + + + 复制图片失败 + Failed to copy image + + + + 打开图片 + Open Image + + + + 打开图片失败 + Open Image Failed + + + + + 保存图片 + Save Image + + + + 保存图片失败 + Failed to save image + + + + ImageWithText + + + 图片:复制%1字 + Image: Copy %1 characters + + + + 图片:无选中文字 + Image: No selected text + + + + 图片:复制全部%1字 + Image: Copy all %1 characters + + + + 复制  (Ctrl+C) + Copy (Ctrl+C) + + + + 全选  (Ctrl+A) + Select all (Ctrl+A) + + + + 复制图片(Ctrl+X) + Copy image (Ctrl+X) + + + + 保存图片(Ctrl+S) + Save image (Ctrl+S) + + + + 显示/隐藏文字(Tab) + Show/Hide text (Tab) + + + + 用默认应用打开图片 + Open with the default application + + + + 删除图片(Ctrl+D) + Delete image (Ctrl+D) + + + + MainWindowManager + + + 欢迎使用 Umi-OCR + Welcome to Umi-OCR + + + + 已启用后台模式,可通过快捷键使用功能。 + Background mode is enabled, functions can be accessed using hotkeys. + + + + MarkdownView + + + 打开网页 + Open webpage + + + + 链接 + URL + + + + MessageBox + + + 取消 + Cancel + + + + 确定 + OK + + + + 警告 + Warning + + + + 发生了一点小问题 + A small problem has occurred + + + + 复制 + Copy + + + + 已复制报错信息 %1 + Error message copied. %1 + + + + 请前往 Issues 页面寻找解答或反馈 + Please visit the Issues page for Q&&A or feedback + + + + 问题反馈 + Issue a problem + + + + MessageBoxWin + + + 确定 + OK + + + + 不再提示 + Don't prompt again + + + + 知道了 + OK + + + + MissionCtrlPanel + + + 开始任务 + Start task + + + + 暂停任务 +暂停后可以待机或休眠。 +但是关机或退出软件,将会丢弃任务内容。 + Pause task +After pausing, you can standby or hibernate. +However, shutting down or exiting the software will discard the task content. + + + + 继续任务 + Resume task + + + + 终止任务 +放弃未完成的内容。 + Terminate task +Abandon unfinished content. + + + + 正在运行 + Running + + + + 正在保存 + Saving + + + + 已暂停 + Task paused + + + + 任务停止 + Task stopped + + + + 任务完成 + Task completed + + + + ModalLayer + + + 保存并返回 + Save and return + + + + 返回 + Back + + + + Navigation + + + 欢迎使用 Umi-OCR + Welcome to Umi-OCR + + + + 请选择功能页 + Please select the function page + + + + 当前版本 + Current version + + + + 项目链接 + Project link + + + + 官方网站 + Official website + + + + 插件拓展 + Plugins + + + + 问题反馈 + Issue a problem + + + + 功能页 + Feature Tab + + + + 请打开对应标签页(如批量OCR、批量文档),再拖入文件。 + Please open the corresponding tab (e.g., Batch OCR, Batch Document) before dragging in files. + + + + OcrManager + + + 文字识别 + Text Recognition + + + + 操作 + Action + + + + + 强制终止任务 + Force Terminate Task + + + + 应用修改 + Apply Modifications + + + + 当前接口 + Current Interface + + + + 没有可用的 OCR 插件。 + No OCR plugin available. + + + + OCR API 列表中不存在%1 + %1 does not exist in the OCR API list + + + + 当前已有%1组任务队列、共%2个任务正在执行。您可【强制终止任务】后修改API。 + There are currently %1 task queues with %2 tasks in progress. You can modify the API after triggering [Force Terminate Task]. + + + + 无法修改 文字识别接口设置 + Unable to modify text recognition interface settings + + + + 文字识别接口应用成功 + Text recognition interface applied successfully + + + + 当前API为【%1】 + Current API: 【%1】 + + + + 文字识别接口应用失败 + Failed to apply text recognition interface + + + + 当前没有运行中的任务 + No currently running tasks + + + + 当前已有%1组任务队列、共%2个任务正在执行。 +要强制终止全部任务吗? + There are currently %1 task queues with %2 tasks in progress. +Do you want to force terminate all tasks? + + + + PagesManager + + + 新标签页 + New Tab + + + + 截图OCR + Screenshot OCR + + + + # 截图OCR + +屏幕截图,快捷转文字。也支持粘贴图片。 + # Screenshot OCR + +Text recognition of the screenshot. Also supports pasted images. + + + + # 全局设置 + +调节全局设置项,对所有页面生效。 + # Global Settings + +Adjust global settings. Applys to all pages. + + + + 批量OCR + Batch OCR + + + + # 批量OCR + +导入本地图片或文件夹,批量转换文字。 + +支持格式: + # Batch OCR + +Import local images or folders for batch text conversion. + +Supported formats: + + + + 批量文档 + Batch Documents OCR + + + + # 批量文档识别 + +批量导入文档,提取文字,或生成双层可搜索PDF。 + +支持格式: + # Batch OCR Documents + +Bulk import documents, extract text, or generate dual-layer searchable PDFs. + +Supported formats: + + + + 二维码 + QR Code + + + + # 二维码 + +识别或生成二维码、条形码。 + +支持协议: + # QR Code + +Recognize or generate QR codes, barcodes. + +Supported protocols: + + + + 全局设置 + Global Settings + + + + 关于 + About + + + + # 关于 + # About + + + + PopupManager + + + 确认 + Confirm + + + + 取消 + Cancel + + + + PreviewDoc + + + 加密 + Encrypted + + + + 打开文档失败 + Open document Failed + + + + 请填写正确的密码 + Please fill in the correct password + + + + 密码正确 + Password correct + + + + 文档预览异常 + Document preview error + + + + 忽略区域说明: +右键拖拽,绘制矩形区域,包含在区域内的文字框将被忽略。可用于排除水印、页眉页脚。 +范围允许填写负数,表示倒数第x页。如-1表示最后一页,-2表示倒数第2页。 +忽略区域的设置对所有文档生效。 + Usage: +Hold your right mouse button, drag and draw a rect area, the contents included will be ignored by the software. It can be used to exclude watermarks, headers or footers. +It is allowed to input negative page number to represent the x page counting backwards.For instance, -1 represents the last page,while -2 is the page before it. +This setting is enabled for all documents. + + + + 密码: + Password: + + + + 预览页面 + Preview pages + + + + 预览PDF时,是否预览OCR结果 + When previewing a PDF, should the OCR result be previewed + + + + OCR页数 + OCR page count + + + + 忽略区域页数(全局) + Ignore area page count (Global) + + + + + 范围 + Range + + + + 撤销 + Undo + + + + 清空 + Clear + + + + PreviewImage + + + 文字 + Text + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + Overlay recognized text on the image +Can be set as default off in global settings + + + + QRCode + + + 获取剪贴板异常 + Failed to retrieve clipboard + + + + 剪贴板中为文本 + The content of the clipboard is text + + + + 无有效图片 + No valid images + + + + 导入%1条图片路径 + Imported %1 image paths + + + + 参数有误,或输入内容不合规定。请参照报错指示修改: + The parameters are incorrect or the input content does not comply with regulations. Please refer to the error instructions for modification: + + + + + 生成二维码失败 + Failed to generate QR code + + + + 已复制到剪贴板 + Copied to clipboard + + + + 识图完成 + Image recognition completed + + + + 无文字 + No text + + + + 识别失败 + Recognition failed + + + + 扫描二维码 + Scan QR code + + + + 截图、拖入或粘贴二维码图片 + Screenshot, drag and drop, or paste QR code image + + + + + 设置 + Settings + + + + 自动刷新 + Auto refresh + + + + 修改文字后,自动生成二维码/条形码 + Automatically generate QR code after modifying text + + + + 刷新 + Refresh + + + + 生成二维码/条形码 + Generate QR code + + + + 记录 + History + + + + 生成 + Generate + + + + QRCodeConfigs + + + 快捷键 + Hotkey + + + + 屏幕截图 + Screenshot + + + + 粘贴图片 + Paste image + + + + 预处理(一般无需改动) + Preprocessing (unnessary to modify) + + + + 中值滤波 + Median filtering + + + + 对图像进行平滑处理 +>0 时生效。可填1~9的奇数(不允许偶数) + Smooth the image +Effective when > 0. Can be filled with odd numbers from 1 to 9 (even numbers are not allowed) + + + + 锐度增强 + Sharpening + + + + 增加图像的锐度 +>0.1 时生效。可填0.1~10的小数 + Increase the sharpness of the image +Effective when > 0.1. Can be filled with decimals from 0.1 to 10 + + + + 对比度增强 + Contrast enhancement + + + + 增加图像的对比度 +>0.1 时生效。可填0.1~10的小数 + Increase the contrast of the image +Effective when > 0.1. Can be filled with decimals from 0.1 to 10 + + + + 转为灰度 + Convert to grayscale + + + + 将图像像素转为灰度 + Convert the image pixels to grayscale + + + + 二值化 + Binarization + + + + 将图像像素转为纯黑和纯白 +启用了灰度,且二值化 >-1 时生效。可填0~255的整数 + Convert the image pixels to pure black and white +Effective when grayscale is enabled and binarization > -1. Can be filled with integers from 0 to 255 + + + + 扫码后的操作 + Action after scanning + + + + 复制结果 + Copy result + + + + 弹出主窗口 + Pop up main window + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + After recognition, display the window if the main window was minimized or in the background + + + + 生成二维码/条形码 + Generate QR code + + + + 类型 + Format + + + + 默认二维码: + Default: + + + + 宽度 + Width + + + + + 填0:自动选择 + 0: Automatic selection + + + + + + 像素 + pixel + + + + 高度 + Height + + + + 边缘空白 + Quiet zone + + + + 填-1:自动选择 + -1: Automatic selection + + + + 纠错等级 + Error Correction Coding + + + + 仅适用于: + Only applicable to: + + + + 自动 + Auto + + + + 其它 + Others + + + + ResultTextContainer + + + 复制 + Copy + + + + ResultsTableView + + + 异常状态码:%1 +异常信息:%2 + Exception status code: %1 +Exception message: %2 + + + + 耗时 %1 + Time elapsed: %1 + + + + 置信度 %1 + Confidence: %1 + + + + 记录:复制%1字 + History: Copy %1 characters + + + + 记录:无选中文字 + History: No selected text + + + + 记录:复制全部%1字 + History: Copy all %1 characters + + + + 删除%1条记录 + Delete %1 record(s) + + + + 清空记录 + Clear all records + + + + 复制    (Ctrl+C) + Copy (Ctrl+C) + + + + 复制全部  (Ctrl+C 双击) + Copy all (Double-press Ctrl+C) + + + + 选中单个  (Ctrl+A) + Select single (Ctrl+A) + + + + 选中全部记录(Ctrl+A 双击) + Select all records (Double-press Ctrl+A) + + + + 删除选中记录 + Delete selected records + + + + 清空全部记录(Ctrl+D 双击) + Clear all records (Double-press Ctrl+D) + + + + 滚动 + Scroll + + + + 自动滚动到底部 + Auto scroll to the bottom + + + + 右键菜单 + Right-click menu + + + + ScreenshotManager + + + 截图失败 + Screenshot failed + + + + 上次截图操作未结束,不能进行新的截图! + Cannot perform screenshot when previous screenshot operation is not yet completed! + + + + 尚未记录截图区域 + No screenshot area has been recorded yet + + + + 显示器: %1 +错误信息: %2 + Monitor: %1 +Error message: %1 + + + + 重复截图失败 + Failed to repeat screenshot + + + + 未找到匹配的屏幕 + No matching screen found + + + + 屏幕设备名称不相同: +%1 +%2 + Screen devices are of different names: +%1 +%2 + + + + ScreenshotOCR + + + 获取剪贴板异常 + Failed to retrieve clipboard + + + + 剪贴板中为文本 + Clipboard contains text + + + + 无有效图片 + No valid images + + + + 导入%1条图片路径 + Imported %1 image paths + + + + 依然关闭 + Confirm + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + Task in progress. +Do you want to end the task and close the page? + + + + + 屏幕截图 + Screenshot + + + + + 粘贴图片 + Paste image + + + + 截图识别任务异常 + Screenshot OCR task exception + + + + 已复制到剪贴板 + Copied to clipboard + + + + 识图完成 + Image recognition completed + + + + 无文字 + No text + + + + 识别失败 + Recognition failed + + + + + 停止任务 + Stop task + + + + 文字 + Text + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + Overlay recognized text on the image +Can be set as default off in global settings + + + + 截图、拖入或粘贴图片 + Screenshot, drag and drop, or paste image + + + + 设置 + Settings + + + + 记录 + History + + + + ScreenshotOcrConfigs + + + OCR文本后处理 + OCR text post-processing + + + + 快捷键 + Hotkey + + + + 屏幕截图 + Screenshot + + + + 粘贴图片 + Paste image + + + + 重复截图 + Repeat screenshot + + + + 重新截取上一次截图的范围 + Retake the previous screenshot's range + + + + 识图后的操作 + Action after recognition + + + + 复制结果 + Copy result + + + + 弹出主窗口 + Pop up main window + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + After recognition, display the window if the main window was minimized or in the background + + + + 其它 + Others + + + + Size_ + + + 1.0 + 0.85 + + + + SystemTray + + + 打开主窗口 + Open main window + + + + 退出 Umi-OCR + Exit Umi-OCR + + + + Theme + + + 珍珠白 + Light + + + + 云墨黑 + Dark + + + + 赛博朋克 + Cyberpunk + + + + UtilsConfigDicts + + + 任务完成后的操作 + Action after task completion + + + + 系统 + System + + + + 无 + None + + + + 关机 + Shutdown + + + + 休眠 + Sleep + + + + %1 系统暂不支持电源控制! + %1 Linux system currently does not support power control! + + + + 继续%1 + Continue %1 + + + + 取消%1 + Cancel %1 + + + + 系统即将%1 + System will %1 + + + + 排版解析方案 + Layout parsing solution + + + + 按什么方式,解析和排序图片中的文字块 + How to parse and sort text blocks within images + + + + 多栏-按自然段换行 + Natural paragraph wrapping + + + + 多栏-总是换行 + Mutil Colum-Always wrap + + + + 多栏-无换行 + Multi column - No wrapping + + + + 单栏-按自然段换行 + Single column - Natural paragraph wrapping + + + + 单栏-总是换行 + Single column - Always wrap + + + + 单栏-无换行 + Single column - No wrapping + + + + 单栏-保留缩进 + Single column - Retain indentation + + + + 不做处理 + No processing + + + + 优先内部 + Internal prioritized + + + + 只允许内部 + Internal only + + + + 只允许外部 + External only + + + + 禁用所有通知 + Disable all notifications + + + + 跟随全局设定 + Follow global settings + + + + 通知弹窗类型 + Notification popup type + + + + 通知类型已更改 + Notification type changed + + + diff --git a/dev-tools/i18n/release/es.ts b/dev-tools/i18n/release/es.ts new file mode 100644 index 0000000..c6f1a12 --- /dev/null +++ b/dev-tools/i18n/release/es.ts @@ -0,0 +1,2458 @@ + + + + + About + + + 开源、免费的离线OCR软件 + Software OCR off-line, gratuito de código abierto + + + + 当前版本 + Versión Actual + + + + 项目链接 + Link al Proyecto + + + + 官方网站 + Página Web Oficial + + + + 插件拓展 + Extensiones + + + + 问题反馈 + Reportar un Problema + + + + 发布地址 + Web de Lanzamientos + + + + 许可协议 + Acuerdo de Licencia + + + + 作者 + Autor + + + + 译者 + Traductor + + + + 运行环境信息(如需请求协助,请提供给开发者) + Entorno de ejecución (Si necesitas ayuda, proporcione esto al desarrollador) + + + + 复制 + Copiar + + + + BatchDOC + + + 加密 + Encriptado + + + + %1个加密文档 + %1 Encriptación del archivo + + + + 请点击文件名填写密码 + Haz clic en el archivo e introduce la contraseña. + + + + 文档已加密 + El Archivo ha sido encriptado + + + + 【%1】 +请点击文档名,设置密码 + Haz clic en el archivo para resetear la contraseña + + + + 失败 + Fallido + + + + 排队 + Esperar en la cola + + + + 依然关闭 + Permanece cerrado + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + + + + + + 文档 + + + + + 状态 + + + + + 打开文档 + + + + + 拖入文档或文件夹 + + + + + 清空 + + + + + 文件读取中… + + + + + 文档识别异常 + + + + + 批量识别完成 + + + + + 页数 + + + + + 请选择文档 + + + + + 设置 + + + + + 记录 + + + + + BatchDOCConfigs + + + OCR文本后处理 + + + + + 文档处理 + + + + + 内容提取模式 + + + + + 若一页文档既存在图片又存在文本,如何进行处理 + + + + + 混合OCR/原文本 + + + + + 整页强制OCR + + + + + 仅OCR图片 + + + + + 仅拷贝原有文本 + + + + + 批量任务 + + + + + 保存到 + + + + + 文档原目录 + + + + + + 指定目录 + + + + + 必须先指定“保存到指定目录”才生效 + + + + + OCR结果保存目录 + + + + + 文件名格式 + + + + + 日期时间格式 + + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + + + + + 保存文件类型 + + + + + layered.pdf 双层可搜索文档 + + + + + text.pdf 单层纯文本文档 + + + + + 创建空白PDF文档,只写入识别文字,不含图片 + + + + + txt 标准格式 + + + + + 含识别文字和页数信息 + + + + + p.txt 纯文字格式 + + + + + 输出所有识别文字 + + + + + csv 表格文件(Excel) + + + + + 将页数信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + + + + + jsonl 原始信息 + + + + + 每行为一条json数据,便于第三方程序读取操作 + + + + + 忽略空白页 + + + + + 若某一页没有文字或识别失败,也不会输出错误提示信息 + + + + + 递归读取子文件夹 + + + + + 点击表格,可设置更多内容 + + + + + 导入文件夹时,导入子文件夹中全部文档 + + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文档名 +%range 识别页数范围。只有识别页数小于总页数时才会显示。 +举例:[OCR]_%name%range_%date +生成:[OCR]_文档A(p2-10)_20230901_1213.txt +添加占位符可以避免旧文件被新文件覆盖。 + + + + + 保留原有图片,叠加一层透明文字,可以搜索和复制 + + + + + 其它 + + + + + BatchOCR + + + 依然关闭 + + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + + + + + 排队 + + + + + 处理 + + + + + %1 张图片识别失败! + + + + + 批量识别完成 + + + + + 批量识别任务异常 + + + + + + 图片 + + + + + 耗时 + + + + + 状态 + + + + + 打开图片 + + + + + 拖入图片或文件夹 + + + + + 清空 + + + + + 请选择图片 + + + + + 设置 + + + + + 记录 + + + + + BatchOCRConfigs + + + OCR文本后处理 + + + + + 忽略区域 + + + + + 进入设置 + + + + + 批量任务 + + + + + 保存到 + + + + + 图片原目录 + + + + + + 指定目录 + + + + + 必须先指定“保存到指定目录”才生效 + + + + + OCR结果保存目录 + + + + + 文件名格式 + + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文件夹名/文件名 +举例:[OCR]_%name_%date +生成:[OCR]_我的图片_2023-09-01_12-13.txt +添加占位符可以避免旧文件被新文件覆盖。 + + + + + 日期时间格式 + + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + + + + + 保存文件类型 + + + + + txt 标准格式 + + + + + 含原图片文件名和识别文字 + + + + + p.txt 纯文字格式 + + + + + 仅输出识别文字,不含图片标题 + + + + + txt 单独文件 + + + + + 对每张图片,生成同名txt文件,仅输出识别文字 + + + + + md 图文混排 + + + + + Markdown图文混排格式,可用Markdown阅读器浏览文件 + + + + + csv 表格文件(Excel) + + + + + 将图片信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + + + + + jsonl 原始信息 + + + + + 每行为一条json数据,便于第三方程序读取操作 + + + + + 输出忽略空白图片 + + + + + 若图片没有文字或识别失败,也不会输出错误提示信息 + + + + + 递归读取子文件夹 + + + + + 导入文件夹时,导入子文件夹中全部图片 + + + + + 其它 + + + + + Configs + + + %1 处理配置项异常: +%2枚举列表为空。 + + + + + 重置 + + + + + 重置本页上的设定 + + + + + 重置设定 + + + + + 要重置本页的设定吗? + + + + + 高级 + + + + + 显示更多高级选项。它们标有 * 号。 +请谨慎修改高级选项。 + + + + + 展开 + + + + + 折叠 + + + + + 必须为整数 + + + + + 不能超过 + + + + + 不能低于 + + + + + 必须为数字 + + + + + 已取消%1的快捷键。 + + + + + 更新热键成功 + + + + + %1的快捷键为 %2 + + + + + %1 快捷键%2已被注册,请尝试另外的按键组合。 + + + + + %1 快捷键%2无法注册,请尝试另外的按键组合。 + + + + + 请按下快捷键组合。按【Esc】退出。 + + + + + 当前快捷键录制已在进行,不能同时录制! + + + + + 无法录制快捷键 + + + + + DropArea_ + + + 松手放入文件 + + + + + FontPanel + + + 界面 + + + + + 内容 + + + + + 界面字体: +软件中大部分UI的字体。 + + + + + 内容字体: +识别结果内容的字体。 + + + + + GlobalConfigs + + + 快捷方式 + + + + + 桌面 + + + + + 开始菜单 + + + + + 开机自启 + + + + + 界面和外观 + + + + + 主题 + + + + + 切换主题 + + + + + 字体 + + + + + 修改字体 + + + + + 界面与文字大小 + + + + + 渲染器 + + + + + 关闭硬件加速 + + + + + 若出现界面闪烁、元素错位等界面异常,尝试切换渲染器或者关闭硬件加速 + + + + + 禁用美化效果 + + + + + 在低配置机器上,禁用动画、阴影等效果可减少部分资源占用 + + + + + 图片预览默认显示叠加层 + + + + + 默认开启/关闭叠加层显示 +对所有图片预览组件生效 + + + + + 窗口 + + + + + 启动时缩小到任务栏 + + + + + 软件启动时,不弹出主窗口 + + + + + 窗口置顶 + + + + + 捷径:窗口左上角图钉 + + + + + 锁定标签栏 + + + + + 捷径:窗口右上角小锁 + + + + + 关闭主窗口时 + + + + + 最小化到系统托盘 + + + + + 退出应用 + + + + + 隐藏托盘图标 + + + + + 若要弹出位于后台的软件窗口,请在文件管理器中重复启动软件。 +若要彻底退出软件,请从任务管理器中结束进程。 + + + + + 截图 + + + + + 截图前隐藏主窗口 + + + + + 截图前,如果主窗口处于前台,则隐藏主窗口 +将会延时等待主窗口关闭 + + + + + 隐藏等待时间 + + + + + 秒 + + + + + 服务 + + + + + 允许HTTP服务 + + + + + Umi-OCR依赖HTTP接口进行本机跨进程通信。如果禁用,将无法使用命令行模式、多开检测等功能。 + + + + + + + 重启软件后生效 + + + + + 主机 + + + + + 仅本地 + + + + + 任何可用地址 + + + + + 将允许局域网访问。请开启对应防火墙权限! + + + + + 将禁止局域网访问。 + + + + + 端口 + + + + + 请尝试更换软件路径! + + + + + 配置文件读写异常 + + + + + 插件加载失败 + + + + + 成功添加快捷方式 + + + + + 添加快捷方式失败 + + + + + 成功移除 %1 个快捷方式 + + + + + 提示 + + + + + 没有找到可移除的快捷方式。 + + + + + 立刻关闭软件 + + + + + 稍后 + + + + + 渲染器变更 将在重启软件后生效 + + + + + 端口号不合法 + + + + + 端口号改为%1 + + + + + 原端口号%1被占用, +切换为新端口号%2。 + +若不想看到此通知,请在全局设置关闭高级模式。 + + + + + GlobalConfigsPage + + + 1. 关于快捷键、OCR语言等选项,请在各个功能页中进行设置。 +2. 勾选左下角的“高级”按钮,可以显示更多进阶选项。 + + + + + 小贴士 + + + + + HTabBar + + + 窗口置顶 + + + + + 锁定标签栏 + + + + + IconButtonBar + + + 右键菜单 + + + + + 保存图片 + + + + + 图片大小:适应窗口 + + + + + 图片大小:实际 + + + + + 截图 + + + + + 粘贴 + + + + + 屏幕截图 + Screenshot + + + + 粘贴图片 + + + + + IgnoreArea + + + 拖入本地图片:OCR预览 +滚轮:缩放 +左键:拖拽 +右键:绘制忽略区域 + +可绘制一个或多个忽略区域矩形框。在执行批量OCR时,完全位于忽略区域内的文本块将被排除。 +比如批量处理影视截图时,可在右上角水印处添加忽略区域,避免输出水印文本。 + + + + + 图像尺寸: + + + + + 区域数量: + + + + + 撤销 + + + + + 清空 + + + + + ImageScale + + + 复制图片 + + + + + 复制图片失败 + + + + + 打开图片 + + + + + 打开图片失败 + + + + + + 保存图片 + + + + + 保存图片失败 + + + + + ImageWithText + + + 图片:复制%1字 + + + + + 图片:无选中文字 + + + + + 图片:复制全部%1字 + + + + + 复制  (Ctrl+C) + + + + + 全选  (Ctrl+A) + + + + + 复制图片(Ctrl+X) + + + + + 保存图片(Ctrl+S) + + + + + 显示/隐藏文字(Tab) + + + + + 用默认应用打开图片 + + + + + 删除图片(Ctrl+D) + + + + + MainWindowManager + + + 欢迎使用 Umi-OCR + + + + + 已启用后台模式,可通过快捷键使用功能。 + + + + + MarkdownView + + + 打开网页 + + + + + 链接 + + + + + MessageBox + + + 取消 + + + + + 确定 + + + + + 警告 + + + + + 发生了一点小问题 + + + + + 复制 + + + + + 已复制报错信息 %1 + + + + + 请前往 Issues 页面寻找解答或反馈 + + + + + 问题反馈 + + + + + MessageBoxWin + + + 确定 + + + + + 不再提示 + + + + + 知道了 + + + + + MissionCtrlPanel + + + 开始任务 + + + + + 暂停任务 +暂停后可以待机或休眠。 +但是关机或退出软件,将会丢弃任务内容。 + + + + + 继续任务 + + + + + 终止任务 +放弃未完成的内容。 + + + + + 正在运行 + + + + + 正在保存 + + + + + 已暂停 + + + + + 任务停止 + + + + + 任务完成 + + + + + ModalLayer + + + 保存并返回 + + + + + 返回 + + + + + Navigation + + + 欢迎使用 Umi-OCR + + + + + 请选择功能页 + + + + + 当前版本 + + + + + 项目链接 + + + + + 官方网站 + + + + + 插件拓展 + + + + + 问题反馈 + + + + + 功能页 + + + + + 请打开对应标签页(如批量OCR、批量文档),再拖入文件。 + + + + + OcrManager + + + 文字识别 + + + + + 操作 + + + + + + 强制终止任务 + + + + + 应用修改 + + + + + 当前接口 + + + + + OCR API 列表中不存在%1 + + + + + 当前已有%1组任务队列、共%2个任务正在执行。您可【强制终止任务】后修改API。 + + + + + 无法修改 文字识别接口设置 + + + + + 文字识别接口应用成功 + + + + + 当前API为【%1】 + + + + + 文字识别接口应用失败 + + + + + 当前没有运行中的任务 + + + + + 当前已有%1组任务队列、共%2个任务正在执行。 +要强制终止全部任务吗? + + + + + PagesManager + + + 新标签页 + + + + + 截图OCR + + + + + # 截图OCR + +屏幕截图,快捷转文字。也支持粘贴图片。 + + + + + # 全局设置 + +调节全局设置项,对所有页面生效。 + + + + + 批量OCR + + + + + # 批量OCR + +导入本地图片或文件夹,批量转换文字。 + +支持格式: + + + + + 批量文档 + + + + + # 批量文档识别 + +批量导入文档,提取文字,或生成双层可搜索PDF。 + +支持格式: + + + + + 二维码 + + + + + # 二维码 + +识别或生成二维码、条形码。 + +支持协议: + + + + + 全局设置 + + + + + 关于 + + + + + # 关于 + + + + + PopupManager + + + 确认 + + + + + 取消 + + + + + PreviewDoc + + + 加密 + + + + + 打开文档失败 + + + + + 请填写正确的密码 + + + + + 密码正确 + + + + + 文档预览异常 + + + + + 忽略区域说明: +右键拖拽,绘制矩形区域,包含在区域内的文字框将被忽略。可用于排除水印、页眉页脚。 +范围允许填写负数,表示倒数第x页。如-1表示最后一页,-2表示倒数第2页。 +忽略区域的设置对所有文档生效。 + + + + + 密码: + + + + + 预览页面 + + + + + 预览PDF时,是否预览OCR结果 + + + + + OCR页数 + OCR page count + + + + 忽略区域页数(全局) + Ignore Area page count (Global) + + + + + 范围 + + + + + 撤销 + + + + + 清空 + + + + + PreviewImage + + + 文字 + + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + + + + + QRCode + + + 获取剪贴板异常 + + + + + 剪贴板中为文本 + + + + + 无有效图片 + + + + + 导入%1条图片路径 + + + + + 参数有误,或输入内容不合规定。请参照报错指示修改: + + + + + + 生成二维码失败 + + + + + 已复制到剪贴板 + + + + + 识图完成 + + + + + 无文字 + + + + + 识别失败 + + + + + 扫描二维码 + + + + + 截图、拖入或粘贴二维码图片 + Screenshot, drag and drop, or paste QR code image + + + + + 设置 + + + + + 自动刷新 + + + + + 修改文字后,自动生成二维码/条形码 + + + + + 刷新 + + + + + 生成二维码/条形码 + + + + + 记录 + + + + + 生成 + + + + + QRCodeConfigs + + + 快捷键 + + + + + 屏幕截图 + Screenshot + + + + 粘贴图片 + + + + + 预处理(一般无需改动) + + + + + 中值滤波 + + + + + 对图像进行平滑处理 +>0 时生效。可填1~9的奇数(不允许偶数) + + + + + 锐度增强 + + + + + 增加图像的锐度 +>0.1 时生效。可填0.1~10的小数 + + + + + 对比度增强 + + + + + 增加图像的对比度 +>0.1 时生效。可填0.1~10的小数 + + + + + 转为灰度 + + + + + 将图像像素转为灰度 + + + + + 二值化 + + + + + 将图像像素转为纯黑和纯白 +启用了灰度,且二值化 >-1 时生效。可填0~255的整数 + + + + + 扫码后的操作 + + + + + 复制结果 + + + + + 弹出主窗口 + + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + + + + + 生成二维码/条形码 + + + + + 类型 + + + + + 默认二维码: + + + + + 宽度 + + + + + + 填0:自动选择 + + + + + + + 像素 + + + + + 高度 + + + + + 边缘空白 + + + + + 填-1:自动选择 + + + + + 纠错等级 + + + + + 仅适用于: + + + + + 自动 + + + + + 其它 + + + + + ResultTextContainer + + + 复制 + + + + + ResultsTableView + + + 异常状态码:%1 +异常信息:%2 + + + + + 耗时 %1 + + + + + 置信度 %1 + + + + + 记录:复制%1字 + + + + + 记录:无选中文字 + + + + + 记录:复制全部%1字 + + + + + 删除%1条记录 + + + + + 清空记录 + + + + + 复制    (Ctrl+C) + + + + + 复制全部  (Ctrl+C 双击) + + + + + 选中单个  (Ctrl+A) + + + + + 选中全部记录(Ctrl+A 双击) + + + + + 删除选中记录 + + + + + 清空全部记录(Ctrl+D 双击) + + + + + 滚动 + + + + + 自动滚动到底部 + + + + + 右键菜单 + + + + + ScreenshotManager + + + 截图失败 + + + + + 上次截图操作未结束,不能进行新的截图! + + + + + 尚未记录截图区域 + + + + + 显示器: %1 +错误信息: %2 + + + + + 重复截图失败 + + + + + 未找到匹配的屏幕 + + + + + 屏幕设备名称不相同: +%1 +%2 + + + + + ScreenshotOCR + + + 获取剪贴板异常 + + + + + 剪贴板中为文本 + + + + + 无有效图片 + + + + + 导入%1条图片路径 + + + + + 依然关闭 + + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + + + + + + 屏幕截图 + Screenshot + + + + + 粘贴图片 + + + + + 截图识别任务异常 + + + + + 已复制到剪贴板 + + + + + 识图完成 + + + + + 无文字 + + + + + 识别失败 + + + + + + 停止任务 + + + + + 文字 + + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + + + + + 截图、拖入或粘贴图片 + Screenshot, drag and drop, or paste image + + + + 设置 + + + + + 记录 + + + + + ScreenshotOcrConfigs + + + OCR文本后处理 + + + + + 快捷键 + + + + + 屏幕截图 + Screenshot + + + + 粘贴图片 + + + + + 重复截图 + + + + + 重新截取上一次截图的范围 + + + + + 识图后的操作 + + + + + 复制结果 + + + + + 弹出主窗口 + + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + + + + + 其它 + + + + + Size_ + + + 1.0 + + + + + SystemTray + + + 打开主窗口 + + + + + 退出 Umi-OCR + + + + + Theme + + + 珍珠白 + + + + + 云墨黑 + + + + + 赛博朋克 + + + + + UtilsConfigDicts + + + 任务完成后的操作 + + + + + 系统 + + + + + 无 + + + + + 关机 + + + + + 休眠 + + + + + %1 系统暂不支持电源控制! + %1 Linux system currently does not support power control! + + + + 继续%1 + + + + + 取消%1 + + + + + 系统即将%1 + + + + + 排版解析方案 + + + + + 按什么方式,解析和排序图片中的文字块 + + + + + 多栏-按自然段换行 + + + + + 多栏-总是换行 + + + + + 多栏-无换行 + + + + + 单栏-按自然段换行 + + + + + 单栏-总是换行 + + + + + 单栏-无换行 + + + + + 单栏-保留缩进 + + + + + 不做处理 + + + + + 优先内部 + + + + + 只允许内部 + + + + + 只允许外部 + + + + + 禁用所有通知 + + + + + 跟随全局设定 + + + + + 通知弹窗类型 + + + + + 通知类型已更改 + + + + diff --git a/dev-tools/i18n/release/fa.ts b/dev-tools/i18n/release/fa.ts new file mode 100644 index 0000000..e2ee46f --- /dev/null +++ b/dev-tools/i18n/release/fa.ts @@ -0,0 +1,2482 @@ + + + + + About + + + 开源、免费的离线OCR软件 + نرم‌افزار OCR آفلاین، متن‌باز و رایگان + + + + 当前版本 + نسخه فعلی + + + + 项目链接 + لینک پروژه + + + + 官方网站 + وب‌سایت رسمی + + + + 插件拓展 + افزونه‌ها + + + + 问题反馈 + ‬‬‬‬‭بازخورد مشکلات + + + + 发布地址 + آدرس انتشار + + + + 许可协议 + توافق‌نامه مجوز + + + + 作者 + نویسنده + + + + 译者 + مترجم + + + + 运行环境信息(如需请求协助,请提供给开发者) + اطلاعات محیط اجرا (در صورت نیاز به کمک، لطفاً به توسعه‌دهنده ارائه و ارجاع دهید) + + + + 复制 + کپی + + + + AsynFilesLoader + + + 正在载入 %1 个文件: +%2 + در حال بارگذاری %1 فایل: %2 + + + + BatchDOC + + + 加密 + رمزگذاری + + + + %1个加密文档 + %1 سند رمزگذاری‌شده + + + + 请点击文件名填写密码 + لطفاً روی نام فایل کلیک کنید و رمز عبور را وارد کنید + + + + 文档已加密 + سند رمزگذاری شده است + + + + 【%1】 +请点击文档名,设置密码 + 【%1】لطفاً روی نام سند کلیک کنید و رمز عبور را تنظیم کنید + + + + 失败 + + + + + 排队 + + + + + 依然关闭 + + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + + + + + + 文档 + + + + + 状态 + + + + + 打开文档 + + + + + 拖入文档或文件夹 + + + + + 清空 + + + + + 文档识别异常 + + + + + 批量识别完成 + + + + + 页数 + + + + + 请选择文档 + + + + + 设置 + + + + + 记录 + + + + + BatchDOCConfigs + + + OCR文本后处理 + + + + + 文档处理 + + + + + 内容提取模式 + + + + + 若一页文档既存在图片又存在文本,如何进行处理 + + + + + 混合OCR/原文本 + + + + + 整页强制OCR + + + + + 仅OCR图片 + + + + + 仅拷贝原有文本 + + + + + 批量任务 + + + + + 保存到 + + + + + 文档原目录 + + + + + + 指定目录 + + + + + 必须先指定“保存到指定目录”才生效 + + + + + OCR结果保存目录 + + + + + 文件名格式 + + + + + 日期时间格式 + + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + + + + + 保存文件类型 + + + + + layered.pdf 双层可搜索文档 + + + + + text.pdf 单层纯文本文档 + + + + + 创建空白PDF文档,只写入识别文字,不含图片 + + + + + txt 标准格式 + + + + + 含识别文字和页数信息 + + + + + p.txt 纯文字格式 + + + + + 输出所有识别文字 + + + + + csv 表格文件(Excel) + + + + + 将页数信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + + + + + jsonl 原始信息 + + + + + 每行为一条json数据,便于第三方程序读取操作 + + + + + 忽略空白页 + + + + + 若某一页没有文字或识别失败,也不会输出错误提示信息 + + + + + 递归读取子文件夹 + + + + + 点击表格,可设置更多内容 + + + + + 导入文件夹时,导入子文件夹中全部文档 + + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文档名 +%range 识别页数范围。只有识别页数小于总页数时才会显示。 +举例:[OCR]_%name%range_%date +生成:[OCR]_文档A(p2-10)_20230901_1213.txt +添加占位符可以避免旧文件被新文件覆盖。 + + + + + 保留原有图片,叠加一层透明文字,可以搜索和复制 + + + + + 其它 + + + + + BatchOCR + + + 依然关闭 + + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + + + + + 排队 + + + + + 处理 + + + + + %1 张图片识别失败! + + + + + 批量识别完成 + + + + + 批量识别任务异常 + + + + + + 图片 + + + + + 耗时 + + + + + 状态 + + + + + 打开图片 + + + + + 拖入图片或文件夹 + + + + + 清空 + + + + + 请选择图片 + + + + + 设置 + + + + + 记录 + + + + + BatchOCRConfigs + + + OCR文本后处理 + + + + + 忽略区域 + + + + + 进入设置 + + + + + 批量任务 + + + + + 保存到 + + + + + 图片原目录 + + + + + + 指定目录 + + + + + 必须先指定“保存到指定目录”才生效 + + + + + OCR结果保存目录 + + + + + 文件名格式 + + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文件夹名/文件名 +举例:[OCR]_%name_%date +生成:[OCR]_我的图片_2023-09-01_12-13.txt +添加占位符可以避免旧文件被新文件覆盖。 + + + + + 日期时间格式 + + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + + + + + 保存文件类型 + + + + + txt 标准格式 + + + + + 含原图片文件名和识别文字 + + + + + p.txt 纯文字格式 + + + + + 仅输出识别文字,不含图片标题 + + + + + txt 单独文件 + + + + + 对每张图片,生成同名txt文件,仅输出识别文字 + + + + + md 图文混排 + + + + + Markdown图文混排格式,可用Markdown阅读器浏览文件 + + + + + csv 表格文件(Excel) + + + + + 将图片信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + + + + + jsonl 原始信息 + + + + + 每行为一条json数据,便于第三方程序读取操作 + + + + + 输出忽略空白图片 + + + + + 若图片没有文字或识别失败,也不会输出错误提示信息 + + + + + 递归读取子文件夹 + + + + + 导入文件夹时,导入子文件夹中全部图片 + + + + + 其它 + + + + + Configs + + + %1 处理配置项异常: +%2枚举列表为空。 + + + + + 重置 + + + + + 重置本页上的设定 + + + + + 重置设定 + + + + + 要重置本页的设定吗? + + + + + 高级 + + + + + 显示更多高级选项。它们标有 * 号。 +请谨慎修改高级选项。 + + + + + 展开 + + + + + 折叠 + + + + + 必须为整数 + + + + + 不能超过 + + + + + 不能低于 + + + + + 必须为数字 + + + + + 已取消%1的快捷键。 + + + + + 更新热键成功 + + + + + %1的快捷键为 %2 + + + + + %1 快捷键%2已被注册,请尝试另外的按键组合。 + + + + + %1 快捷键%2无法注册,请尝试另外的按键组合。 + + + + + 请按下快捷键组合。按【Esc】退出。 + + + + + 当前快捷键录制已在进行,不能同时录制! + + + + + 无法录制快捷键 + + + + + DropArea_ + + + 松手放入文件 + + + + + FontPanel + + + 界面 + + + + + 内容 + + + + + 界面字体: +软件中大部分UI的字体。 + + + + + 内容字体: +识别结果内容的字体。 + + + + + GlobalConfigs + + + 快捷方式 + + + + + 桌面 + + + + + 开始菜单 + + + + + 开机自启 + + + + + 界面和外观 + + + + + 主题 + + + + + 切换主题 + + + + + 字体 + + + + + 修改字体 + + + + + 界面与文字大小 + + + + + 渲染器 + + + + + 关闭硬件加速 + + + + + 若出现界面闪烁、元素错位等界面异常,尝试切换渲染器或者关闭硬件加速 + + + + + 禁用美化效果 + + + + + 在低配置机器上,禁用动画、阴影等效果可减少部分资源占用 + + + + + 图片预览默认显示叠加层 + + + + + 默认开启/关闭叠加层显示 +对所有图片预览组件生效 + + + + + 窗口 + + + + + 启动时缩小到任务栏 + + + + + 软件启动时,不弹出主窗口 + + + + + 窗口置顶 + + + + + 捷径:窗口左上角图钉 + + + + + 锁定标签栏 + + + + + 捷径:窗口右上角小锁 + + + + + 关闭主窗口时 + + + + + 最小化到系统托盘 + + + + + 退出应用 + + + + + 隐藏托盘图标 + + + + + 若要弹出位于后台的软件窗口,请在文件管理器中重复启动软件。 +若要彻底退出软件,请从任务管理器中结束进程。 + + + + + 截图 + + + + + 截图前隐藏主窗口 + + + + + 截图前,如果主窗口处于前台,则隐藏主窗口 +将会延时等待主窗口关闭 + + + + + 隐藏等待时间 + + + + + 秒 + + + + + 服务 + + + + + 允许HTTP服务 + + + + + Umi-OCR依赖HTTP接口进行本机跨进程通信。如果禁用,将无法使用命令行模式、多开检测等功能。 + + + + + + + 重启软件后生效 + + + + + 主机 + + + + + 仅本地 + + + + + 任何可用地址 + + + + + 将允许局域网访问。请开启对应防火墙权限! + + + + + 将禁止局域网访问。 + + + + + 端口 + + + + + 日志 + + + + + 操作 + + + + + 打开日志保存目录 + + + + + 保存的日志级别 + + + + + 已禁用日志文件保存 + + + + + 请尝试更换软件路径! + + + + + 配置文件读写异常 + + + + + 插件加载失败 + + + + + 成功添加快捷方式 + + + + + 添加快捷方式失败 + + + + + 成功移除 %1 个快捷方式 + + + + + 提示 + + + + + 没有找到可移除的快捷方式。 + + + + + 渲染器变更 将在重启软件后生效 + + + + + 端口号不合法 + + + + + 端口号改为%1 + + + + + 原端口号%1被占用, +切换为新端口号%2。 + +若不想看到此通知,请在全局设置关闭高级模式。 + + + + + GlobalConfigsPage + + + 1. 关于快捷键、OCR语言等选项,请在各个功能页中进行设置。 +2. 勾选左下角的“高级”按钮,可以显示更多进阶选项。 + + + + + 小贴士 + + + + + HTabBar + + + 窗口置顶 + + + + + 锁定标签栏 + + + + + IconButtonBar + + + 右键菜单 + + + + + 保存图片 + + + + + 图片大小:适应窗口 + + + + + 图片大小:实际 + + + + + 截图 + + + + + 粘贴 + + + + + 屏幕截图 + + + + + 粘贴图片 + + + + + IgnoreArea + + + 拖入本地图片:OCR预览 +滚轮:缩放 +左键:拖拽 +右键:绘制忽略区域 + +可绘制一个或多个忽略区域矩形框。在执行批量OCR时,完全位于忽略区域内的文本块将被排除。 +比如批量处理影视截图时,可在右上角水印处添加忽略区域,避免输出水印文本。 + + + + + 图像尺寸: + + + + + 区域数量: + + + + + 撤销 + + + + + 清空 + + + + + ImageScale + + + 复制图片 + + + + + 复制图片失败 + + + + + 打开图片 + + + + + 打开图片失败 + + + + + + 保存图片 + + + + + 保存图片失败 + + + + + ImageWithText + + + 图片:复制%1字 + + + + + 图片:无选中文字 + + + + + 图片:复制全部%1字 + + + + + 复制  (Ctrl+C) + + + + + 全选  (Ctrl+A) + + + + + 复制图片(Ctrl+X) + + + + + 保存图片(Ctrl+S) + + + + + 显示/隐藏文字(Tab) + + + + + 用默认应用打开图片 + + + + + 删除图片(Ctrl+D) + + + + + MainWindowManager + + + 欢迎使用 Umi-OCR + + + + + 已启用后台模式,可通过快捷键使用功能。 + + + + + MarkdownView + + + 打开网页 + + + + + 链接 + + + + + MessageBox + + + 取消 + + + + + 确定 + + + + + 警告 + + + + + 发生了一点小问题 + + + + + 复制 + + + + + 已复制报错信息 %1 + + + + + 请前往 Issues 页面寻找解答或反馈 + + + + + 问题反馈 + + + + + MessageBoxWin + + + 确定 + + + + + 不再提示 + + + + + 知道了 + + + + + MissionCtrlPanel + + + 开始任务 + + + + + 暂停任务 +暂停后可以待机或休眠。 +但是关机或退出软件,将会丢弃任务内容。 + + + + + 继续任务 + + + + + 终止任务 +放弃未完成的内容。 + + + + + 正在运行 + + + + + 正在保存 + + + + + 已暂停 + + + + + 任务停止 + + + + + 任务完成 + + + + + ModalLayer + + + 保存并返回 + + + + + 返回 + + + + + Navigation + + + 欢迎使用 Umi-OCR + + + + + 请选择功能页 + + + + + 当前版本 + + + + + 项目链接 + + + + + 官方网站 + + + + + 插件拓展 + + + + + 问题反馈 + + + + + 功能页 + + + + + 请打开对应标签页(如批量OCR、批量文档),再拖入文件。 + + + + + OcrManager + + + 文字识别 + + + + + 操作 + + + + + + 强制终止任务 + + + + + 应用修改 + + + + + 当前接口 + + + + + 没有可用的 OCR 插件。 + + + + + OCR API 列表中不存在%1 + + + + + 当前已有%1组任务队列、共%2个任务正在执行。您可【强制终止任务】后修改API。 + + + + + 无法修改 文字识别接口设置 + + + + + 文字识别接口应用成功 + + + + + 当前API为【%1】 + + + + + 文字识别接口应用失败 + + + + + 当前没有运行中的任务 + + + + + 当前已有%1组任务队列、共%2个任务正在执行。 +要强制终止全部任务吗? + + + + + PagesManager + + + 新标签页 + + + + + 截图OCR + + + + + # 截图OCR + +屏幕截图,快捷转文字。也支持粘贴图片。 + + + + + # 全局设置 + +调节全局设置项,对所有页面生效。 + + + + + 批量OCR + + + + + # 批量OCR + +导入本地图片或文件夹,批量转换文字。 + +支持格式: + + + + + 批量文档 + + + + + # 批量文档识别 + +批量导入文档,提取文字,或生成双层可搜索PDF。 + +支持格式: + + + + + 二维码 + + + + + # 二维码 + +识别或生成二维码、条形码。 + +支持协议: + + + + + 全局设置 + + + + + 关于 + + + + + # 关于 + + + + + PopupManager + + + 确认 + + + + + 取消 + + + + + PreviewDoc + + + 加密 + + + + + 打开文档失败 + + + + + 请填写正确的密码 + + + + + 密码正确 + + + + + 文档预览异常 + + + + + 忽略区域说明: +右键拖拽,绘制矩形区域,包含在区域内的文字框将被忽略。可用于排除水印、页眉页脚。 +范围允许填写负数,表示倒数第x页。如-1表示最后一页,-2表示倒数第2页。 +忽略区域的设置对所有文档生效。 + + + + + 密码: + + + + + 预览页面 + + + + + 预览PDF时,是否预览OCR结果 + + + + + OCR页数 + + + + + 忽略区域页数(全局) + + + + + + 范围 + + + + + 撤销 + + + + + 清空 + + + + + PreviewImage + + + 文字 + + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + + + + + QRCode + + + 获取剪贴板异常 + + + + + 剪贴板中为文本 + + + + + 无有效图片 + + + + + 导入%1条图片路径 + + + + + 参数有误,或输入内容不合规定。请参照报错指示修改: + + + + + + 生成二维码失败 + + + + + 已复制到剪贴板 + + + + + 识图完成 + + + + + 无文字 + + + + + 识别失败 + + + + + 扫描二维码 + + + + + 截图、拖入或粘贴二维码图片 + + + + + + 设置 + + + + + 自动刷新 + + + + + 修改文字后,自动生成二维码/条形码 + + + + + 刷新 + + + + + 生成二维码/条形码 + + + + + 记录 + + + + + 生成 + + + + + QRCodeConfigs + + + 快捷键 + + + + + 屏幕截图 + + + + + 粘贴图片 + + + + + 预处理(一般无需改动) + + + + + 中值滤波 + + + + + 对图像进行平滑处理 +>0 时生效。可填1~9的奇数(不允许偶数) + + + + + 锐度增强 + + + + + 增加图像的锐度 +>0.1 时生效。可填0.1~10的小数 + + + + + 对比度增强 + + + + + 增加图像的对比度 +>0.1 时生效。可填0.1~10的小数 + + + + + 转为灰度 + + + + + 将图像像素转为灰度 + + + + + 二值化 + + + + + 将图像像素转为纯黑和纯白 +启用了灰度,且二值化 >-1 时生效。可填0~255的整数 + + + + + 扫码后的操作 + + + + + 复制结果 + + + + + 弹出主窗口 + + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + + + + + 生成二维码/条形码 + + + + + 类型 + + + + + 默认二维码: + + + + + 宽度 + + + + + + 填0:自动选择 + + + + + + + 像素 + + + + + 高度 + + + + + 边缘空白 + + + + + 填-1:自动选择 + + + + + 纠错等级 + + + + + 仅适用于: + + + + + 自动 + + + + + 其它 + + + + + ResultTextContainer + + + 复制 + + + + + ResultsTableView + + + 异常状态码:%1 +异常信息:%2 + + + + + 耗时 %1 + + + + + 置信度 %1 + + + + + 记录:复制%1字 + + + + + 记录:无选中文字 + + + + + 记录:复制全部%1字 + + + + + 删除%1条记录 + + + + + 清空记录 + + + + + 复制    (Ctrl+C) + + + + + 复制全部  (Ctrl+C 双击) + + + + + 选中单个  (Ctrl+A) + + + + + 选中全部记录(Ctrl+A 双击) + + + + + 删除选中记录 + + + + + 清空全部记录(Ctrl+D 双击) + + + + + 滚动 + + + + + 自动滚动到底部 + + + + + 右键菜单 + + + + + ScreenshotManager + + + 截图失败 + + + + + 上次截图操作未结束,不能进行新的截图! + + + + + 尚未记录截图区域 + + + + + 显示器: %1 +错误信息: %2 + + + + + 重复截图失败 + + + + + 未找到匹配的屏幕 + + + + + 屏幕设备名称不相同: +%1 +%2 + + + + + ScreenshotOCR + + + 获取剪贴板异常 + + + + + 剪贴板中为文本 + + + + + 无有效图片 + + + + + 导入%1条图片路径 + + + + + 依然关闭 + + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + + + + + + 屏幕截图 + + + + + + 粘贴图片 + + + + + 截图识别任务异常 + + + + + 已复制到剪贴板 + + + + + 识图完成 + + + + + 无文字 + + + + + 识别失败 + + + + + + 停止任务 + + + + + 文字 + + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + + + + + 截图、拖入或粘贴图片 + + + + + 设置 + + + + + 记录 + + + + + ScreenshotOcrConfigs + + + OCR文本后处理 + + + + + 快捷键 + + + + + 屏幕截图 + + + + + 粘贴图片 + + + + + 重复截图 + + + + + 重新截取上一次截图的范围 + + + + + 识图后的操作 + + + + + 复制结果 + + + + + 弹出主窗口 + + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + + + + + 其它 + + + + + Size_ + + + 1.0 + + + + + SystemTray + + + 打开主窗口 + + + + + 退出 Umi-OCR + + + + + Theme + + + 珍珠白 + + + + + 云墨黑 + + + + + 赛博朋克 + + + + + UtilsConfigDicts + + + 任务完成后的操作 + + + + + 系统 + + + + + 无 + + + + + 关机 + + + + + 休眠 + + + + + %1 系统暂不支持电源控制! + + + + + 继续%1 + + + + + 取消%1 + + + + + 系统即将%1 + + + + + 排版解析方案 + + + + + 按什么方式,解析和排序图片中的文字块 + + + + + 多栏-按自然段换行 + + + + + 多栏-总是换行 + + + + + 多栏-无换行 + + + + + 单栏-按自然段换行 + + + + + 单栏-总是换行 + + + + + 单栏-无换行 + + + + + 单栏-保留缩进 + + + + + 不做处理 + + + + + 优先内部 + + + + + 只允许内部 + + + + + 只允许外部 + + + + + 禁用所有通知 + + + + + 跟随全局设定 + + + + + 通知弹窗类型 + + + + + 通知类型已更改 + + + + diff --git a/dev-tools/i18n/release/fr_FR.ts b/dev-tools/i18n/release/fr_FR.ts new file mode 100644 index 0000000..966517f --- /dev/null +++ b/dev-tools/i18n/release/fr_FR.ts @@ -0,0 +1,2486 @@ + + + + + About + + + 开源、免费的离线OCR软件 + Logiciel OCR hors ligne, gratuit et open source + + + + 当前版本 + Version actuelle + + + + 项目链接 + Lien du projet + + + + 官方网站 + Site officiel + + + + 插件拓展 + Extensions + + + + 问题反馈 + Rétroaction sur les questions + + + + 发布地址 + Adresse + + + + 许可协议 + Accord de licence + + + + 作者 + Auteur + + + + 译者 + Traducteur + + + + 运行环境信息(如需请求协助,请提供给开发者) + Information sur l'environnement opérationnel (à fournir au développeur en cas de demande d'assistance) + + + + 复制 + Copier + + + + AsynFilesLoader + + + 正在载入 %1 个文件: +%2 + + + + + BatchDOC + + + 加密 + Cryptage + + + + %1个加密文档 + %1 document(s) crypté(s) + + + + 请点击文件名填写密码 + Veuillez cliquer sur le nom du fichier pour entrer le mot de passe + + + + 文档已加密 + Les fichiers sont cryptés + + + + 【%1】 +请点击文档名,设置密码 + 【%1】 +Veuillez cliquer sur le nom du document pour définir le mot de passe + + + + 失败 + Échec + + + + 排队 + En attente + + + + 依然关闭 + Confirmer la fermeture + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + Task in progress. +Do you want to end the task and close the page? + + + + 文档识别异常 + Anomalie de reconnaissance du document + + + + 批量识别完成 + Reconnaissance par lot terminée + + + + + 文档 + Documents + + + + 状态 + État + + + + 页数 + Nombre de pages + + + + 打开文档 + Ouvrir un document + + + + 清空 + Effacer + + + + 拖入文档或文件夹 + Faites glisser le document ou le dossier + + + + 请选择文档 + Veuillez sélectionner un document + + + + 设置 + Réglages + + + + 记录 + Historique + + + + BatchDOCConfigs + + + OCR文本后处理 + Post-traitement du texte OCR + + + + 点击表格,可设置更多内容 + Cliquez sur le tableau pour configurer davantage de paramètres + + + + 文档处理 + taitement des documents + + + + 内容提取模式 + Mode d'extraction de contenu + + + + 若一页文档既存在图片又存在文本,如何进行处理 + Comment traiter une page de document contenant à la fois des images et du texte + + + + 混合OCR/原文本 + OCR/texte original mixte + + + + 整页强制OCR + OCR forcé pour la page entière + + + + 仅OCR图片 + OCR des images uniquement + + + + 仅拷贝原有文本 + Copier uniquement le texte original + + + + 批量任务 + Tâches par lot + + + + 递归读取子文件夹 + Lecture récursive des sous-dossiers + + + + 导入文件夹时,导入子文件夹中全部文档 + Lors de l'importation d'un dossier, importer tous les documents des sous-dossiers + + + + 保存到 + Enregistrer dans + + + + 文档原目录 + Répertoire d'origine du document + + + + + 指定目录 + Dossier spécifié + + + + 必须先指定“保存到指定目录”才生效 + + + + + OCR结果保存目录 + Répertoire des résultats de la reconnaissance optique de caractères (OCR) enregistré + + + + 文件名格式 + Format du nom de fichier + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文档名 +%range 识别页数范围。只有识别页数小于总页数时才会显示。 +举例:[OCR]_%name%range_%date +生成:[OCR]_文档A(p2-10)_20230901_1213.txt +添加占位符可以避免旧文件被新文件覆盖。 + + + + + 日期时间格式 + + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + + + + + 保存文件类型 + + + + + layered.pdf 双层可搜索文档 + + + + + 保留原有图片,叠加一层透明文字,可以搜索和复制 + + + + + text.pdf 单层纯文本文档 + + + + + 创建空白PDF文档,只写入识别文字,不含图片 + + + + + txt 标准格式 + + + + + 含识别文字和页数信息 + + + + + p.txt 纯文字格式 + + + + + 输出所有识别文字 + + + + + csv 表格文件(Excel) + + + + + 将页数信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + + + + + jsonl 原始信息 + + + + + 每行为一条json数据,便于第三方程序读取操作 + + + + + 忽略空白页 + + + + + 若某一页没有文字或识别失败,也不会输出错误提示信息 + + + + + 其它 + Autres + + + + BatchOCR + + + 排队 + En attente + + + + 依然关闭 + Confirmer + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + Task in progress. +Do you want to end the task and close the page? + + + + 处理 + Traitement + + + + %1 张图片识别失败! + + + + + 批量识别完成 + Reconnaissance par lot terminée + + + + 批量识别任务异常 + + + + + + 图片 + Image + + + + 耗时 + Durée + + + + 状态 + État + + + + 打开图片 + + + + + 清空 + Effacer + + + + 拖入图片或文件夹 + + + + + 请选择图片 + + + + + 设置 + Réglages + + + + 记录 + Historique + + + + BatchOCRConfigs + + + OCR文本后处理 + Post-traitement du texte OCR + + + + 忽略区域 + + + + + 进入设置 + + + + + 批量任务 + Tâche par lot + + + + 递归读取子文件夹 + Lecture récursive des sous-dossiers + + + + 导入文件夹时,导入子文件夹中全部图片 + + + + + 保存到 + + + + + 图片原目录 + + + + + + 指定目录 + Dossier spécifié + + + + 必须先指定“保存到指定目录”才生效 + + + + + OCR结果保存目录 + + + + + 文件名格式 + + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文件夹名/文件名 +举例:[OCR]_%name_%date +生成:[OCR]_我的图片_2023-09-01_12-13.txt +添加占位符可以避免旧文件被新文件覆盖。 + + + + + 日期时间格式 + + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + + + + + 保存文件类型 + + + + + txt 标准格式 + + + + + 含原图片文件名和识别文字 + + + + + p.txt 纯文字格式 + + + + + 仅输出识别文字,不含图片标题 + + + + + txt 单独文件 + + + + + 对每张图片,生成同名txt文件,仅输出识别文字 + + + + + md 图文混排 + + + + + Markdown图文混排格式,可用Markdown阅读器浏览文件 + + + + + csv 表格文件(Excel) + + + + + 将图片信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + + + + + jsonl 原始信息 + + + + + 每行为一条json数据,便于第三方程序读取操作 + + + + + 输出忽略空白图片 + + + + + 若图片没有文字或识别失败,也不会输出错误提示信息 + + + + + 其它 + Autres + + + + Configs + + + %1 处理配置项异常: +%2枚举列表为空。 + + + + + 重置 + Réinitialiser + + + + 重置本页上的设定 + + + + + 重置设定 + Réinitialiser les réglages + + + + 要重置本页的设定吗? + + + + + 高级 + Avancé + + + + 显示更多高级选项。它们标有 * 号。 +请谨慎修改高级选项。 + + + + + 展开 + + + + + 折叠 + + + + + 必须为整数 + + + + + 不能超过 + + + + + 不能低于 + + + + + 必须为数字 + + + + + 已取消%1的快捷键。 + + + + + 更新热键成功 + + + + + %1的快捷键为 %2 + + + + + %1 快捷键%2已被注册,请尝试另外的按键组合。 + + + + + %1 快捷键%2无法注册,请尝试另外的按键组合。 + + + + + 请按下快捷键组合。按【Esc】退出。 + + + + + 当前快捷键录制已在进行,不能同时录制! + + + + + 无法录制快捷键 + + + + + DropArea_ + + + 松手放入文件 + + + + + FontPanel + + + 界面 + + + + + 内容 + + + + + 界面字体: +软件中大部分UI的字体。 + + + + + 内容字体: +识别结果内容的字体。 + + + + + GlobalConfigs + + + 快捷方式 + Raccourci + + + + 桌面 + Bureau + + + + 开始菜单 + Menu démarrer + + + + 开机自启 + Lancer au démarrage + + + + 界面和外观 + + + + + 主题 + Thème + + + + 切换主题 + + + + + 字体 + Police de caractères + + + + 修改字体 + + + + + 界面与文字大小 + + + + + 渲染器 + + + + + 关闭硬件加速 + + + + + 若出现界面闪烁、元素错位等界面异常,尝试切换渲染器或者关闭硬件加速 + + + + + 禁用美化效果 + + + + + 在低配置机器上,禁用动画、阴影等效果可减少部分资源占用 + + + + + 图片预览默认显示叠加层 + + + + + 默认开启/关闭叠加层显示 +对所有图片预览组件生效 + + + + + 窗口 + Fenêtre + + + + 启动时缩小到任务栏 + + + + + 软件启动时,不弹出主窗口 + + + + + 窗口置顶 + + + + + 捷径:窗口左上角图钉 + + + + + 锁定标签栏 + + + + + 捷径:窗口右上角小锁 + + + + + 关闭主窗口时 + + + + + 最小化到系统托盘 + + + + + 退出应用 + Quitter l'application + + + + 隐藏托盘图标 + + + + + 若要弹出位于后台的软件窗口,请在文件管理器中重复启动软件。 +若要彻底退出软件,请从任务管理器中结束进程。 + + + + + 截图 + Capture d'écran + + + + 截图前隐藏主窗口 + + + + + 截图前,如果主窗口处于前台,则隐藏主窗口 +将会延时等待主窗口关闭 + + + + + 隐藏等待时间 + + + + + 秒 + secondes + + + + 服务 + Service + + + + 允许HTTP服务 + + + + + Umi-OCR依赖HTTP接口进行本机跨进程通信。如果禁用,将无法使用命令行模式、多开检测等功能。 + + + + + + + 重启软件后生效 + + + + + 主机 + Hôte + + + + 仅本地 + Local uniquement + + + + 任何可用地址 + + + + + 将允许局域网访问。请开启对应防火墙权限! + + + + + 将禁止局域网访问。 + + + + + 端口 + Port + + + + 日志 + + + + + 操作 + Action + + + + 打开日志保存目录 + + + + + 保存的日志级别 + + + + + 已禁用日志文件保存 + + + + + 请尝试更换软件路径! + + + + + 配置文件读写异常 + + + + + 插件加载失败 + + + + + 成功添加快捷方式 + + + + + 添加快捷方式失败 + + + + + 成功移除 %1 个快捷方式 + + + + + 提示 + Astuce + + + + 没有找到可移除的快捷方式。 + + + + + 渲染器变更 将在重启软件后生效 + + + + + 端口号不合法 + + + + + 端口号改为%1 + + + + + 原端口号%1被占用, +切换为新端口号%2。 + +若不想看到此通知,请在全局设置关闭高级模式。 + + + + + GlobalConfigsPage + + + 1. 关于快捷键、OCR语言等选项,请在各个功能页中进行设置。 +2. 勾选左下角的“高级”按钮,可以显示更多进阶选项。 + + + + + 小贴士 + + + + + HTabBar + + + 窗口置顶 + + + + + 锁定标签栏 + + + + + IconButtonBar + + + 右键菜单 + Right-click menu + + + + 保存图片 + Sauvegarder l'image + + + + 图片大小:适应窗口 + + + + + 图片大小:实际 + + + + + 截图 + Capture d'écran + + + + 粘贴 + + + + + 屏幕截图 + Capture screen + + + + 粘贴图片 + Coller l'image + + + + IgnoreArea + + + 拖入本地图片:OCR预览 +滚轮:缩放 +左键:拖拽 +右键:绘制忽略区域 + +可绘制一个或多个忽略区域矩形框。在执行批量OCR时,完全位于忽略区域内的文本块将被排除。 +比如批量处理影视截图时,可在右上角水印处添加忽略区域,避免输出水印文本。 + + + + + 图像尺寸: + + + + + 区域数量: + + + + + 撤销 + Défaire + + + + 清空 + Effacer + + + + ImageScale + + + 复制图片 + Copier l'image + + + + 复制图片失败 + + + + + 打开图片 + + + + + 打开图片失败 + + + + + + 保存图片 + Enregistrer l'image + + + + 保存图片失败 + + + + + ImageWithText + + + 图片:复制%1字 + + + + + 图片:无选中文字 + + + + + 图片:复制全部%1字 + + + + + 复制  (Ctrl+C) + + + + + 全选  (Ctrl+A) + + + + + 复制图片(Ctrl+X) + + + + + 保存图片(Ctrl+S) + + + + + 显示/隐藏文字(Tab) + + + + + 用默认应用打开图片 + + + + + 删除图片(Ctrl+D) + + + + + MainWindowManager + + + 欢迎使用 Umi-OCR + + + + + 已启用后台模式,可通过快捷键使用功能。 + + + + + MarkdownView + + + 打开网页 + + + + + 链接 + + + + + MessageBox + + + 取消 + Annuler + + + + 确定 + OK + + + + 警告 + Avertissement + + + + 发生了一点小问题 + + + + + 复制 + Copier + + + + 已复制报错信息 %1 + + + + + 请前往 Issues 页面寻找解答或反馈 + + + + + 问题反馈 + Rétroaction sur les questions + + + + MessageBoxWin + + + 确定 + OK + + + + 不再提示 + Don't prompt again + + + + 知道了 + OK + + + + MissionCtrlPanel + + + 开始任务 + + + + + 暂停任务 +暂停后可以待机或休眠。 +但是关机或退出软件,将会丢弃任务内容。 + + + + + 继续任务 + + + + + 终止任务 +放弃未完成的内容。 + + + + + 正在运行 + + + + + 正在保存 + + + + + 已暂停 + + + + + 任务停止 + + + + + 任务完成 + + + + + ModalLayer + + + 保存并返回 + + + + + 返回 + + + + + Navigation + + + 欢迎使用 Umi-OCR + + + + + 请选择功能页 + + + + + 当前版本 + Version actuelle + + + + 项目链接 + Lien du projet + + + + 官方网站 + Site officiel + + + + 插件拓展 + Extensions + + + + 问题反馈 + Rétroaction sur les questions + + + + 功能页 + + + + + 请打开对应标签页(如批量OCR、批量文档),再拖入文件。 + + + + + OcrManager + + + 文字识别 + Reconnaissance de texte + + + + 操作 + Action + + + + + 强制终止任务 + + + + + 应用修改 + Appliquer les modifications + + + + 当前接口 + Interface actuelle + + + + 没有可用的 OCR 插件。 + + + + + OCR API 列表中不存在%1 + + + + + 当前已有%1组任务队列、共%2个任务正在执行。您可【强制终止任务】后修改API。 + + + + + 无法修改 文字识别接口设置 + + + + + 文字识别接口应用成功 + + + + + 当前API为【%1】 + + + + + 文字识别接口应用失败 + + + + + 当前没有运行中的任务 + + + + + 当前已有%1组任务队列、共%2个任务正在执行。 +要强制终止全部任务吗? + + + + + PagesManager + + + 新标签页 + Nouvel onglet + + + + 截图OCR + + + + + # 截图OCR + +屏幕截图,快捷转文字。也支持粘贴图片。 + + + + + 批量OCR + + + + + # 批量OCR + +导入本地图片或文件夹,批量转换文字。 + +支持格式: + + + + + 批量文档 + + + + + # 批量文档识别 + +批量导入文档,提取文字,或生成双层可搜索PDF。 + +支持格式: + + + + + 二维码 + Code QR + + + + # 二维码 + +识别或生成二维码、条形码。 + +支持协议: + + + + + 全局设置 + Réglages généraux + + + + # 全局设置 + +调节全局设置项,对所有页面生效。 + + + + + 关于 + + + + + # 关于 + + + + + PopupManager + + + 确认 + Confirmer + + + + 取消 + Annuler + + + + PreviewDoc + + + 加密 + Chiffré + + + + 打开文档失败 + + + + + 请填写正确的密码 + + + + + 密码正确 + + + + + 文档预览异常 + + + + + 忽略区域说明: +右键拖拽,绘制矩形区域,包含在区域内的文字框将被忽略。可用于排除水印、页眉页脚。 +范围允许填写负数,表示倒数第x页。如-1表示最后一页,-2表示倒数第2页。 +忽略区域的设置对所有文档生效。 + + + + + 密码: + + + + + 预览页面 + + + + + 预览PDF时,是否预览OCR结果 + + + + + OCR页数 + + + + + 忽略区域页数(全局) + + + + + + 范围 + + + + + 撤销 + Undo + + + + 清空 + Clear + + + + PreviewImage + + + 文字 + Texte + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + + + + + QRCode + + + 获取剪贴板异常 + + + + + 剪贴板中为文本 + + + + + 无有效图片 + + + + + 导入%1条图片路径 + + + + + 参数有误,或输入内容不合规定。请参照报错指示修改: + + + + + + 生成二维码失败 + Failed to generate QR code + + + + 已复制到剪贴板 + + + + + 识图完成 + + + + + 无文字 + Aucun texte + + + + 识别失败 + La reconnaissance a échoué + + + + 扫描二维码 + + + + + 截图、拖入或粘贴二维码图片 + + + + + + 设置 + Réglages + + + + 自动刷新 + Rafraîchissement automatique + + + + 修改文字后,自动生成二维码/条形码 + Automatically generate QR code after modifying text + + + + 刷新 + Rafraîchir + + + + 生成二维码/条形码 + Générer un code QR + + + + 记录 + Historique + + + + 生成 + Générer + + + + QRCodeConfigs + + + 快捷键 + + + + + 屏幕截图 + Capture d'écran + + + + 粘贴图片 + Coller l'image + + + + 预处理(一般无需改动) + + + + + 中值滤波 + + + + + 对图像进行平滑处理 +>0 时生效。可填1~9的奇数(不允许偶数) + + + + + 锐度增强 + + + + + 增加图像的锐度 +>0.1 时生效。可填0.1~10的小数 + + + + + 对比度增强 + + + + + 增加图像的对比度 +>0.1 时生效。可填0.1~10的小数 + + + + + 转为灰度 + + + + + 将图像像素转为灰度 + + + + + 二值化 + + + + + 将图像像素转为纯黑和纯白 +启用了灰度,且二值化 >-1 时生效。可填0~255的整数 + + + + + 扫码后的操作 + + + + + 复制结果 + + + + + 弹出主窗口 + + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + + + + + 生成二维码/条形码 + Générer un code QR + + + + 类型 + Format + + + + 默认二维码: + Par défaut : + + + + 宽度 + Largeur + + + + + 填0:自动选择 + 0: Sélection automatique + + + + + + 像素 + pixel + + + + 高度 + + + + + 边缘空白 + + + + + 填-1:自动选择 + -1: Sélection automatique + + + + 纠错等级 + Error Correction Coding + + + + 仅适用于: + Only applicable to: + + + + 自动 + Auto + + + + 其它 + Autres + + + + ResultTextContainer + + + 复制 + Copier + + + + ResultsTableView + + + 异常状态码:%1 +异常信息:%2 + + + + + 耗时 %1 + + + + + 置信度 %1 + + + + + 记录:复制%1字 + + + + + 记录:无选中文字 + + + + + 记录:复制全部%1字 + + + + + 删除%1条记录 + + + + + 清空记录 + + + + + 复制    (Ctrl+C) + + + + + 复制全部  (Ctrl+C 双击) + + + + + 选中单个  (Ctrl+A) + + + + + 选中全部记录(Ctrl+A 双击) + + + + + 删除选中记录 + + + + + 清空全部记录(Ctrl+D 双击) + + + + + 滚动 + Défiler + + + + 自动滚动到底部 + + + + + 右键菜单 + Right-click menu + + + + ScreenshotManager + + + 屏幕设备名称不相同: +%1 +%2 + + + + + 尚未记录截图区域 + + + + + 重复截图失败 + + + + + 未找到匹配的屏幕 + + + + + 显示器: %1 +错误信息: %2 + + + + + 上次截图操作未结束,不能进行新的截图! + + + + + 截图失败 + + + + + ScreenshotOCR + + + 获取剪贴板异常 + + + + + 剪贴板中为文本 + + + + + 无有效图片 + + + + + 导入%1条图片路径 + + + + + 依然关闭 + Confirm + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + Task in progress. +Do you want to end the task and close the page? + + + + + 屏幕截图 + Capture screen + + + + + 粘贴图片 + Coller l'image + + + + 截图识别任务异常 + + + + + 已复制到剪贴板 + + + + + 识图完成 + + + + + 无文字 + Aucun texte + + + + 识别失败 + La reconnaissance a échoué + + + + + 停止任务 + + + + + 文字 + Text + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + + + + + 截图、拖入或粘贴图片 + + + + + 设置 + Réglages + + + + 记录 + Historique + + + + ScreenshotOcrConfigs + + + OCR文本后处理 + Post-traitement du texte OCR + + + + 快捷键 + + + + + 屏幕截图 + Capture screen + + + + 粘贴图片 + Coller l'image + + + + 重复截图 + + + + + 重新截取上一次截图的范围 + + + + + 识图后的操作 + + + + + 复制结果 + + + + + 弹出主窗口 + + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + + + + + 其它 + Autres + + + + Size_ + + + 1.0 + 1.0 + + + + SystemTray + + + 打开主窗口 + + + + + 退出 Umi-OCR + + + + + Theme + + + 珍珠白 + + + + + 云墨黑 + + + + + 赛博朋克 + + + + + UtilsConfigDicts + + + 任务完成后的操作 + + + + + 系统 + + + + + 无 + Aucun + + + + 关机 + + + + + 休眠 + + + + + %1 系统暂不支持电源控制! + + + + + 继续%1 + + + + + 取消%1 + + + + + 系统即将%1 + + + + + 排版解析方案 + + + + + 按什么方式,解析和排序图片中的文字块 + + + + + 多栏-按自然段换行 + + + + + 多栏-总是换行 + + + + + 多栏-无换行 + + + + + 单栏-按自然段换行 + + + + + 单栏-总是换行 + + + + + 单栏-无换行 + + + + + 单栏-保留缩进 + + + + + 不做处理 + + + + + 优先内部 + + + + + 只允许内部 + + + + + 只允许外部 + + + + + 禁用所有通知 + + + + + 跟随全局设定 + + + + + 通知弹窗类型 + + + + + 通知类型已更改 + + + + diff --git a/dev-tools/i18n/release/he.ts b/dev-tools/i18n/release/he.ts new file mode 100644 index 0000000..c2eaf26 --- /dev/null +++ b/dev-tools/i18n/release/he.ts @@ -0,0 +1,2466 @@ + + + + + About + + + 开源、免费的离线OCR软件 + קוד פתוח, תוכנת OCR לא מקוונת בחינם + + + + 当前版本 + גרסה נוכחית + + + + 项目链接 + קישור לפרויקט + + + + 官方网站 + אתר רשמי + + + + 插件拓展 + סיומת תוסף + + + + 问题反馈 + משוב על בעיה + + + + 发布地址 + + + + + 许可协议 + תנאי רישיון + + + + 作者 + מחבר + + + + 译者 + מתרגם + + + + 运行环境信息(如需请求协助,请提供给开发者) + מידע סביבת הפעלה (אם אתה זקוק לעזרה, אנא ספק אותו למפתח) + + + + 复制 + העתק + + + + BatchDOC + + + 加密 + הצפנה + + + + %1个加密文档 + %1 מסמכים מוצפנים + + + + 请点击文件名填写密码 + אנא לחץ על שם הקובץ כדי למלא את הסיסמה + + + + 文档已加密 + המסמך מוצפן + + + + 【%1】 +请点击文档名,设置密码 + 【%1】 +אנא לחץ על שם המסמך כדי להגדיר סיסמה + + + + 失败 + נכשל + + + + 排队 + תור + + + + 依然关闭 + עדיין סגור + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + התהליך פועל. +לסיים את התהליך ולסגור את הדף? + + + + + 文档 + מסמך + + + + 状态 + מדינה + + + + 打开文档 + פתח את המסמך + + + + 拖入文档或文件夹 + גרור פנימה מסמך או תיקיה + + + + 清空 + + + + + 文件读取中… + טוען קובץ… + + + + 文档识别异常 + + + + + 批量识别完成 + + + + + 页数 + מספר עמודים + + + + 请选择文档 + אנא בחר מסמך + + + + 设置 + + + + + 记录 + + + + + BatchDOCConfigs + + + OCR文本后处理 + OCR לאחר עיבוד טקסט + + + + 文档处理 + עיבוד מסמכים + + + + 内容提取模式 + + + + + 若一页文档既存在图片又存在文本,如何进行处理 + איך מעבדים מסמך שמכיל גם תמונות וגם טקסט? + + + + 混合OCR/原文本 + + + + + 整页强制OCR + + + + + 仅OCR图片 + + + + + 仅拷贝原有文本 + + + + + 批量任务 + + + + + 保存到 + + + + + 文档原目录 + + + + + + 指定目录 + + + + + 必须先指定“保存到指定目录”才生效 + + + + + OCR结果保存目录 + + + + + 文件名格式 + + + + + 日期时间格式 + + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + + + + + 保存文件类型 + + + + + layered.pdf 双层可搜索文档 + + + + + text.pdf 单层纯文本文档 + + + + + 创建空白PDF文档,只写入识别文字,不含图片 + + + + + txt 标准格式 + + + + + 含识别文字和页数信息 + + + + + p.txt 纯文字格式 + + + + + 输出所有识别文字 + + + + + csv 表格文件(Excel) + + + + + 将页数信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + + + + + jsonl 原始信息 + + + + + 每行为一条json数据,便于第三方程序读取操作 + + + + + 忽略空白页 + + + + + 若某一页没有文字或识别失败,也不会输出错误提示信息 + + + + + 递归读取子文件夹 + + + + + 点击表格,可设置更多内容 + + + + + 导入文件夹时,导入子文件夹中全部文档 + + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文档名 +%range 识别页数范围。只有识别页数小于总页数时才会显示。 +举例:[OCR]_%name%range_%date +生成:[OCR]_文档A(p2-10)_20230901_1213.txt +添加占位符可以避免旧文件被新文件覆盖。 + No need to fill in the extension name. The following placeholders are supported: +%date Date and time +%name Original folder name/file name +%range Page range. Only displayed when the OCR page count is less than the total page count. +Example: [OCR]_%name%range_%date +Generated: [OCR]_Document(p2-10)_2023-09-01_12-13.txt +Adding placeholders can prevent old files from being overwritten by new files. + + + + 保留原有图片,叠加一层透明文字,可以搜索和复制 + + + + + 其它 + + + + + BatchOCR + + + 依然关闭 + + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + + + + + 排队 + + + + + 处理 + + + + + %1 张图片识别失败! + + + + + 批量识别完成 + + + + + 批量识别任务异常 + + + + + + 图片 + + + + + 耗时 + + + + + 状态 + + + + + 打开图片 + + + + + 拖入图片或文件夹 + + + + + 清空 + + + + + 请选择图片 + + + + + 设置 + + + + + 记录 + + + + + BatchOCRConfigs + + + OCR文本后处理 + + + + + 忽略区域 + + + + + 进入设置 + + + + + 批量任务 + + + + + 保存到 + + + + + 图片原目录 + + + + + + 指定目录 + + + + + 必须先指定“保存到指定目录”才生效 + + + + + OCR结果保存目录 + + + + + 文件名格式 + + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文件夹名/文件名 +举例:[OCR]_%name_%date +生成:[OCR]_我的图片_2023-09-01_12-13.txt +添加占位符可以避免旧文件被新文件覆盖。 + + + + + 日期时间格式 + + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + + + + + 保存文件类型 + + + + + txt 标准格式 + + + + + 含原图片文件名和识别文字 + + + + + p.txt 纯文字格式 + + + + + 仅输出识别文字,不含图片标题 + + + + + txt 单独文件 + + + + + 对每张图片,生成同名txt文件,仅输出识别文字 + + + + + md 图文混排 + + + + + Markdown图文混排格式,可用Markdown阅读器浏览文件 + + + + + csv 表格文件(Excel) + + + + + 将图片信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + + + + + jsonl 原始信息 + + + + + 每行为一条json数据,便于第三方程序读取操作 + + + + + 输出忽略空白图片 + + + + + 若图片没有文字或识别失败,也不会输出错误提示信息 + + + + + 递归读取子文件夹 + + + + + 导入文件夹时,导入子文件夹中全部图片 + + + + + 其它 + + + + + Configs + + + %1 处理配置项异常: +%2枚举列表为空。 + + + + + 重置 + + + + + 重置本页上的设定 + + + + + 重置设定 + + + + + 要重置本页的设定吗? + + + + + 高级 + + + + + 显示更多高级选项。它们标有 * 号。 +请谨慎修改高级选项。 + + + + + 展开 + + + + + 折叠 + + + + + 必须为整数 + + + + + 不能超过 + + + + + 不能低于 + + + + + 必须为数字 + + + + + 已取消%1的快捷键。 + + + + + 更新热键成功 + + + + + %1的快捷键为 %2 + + + + + %1 快捷键%2已被注册,请尝试另外的按键组合。 + + + + + %1 快捷键%2无法注册,请尝试另外的按键组合。 + + + + + 请按下快捷键组合。按【Esc】退出。 + + + + + 当前快捷键录制已在进行,不能同时录制! + + + + + 无法录制快捷键 + + + + + DropArea_ + + + 松手放入文件 + + + + + FontPanel + + + 界面 + + + + + 内容 + + + + + 界面字体: +软件中大部分UI的字体。 + + + + + 内容字体: +识别结果内容的字体。 + + + + + GlobalConfigs + + + 快捷方式 + + + + + 桌面 + + + + + 开始菜单 + + + + + 开机自启 + + + + + 界面和外观 + + + + + 主题 + + + + + 切换主题 + + + + + 字体 + + + + + 修改字体 + + + + + 界面与文字大小 + + + + + 渲染器 + + + + + 关闭硬件加速 + + + + + 若出现界面闪烁、元素错位等界面异常,尝试切换渲染器或者关闭硬件加速 + + + + + 禁用美化效果 + + + + + 在低配置机器上,禁用动画、阴影等效果可减少部分资源占用 + + + + + 图片预览默认显示叠加层 + + + + + 默认开启/关闭叠加层显示 +对所有图片预览组件生效 + + + + + 窗口 + + + + + 启动时缩小到任务栏 + + + + + 软件启动时,不弹出主窗口 + + + + + 窗口置顶 + + + + + 捷径:窗口左上角图钉 + + + + + 锁定标签栏 + + + + + 捷径:窗口右上角小锁 + + + + + 关闭主窗口时 + + + + + 最小化到系统托盘 + + + + + 退出应用 + + + + + 隐藏托盘图标 + + + + + 若要弹出位于后台的软件窗口,请在文件管理器中重复启动软件。 +若要彻底退出软件,请从任务管理器中结束进程。 + + + + + 截图 + + + + + 截图前隐藏主窗口 + + + + + 截图前,如果主窗口处于前台,则隐藏主窗口 +将会延时等待主窗口关闭 + + + + + 隐藏等待时间 + + + + + 秒 + + + + + 服务 + + + + + 允许HTTP服务 + + + + + Umi-OCR依赖HTTP接口进行本机跨进程通信。如果禁用,将无法使用命令行模式、多开检测等功能。 + + + + + + + 重启软件后生效 + + + + + 主机 + + + + + 仅本地 + + + + + 任何可用地址 + + + + + 将允许局域网访问。请开启对应防火墙权限! + + + + + 将禁止局域网访问。 + + + + + 端口 + + + + + 请尝试更换软件路径! + + + + + 配置文件读写异常 + + + + + 插件加载失败 + + + + + 成功添加快捷方式 + + + + + 添加快捷方式失败 + + + + + 成功移除 %1 个快捷方式 + + + + + 提示 + + + + + 没有找到可移除的快捷方式。 + + + + + 立刻关闭软件 + + + + + 稍后 + + + + + 渲染器变更 将在重启软件后生效 + + + + + 端口号不合法 + + + + + 端口号改为%1 + + + + + 原端口号%1被占用, +切换为新端口号%2。 + +若不想看到此通知,请在全局设置关闭高级模式。 + + + + + GlobalConfigsPage + + + 1. 关于快捷键、OCR语言等选项,请在各个功能页中进行设置。 +2. 勾选左下角的“高级”按钮,可以显示更多进阶选项。 + + + + + 小贴士 + + + + + HTabBar + + + 窗口置顶 + + + + + 锁定标签栏 + + + + + IconButtonBar + + + 右键菜单 + + + + + 保存图片 + + + + + 图片大小:适应窗口 + + + + + 图片大小:实际 + + + + + 截图 + + + + + 粘贴 + + + + + 屏幕截图 + Screenshot + + + + 粘贴图片 + + + + + IgnoreArea + + + 拖入本地图片:OCR预览 +滚轮:缩放 +左键:拖拽 +右键:绘制忽略区域 + +可绘制一个或多个忽略区域矩形框。在执行批量OCR时,完全位于忽略区域内的文本块将被排除。 +比如批量处理影视截图时,可在右上角水印处添加忽略区域,避免输出水印文本。 + + + + + 图像尺寸: + + + + + 区域数量: + + + + + 撤销 + + + + + 清空 + + + + + ImageScale + + + 复制图片 + + + + + 复制图片失败 + + + + + 打开图片 + + + + + 打开图片失败 + + + + + + 保存图片 + + + + + 保存图片失败 + + + + + ImageWithText + + + 图片:复制%1字 + + + + + 图片:无选中文字 + + + + + 图片:复制全部%1字 + + + + + 复制  (Ctrl+C) + + + + + 全选  (Ctrl+A) + + + + + 复制图片(Ctrl+X) + + + + + 保存图片(Ctrl+S) + + + + + 显示/隐藏文字(Tab) + + + + + 用默认应用打开图片 + + + + + 删除图片(Ctrl+D) + + + + + MainWindowManager + + + 欢迎使用 Umi-OCR + + + + + 已启用后台模式,可通过快捷键使用功能。 + + + + + MarkdownView + + + 打开网页 + + + + + 链接 + + + + + MessageBox + + + 取消 + + + + + 确定 + + + + + 警告 + + + + + 发生了一点小问题 + + + + + 复制 + + + + + 已复制报错信息 %1 + + + + + 请前往 Issues 页面寻找解答或反馈 + + + + + 问题反馈 + + + + + MessageBoxWin + + + 确定 + + + + + 不再提示 + + + + + 知道了 + + + + + MissionCtrlPanel + + + 开始任务 + + + + + 暂停任务 +暂停后可以待机或休眠。 +但是关机或退出软件,将会丢弃任务内容。 + + + + + 继续任务 + + + + + 终止任务 +放弃未完成的内容。 + + + + + 正在运行 + + + + + 正在保存 + + + + + 已暂停 + + + + + 任务停止 + + + + + 任务完成 + + + + + ModalLayer + + + 保存并返回 + + + + + 返回 + + + + + Navigation + + + 欢迎使用 Umi-OCR + + + + + 请选择功能页 + + + + + 当前版本 + + + + + 项目链接 + + + + + 官方网站 + + + + + 插件拓展 + + + + + 问题反馈 + + + + + 功能页 + + + + + 请打开对应标签页(如批量OCR、批量文档),再拖入文件。 + + + + + OcrManager + + + 文字识别 + + + + + 操作 + + + + + + 强制终止任务 + + + + + 应用修改 + + + + + 当前接口 + + + + + OCR API 列表中不存在%1 + + + + + 当前已有%1组任务队列、共%2个任务正在执行。您可【强制终止任务】后修改API。 + + + + + 无法修改 文字识别接口设置 + + + + + 文字识别接口应用成功 + + + + + 当前API为【%1】 + + + + + 文字识别接口应用失败 + + + + + 当前没有运行中的任务 + + + + + 当前已有%1组任务队列、共%2个任务正在执行。 +要强制终止全部任务吗? + + + + + PagesManager + + + 新标签页 + + + + + 截图OCR + + + + + # 截图OCR + +屏幕截图,快捷转文字。也支持粘贴图片。 + + + + + # 全局设置 + +调节全局设置项,对所有页面生效。 + + + + + 批量OCR + + + + + # 批量OCR + +导入本地图片或文件夹,批量转换文字。 + +支持格式: + + + + + 批量文档 + + + + + # 批量文档识别 + +批量导入文档,提取文字,或生成双层可搜索PDF。 + +支持格式: + + + + + 二维码 + + + + + # 二维码 + +识别或生成二维码、条形码。 + +支持协议: + + + + + 全局设置 + + + + + 关于 + + + + + # 关于 + + + + + PopupManager + + + 确认 + + + + + 取消 + + + + + PreviewDoc + + + 加密 + + + + + 打开文档失败 + + + + + 请填写正确的密码 + + + + + 密码正确 + + + + + 文档预览异常 + + + + + 忽略区域说明: +右键拖拽,绘制矩形区域,包含在区域内的文字框将被忽略。可用于排除水印、页眉页脚。 +范围允许填写负数,表示倒数第x页。如-1表示最后一页,-2表示倒数第2页。 +忽略区域的设置对所有文档生效。 + + + + + 密码: + + + + + 预览页面 + + + + + 预览PDF时,是否预览OCR结果 + + + + + OCR页数 + OCR page count + + + + 忽略区域页数(全局) + Ignore Area page count (Global) + + + + + 范围 + + + + + 撤销 + + + + + 清空 + + + + + PreviewImage + + + 文字 + + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + + + + + QRCode + + + 获取剪贴板异常 + + + + + 剪贴板中为文本 + + + + + 无有效图片 + + + + + 导入%1条图片路径 + + + + + 参数有误,或输入内容不合规定。请参照报错指示修改: + + + + + + 生成二维码失败 + + + + + 已复制到剪贴板 + + + + + 识图完成 + + + + + 无文字 + + + + + 识别失败 + + + + + 扫描二维码 + + + + + 截图、拖入或粘贴二维码图片 + Screenshot, drag and drop, or paste QR code image + + + + + 设置 + + + + + 自动刷新 + + + + + 修改文字后,自动生成二维码/条形码 + + + + + 刷新 + + + + + 生成二维码/条形码 + + + + + 记录 + + + + + 生成 + + + + + QRCodeConfigs + + + 快捷键 + + + + + 屏幕截图 + Screenshot + + + + 粘贴图片 + + + + + 预处理(一般无需改动) + + + + + 中值滤波 + + + + + 对图像进行平滑处理 +>0 时生效。可填1~9的奇数(不允许偶数) + + + + + 锐度增强 + + + + + 增加图像的锐度 +>0.1 时生效。可填0.1~10的小数 + + + + + 对比度增强 + + + + + 增加图像的对比度 +>0.1 时生效。可填0.1~10的小数 + + + + + 转为灰度 + + + + + 将图像像素转为灰度 + + + + + 二值化 + + + + + 将图像像素转为纯黑和纯白 +启用了灰度,且二值化 >-1 时生效。可填0~255的整数 + + + + + 扫码后的操作 + + + + + 复制结果 + + + + + 弹出主窗口 + + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + + + + + 生成二维码/条形码 + + + + + 类型 + + + + + 默认二维码: + + + + + 宽度 + + + + + + 填0:自动选择 + + + + + + + 像素 + + + + + 高度 + + + + + 边缘空白 + + + + + 填-1:自动选择 + + + + + 纠错等级 + + + + + 仅适用于: + + + + + 自动 + + + + + 其它 + + + + + ResultTextContainer + + + 复制 + + + + + ResultsTableView + + + 异常状态码:%1 +异常信息:%2 + + + + + 耗时 %1 + + + + + 置信度 %1 + + + + + 记录:复制%1字 + + + + + 记录:无选中文字 + + + + + 记录:复制全部%1字 + + + + + 删除%1条记录 + + + + + 清空记录 + + + + + 复制    (Ctrl+C) + + + + + 复制全部  (Ctrl+C 双击) + + + + + 选中单个  (Ctrl+A) + + + + + 选中全部记录(Ctrl+A 双击) + + + + + 删除选中记录 + + + + + 清空全部记录(Ctrl+D 双击) + + + + + 滚动 + + + + + 自动滚动到底部 + + + + + 右键菜单 + + + + + ScreenshotManager + + + 截图失败 + + + + + 上次截图操作未结束,不能进行新的截图! + + + + + 尚未记录截图区域 + + + + + 显示器: %1 +错误信息: %2 + + + + + 重复截图失败 + + + + + 未找到匹配的屏幕 + + + + + 屏幕设备名称不相同: +%1 +%2 + + + + + ScreenshotOCR + + + 获取剪贴板异常 + + + + + 剪贴板中为文本 + + + + + 无有效图片 + + + + + 导入%1条图片路径 + + + + + 依然关闭 + + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + + + + + + 屏幕截图 + Screenshot + + + + + 粘贴图片 + + + + + 截图识别任务异常 + + + + + 已复制到剪贴板 + + + + + 识图完成 + + + + + 无文字 + + + + + 识别失败 + + + + + + 停止任务 + + + + + 文字 + + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + + + + + 截图、拖入或粘贴图片 + Screenshot, drag and drop, or paste image + + + + 设置 + + + + + 记录 + + + + + ScreenshotOcrConfigs + + + OCR文本后处理 + + + + + 快捷键 + + + + + 屏幕截图 + Screenshot + + + + 粘贴图片 + + + + + 重复截图 + + + + + 重新截取上一次截图的范围 + + + + + 识图后的操作 + + + + + 复制结果 + + + + + 弹出主窗口 + + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + + + + + 其它 + + + + + Size_ + + + 1.0 + + + + + SystemTray + + + 打开主窗口 + + + + + 退出 Umi-OCR + + + + + Theme + + + 珍珠白 + + + + + 云墨黑 + + + + + 赛博朋克 + + + + + UtilsConfigDicts + + + 任务完成后的操作 + + + + + 系统 + + + + + 无 + + + + + 关机 + + + + + 休眠 + + + + + %1 系统暂不支持电源控制! + %1 Linux system currently does not support power control! + + + + 继续%1 + + + + + 取消%1 + + + + + 系统即将%1 + + + + + 排版解析方案 + + + + + 按什么方式,解析和排序图片中的文字块 + + + + + 多栏-按自然段换行 + + + + + 多栏-总是换行 + + + + + 多栏-无换行 + + + + + 单栏-按自然段换行 + + + + + 单栏-总是换行 + + + + + 单栏-无换行 + + + + + 单栏-保留缩进 + + + + + 不做处理 + + + + + 优先内部 + + + + + 只允许内部 + + + + + 只允许外部 + + + + + 禁用所有通知 + + + + + 跟随全局设定 + + + + + 通知弹窗类型 + + + + + 通知类型已更改 + + + + diff --git a/dev-tools/i18n/release/ja_JP.ts b/dev-tools/i18n/release/ja_JP.ts new file mode 100644 index 0000000..b4afed5 --- /dev/null +++ b/dev-tools/i18n/release/ja_JP.ts @@ -0,0 +1,2556 @@ + + + + + About + + + 开源、免费的离线OCR软件 + オフラインで利用できるOCRフリーソフト + + + + 当前版本 + 最新バージョン + + + + 项目链接 + リンク + + + + 官方网站 + ホームページ + + + + 插件拓展 + プラグイン + + + + 问题反馈 + フィードバック + + + + 发布地址 + リリースアドレス + + + + 许可协议 + オープンソースライセンス + + + + 作者 + ソフトウェア作者 + + + + 译者 + 翻訳者 + + + + 运行环境信息(如需请求协助,请提供给开发者) + 動作環境 + + + + 复制 + コピー + + + + AsynFilesLoader + + + 正在载入 %1 个文件: +%2 + %1 個のファイルを読み込み中: +%2 + + + + BatchDOC + + + 加密 + 暗号化 + + + + %1个加密文档 + %1個の暗号化文書 + + + + 请点击文件名填写密码 + ファイル名をクリックしてパスワードを入力してください + + + + 文档已加密 + 文書は暗号化されていま + + + + 【%1】 +请点击文档名,设置密码 + 【%1】 +文書名をクリックしてパスワードを設定してください + + + + 失败 + 失敗 + + + + 排队 + 待機 + + + + 依然关闭 + 依然として閉じます + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + タスクが進行中です。 +タスクを終了してページを閉じますか? + + + + + 文档 + 文書 + + + + 状态 + 状態 + + + + 打开文档 + ファイルを開く + + + + 拖入文档或文件夹 + 画像またはフォルダーをウィンドウにドラッグします + + + + 清空 + クリア + + + + 文档识别异常 + ドキュメント認識エラー + + + + 批量识别完成 + 一括認識が完了しました + + + + 页数 + ページ数 + + + + 请选择文档 + 文書を選択してください + + + + 设置 + 設定 + + + + 记录 + 記録 + + + + BatchDOCConfigs + + + OCR文本后处理 + OCRテキストの後処理 + + + + 文档处理 + 文書処理 + + + + 内容提取模式 + 内容抽出モード + + + + 若一页文档既存在图片又存在文本,如何进行处理 + もし一頁の文書に画像とテキストが両方存在する場合,どのように処理すればよいか + + + + 混合OCR/原文本 + ミックスOCR/元のテキスト + + + + 整页强制OCR + 全ページOCRを強制 + + + + 仅OCR图片 + 画像のみOCR + + + + 仅拷贝原有文本 + 元のテキストのみコピー + + + + 批量任务 + 一括タスク + + + + 保存到 + 保存先 + + + + 文档原目录 + 文書の元のディレクトリ + + + + + 指定目录 + 指定したディレクトリ + + + + 必须先指定“保存到指定目录”才生效 + 有効にするには「保存先を指定する」必要があります + + + + OCR结果保存目录 + OCR 結果の保存ディレクトリ + + + + 文件名格式 + ファイル名の形式 + + + + 日期时间格式 + 日付と時刻の形式 + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + ファイル名内の %date の日付形式です。次のプレースホルダを挿入できます: +%Y 年、 %m 月、 %d 日、 %H 時、 +%M 分、 %S 秒、 %unix タイムスタンプ +例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + + + + 保存文件类型 + 保存するファイルの種類 + + + + layered.pdf 双层可搜索文档 + layered.pdf レイヤー化された検索可能な文書 + + + + text.pdf 单层纯文本文档 + text.pdf 単層のテキストのみのドキュメント + + + + 创建空白PDF文档,只写入识别文字,不含图片 + 空白のPDFドキュメントを作成し、認識されたテキストのみを挿入(画像なし) + + + + txt 标准格式 + txt 標準フォーマット + + + + 含识别文字和页数信息 + 認識されたテキストとページ情報を含む + + + + p.txt 纯文字格式 + p.txt 純粋なテキストフォーマット + + + + 输出所有识别文字 + すべての認識されたテキストを出力 + + + + csv 表格文件(Excel) + csv テーブル形式(Excel) + + + + 将页数信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + ページ情報と認識内容をCSV表に書き込むファイル。Excelで開き、xlsx形式に保存することができます。 + + + + jsonl 原始信息 + jsonl 生データ + + + + 每行为一条json数据,便于第三方程序读取操作 + 各行が 1 つの JSON データであり、サードパーティのプログラムが読み取り操作を行いやすい形式です + + + + 忽略空白页 + 空のページを無視 + + + + 若某一页没有文字或识别失败,也不会输出错误提示信息 + ページにテキストがないか認識に失敗した場合、エラーメッセージは表示されません + + + + 递归读取子文件夹 + サブフォルダを再帰的に読み込む + + + + 点击表格,可设置更多内容 + 表にはより多くの内容を設定することができます + + + + 导入文件夹时,导入子文件夹中全部文档 + フォルダをインポートする際、サブフォルダ内のすべての文書もインポートします + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文档名 +%range 识别页数范围。只有识别页数小于总页数时才会显示。 +举例:[OCR]_%name%range_%date +生成:[OCR]_文档A(p2-10)_20230901_1213.txt +添加占位符可以避免旧文件被新文件覆盖。 + 拡張子は不要です。次のプレースホルダを挿入できます: +%date 日付と時刻 +%name 元のフォルダ名/ファイル名 +%range OCRページ範囲。認識されたページ数が総ページ数より少ない場合にのみ表示されます。 +例:[OCR]_%name%range_%date +生成:[OCR]_ドキュメント(p2-10)_2023-09-01_12-13.txt +プレースホルダを追加すると、古いファイルが新しいファイルに上書きされるのを防ぐことができます。 + + + + 保留原有图片,叠加一层透明文字,可以搜索和复制 + 元の画像を保持し、検索してコピーできる透明なテキストのレイヤーを重ねます + + + + 其它 + その他 + + + + BatchOCR + + + 依然关闭 + まだ閉じています + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + タスクが進行中です。 +タスクを終了してページを閉じますか? + + + + 排队 + 待機 + + + + 处理 + 処理 + + + + %1 张图片识别失败! + %1 枚の画像認識に失敗しました! + + + + 批量识别完成 + 一括認識が完了しました + + + + 批量识别任务异常 + 一括認識タスクの例外 + + + + + 图片 + 画像 + + + + 耗时 + 所要時間 + + + + 状态 + ステータス + + + + 打开图片 + 画像を開く + + + + 拖入图片或文件夹 + 画像またはフォルダーをウィンドウにドラッグします + + + + 清空 + クリア + + + + 请选择图片 + 画像を選択してください + + + + 设置 + 設定 + + + + 记录 + 記録 + + + + BatchOCRConfigs + + + OCR文本后处理 + OCR テキストの後処理 + + + + 忽略区域 + 無視する領域 + + + + 进入设置 + 設定に入る + + + + 批量任务 + 一括タスク + + + + 保存到 + 保存先 + + + + 图片原目录 + 元のディレクトリ + + + + + 指定目录 + 指定したディレクトリ + + + + 必须先指定“保存到指定目录”才生效 + 有効にするには「保存先を指定する」必要があります + + + + OCR结果保存目录 + OCR 結果の保存ディレクトリ + + + + 文件名格式 + ファイル名の形式 + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文件夹名/文件名 +举例:[OCR]_%name_%date +生成:[OCR]_我的图片_2023-09-01_12-13.txt +添加占位符可以避免旧文件被新文件覆盖。 + 拡張子は不要です。次のプレースホルダを挿入できます: +%date 日付と時刻 +%name 元のフォルダ名/ファイル名 +例:[OCR]_%name_%date +生成:[OCR]_マイピクチャ_2023-09-01_12-13.txt +プレースホルダを追加すると、古いファイルが新しいファイルに上書きされるのを防ぐことができます。 + + + + 日期时间格式 + 日付と時刻の形式 + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + ファイル名内の %date の日付形式です。次のプレースホルダを挿入できます: +%Y 年、 %m 月、 %d 日、 %H 時、 +%M 分、 %S 秒、 %unix タイムスタンプ +例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + + + + 保存文件类型 + 保存するファイルの種類 + + + + txt 标准格式 + txt 標準形式 + + + + 含原图片文件名和识别文字 + 元の画像ファイル名と認識されたテキストを含む + + + + p.txt 纯文字格式 + p.txt 純粋なテキスト形式 + + + + 仅输出识别文字,不含图片标题 + 認識されたテキストのみを出力し、画像のタイトルを含まない + + + + txt 单独文件 + txt 個別ファイル + + + + 对每张图片,生成同名txt文件,仅输出识别文字 + 各画像に同名の txt ファイルを生成し、認識されたテキストのみを出力します + + + + md 图文混排 + md 画像とテキストの混在 + + + + Markdown图文混排格式,可用Markdown阅读器浏览文件 + Markdown の画像とテキストの混在形式で、Markdown リーダーでファイルを閲覧できます + + + + csv 表格文件(Excel) + csv テーブル形式(Excel) + + + + 将图片信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + 画像情報と認識内容を csv テーブルファイルに書き込みます。Excel で開くことができ、xlsx 形式で保存できます。 + + + + jsonl 原始信息 + jsonl 生データ + + + + 每行为一条json数据,便于第三方程序读取操作 + 各行が 1 つの JSON データであり、サードパーティのプログラムが読み取り操作を行いやすい形式です + + + + 输出忽略空白图片 + 空白の画像を無視して出力する + + + + 若图片没有文字或识别失败,也不会输出错误提示信息 + 画像にテキストがないか、認識に失敗してもエラーメッセージを出力しません + + + + 递归读取子文件夹 + サブフォルダを再帰的に読み込む + + + + 导入文件夹时,导入子文件夹中全部图片 + フォルダをインポートする際、サブフォルダ内のすべての画像をインポートします + + + + 其它 + その他 + + + + Configs + + + %1 处理配置项异常: +%2枚举列表为空。 + %1 の設定項目が例外です: +%2 の列挙リストが空です。 + + + + 重置 + リセット + + + + 重置本页上的设定 + このページの設定をリセットする + + + + 重置设定 + 設定をリセット + + + + 要重置本页的设定吗? + 設定をリセットしますか? + + + + 高级 + 高級 + + + + 显示更多高级选项。它们标有 * 号。 +请谨慎修改高级选项。 + さらに高級なオプションを表示します。それらは * でマークされています。 +高度なオプションの変更には注意してください。 + + + + 展开 + 展開 + + + + 折叠 + 折りたたむ + + + + 必须为整数 + 整数である必要があります + + + + 不能超过 + 超えることはできません + + + + 不能低于 + 以下にすることはできません + + + + 必须为数字 + 数字である必要があります + + + + 已取消%1的快捷键。 + %1 のショートカットがキャンセルされました。 + + + + 更新热键成功 + ホットキーの更新に成功しました + + + + %1的快捷键为 %2 + %1 のホットキーは %2 です + + + + %1 快捷键%2已被注册,请尝试另外的按键组合。 + %1 のホットキー %2 は既に登録されています。別のキーボードショートカットを試してください。 + + + + %1 快捷键%2无法注册,请尝试另外的按键组合。 + %1 のホットキー %2 を登録できません。別のキーボードショートカットを試してください。 + + + + 请按下快捷键组合。按【Esc】退出。 + キーボードショートカットの組み合わせを押してください。【Esc】キーで終了します。 + + + + 当前快捷键录制已在进行,不能同时录制! + 現在、キーボードショートカットの記録が進行中です。同時に記録することはできません! + + + + 无法录制快捷键 + キーボードショートカットを記録できません + + + + DropArea_ + + + 松手放入文件 + ファイルをドロップしてください + + + + FontPanel + + + 界面 + インターフェース + + + + 内容 + コンテンツ + + + + 界面字体: +软件中大部分UI的字体。 + インターフェースのフォント: +ソフトウェア内のほとんどの UI のフォントです。 + + + + 内容字体: +识别结果内容的字体。 + コンテンツのフォント: +認識結果のコンテンツのフォントです。 + + + + GlobalConfigs + + + 快捷方式 + ショートカット + + + + 桌面 + デスクトップ + + + + 开始菜单 + スタートメニュー + + + + 开机自启 + 起動時に自動起動 + + + + 界面和外观 + インターフェースと外観 + + + + 主题 + テーマ + + + + 切换主题 + テーマの切り替え + + + + 字体 + フォント + + + + 修改字体 + フォントの変更 + + + + 界面与文字大小 + インターフェースとテキストのサイズ + + + + 渲染器 + レンダラー + + + + 关闭硬件加速 + ハードウェアアクセラレーションを無効にする + + + + 若出现界面闪烁、元素错位等界面异常,尝试切换渲染器或者关闭硬件加速 + インターフェースのフリッカーや要素のずれなどの問題が発生した場合、レンダラーを切り替えるか、ハードウェアアクセラレーションを無効にしてください + + + + 禁用美化效果 + 美化効果を無効にする + + + + 在低配置机器上,禁用动画、阴影等效果可减少部分资源占用 + 低スペックのマシンでは、アニメーションやシャドウなどの効果を無効にするとリソースの一部を節約できます + + + + 图片预览默认显示叠加层 + 画像プレビューのデフォルトでオーバーレイを表示 + + + + 默认开启/关闭叠加层显示 +对所有图片预览组件生效 + オーバーレイ表示のデフォルトでオン/オフ +すべての画像プレビューコンポーネントに適用 + + + + 窗口 + ウィンドウ + + + + 启动时缩小到任务栏 + 起動時にタスクバーに最小化する + + + + 软件启动时,不弹出主窗口 + ソフトウェアを起動するときに、メインウィンドウを表示しない + + + + 窗口置顶 + ウィンドウを最前面に固定する + + + + 捷径:窗口左上角图钉 + ショートカット:ウィンドウの左上にあるピン + + + + 锁定标签栏 + タブバーをロックする + + + + 捷径:窗口右上角小锁 + ショートカット:ウィンドウの右上にある小さなロック + + + + 关闭主窗口时 + メインウィンドウを閉じるとき + + + + 最小化到系统托盘 + システムトレイに最小化する + + + + 退出应用 + アプリを終了する + + + + 隐藏托盘图标 + トレイアイコンを非表示 + + + + 若要弹出位于后台的软件窗口,请在文件管理器中重复启动软件。 +若要彻底退出软件,请从任务管理器中结束进程。 + バックグラウンドで動作しているソフトウェアのウィンドウを表示するには、ファイルマネージャーでソフトウェアを再度起動してください。 +ソフトウェアを完全に終了するには、タスクマネージャーからプロセスを終了してください。 + + + + 截图 + スクリーンショット + + + + 截图前隐藏主窗口 + スクリーンショット前にメインウィンドウを非表示にする + + + + 截图前,如果主窗口处于前台,则隐藏主窗口 +将会延时等待主窗口关闭 + スクリーンショット前にメインウィンドウが前面にある場合、非表示にします。 +メインウィンドウの閉じるまで待機します + + + + 隐藏等待时间 + 非表示待機時間 + + + + 秒 + + + + + 服务 + サービス + + + + 允许HTTP服务 + HTTP サービスを許可する + + + + Umi-OCR依赖HTTP接口进行本机跨进程通信。如果禁用,将无法使用命令行模式、多开检测等功能。 + Umi-OCR は、ローカルのプロセス間通信に HTTP インターフェースを使用しています。無効にすると、コマンドラインモードやマルチインスタンス検出などの機能を使用できません。 + + + + + + 重启软件后生效 + ソフトウェアを再起動して有効にする + + + + 主机 + ホスト + + + + 仅本地 + ローカルのみ + + + + 任何可用地址 + 利用可能なすべてのアドレス + + + + 将允许局域网访问。请开启对应防火墙权限! + ローカルエリアネットワークからのアクセスを許可します。対応するファイアウォールの許可を確認してください! + + + + 将禁止局域网访问。 + ローカルエリアネットワークからのアクセスを禁止します。 + + + + 端口 + ポート + + + + 日志 + ログ + + + + 操作 + 操作 + + + + 打开日志保存目录 + ログ保存ディレクトリを開く + + + + 保存的日志级别 + 保存対象ログレベル + + + + 已禁用日志文件保存 + ログファイル保存は無効です + + + + 请尝试更换软件路径! + ソフトウェアパスを交換してみてください! + + + + 配置文件读写异常 + プロファイル読み書き例外 + + + + 插件加载失败 + プラグインの読み込みに失敗しました + + + + 成功添加快捷方式 + ショートカットが正常に追加されました + + + + 添加快捷方式失败 + ショートカットの追加に失敗しました + + + + 成功移除 %1 个快捷方式 + %1 個のショートカットが正常に削除されました + + + + 提示 + ヒント + + + + 没有找到可移除的快捷方式。 + ショートカットが見つかりませんでした。 + + + + 渲染器变更 将在重启软件后生效 + レンダラーが変更されました。ソフトウェアの再起動後に有効になります + + + + 端口号不合法 + ポート番号が無効です + + + + 端口号改为%1 + ポート番号が%1に変更されました + + + + 原端口号%1被占用, +切换为新端口号%2。 + +若不想看到此通知,请在全局设置关闭高级模式。 + 元のポート番号%1が使用中です。 +新しいポート番号%2に切り替えてください。 + +この通知を表示したくない場合は、グローバル設定で高度モードをオフにしてください。 + + + + GlobalConfigsPage + + + 1. 关于快捷键、OCR语言等选项,请在各个功能页中进行设置。 +2. 勾选左下角的“高级”按钮,可以显示更多进阶选项。 + 1.ショートカットキー、OCR言語などのオプションについては、各機能ページで設定してください。 +2.左下の「高度」ボタンをチェックすると、より高度なオプションが表示されます。 + + + + 小贴士 + コツ(ヒント) + + + + HTabBar + + + 窗口置顶 + ウィンドウを最前面に固定する + + + + 锁定标签栏 + タブバーをロックする + + + + IconButtonBar + + + 右键菜单 + 右クリックメニュー + + + + 保存图片 + 画像を保存 + + + + 图片大小:适应窗口 + 画像サイズ:ウィンドウに合わせる + + + + 图片大小:实际 + 画像サイズ:実際のサイズ + + + + 截图 + スクリーンショット + + + + 粘贴 + 貼り付け + + + + 屏幕截图 + スクリーンショット + + + + 粘贴图片 + 画像を貼り付け + + + + IgnoreArea + + + 拖入本地图片:OCR预览 +滚轮:缩放 +左键:拖拽 +右键:绘制忽略区域 + +可绘制一个或多个忽略区域矩形框。在执行批量OCR时,完全位于忽略区域内的文本块将被排除。 +比如批量处理影视截图时,可在右上角水印处添加忽略区域,避免输出水印文本。 + ローカル画像をドラッグ:OCR プレビュー +スクロール:ズーム +左クリック:ドラッグ +右クリック:無視領域の描画 + +無視領域の矩形を描画できます。バッチ OCR 実行時、完全に無視領域内にあるテキストブロックは除外されます。 +例えば、映画のスクリーンショットをバッチ処理する場合、右上のウォーターマークに無視領域を追加して、出力されるウォーターマークテキストを回避します。 + + + + 图像尺寸: + 画像サイズ: + + + + 区域数量: + 領域の数: + + + + 撤销 + 元に戻す + + + + 清空 + クリア + + + + ImageScale + + + 复制图片 + 画像をコピー + + + + 复制图片失败 + 画像のコピーに失敗しました + + + + 打开图片 + 画像を開く + + + + 打开图片失败 + 画像の開きに失敗しました + + + + + 保存图片 + 画像を保存 + + + + 保存图片失败 + 画像の保存に失敗しました + + + + ImageWithText + + + 图片:复制%1字 + 画像:コピー%1字 + + + + 图片:无选中文字 + 画像:選択されているテキストなし + + + + 图片:复制全部%1字 + 画像:すべての%1文字をコピー + + + + 复制  (Ctrl+C) + コピー   (Ctrl+C) + + + + 全选  (Ctrl+A) + すべて選択 (Ctrl+A) + + + + 复制图片(Ctrl+X) + 画像のコピー(Ctrl+X) + + + + 保存图片(Ctrl+S) + 画像を保存 + + + + 显示/隐藏文字(Tab) + 表示/非表示(Tab) + + + + 用默认应用打开图片 + デフォルトのアプリで画像を開く + + + + 删除图片(Ctrl+D) + 画像を削除(Ctrl+D) + + + + MainWindowManager + + + 欢迎使用 Umi-OCR + Umi-OCR をご利用いただきありがとうございます + + + + 已启用后台模式,可通过快捷键使用功能。 + バックグラウンドモードが有効になっており、ショートカットキーを使用して機能を利用できます。 + + + + MarkdownView + + + 打开网页 + ウェブページを開く + + + + 链接 + リンク(URL) + + + + MessageBox + + + 取消 + キャンセル + + + + 确定 + 確認 + + + + 警告 + 警告 + + + + 发生了一点小问题 + 少し問題が発生しました + + + + 复制 + コピー + + + + 已复制报错信息 %1 + エラーメッセージがコピーされました %1 + + + + 请前往 Issues 页面寻找解答或反馈 + Issuesページに答えやフィードバックを探しに行ってください + + + + 问题反馈 + フィードバック + + + + MessageBoxWin + + + 确定 + 確認 + + + + 不再提示 + 再表示せず + + + + 知道了 + わかりました + + + + MissionCtrlPanel + + + 开始任务 + 開始する + + + + 暂停任务 +暂停后可以待机或休眠。 +但是关机或退出软件,将会丢弃任务内容。 + タスクを一時停止 +一時停止後、待機またはスリープ状態にできます。 +ただし、シャットダウンやソフトウェアの終了を行うと、タスク内容は失われます。 + + + + 继续任务 + タスクを再開 + + + + 终止任务 +放弃未完成的内容。 + タスクを中止 +未完了の内容を放棄します。 + + + + 正在运行 + 実行中 + + + + 正在保存 + 保存中 + + + + 已暂停 + 一時停止中 + + + + 任务停止 + タスク停止 + + + + 任务完成 + タスク完了 + + + + ModalLayer + + + 保存并返回 + 保存して戻る + + + + 返回 + 戻る + + + + Navigation + + + 欢迎使用 Umi-OCR + Umi-OCR をご利用いただきありがとうございます + + + + 请选择功能页 + 機能ページを選択してください + + + + 当前版本 + 現在のバージョン + + + + 项目链接 + リンク + + + + 官方网站 + ホームページ + + + + 插件拓展 + プラグイン + + + + 问题反馈 + フィードバック + + + + 功能页 + 機能ページ + + + + 请打开对应标签页(如批量OCR、批量文档),再拖入文件。 + 対応するタブ(例:バッチOCR、バッチドキュメント)を開いてから、ファイルをドラッグしてください。 + + + + OcrManager + + + 文字识别 + テキスト認識 + + + + 操作 + 操作 + + + + + 强制终止任务 + タスクを強制終了する + + + + 应用修改 + アプリケーションの変更 + + + + 当前接口 + 現在のインターフェース + + + + 没有可用的 OCR 插件。 + 利用可能なOCRプラグインがありません。 + + + + OCR API 列表中不存在%1 + OCR API リストに%1が存在しません + + + + 当前已有%1组任务队列、共%2个任务正在执行。您可【强制终止任务】后修改API。 + 現在、%1 グループのタスクキューがあり、%2 個のタスクが実行中です。API を変更する前に【タスクを強制終了する】ことができます。 + + + + 无法修改 文字识别接口设置 + テキスト認識の設定を変更できません + + + + 文字识别接口应用成功 + テキスト認識の設定を適用しました + + + + 当前API为【%1】 + 現在の API は【%1】です + + + + 文字识别接口应用失败 + テキスト認識の設定を適用できませんでした + + + + 当前没有运行中的任务 + 現在実行中のタスクはありません + + + + 当前已有%1组任务队列、共%2个任务正在执行。 +要强制终止全部任务吗? + 現在、%1 グループのタスクキューがあり、%2 個のタスクが実行中です。 +すべてのタスクを強制終了しますか? + + + + PagesManager + + + 新标签页 + 新しいタブ + + + + 截图OCR + スクリーンショット OCR + + + + # 截图OCR + +屏幕截图,快捷转文字。也支持粘贴图片。 + # スクリーンショット OCR + +スクリーンショットを撮って簡単にテキストに変換します。画像の貼り付けもサポートしています。 + + + + # 全局设置 + +调节全局设置项,对所有页面生效。 + # グローバル設定 + +すべてのページに影響するグローバル設定項目の調整。 + + + + 批量OCR + バッチ OCR + + + + # 批量OCR + +导入本地图片或文件夹,批量转换文字。 + +支持格式: + # バッチ OCR + +ローカルの画像またはフォルダをインポートして、一括でテキストに変換します。 + +対応フォーマット: + + + + 批量文档 + バルクドキュメント認識 + + + + # 批量文档识别 + +批量导入文档,提取文字,或生成双层可搜索PDF。 + +支持格式: + # バルクドキュメント認識 + +ドキュメントを一括でインポートし、テキストを抽出したり、二層の検索可能なPDFを生成します。 + +対応フォーマット: + + + + 二维码 + QR コード + + + + # 二维码 + +识别或生成二维码、条形码。 + +支持协议: + # QR コード + +QR コードやバーコードを識別、生成する。 + +サポートされているプロトコル: + + + + 全局设置 + グローバル設定 + + + + 关于 + について + + + + # 关于 + # 關於 + + + + PopupManager + + + 确认 + 確認 + + + + 取消 + キャンセル + + + + PreviewDoc + + + 加密 + 暗号化 + + + + 打开文档失败 + 文書の開きに失敗しました + + + + 请填写正确的密码 + 正しいパスワードを入力してください + + + + 密码正确 + パスワードが正しい + + + + 文档预览异常 + 文書のプレビューが異常です + + + + 忽略区域说明: +右键拖拽,绘制矩形区域,包含在区域内的文字框将被忽略。可用于排除水印、页眉页脚。 +范围允许填写负数,表示倒数第x页。如-1表示最后一页,-2表示倒数第2页。 +忽略区域的设置对所有文档生效。 + 「無視領域」の説明: +右クリックしてドラッグすると、長方形の領域が描画されます。領域内に含まれるテキスト ボックスは無視されます。 透かし、ヘッダー、フッターを除外するために使用できます。 +この範囲では負の数値を入力でき、下から x 番目のページを示します。 たとえば、-1 は最後のページを表し、-2 は最後から 2 番目のページを表します。 +無視ゾーンの設定はすべてのドキュメントに有効になります。 + + + + 密码: + パスワード: + + + + 预览页面 + プレビューページ + + + + 预览PDF时,是否预览OCR结果 + PDFをプレビューする際、OCR結果をプレビューしますか + + + + OCR页数 + OCRページ数 + + + + 忽略区域页数(全局) + 無視領域ページ数(グローバル) + + + + + 范围 + 範囲 + + + + 撤销 + 元に戻す + + + + 清空 + クリア + + + + PreviewImage + + + 文字 + テキスト + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + 認識テキストを画像にオーバーレイ表示します +グローバル設定でデフォルトのオフに設定できます + + + + QRCode + + + 获取剪贴板异常 + クリップボードの取得に失敗しました + + + + 剪贴板中为文本 + クリップボードの内容はテキストです + + + + 无有效图片 + 有効な画像がありません + + + + 导入%1条图片路径 + %1 個の画像パスをインポートしました + + + + 参数有误,或输入内容不合规定。请参照报错指示修改: + パラメータに誤りがあるか、入力内容が規定に合わない。エラー通知修正を参照してください: + + + + + 生成二维码失败 + QRコードの生成に失敗しました + + + + 已复制到剪贴板 + クリップボードにコピーしました + + + + 识图完成 + OCR完了 + + + + 无文字 + テキストなし + + + + 识别失败 + 認識失敗 + + + + 扫描二维码 + QRコードをスキャン + + + + 截图、拖入或粘贴二维码图片 + スクリーンショット、ドラッグ&ドロップ、またはQRコード画像を貼り付け + + + + + 设置 + 設定 + + + + 自动刷新 + 自動リフレッシュ + + + + 修改文字后,自动生成二维码/条形码 + テキストを変更すると、自動的にQRコード/バーコードが生成されます + + + + 刷新 + リフレッシュ + + + + 生成二维码/条形码 + QRコード/バーコードの生成 + + + + 记录 + 記録 + + + + 生成 + 生成 + + + + QRCodeConfigs + + + 快捷键 + ショートカットキー + + + + 屏幕截图 + スクリーンショット + + + + 粘贴图片 + 画像を貼り付け + + + + 预处理(一般无需改动) + 前処理(通常は変更の必要なし) + + + + 中值滤波 + メディアンフィルタ + + + + 对图像进行平滑处理 +>0 时生效。可填1~9的奇数(不允许偶数) + 画像をスムーズに処理します +>0 の場合に有効です。1〜9 の奇数(偶数は不可)を入力できます + + + + 锐度增强 + シャープネス強化 + + + + 增加图像的锐度 +>0.1 时生效。可填0.1~10的小数 + 画像のシャープネスを増強します +>0.1 の場合に有効です。0.1〜10 の小数を入力できます + + + + 对比度增强 + コントラスト強化 + + + + 增加图像的对比度 +>0.1 时生效。可填0.1~10的小数 + 画像のコントラストを増強します +>0.1 の場合に有効です。0.1〜10 の小数を入力できます + + + + 转为灰度 + グレースケールに変換 + + + + 将图像像素转为灰度 + 画像のピクセルをグレースケールに変換します + + + + 二值化 + 二値化 + + + + 将图像像素转为纯黑和纯白 +启用了灰度,且二值化 >-1 时生效。可填0~255的整数 + 画像のピクセルを純黒と純白に変換します +グレースケールが有効で、二値化 >-1 の場合に有効です。0〜255 の整数を入力できます + + + + 扫码后的操作 + スキャン後の操作 + + + + 复制结果 + 結果をコピー + + + + 弹出主窗口 + メインウィンドウを表示 + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + 画像認識後、メインウィンドウが最小化またはバックグラウンドにある場合、前面に表示します + + + + 生成二维码/条形码 + QRコード/バーコードの生成 + + + + 类型 + タイプ + + + + 默认二维码: + デフォルト QRコード: + + + + 宽度 + Width + + + + + 填0:自动选择 + 0を入力:自動選択 + + + + + + 像素 + ピクセル + + + + 高度 + Height + + + + 边缘空白 + 端の空白 + + + + 填-1:自动选择 + -1を入力:自動選択 + + + + 纠错等级 + 誤り訂正レベル + + + + 仅适用于: + 適用されます: + + + + 自动 + 自動 + + + + 其它 + その他 + + + + ResultTextContainer + + + 复制 + コピー + + + + ResultsTableView + + + 异常状态码:%1 +异常信息:%2 + 異常なステータスコード:%1 +異常な情報:%2 + + + + 耗时 %1 + 所要時間 %1 + + + + 置信度 %1 + 信頼度 %1 + + + + 记录:复制%1字 + 記録:%1 文字をコピー + + + + 记录:无选中文字 + 記録:選択されたテキストなし + + + + 记录:复制全部%1字 + 記録:全ての%1 文字をコピー + + + + 删除%1条记录 + %1 の記録を削除 + + + + 清空记录 + 記録をクリア + + + + 复制    (Ctrl+C) + コピー    (Ctrl+C) + + + + 复制全部  (Ctrl+C 双击) + 全てをコピー (Ctrl+C ダブルクリック) + + + + 选中单个  (Ctrl+A) + 単一選択  (Ctrl+A) + + + + 选中全部记录(Ctrl+A 双击) + 全て選択  (Ctrl+A ダブルクリック) + + + + 删除选中记录 + 選択された記録を削除 + + + + 清空全部记录(Ctrl+D 双击) + 全ての記録をクリア(Ctrl+D ダブルクリック) + + + + 滚动 + スクロール + + + + 自动滚动到底部 + 自動的に最下部までスクロール + + + + 右键菜单 + 右クリックメニュー + + + + ScreenshotManager + + + 截图失败 + スクリーンショット失敗 + + + + 上次截图操作未结束,不能进行新的截图! + 前回のスクリーンショット操作が終了していないため、新しいスクリーンショットを実行できません! + + + + 尚未记录截图区域 + まだスクショ区域が記録されていません + + + + 显示器: %1 +错误信息: %2 + モニタ: %1 +エラーメッセージ: %2 + + + + 重复截图失败 + スクショの再撮影に失敗しました + + + + 未找到匹配的屏幕 + 一致するスクリーンが見つかりませんでした + + + + 屏幕设备名称不相同: +%1 +%2 + スクリーンデバイス名が異なります: +%1 +%2 + + + + ScreenshotOCR + + + 获取剪贴板异常 + クリップボードの取得に失敗しました + + + + 剪贴板中为文本 + クリップボードにテキストがあります + + + + 无有效图片 + 有効な画像がありません + + + + 导入%1条图片路径 + %1 個の画像パスをインポートしました + + + + 依然关闭 + 閉じる + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + タスクが進行中です。 +タスクを終了してページを閉じますか? + + + + + 屏幕截图 + スクリーンショット + + + + + 粘贴图片 + 画像を貼り付け + + + + 截图识别任务异常 + 画像認識タスクの例外 + + + + 已复制到剪贴板 + クリップボードにコピーしました + + + + 识图完成 + 画像認識が完了しました + + + + 无文字 + テキストなし + + + + 识别失败 + 認識失敗 + + + + + 停止任务 + タスクを停止 + + + + 文字 + テキスト + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + 認識テキストを画像にオーバーレイ表示します +グローバル設定でデフォルトのオフに設定できます + + + + 截图、拖入或粘贴图片 + スクリーンショット、ドラッグ&ドロップ、または画像を貼り付け + + + + 设置 + 設定 + + + + 记录 + 記録 + + + + ScreenshotOcrConfigs + + + OCR文本后处理 + OCRテキストの後処理 + + + + 快捷键 + ショートカットキー + + + + 屏幕截图 + スクリーンショット + + + + 粘贴图片 + 画像を貼り付け + + + + 重复截图 + スクショの再撮影 + + + + 重新截取上一次截图的范围 + 前回のスクショの範囲を再截取します + + + + 识图后的操作 + スキャン後の操作 + + + + 复制结果 + 結果をコピー + + + + 弹出主窗口 + メインウィンドウを表示 + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + 画像認識後、メインウィンドウが最小化またはバックグラウンドにある場合、前面に表示します + + + + 其它 + その他 + + + + Size_ + + + 1.0 + 1.0 + + + + SystemTray + + + 打开主窗口 + メインウィンドウを開く + + + + 退出 Umi-OCR + Umi-OCRを終了 + + + + Theme + + + 珍珠白 + 明色 + + + + 云墨黑 + 暗色 + + + + 赛博朋克 + サイバーパンク + + + + UtilsConfigDicts + + + 任务完成后的操作 + タスク完了後の操作 + + + + 系统 + システム + + + + 无 + なし + + + + 关机 + シャットダウン + + + + 休眠 + スリープ + + + + %1 系统暂不支持电源控制! + %1 システムは電源制御をサポートしていません! + + + + 继续%1 + 続行%1 + + + + 取消%1 + キャンセル%1 + + + + 系统即将%1 + システムが%1する予定です + + + + 排版解析方案 + レイアウト解析ソリューション + + + + 按什么方式,解析和排序图片中的文字块 + どのように画像内のテキストブロックを解析および並べ替えますか + + + + 多栏-按自然段换行 + 複数カラム-自然な段落で改行 + + + + 多栏-总是换行 + 複数カラム-常に改行 + + + + 多栏-无换行 + 複数カラム-改行なし + + + + 单栏-按自然段换行 + 単一カラム-自然な段落で改行 + + + + 单栏-总是换行 + 単一カラム-常に改行 + + + + 单栏-无换行 + 単一カラム-改行なし + + + + 单栏-保留缩进 + 単一カラム-インデントを保持 + + + + 不做处理 + 処理なし + + + + 优先内部 + 内部を優先 + + + + 只允许内部 + 内部のみ許可 + + + + 只允许外部 + 外部のみ許可 + + + + 禁用所有通知 + すべての通知を無効にする + + + + 跟随全局设定 + グローバル設定に従う + + + + 通知弹窗类型 + 通知のポップアップタイプ + + + + 通知类型已更改 + 通知タイプが変更されました + + + diff --git a/dev-tools/i18n/release/kab.ts b/dev-tools/i18n/release/kab.ts new file mode 100644 index 0000000..5409de1 --- /dev/null +++ b/dev-tools/i18n/release/kab.ts @@ -0,0 +1,2482 @@ + + + + + About + + + 开源、免费的离线OCR软件 + + + + + 当前版本 + + + + + 项目链接 + + + + + 官方网站 + + + + + 插件拓展 + + + + + 问题反馈 + + + + + 发布地址 + + + + + 许可协议 + + + + + 作者 + Ameskar + + + + 译者 + Imsuqal + + + + 运行环境信息(如需请求协助,请提供给开发者) + + + + + 复制 + Nɣel + + + + AsynFilesLoader + + + 正在载入 %1 个文件: +%2 + + + + + BatchDOC + + + 加密 + Awgelhen + + + + %1个加密文档 + + + + + 请点击文件名填写密码 + + + + + 文档已加密 + + + + + 【%1】 +请点击文档名,设置密码 + + + + + 失败 + Tuccḍa + + + + 排队 + + + + + 依然关闭 + + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + + + + + + 文档 + Isemliyen + + + + 状态 + Addad + + + + 打开文档 + + + + + 拖入文档或文件夹 + + + + + 清空 + Sfeḍ + + + + 文档识别异常 + + + + + 批量识别完成 + + + + + 页数 + Igburen + + + + 请选择文档 + + + + + 设置 + Iɣewwaren + + + + 记录 + Amazray + + + + BatchDOCConfigs + + + OCR文本后处理 + + + + + 文档处理 + + + + + 内容提取模式 + + + + + 若一页文档既存在图片又存在文本,如何进行处理 + + + + + 混合OCR/原文本 + + + + + 整页强制OCR + + + + + 仅OCR图片 + + + + + 仅拷贝原有文本 + + + + + 批量任务 + + + + + 保存到 + + + + + 文档原目录 + + + + + + 指定目录 + + + + + 必须先指定“保存到指定目录”才生效 + + + + + OCR结果保存目录 + + + + + 文件名格式 + + + + + 日期时间格式 + + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + + + + + 保存文件类型 + + + + + layered.pdf 双层可搜索文档 + + + + + text.pdf 单层纯文本文档 + + + + + 创建空白PDF文档,只写入识别文字,不含图片 + + + + + txt 标准格式 + + + + + 含识别文字和页数信息 + + + + + p.txt 纯文字格式 + + + + + 输出所有识别文字 + + + + + csv 表格文件(Excel) + + + + + 将页数信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + + + + + jsonl 原始信息 + + + + + 每行为一条json数据,便于第三方程序读取操作 + + + + + 忽略空白页 + + + + + 若某一页没有文字或识别失败,也不会输出错误提示信息 + + + + + 递归读取子文件夹 + + + + + 点击表格,可设置更多内容 + + + + + 导入文件夹时,导入子文件夹中全部文档 + + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文档名 +%range 识别页数范围。只有识别页数小于总页数时才会显示。 +举例:[OCR]_%name%range_%date +生成:[OCR]_文档A(p2-10)_20230901_1213.txt +添加占位符可以避免旧文件被新文件覆盖。 + + + + + 保留原有图片,叠加一层透明文字,可以搜索和复制 + + + + + 其它 + Wiyyaḍ + + + + BatchOCR + + + 依然关闭 + + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + + + + + 排队 + + + + + 处理 + + + + + %1 张图片识别失败! + + + + + 批量识别完成 + + + + + 批量识别任务异常 + + + + + + 图片 + Tugna + + + + 耗时 + + + + + 状态 + Addad + + + + 打开图片 + + + + + 拖入图片或文件夹 + + + + + 清空 + Sfeḍ + + + + 请选择图片 + + + + + 设置 + Iɣewwaren + + + + 记录 + Amazray + + + + BatchOCRConfigs + + + OCR文本后处理 + + + + + 忽略区域 + + + + + 进入设置 + Ldi iɣewwaren + + + + 批量任务 + + + + + 保存到 + + + + + 图片原目录 + + + + + + 指定目录 + + + + + 必须先指定“保存到指定目录”才生效 + + + + + OCR结果保存目录 + + + + + 文件名格式 + + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文件夹名/文件名 +举例:[OCR]_%name_%date +生成:[OCR]_我的图片_2023-09-01_12-13.txt +添加占位符可以避免旧文件被新文件覆盖。 + + + + + 日期时间格式 + + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + + + + + 保存文件类型 + + + + + txt 标准格式 + + + + + 含原图片文件名和识别文字 + + + + + p.txt 纯文字格式 + + + + + 仅输出识别文字,不含图片标题 + + + + + txt 单独文件 + + + + + 对每张图片,生成同名txt文件,仅输出识别文字 + + + + + md 图文混排 + + + + + Markdown图文混排格式,可用Markdown阅读器浏览文件 + + + + + csv 表格文件(Excel) + + + + + 将图片信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + + + + + jsonl 原始信息 + + + + + 每行为一条json数据,便于第三方程序读取操作 + + + + + 输出忽略空白图片 + + + + + 若图片没有文字或识别失败,也不会输出错误提示信息 + + + + + 递归读取子文件夹 + + + + + 导入文件夹时,导入子文件夹中全部图片 + + + + + 其它 + Wiyyaḍ + + + + Configs + + + %1 处理配置项异常: +%2枚举列表为空。 + + + + + 重置 + + + + + 重置本页上的设定 + + + + + 重置设定 + + + + + 要重置本页的设定吗? + + + + + 高级 + Leqqayen + + + + 显示更多高级选项。它们标有 * 号。 +请谨慎修改高级选项。 + + + + + 展开 + + + + + 折叠 + + + + + 必须为整数 + + + + + 不能超过 + + + + + 不能低于 + + + + + 必须为数字 + + + + + 已取消%1的快捷键。 + + + + + 更新热键成功 + + + + + %1的快捷键为 %2 + + + + + %1 快捷键%2已被注册,请尝试另外的按键组合。 + + + + + %1 快捷键%2无法注册,请尝试另外的按键组合。 + + + + + 请按下快捷键组合。按【Esc】退出。 + + + + + 当前快捷键录制已在进行,不能同时录制! + + + + + 无法录制快捷键 + + + + + DropArea_ + + + 松手放入文件 + + + + + FontPanel + + + 界面 + Agrudem + + + + 内容 + Agbur + + + + 界面字体: +软件中大部分UI的字体。 + + + + + 内容字体: +识别结果内容的字体。 + + + + + GlobalConfigs + + + 快捷方式 + + + + + 桌面 + Tanarit + + + + 开始菜单 + + + + + 开机自启 + + + + + 界面和外观 + + + + + 主题 + Asentel + + + + 切换主题 + + + + + 字体 + Tasefsit + + + + 修改字体 + + + + + 界面与文字大小 + + + + + 渲染器 + + + + + 关闭硬件加速 + + + + + 若出现界面闪烁、元素错位等界面异常,尝试切换渲染器或者关闭硬件加速 + + + + + 禁用美化效果 + + + + + 在低配置机器上,禁用动画、阴影等效果可减少部分资源占用 + + + + + 图片预览默认显示叠加层 + + + + + 默认开启/关闭叠加层显示 +对所有图片预览组件生效 + + + + + 窗口 + Asfaylu + + + + 启动时缩小到任务栏 + + + + + 软件启动时,不弹出主窗口 + + + + + 窗口置顶 + + + + + 捷径:窗口左上角图钉 + + + + + 锁定标签栏 + + + + + 捷径:窗口右上角小锁 + + + + + 关闭主窗口时 + + + + + 最小化到系统托盘 + + + + + 退出应用 + + + + + 隐藏托盘图标 + + + + + 若要弹出位于后台的软件窗口,请在文件管理器中重复启动软件。 +若要彻底退出软件,请从任务管理器中结束进程。 + + + + + 截图 + Tuṭṭfa n ugdil + + + + 截图前隐藏主窗口 + + + + + 截图前,如果主窗口处于前台,则隐藏主窗口 +将会延时等待主窗口关闭 + + + + + 隐藏等待时间 + + + + + 秒 + + + + + 服务 + Amezlu + + + + 允许HTTP服务 + + + + + Umi-OCR依赖HTTP接口进行本机跨进程通信。如果禁用,将无法使用命令行模式、多开检测等功能。 + + + + + + + 重启软件后生效 + + + + + 主机 + + + + + 仅本地 + + + + + 任何可用地址 + + + + + 将允许局域网访问。请开启对应防火墙权限! + + + + + 将禁止局域网访问。 + + + + + 端口 + Tawwurt + + + + 日志 + Aɣmis + + + + 操作 + Tigawt + + + + 打开日志保存目录 + + + + + 保存的日志级别 + + + + + 已禁用日志文件保存 + + + + + 请尝试更换软件路径! + + + + + 配置文件读写异常 + + + + + 插件加载失败 + + + + + 成功添加快捷方式 + + + + + 添加快捷方式失败 + + + + + 成功移除 %1 个快捷方式 + + + + + 提示 + + + + + 没有找到可移除的快捷方式。 + + + + + 渲染器变更 将在重启软件后生效 + + + + + 端口号不合法 + + + + + 端口号改为%1 + + + + + 原端口号%1被占用, +切换为新端口号%2。 + +若不想看到此通知,请在全局设置关闭高级模式。 + + + + + GlobalConfigsPage + + + 1. 关于快捷键、OCR语言等选项,请在各个功能页中进行设置。 +2. 勾选左下角的“高级”按钮,可以显示更多进阶选项。 + + + + + 小贴士 + + + + + HTabBar + + + 窗口置顶 + + + + + 锁定标签栏 + + + + + IconButtonBar + + + 右键菜单 + + + + + 保存图片 + + + + + 图片大小:适应窗口 + + + + + 图片大小:实际 + + + + + 截图 + Tuṭṭfa n ugdil + + + + 粘贴 + Senṭeḍ + + + + 屏幕截图 + + + + + 粘贴图片 + + + + + IgnoreArea + + + 拖入本地图片:OCR预览 +滚轮:缩放 +左键:拖拽 +右键:绘制忽略区域 + +可绘制一个或多个忽略区域矩形框。在执行批量OCR时,完全位于忽略区域内的文本块将被排除。 +比如批量处理影视截图时,可在右上角水印处添加忽略区域,避免输出水印文本。 + + + + + 图像尺寸: + + + + + 区域数量: + + + + + 撤销 + + + + + 清空 + Sfeḍ + + + + ImageScale + + + 复制图片 + + + + + 复制图片失败 + + + + + 打开图片 + + + + + 打开图片失败 + + + + + + 保存图片 + + + + + 保存图片失败 + + + + + ImageWithText + + + 图片:复制%1字 + + + + + 图片:无选中文字 + + + + + 图片:复制全部%1字 + + + + + 复制  (Ctrl+C) + + + + + 全选  (Ctrl+A) + + + + + 复制图片(Ctrl+X) + + + + + 保存图片(Ctrl+S) + + + + + 显示/隐藏文字(Tab) + + + + + 用默认应用打开图片 + + + + + 删除图片(Ctrl+D) + + + + + MainWindowManager + + + 欢迎使用 Umi-OCR + + + + + 已启用后台模式,可通过快捷键使用功能。 + + + + + MarkdownView + + + 打开网页 + + + + + 链接 + URL + + + + MessageBox + + + 取消 + Semmet + + + + 确定 + Ih + + + + 警告 + + + + + 发生了一点小问题 + + + + + 复制 + Nɣel + + + + 已复制报错信息 %1 + + + + + 请前往 Issues 页面寻找解答或反馈 + + + + + 问题反馈 + + + + + MessageBoxWin + + + 确定 + Ih + + + + 不再提示 + + + + + 知道了 + + + + + MissionCtrlPanel + + + 开始任务 + + + + + 暂停任务 +暂停后可以待机或休眠。 +但是关机或退出软件,将会丢弃任务内容。 + + + + + 继续任务 + + + + + 终止任务 +放弃未完成的内容。 + + + + + 正在运行 + + + + + 正在保存 + + + + + 已暂停 + + + + + 任务停止 + + + + + 任务完成 + + + + + ModalLayer + + + 保存并返回 + + + + + 返回 + Uɣal + + + + Navigation + + + 欢迎使用 Umi-OCR + + + + + 请选择功能页 + + + + + 当前版本 + + + + + 项目链接 + + + + + 官方网站 + + + + + 插件拓展 + + + + + 问题反馈 + + + + + 功能页 + + + + + 请打开对应标签页(如批量OCR、批量文档),再拖入文件。 + + + + + OcrManager + + + 文字识别 + + + + + 操作 + Tigawt + + + + + 强制终止任务 + + + + + 应用修改 + + + + + 当前接口 + + + + + 没有可用的 OCR 插件。 + + + + + OCR API 列表中不存在%1 + + + + + 当前已有%1组任务队列、共%2个任务正在执行。您可【强制终止任务】后修改API。 + + + + + 无法修改 文字识别接口设置 + + + + + 文字识别接口应用成功 + + + + + 当前API为【%1】 + + + + + 文字识别接口应用失败 + + + + + 当前没有运行中的任务 + + + + + 当前已有%1组任务队列、共%2个任务正在执行。 +要强制终止全部任务吗? + + + + + PagesManager + + + 新标签页 + + + + + 截图OCR + + + + + # 截图OCR + +屏幕截图,快捷转文字。也支持粘贴图片。 + + + + + # 全局设置 + +调节全局设置项,对所有页面生效。 + + + + + 批量OCR + + + + + # 批量OCR + +导入本地图片或文件夹,批量转换文字。 + +支持格式: + + + + + 批量文档 + + + + + # 批量文档识别 + +批量导入文档,提取文字,或生成双层可搜索PDF。 + +支持格式: + + + + + 二维码 + + + + + # 二维码 + +识别或生成二维码、条形码。 + +支持协议: + + + + + 全局设置 + Iɣewwaren imuta + + + + 关于 + Ɣef + + + + # 关于 + + + + + PopupManager + + + 确认 + Sentem + + + + 取消 + Semmet + + + + PreviewDoc + + + 加密 + D awgelhan + + + + 打开文档失败 + + + + + 请填写正确的密码 + + + + + 密码正确 + + + + + 文档预览异常 + + + + + 忽略区域说明: +右键拖拽,绘制矩形区域,包含在区域内的文字框将被忽略。可用于排除水印、页眉页脚。 +范围允许填写负数,表示倒数第x页。如-1表示最后一页,-2表示倒数第2页。 +忽略区域的设置对所有文档生效。 + + + + + 密码: + + + + + 预览页面 + + + + + 预览PDF时,是否预览OCR结果 + + + + + OCR页数 + + + + + 忽略区域页数(全局) + + + + + + 范围 + + + + + 撤销 + + + + + 清空 + Sfeḍ + + + + PreviewImage + + + 文字 + Aḍris + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + + + + + QRCode + + + 获取剪贴板异常 + + + + + 剪贴板中为文本 + + + + + 无有效图片 + + + + + 导入%1条图片路径 + + + + + 参数有误,或输入内容不合规定。请参照报错指示修改: + + + + + + 生成二维码失败 + + + + + 已复制到剪贴板 + + + + + 识图完成 + + + + + 无文字 + + + + + 识别失败 + + + + + 扫描二维码 + + + + + 截图、拖入或粘贴二维码图片 + + + + + + 设置 + Iɣewwaren + + + + 自动刷新 + + + + + 修改文字后,自动生成二维码/条形码 + + + + + 刷新 + + + + + 生成二维码/条形码 + + + + + 记录 + Amazray + + + + 生成 + Sarew + + + + QRCodeConfigs + + + 快捷键 + + + + + 屏幕截图 + + + + + 粘贴图片 + + + + + 预处理(一般无需改动) + + + + + 中值滤波 + + + + + 对图像进行平滑处理 +>0 时生效。可填1~9的奇数(不允许偶数) + + + + + 锐度增强 + + + + + 增加图像的锐度 +>0.1 时生效。可填0.1~10的小数 + + + + + 对比度增强 + + + + + 增加图像的对比度 +>0.1 时生效。可填0.1~10的小数 + + + + + 转为灰度 + + + + + 将图像像素转为灰度 + + + + + 二值化 + + + + + 将图像像素转为纯黑和纯白 +启用了灰度,且二值化 >-1 时生效。可填0~255的整数 + + + + + 扫码后的操作 + + + + + 复制结果 + + + + + 弹出主窗口 + + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + + + + + 生成二维码/条形码 + + + + + 类型 + Amasal + + + + 默认二维码: + + + + + 宽度 + + + + + + 填0:自动选择 + + + + + + + 像素 + + + + + 高度 + + + + + 边缘空白 + + + + + 填-1:自动选择 + + + + + 纠错等级 + + + + + 仅适用于: + + + + + 自动 + Awurman + + + + 其它 + Wiyyaḍ + + + + ResultTextContainer + + + 复制 + Nɣel + + + + ResultsTableView + + + 异常状态码:%1 +异常信息:%2 + + + + + 耗时 %1 + + + + + 置信度 %1 + + + + + 记录:复制%1字 + + + + + 记录:无选中文字 + + + + + 记录:复制全部%1字 + + + + + 删除%1条记录 + + + + + 清空记录 + + + + + 复制    (Ctrl+C) + + + + + 复制全部  (Ctrl+C 双击) + + + + + 选中单个  (Ctrl+A) + + + + + 选中全部记录(Ctrl+A 双击) + + + + + 删除选中记录 + + + + + 清空全部记录(Ctrl+D 双击) + + + + + 滚动 + + + + + 自动滚动到底部 + + + + + 右键菜单 + + + + + ScreenshotManager + + + 截图失败 + + + + + 上次截图操作未结束,不能进行新的截图! + + + + + 尚未记录截图区域 + + + + + 显示器: %1 +错误信息: %2 + + + + + 重复截图失败 + + + + + 未找到匹配的屏幕 + + + + + 屏幕设备名称不相同: +%1 +%2 + + + + + ScreenshotOCR + + + 获取剪贴板异常 + + + + + 剪贴板中为文本 + + + + + 无有效图片 + + + + + 导入%1条图片路径 + + + + + 依然关闭 + + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + + + + + + 屏幕截图 + + + + + + 粘贴图片 + + + + + 截图识别任务异常 + + + + + 已复制到剪贴板 + + + + + 识图完成 + + + + + 无文字 + + + + + 识别失败 + + + + + + 停止任务 + + + + + 文字 + Aḍris + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + + + + + 截图、拖入或粘贴图片 + + + + + 设置 + Iɣewwaren + + + + 记录 + + + + + ScreenshotOcrConfigs + + + OCR文本后处理 + + + + + 快捷键 + + + + + 屏幕截图 + + + + + 粘贴图片 + + + + + 重复截图 + + + + + 重新截取上一次截图的范围 + + + + + 识图后的操作 + + + + + 复制结果 + + + + + 弹出主窗口 + + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + + + + + 其它 + + + + + Size_ + + + 1.0 + + + + + SystemTray + + + 打开主窗口 + + + + + 退出 Umi-OCR + + + + + Theme + + + 珍珠白 + + + + + 云墨黑 + + + + + 赛博朋克 + + + + + UtilsConfigDicts + + + 任务完成后的操作 + + + + + 系统 + + + + + 无 + Ula yiwen + + + + 关机 + + + + + 休眠 + + + + + %1 系统暂不支持电源控制! + + + + + 继续%1 + + + + + 取消%1 + + + + + 系统即将%1 + + + + + 排版解析方案 + + + + + 按什么方式,解析和排序图片中的文字块 + + + + + 多栏-按自然段换行 + + + + + 多栏-总是换行 + + + + + 多栏-无换行 + + + + + 单栏-按自然段换行 + + + + + 单栏-总是换行 + + + + + 单栏-无换行 + + + + + 单栏-保留缩进 + + + + + 不做处理 + + + + + 优先内部 + + + + + 只允许内部 + + + + + 只允许外部 + + + + + 禁用所有通知 + + + + + 跟随全局设定 + + + + + 通知弹窗类型 + + + + + 通知类型已更改 + + + + diff --git a/dev-tools/i18n/release/ko_KR.ts b/dev-tools/i18n/release/ko_KR.ts new file mode 100644 index 0000000..6e28137 --- /dev/null +++ b/dev-tools/i18n/release/ko_KR.ts @@ -0,0 +1,2464 @@ + + + + + About + + + 开源、免费的离线OCR软件 + 오픈소스이며 무료인 오프라인 OCR 소프트웨어 + + + + 当前版本 + 현재 버전 + + + + 项目链接 + 프로젝트 링크 + + + + 官方网站 + 공식 웹사이트 + + + + 插件拓展 + 플러그인 확장 + + + + 问题反馈 + 문제 피드백 + + + + 发布地址 + 발표 주소 + + + + 许可协议 + 라이선스 계약 + + + + 作者 + 작가 + + + + 译者 + 번역가 + + + + 运行环境信息(如需请求协助,请提供给开发者) + 실행 환경 정보(도움을 요청하려면 개발자에게 제공해 주세요) + + + + 复制 + 복사 + + + + BatchDOC + + + 加密 + + + + + %1个加密文档 + + + + + 请点击文件名填写密码 + 파일명을 클릭해서 패스워드를 입력해주세요 + + + + 文档已加密 + + + + + 【%1】 +请点击文档名,设置密码 + + + + + 失败 + + + + + 排队 + + + + + 依然关闭 + + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + + + + + + 文档 + + + + + 状态 + + + + + 打开文档 + + + + + 拖入文档或文件夹 + + + + + 清空 + + + + + 文件读取中… + + + + + 文档识别异常 + + + + + 批量识别完成 + + + + + 页数 + + + + + 请选择文档 + + + + + 设置 + + + + + 记录 + + + + + BatchDOCConfigs + + + OCR文本后处理 + + + + + 文档处理 + + + + + 内容提取模式 + + + + + 若一页文档既存在图片又存在文本,如何进行处理 + + + + + 混合OCR/原文本 + + + + + 整页强制OCR + + + + + 仅OCR图片 + + + + + 仅拷贝原有文本 + + + + + 批量任务 + + + + + 保存到 + + + + + 文档原目录 + + + + + + 指定目录 + + + + + 必须先指定“保存到指定目录”才生效 + + + + + OCR结果保存目录 + + + + + 文件名格式 + + + + + 日期时间格式 + + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + + + + + 保存文件类型 + + + + + layered.pdf 双层可搜索文档 + + + + + text.pdf 单层纯文本文档 + + + + + 创建空白PDF文档,只写入识别文字,不含图片 + + + + + txt 标准格式 + + + + + 含识别文字和页数信息 + + + + + p.txt 纯文字格式 + + + + + 输出所有识别文字 + + + + + csv 表格文件(Excel) + + + + + 将页数信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + + + + + jsonl 原始信息 + + + + + 每行为一条json数据,便于第三方程序读取操作 + + + + + 忽略空白页 + + + + + 若某一页没有文字或识别失败,也不会输出错误提示信息 + + + + + 递归读取子文件夹 + + + + + 点击表格,可设置更多内容 + + + + + 导入文件夹时,导入子文件夹中全部文档 + + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文档名 +%range 识别页数范围。只有识别页数小于总页数时才会显示。 +举例:[OCR]_%name%range_%date +生成:[OCR]_文档A(p2-10)_20230901_1213.txt +添加占位符可以避免旧文件被新文件覆盖。 + No need to fill in the extension name. The following placeholders are supported: +%date Date and time +%name Original folder name/file name +%range Page range. Only displayed when the OCR page count is less than the total page count. +Example: [OCR]_%name%range_%date +Generated: [OCR]_Document(p2-10)_2023-09-01_12-13.txt +Adding placeholders can prevent old files from being overwritten by new files. + + + + 保留原有图片,叠加一层透明文字,可以搜索和复制 + + + + + 其它 + + + + + BatchOCR + + + 依然关闭 + + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + + + + + 排队 + + + + + 处理 + + + + + %1 张图片识别失败! + + + + + 批量识别完成 + + + + + 批量识别任务异常 + + + + + + 图片 + + + + + 耗时 + + + + + 状态 + + + + + 打开图片 + + + + + 拖入图片或文件夹 + + + + + 清空 + + + + + 请选择图片 + + + + + 设置 + + + + + 记录 + + + + + BatchOCRConfigs + + + OCR文本后处理 + + + + + 忽略区域 + + + + + 进入设置 + + + + + 批量任务 + + + + + 保存到 + + + + + 图片原目录 + + + + + + 指定目录 + + + + + 必须先指定“保存到指定目录”才生效 + + + + + OCR结果保存目录 + + + + + 文件名格式 + + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文件夹名/文件名 +举例:[OCR]_%name_%date +生成:[OCR]_我的图片_2023-09-01_12-13.txt +添加占位符可以避免旧文件被新文件覆盖。 + + + + + 日期时间格式 + + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + + + + + 保存文件类型 + + + + + txt 标准格式 + + + + + 含原图片文件名和识别文字 + + + + + p.txt 纯文字格式 + + + + + 仅输出识别文字,不含图片标题 + + + + + txt 单独文件 + + + + + 对每张图片,生成同名txt文件,仅输出识别文字 + + + + + md 图文混排 + + + + + Markdown图文混排格式,可用Markdown阅读器浏览文件 + + + + + csv 表格文件(Excel) + + + + + 将图片信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + + + + + jsonl 原始信息 + + + + + 每行为一条json数据,便于第三方程序读取操作 + + + + + 输出忽略空白图片 + + + + + 若图片没有文字或识别失败,也不会输出错误提示信息 + + + + + 递归读取子文件夹 + + + + + 导入文件夹时,导入子文件夹中全部图片 + + + + + 其它 + + + + + Configs + + + %1 处理配置项异常: +%2枚举列表为空。 + + + + + 重置 + + + + + 重置本页上的设定 + + + + + 重置设定 + + + + + 要重置本页的设定吗? + + + + + 高级 + + + + + 显示更多高级选项。它们标有 * 号。 +请谨慎修改高级选项。 + + + + + 展开 + + + + + 折叠 + + + + + 必须为整数 + + + + + 不能超过 + + + + + 不能低于 + + + + + 必须为数字 + + + + + 已取消%1的快捷键。 + + + + + 更新热键成功 + + + + + %1的快捷键为 %2 + + + + + %1 快捷键%2已被注册,请尝试另外的按键组合。 + + + + + %1 快捷键%2无法注册,请尝试另外的按键组合。 + + + + + 请按下快捷键组合。按【Esc】退出。 + + + + + 当前快捷键录制已在进行,不能同时录制! + + + + + 无法录制快捷键 + + + + + DropArea_ + + + 松手放入文件 + + + + + FontPanel + + + 界面 + + + + + 内容 + + + + + 界面字体: +软件中大部分UI的字体。 + + + + + 内容字体: +识别结果内容的字体。 + + + + + GlobalConfigs + + + 快捷方式 + + + + + 桌面 + + + + + 开始菜单 + + + + + 开机自启 + + + + + 界面和外观 + + + + + 主题 + + + + + 切换主题 + + + + + 字体 + + + + + 修改字体 + + + + + 界面与文字大小 + + + + + 渲染器 + + + + + 关闭硬件加速 + + + + + 若出现界面闪烁、元素错位等界面异常,尝试切换渲染器或者关闭硬件加速 + + + + + 禁用美化效果 + + + + + 在低配置机器上,禁用动画、阴影等效果可减少部分资源占用 + + + + + 图片预览默认显示叠加层 + + + + + 默认开启/关闭叠加层显示 +对所有图片预览组件生效 + + + + + 窗口 + + + + + 启动时缩小到任务栏 + + + + + 软件启动时,不弹出主窗口 + + + + + 窗口置顶 + + + + + 捷径:窗口左上角图钉 + + + + + 锁定标签栏 + + + + + 捷径:窗口右上角小锁 + + + + + 关闭主窗口时 + + + + + 最小化到系统托盘 + + + + + 退出应用 + + + + + 隐藏托盘图标 + + + + + 若要弹出位于后台的软件窗口,请在文件管理器中重复启动软件。 +若要彻底退出软件,请从任务管理器中结束进程。 + + + + + 截图 + + + + + 截图前隐藏主窗口 + + + + + 截图前,如果主窗口处于前台,则隐藏主窗口 +将会延时等待主窗口关闭 + + + + + 隐藏等待时间 + + + + + 秒 + + + + + 服务 + + + + + 允许HTTP服务 + + + + + Umi-OCR依赖HTTP接口进行本机跨进程通信。如果禁用,将无法使用命令行模式、多开检测等功能。 + + + + + + + 重启软件后生效 + + + + + 主机 + + + + + 仅本地 + + + + + 任何可用地址 + + + + + 将允许局域网访问。请开启对应防火墙权限! + + + + + 将禁止局域网访问。 + + + + + 端口 + + + + + 请尝试更换软件路径! + + + + + 配置文件读写异常 + + + + + 插件加载失败 + + + + + 成功添加快捷方式 + + + + + 添加快捷方式失败 + + + + + 成功移除 %1 个快捷方式 + + + + + 提示 + + + + + 没有找到可移除的快捷方式。 + + + + + 立刻关闭软件 + + + + + 稍后 + + + + + 渲染器变更 将在重启软件后生效 + + + + + 端口号不合法 + + + + + 端口号改为%1 + + + + + 原端口号%1被占用, +切换为新端口号%2。 + +若不想看到此通知,请在全局设置关闭高级模式。 + + + + + GlobalConfigsPage + + + 1. 关于快捷键、OCR语言等选项,请在各个功能页中进行设置。 +2. 勾选左下角的“高级”按钮,可以显示更多进阶选项。 + + + + + 小贴士 + + + + + HTabBar + + + 窗口置顶 + + + + + 锁定标签栏 + + + + + IconButtonBar + + + 右键菜单 + + + + + 保存图片 + + + + + 图片大小:适应窗口 + + + + + 图片大小:实际 + + + + + 截图 + + + + + 粘贴 + + + + + 屏幕截图 + Screenshot + + + + 粘贴图片 + + + + + IgnoreArea + + + 拖入本地图片:OCR预览 +滚轮:缩放 +左键:拖拽 +右键:绘制忽略区域 + +可绘制一个或多个忽略区域矩形框。在执行批量OCR时,完全位于忽略区域内的文本块将被排除。 +比如批量处理影视截图时,可在右上角水印处添加忽略区域,避免输出水印文本。 + + + + + 图像尺寸: + + + + + 区域数量: + + + + + 撤销 + + + + + 清空 + + + + + ImageScale + + + 复制图片 + + + + + 复制图片失败 + + + + + 打开图片 + + + + + 打开图片失败 + + + + + + 保存图片 + + + + + 保存图片失败 + + + + + ImageWithText + + + 图片:复制%1字 + + + + + 图片:无选中文字 + + + + + 图片:复制全部%1字 + + + + + 复制  (Ctrl+C) + + + + + 全选  (Ctrl+A) + + + + + 复制图片(Ctrl+X) + + + + + 保存图片(Ctrl+S) + + + + + 显示/隐藏文字(Tab) + + + + + 用默认应用打开图片 + + + + + 删除图片(Ctrl+D) + + + + + MainWindowManager + + + 欢迎使用 Umi-OCR + + + + + 已启用后台模式,可通过快捷键使用功能。 + + + + + MarkdownView + + + 打开网页 + + + + + 链接 + + + + + MessageBox + + + 取消 + + + + + 确定 + + + + + 警告 + + + + + 发生了一点小问题 + + + + + 复制 + + + + + 已复制报错信息 %1 + + + + + 请前往 Issues 页面寻找解答或反馈 + + + + + 问题反馈 + + + + + MessageBoxWin + + + 确定 + + + + + 不再提示 + + + + + 知道了 + + + + + MissionCtrlPanel + + + 开始任务 + + + + + 暂停任务 +暂停后可以待机或休眠。 +但是关机或退出软件,将会丢弃任务内容。 + + + + + 继续任务 + + + + + 终止任务 +放弃未完成的内容。 + + + + + 正在运行 + + + + + 正在保存 + + + + + 已暂停 + + + + + 任务停止 + + + + + 任务完成 + + + + + ModalLayer + + + 保存并返回 + + + + + 返回 + + + + + Navigation + + + 欢迎使用 Umi-OCR + + + + + 请选择功能页 + + + + + 当前版本 + + + + + 项目链接 + + + + + 官方网站 + + + + + 插件拓展 + + + + + 问题反馈 + + + + + 功能页 + + + + + 请打开对应标签页(如批量OCR、批量文档),再拖入文件。 + + + + + OcrManager + + + 文字识别 + + + + + 操作 + + + + + + 强制终止任务 + + + + + 应用修改 + + + + + 当前接口 + + + + + OCR API 列表中不存在%1 + + + + + 当前已有%1组任务队列、共%2个任务正在执行。您可【强制终止任务】后修改API。 + + + + + 无法修改 文字识别接口设置 + + + + + 文字识别接口应用成功 + + + + + 当前API为【%1】 + + + + + 文字识别接口应用失败 + + + + + 当前没有运行中的任务 + + + + + 当前已有%1组任务队列、共%2个任务正在执行。 +要强制终止全部任务吗? + + + + + PagesManager + + + 新标签页 + + + + + 截图OCR + + + + + # 截图OCR + +屏幕截图,快捷转文字。也支持粘贴图片。 + + + + + # 全局设置 + +调节全局设置项,对所有页面生效。 + + + + + 批量OCR + + + + + # 批量OCR + +导入本地图片或文件夹,批量转换文字。 + +支持格式: + + + + + 批量文档 + + + + + # 批量文档识别 + +批量导入文档,提取文字,或生成双层可搜索PDF。 + +支持格式: + + + + + 二维码 + + + + + # 二维码 + +识别或生成二维码、条形码。 + +支持协议: + + + + + 全局设置 + + + + + 关于 + + + + + # 关于 + + + + + PopupManager + + + 确认 + + + + + 取消 + + + + + PreviewDoc + + + 加密 + + + + + 打开文档失败 + + + + + 请填写正确的密码 + + + + + 密码正确 + + + + + 文档预览异常 + + + + + 忽略区域说明: +右键拖拽,绘制矩形区域,包含在区域内的文字框将被忽略。可用于排除水印、页眉页脚。 +范围允许填写负数,表示倒数第x页。如-1表示最后一页,-2表示倒数第2页。 +忽略区域的设置对所有文档生效。 + + + + + 密码: + + + + + 预览页面 + + + + + 预览PDF时,是否预览OCR结果 + + + + + OCR页数 + OCR page count + + + + 忽略区域页数(全局) + Ignore Area page count (Global) + + + + + 范围 + + + + + 撤销 + + + + + 清空 + + + + + PreviewImage + + + 文字 + + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + + + + + QRCode + + + 获取剪贴板异常 + + + + + 剪贴板中为文本 + + + + + 无有效图片 + + + + + 导入%1条图片路径 + + + + + 参数有误,或输入内容不合规定。请参照报错指示修改: + + + + + + 生成二维码失败 + + + + + 已复制到剪贴板 + + + + + 识图完成 + + + + + 无文字 + + + + + 识别失败 + + + + + 扫描二维码 + + + + + 截图、拖入或粘贴二维码图片 + Screenshot, drag and drop, or paste QR code image + + + + + 设置 + + + + + 自动刷新 + + + + + 修改文字后,自动生成二维码/条形码 + + + + + 刷新 + + + + + 生成二维码/条形码 + + + + + 记录 + + + + + 生成 + + + + + QRCodeConfigs + + + 快捷键 + + + + + 屏幕截图 + Screenshot + + + + 粘贴图片 + + + + + 预处理(一般无需改动) + + + + + 中值滤波 + + + + + 对图像进行平滑处理 +>0 时生效。可填1~9的奇数(不允许偶数) + + + + + 锐度增强 + + + + + 增加图像的锐度 +>0.1 时生效。可填0.1~10的小数 + + + + + 对比度增强 + + + + + 增加图像的对比度 +>0.1 时生效。可填0.1~10的小数 + + + + + 转为灰度 + + + + + 将图像像素转为灰度 + + + + + 二值化 + + + + + 将图像像素转为纯黑和纯白 +启用了灰度,且二值化 >-1 时生效。可填0~255的整数 + + + + + 扫码后的操作 + + + + + 复制结果 + + + + + 弹出主窗口 + + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + + + + + 生成二维码/条形码 + + + + + 类型 + + + + + 默认二维码: + + + + + 宽度 + + + + + + 填0:自动选择 + + + + + + + 像素 + + + + + 高度 + + + + + 边缘空白 + + + + + 填-1:自动选择 + + + + + 纠错等级 + + + + + 仅适用于: + + + + + 自动 + + + + + 其它 + + + + + ResultTextContainer + + + 复制 + + + + + ResultsTableView + + + 异常状态码:%1 +异常信息:%2 + + + + + 耗时 %1 + + + + + 置信度 %1 + + + + + 记录:复制%1字 + + + + + 记录:无选中文字 + + + + + 记录:复制全部%1字 + + + + + 删除%1条记录 + + + + + 清空记录 + + + + + 复制    (Ctrl+C) + + + + + 复制全部  (Ctrl+C 双击) + + + + + 选中单个  (Ctrl+A) + + + + + 选中全部记录(Ctrl+A 双击) + + + + + 删除选中记录 + + + + + 清空全部记录(Ctrl+D 双击) + + + + + 滚动 + + + + + 自动滚动到底部 + + + + + 右键菜单 + + + + + ScreenshotManager + + + 截图失败 + + + + + 上次截图操作未结束,不能进行新的截图! + + + + + 尚未记录截图区域 + + + + + 显示器: %1 +错误信息: %2 + + + + + 重复截图失败 + + + + + 未找到匹配的屏幕 + + + + + 屏幕设备名称不相同: +%1 +%2 + + + + + ScreenshotOCR + + + 获取剪贴板异常 + + + + + 剪贴板中为文本 + + + + + 无有效图片 + + + + + 导入%1条图片路径 + + + + + 依然关闭 + + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + + + + + + 屏幕截图 + Screenshot + + + + + 粘贴图片 + + + + + 截图识别任务异常 + + + + + 已复制到剪贴板 + + + + + 识图完成 + + + + + 无文字 + + + + + 识别失败 + + + + + + 停止任务 + + + + + 文字 + + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + + + + + 截图、拖入或粘贴图片 + Screenshot, drag and drop, or paste image + + + + 设置 + + + + + 记录 + + + + + ScreenshotOcrConfigs + + + OCR文本后处理 + + + + + 快捷键 + + + + + 屏幕截图 + Screenshot + + + + 粘贴图片 + + + + + 重复截图 + + + + + 重新截取上一次截图的范围 + + + + + 识图后的操作 + + + + + 复制结果 + + + + + 弹出主窗口 + + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + + + + + 其它 + + + + + Size_ + + + 1.0 + + + + + SystemTray + + + 打开主窗口 + + + + + 退出 Umi-OCR + + + + + Theme + + + 珍珠白 + + + + + 云墨黑 + + + + + 赛博朋克 + + + + + UtilsConfigDicts + + + 任务完成后的操作 + + + + + 系统 + + + + + 无 + + + + + 关机 + + + + + 休眠 + + + + + %1 系统暂不支持电源控制! + %1 Linux system currently does not support power control! + + + + 继续%1 + + + + + 取消%1 + + + + + 系统即将%1 + + + + + 排版解析方案 + + + + + 按什么方式,解析和排序图片中的文字块 + + + + + 多栏-按自然段换行 + + + + + 多栏-总是换行 + + + + + 多栏-无换行 + + + + + 单栏-按自然段换行 + + + + + 单栏-总是换行 + + + + + 单栏-无换行 + + + + + 单栏-保留缩进 + + + + + 不做处理 + + + + + 优先内部 + + + + + 只允许内部 + + + + + 只允许外部 + + + + + 禁用所有通知 + + + + + 跟随全局设定 + + + + + 通知弹窗类型 + + + + + 通知类型已更改 + + + + diff --git a/dev-tools/i18n/release/pt.ts b/dev-tools/i18n/release/pt.ts new file mode 100644 index 0000000..5774a19 --- /dev/null +++ b/dev-tools/i18n/release/pt.ts @@ -0,0 +1,2555 @@ + + + + + About + + + 开源、免费的离线OCR软件 + Software de OCR off-line gratuito e de código aberto + + + + 当前版本 + Versão atual + + + + 项目链接 + Links de projetos + + + + 官方网站 + Site oficial + + + + 插件拓展 + Expansão de plug-in + + + + 问题反馈 + Feedback e pergunta + + + + 发布地址 + Endereço de publicação + + + + 许可协议 + Licença de acordo + + + + 作者 + Autor + + + + 译者 + Tradutor + + + + 运行环境信息(如需请求协助,请提供给开发者) + Informações do ambiente de execução (se precisar de assistência, forneça aos desenvolvedores) + + + + 复制 + Copiar + + + + AsynFilesLoader + + + 正在载入 %1 个文件: +%2 + + + + + BatchDOC + + + 加密 + Criptografar + + + + %1个加密文档 + %1 documento encriptado + + + + 请点击文件名填写密码 + Clique no nome do ficheiro e introduza a palavra-passe + + + + 文档已加密 + O documento é encriptado + + + + 【%1】 +请点击文档名,设置密码 + 【%1】 +Clique no nome do documento para definir uma palavra-passe + + + + 失败 + reprovado + + + + 排队 + Fila + + + + 依然关闭 + Ainda fechado + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + A missão está em curso. +Quer terminar uma tarefa e fechar a página? + + + + + 文档 + documentação + + + + 状态 + Estado + + + + 打开文档 + Abrir o documento + + + + 拖入文档或文件夹 + Arrastar para dentro de um documento ou pasta + + + + 清空 + vazio + + + + 文档识别异常 + Anomalias no reconhecimento de documentos + + + + 批量识别完成 + A identificação do lote está completa + + + + 页数 + Pages + + + + 请选择文档 + Selecione um documento + + + + 设置 + Configurar + + + + 记录 + registos + + + + BatchDOCConfigs + + + OCR文本后处理 + OCR pós-processamento de texto + + + + 文档处理 + Processamento de documentos + + + + 内容提取模式 + Modo de extração de conteúdo + + + + 若一页文档既存在图片又存在文本,如何进行处理 + O que fazer se uma página de documento contém imagens e texto + + + + 混合OCR/原文本 + OCR híbrido/texto original + + + + 整页强制OCR + Página inteira forçada OCR + + + + 仅OCR图片 + Apenas imagens OCR + + + + 仅拷贝原有文本 + Copiar apenas o texto original + + + + 批量任务 + Tarefas em massa + + + + 保存到 + Salvar para + + + + 文档原目录 + Catálogo original do documento + + + + + 指定目录 + Especificar um diretório + + + + 必须先指定“保存到指定目录”才生效 + "Salvar no diretório especificado" deve ser especificado primeiro para entrar em vigor + + + + OCR结果保存目录 + OCR resultados salvar diretório + + + + 文件名格式 + Formato do arquivo + + + + 日期时间格式 + Data hora formato + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + Formato de data para %date no nome do arquivo. Suporte para inserir os seguintes espaços reservados: +% ano Y, %m mês, %d dia, %H hora, +%M minutos, %S segundos, %unix timestamp +Exemplo: % ano Y %m mês %d dia _%H-% m +Gerado: 09-01-2023 _12-13.txt + + + + 保存文件类型 + Salvar o tipo de arquivo + + + + layered.pdf 双层可搜索文档 + Layered.pdf documento pesquisável de duas camadas + + + + text.pdf 单层纯文本文档 + Text.pdf documento de texto simples de camada única + + + + 创建空白PDF文档,只写入识别文字,不含图片 + Crie documentos PDF em branco e escreva apenas palavras de reconhecimento, sem imagens + + + + txt 标准格式 + Formato padrão de TXT + + + + 含识别文字和页数信息 + Contém texto de identificação e informações sobre o número de páginas + + + + p.txt 纯文字格式 + P.xt formato de texto simples + + + + 输出所有识别文字 + Saída de todas as palavras de reconhecimento + + + + csv 表格文件(Excel) + Arquivo de tabela CSV (Excel) + + + + 将页数信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + Escrever informações sobre o número de páginas e conteúdo de identificação em um arquivo de tabela CSV. Disponível para abrir em Excel, salvar como formato XLSX. + + + + jsonl 原始信息 + Jsonl informação original + + + + 每行为一条json数据,便于第三方程序读取操作 + Um dado json por comportamento para facilitar a leitura de operações por programas de terceiros + + + + 忽略空白页 + Ignorar páginas em branco + + + + 若某一页没有文字或识别失败,也不会输出错误提示信息 + Se não houver texto em uma página ou o reconhecimento falhar, uma mensagem de erro não será exibida + + + + 递归读取子文件夹 + Leitura recursiva de subpastas + + + + 点击表格,可设置更多内容 + Click on the table to set more options + + + + 导入文件夹时,导入子文件夹中全部文档 + Ao importar pastas, importe todos os documentos em subpastas + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文档名 +%range 识别页数范围。只有识别页数小于总页数时才会显示。 +举例:[OCR]_%name%range_%date +生成:[OCR]_文档A(p2-10)_20230901_1213.txt +添加占位符可以避免旧文件被新文件覆盖。 + No need to fill in the extension name. The following placeholders are supported: +%date Date and time +%name Original folder name/file name +%range Page range. Only displayed when the OCR page count is less than the total page count. +Example: [OCR]_%name%range_%date +Generated: [OCR]_Document(p2-10)_2023-09-01_12-13.txt +Adding placeholders can prevent old files from being overwritten by new files. + + + + 保留原有图片,叠加一层透明文字,可以搜索和复制 + Mantenha a imagem original e sobreponha uma camada de texto transparente que pode ser pesquisada e copiada + + + + 其它 + outros + + + + BatchOCR + + + 依然关闭 + Ainda está fechado + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + A missão está em curso. +Quer terminar a tarefa e fechar a página? + + + + 排队 + fila + + + + 处理 + transformação + + + + %1 张图片识别失败! + %1 imagem falha no reconhecimento! + + + + 批量识别完成 + Identificação em massa concluída + + + + 批量识别任务异常 + Identificação em massa de anomalias de tarefa + + + + + 图片 + imagens + + + + 耗时 + efectuado + + + + 状态 + estado + + + + 打开图片 + Abrir a imagem + + + + 拖入图片或文件夹 + Arrastar em imagens ou pastas + + + + 清空 + esvaziar + + + + 请选择图片 + Por favor, selecione uma imagem + + + + 设置 + As configurações + + + + 记录 + registos + + + + BatchOCRConfigs + + + OCR文本后处理 + OCR pós-processamento de texto + + + + 忽略区域 + Ignorar a região + + + + 进入设置 + Ir para configurações + + + + 批量任务 + Tarefas em massa + + + + 保存到 + Salvar para + + + + 图片原目录 + Catálogo original da imagem + + + + + 指定目录 + Especificar um diretório + + + + 必须先指定“保存到指定目录”才生效 + "Salvar no diretório especificado" deve ser especificado primeiro para entrar em vigor + + + + OCR结果保存目录 + OCR resultados salvar diretório + + + + 文件名格式 + Formato do arquivo + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文件夹名/文件名 +举例:[OCR]_%name_%date +生成:[OCR]_我的图片_2023-09-01_12-13.txt +添加占位符可以避免旧文件被新文件覆盖。 + Não é necessário preencher o nome estendido. Suporte para inserir os seguintes espaços reservados: +% data hora +%name nome da pasta original/nome do arquivo +Exemplo: [OCR]_%name_% data +Gerado: [OCR]_ minhas images_2023-09-01_12-13.txt +A adição de espaços reservados evita que os arquivos antigos sejam substituídos por novos. + + + + 日期时间格式 + Data hora formato + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + Formato de data para %date no nome do arquivo. Suporte para inserir os seguintes espaços reservados: +% ano Y, %m mês, %d dia, %H hora, +%M minutos, %S segundos, %unix timestamp +Exemplo: % ano Y %m mês %d dia _%H-% m +Gerado: 09-01-2023 _12-13.txt + + + + 保存文件类型 + Salvar o tipo de arquivo + + + + txt 标准格式 + Formato padrão de TXT + + + + 含原图片文件名和识别文字 + Contém o nome do arquivo da imagem original e o texto de reconhecimento + + + + p.txt 纯文字格式 + P.txt formato de texto simples + + + + 仅输出识别文字,不含图片标题 + Apenas texto de reconhecimento de saída, sem título de imagem + + + + txt 单独文件 + TXT arquivo separado + + + + 对每张图片,生成同名txt文件,仅输出识别文字 + Para cada imagem, um arquivo TXT com o mesmo nome é gerado e apenas as palavras de reconhecimento são exportadas + + + + md 图文混排 + Md mix gráfico e textual + + + + Markdown图文混排格式,可用Markdown阅读器浏览文件 + Formato de mistura de imagem e texto Markdown, leitor Markdown disponível para navegar no arquivo + + + + csv 表格文件(Excel) + Arquivo de tabela CSV (Excel) + + + + 将图片信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + Escreva as informações da imagem e o conteúdo de reconhecimento em um arquivo de tabela CSV. Disponível para abrir em Excel, salvar como formato XLSX. + + + + jsonl 原始信息 + Jsonl informação original + + + + 每行为一条json数据,便于第三方程序读取操作 + Um dado json por comportamento para facilitar a leitura de operações por programas de terceiros + + + + 输出忽略空白图片 + Saída ignorar imagens em branco + + + + 若图片没有文字或识别失败,也不会输出错误提示信息 + Se a imagem não tem texto ou falha de reconhecimento, também não irá emitir mensagem de erro + + + + 递归读取子文件夹 + Leitura recursiva de subpastas + + + + 导入文件夹时,导入子文件夹中全部图片 + Importar todas as imagens em subpastas ao importar pastas + + + + 其它 + outros + + + + Configs + + + %1 处理配置项异常: +%2枚举列表为空。 + %1 lida com exceções de itens de configuração: +%2 a lista de enumeração está vazia. + + + + 重置 + substituição + + + + 重置本页上的设定 + Redefinir as configurações nesta página + + + + 重置设定 + Redefinir as configurações + + + + 要重置本页的设定吗? + Deseja redefinir as configurações desta página? + + + + 高级 + sénior + + + + 显示更多高级选项。它们标有 * 号。 +请谨慎修改高级选项。 + Mostrar mais opções avançadas. Estão marcados com *. +Por favor, modifique as opções avançadas com cuidado. + + + + 展开 + desdobrar + + + + 折叠 + dobras + + + + 必须为整数 + Deve ser um número inteiro + + + + 不能超过 + Não pode exceder + + + + 不能低于 + Não pode ser inferior + + + + 必须为数字 + Deve ser um número + + + + 已取消%1的快捷键。 + Os atalhos para %1 foram cancelados. + + + + 更新热键成功 + Atualizar hotkey com sucesso + + + + %1的快捷键为 %2 + O atalho para %1 é %2 + + + + %1 快捷键%2已被注册,请尝试另外的按键组合。 + O atalho %1 %2 já foi registrado. Tente uma combinação de teclas diferente. + + + + %1 快捷键%2无法注册,请尝试另外的按键组合。 + %1 atalhos %2 não foi possível registrar. Por favor, tente uma combinação de teclas diferente. + + + + 请按下快捷键组合。按【Esc】退出。 + Por favor, pressione a combinação de atalhos. Pressione [Esc] para sair. + + + + 当前快捷键录制已在进行,不能同时录制! + A gravação de atalho atual já está em andamento e não pode ser gravada ao mesmo tempo! + + + + 无法录制快捷键 + Não é possível gravar atalhos + + + + DropArea_ + + + 松手放入文件 + Solta Põe o ficheiro + + + + FontPanel + + + 界面 + interface + + + + 内容 + conteúdo + + + + 界面字体: +软件中大部分UI的字体。 + Fontes da interface: +Fontes para a maioria da interface do usuário no software. + + + + 内容字体: +识别结果内容的字体。 + Fonte de conteúdo: +Identifique a fonte do conteúdo resultante. + + + + GlobalConfigs + + + 快捷方式 + Atalhos para + + + + 桌面 + Área de trabalho + + + + 开始菜单 + Iniciar o menu + + + + 开机自启 + Inicialização e auto arranque + + + + 界面和外观 + Interface e aparência + + + + 主题 + tema + + + + 切换主题 + Mudar de tema + + + + 字体 + O tipo de letra + + + + 修改字体 + Modificar a fonte + + + + 界面与文字大小 + Interface e tamanho do texto + + + + 渲染器 + O renderizador + + + + 关闭硬件加速 + Desligue a aceleração de hardware + + + + 若出现界面闪烁、元素错位等界面异常,尝试切换渲染器或者关闭硬件加速 + Em caso de anormalidades na interface, como cintilação da interface ou deslocamento de elementos, tente alternar o renderizador ou desligue a aceleração de hardware + + + + 禁用美化效果 + Desativar o efeito embelezamento + + + + 在低配置机器上,禁用动画、阴影等效果可减少部分资源占用 + Desativar efeitos como animações e sombras reduz parte do uso de recursos em máquinas de configuração baixa + + + + 图片预览默认显示叠加层 + Pré-visualização de imagem exibe camadas de sobreposição por padrão + + + + 默认开启/关闭叠加层显示 +对所有图片预览组件生效 + Por padrão ativar/desativar a exibição de camadas de sobreposição +Funciona em todos os componentes de visualização de imagens + + + + 窗口 + janela + + + + 启动时缩小到任务栏 + Redução para a barra de tarefas na inicialização + + + + 软件启动时,不弹出主窗口 + Quando o software é iniciado, a janela principal não aparece + + + + 窗口置顶 + A janela está fixada + + + + 捷径:窗口左上角图钉 + Atalho: Fixar no canto superior esquerdo da janela + + + + 锁定标签栏 + Bloquear a barra de etiquetas + + + + 捷径:窗口右上角小锁 + Atalho: pequeno bloqueio no canto superior direito da janela + + + + 关闭主窗口时 + Ao fechar a janela principal + + + + 最小化到系统托盘 + Minimização para a bandeja do sistema + + + + 退出应用 + Sair do aplicativo + + + + 隐藏托盘图标 + Ocultar o ícone da bandeja + + + + 若要弹出位于后台的软件窗口,请在文件管理器中重复启动软件。 +若要彻底退出软件,请从任务管理器中结束进程。 + Para abrir a janela do software localizada em segundo plano, repita o lançamento do software no gerenciador de arquivos. +Para sair completamente do software, termine o processo do gerenciador de tarefas. + + + + 截图 + Captura de tela + + + + 截图前隐藏主窗口 + Ocultar a janela principal antes de tirar uma captura de tela + + + + 截图前,如果主窗口处于前台,则隐藏主窗口 +将会延时等待主窗口关闭 + Antes de tirar uma captura de tela, se a janela principal estiver em primeiro plano, oculte a janela principal +vai esperar até que a janela principal feche + + + + 隐藏等待时间 + Ocultar tempos de espera + + + + 秒 + segundo + + + + 服务 + servir + + + + 允许HTTP服务 + Serviços HTTP são permitidos + + + + Umi-OCR依赖HTTP接口进行本机跨进程通信。如果禁用,将无法使用命令行模式、多开检测等功能。 + Umi-OCR depende de interfaces HTTP para comunicação nativa entre processos. Se desativado, você não poderá usar funções como modo de linha de comando e deteção de abertura múltipla. + + + + + + 重启软件后生效 + Entra em vigor depois de reiniciar o software + + + + 主机 + anfitrião + + + + 仅本地 + Apenas local + + + + 任何可用地址 + Qualquer endereço disponível + + + + 将允许局域网访问。请开启对应防火墙权限! + O acesso LAN será permitido. Por favor, habilite as permissões de firewall correspondentes! + + + + 将禁止局域网访问。 + O acesso LAN será bloqueado. + + + + 端口 + porto + + + + 日志 + + + + + 操作 + operar + + + + 打开日志保存目录 + + + + + 保存的日志级别 + + + + + 已禁用日志文件保存 + + + + + 请尝试更换软件路径! + Por favor, tente alterar o caminho do software! + + + + 配置文件读写异常 + O arquivo de configuração é lido e gravado + + + + 插件加载失败 + O plugin falhou ao carregar + + + + 成功添加快捷方式 + O atalho foi adicionado com êxito + + + + 添加快捷方式失败 + Falha ao adicionar atalho + + + + 成功移除 %1 个快捷方式 + %1 atalho removido com êxito + + + + 提示 + Prompt + + + + 没有找到可移除的快捷方式。 + Nenhum atalho removível encontrado. + + + + 渲染器变更 将在重启软件后生效 + As alterações do renderizador entrarão em vigor após reiniciar o software + + + + 端口号不合法 + O número da porta é inválido + + + + 端口号改为%1 + Altere o número da porta para %1 + + + + 原端口号%1被占用, +切换为新端口号%2。 + +若不想看到此通知,请在全局设置关闭高级模式。 + O número de porta original %1 está ocupado, +Mude para o novo número de porta %2. + +Se não quiser ver esta notificação, desative o modo avançado nas definições globais. + + + + GlobalConfigsPage + + + 1. 关于快捷键、OCR语言等选项,请在各个功能页中进行设置。 +2. 勾选左下角的“高级”按钮,可以显示更多进阶选项。 + 1. Para teclas de atalho, linguagem OCR e outras opções, defina-as em cada página de função. +2. Marque o botão "Avançado" no canto inferior esquerdo para exibir opções mais avançadas. + + + + 小贴士 + Dicas + + + + HTabBar + + + 窗口置顶 + A janela está fixada + + + + 锁定标签栏 + Bloquear a barra de separadores + + + + IconButtonBar + + + 右键菜单 + Menu do botão direito do rato + + + + 保存图片 + Guardar a imagem + + + + 图片大小:适应窗口 + Tamanho da imagem: Adapta-se à janela + + + + 图片大小:实际 + Tamanho da imagem: Real + + + + 截图 + Captura de tela + + + + 粘贴 + aderência + + + + 屏幕截图 + Screenshot + + + + 粘贴图片 + Cole a imagem + + + + IgnoreArea + + + 拖入本地图片:OCR预览 +滚轮:缩放 +左键:拖拽 +右键:绘制忽略区域 + +可绘制一个或多个忽略区域矩形框。在执行批量OCR时,完全位于忽略区域内的文本块将被排除。 +比如批量处理影视截图时,可在右上角水印处添加忽略区域,避免输出水印文本。 + Arraste em imagens locais: visualização de OCR +roda de rolagem: zoom +Clique com o botão esquerdo: arraste +Clique com o botão direito do mouse: Desenhe a área ignorada + +Você pode desenhar uma ou mais caixas retangulares de área ignorada. Ao executar OCR em massa, blocos de texto que estão completamente dentro da área de ignorar serão excluídos. +Por exemplo, ao processar capturas de tela de filme e televisão em lotes, você pode adicionar uma área de ignorar à marca d'água no canto superior direito para evitar a saída de texto de marca d'água. + + + + 图像尺寸: + Tamanho da imagem: + + + + 区域数量: + Número de zonas: + + + + 撤销 + Quash + + + + 清空 + vazio + + + + ImageScale + + + 复制图片 + Copiar a imagem + + + + 复制图片失败 + Falha ao copiar a imagem + + + + 打开图片 + Abrir a imagem + + + + 打开图片失败 + Falha ao abrir a imagem + + + + + 保存图片 + Guardar a imagem + + + + 保存图片失败 + Falha ao salvar a imagem + + + + ImageWithText + + + 图片:复制%1字 + Imagem: Copiar %1 palavra + + + + 图片:无选中文字 + Imagem: Nenhum caractere chinês selecionado + + + + 图片:复制全部%1字 + Imagem: Copiar todas as palavras %1 + + + + 复制  (Ctrl+C) + Copiar (Ctrl+C) + + + + 全选  (Ctrl+A) + Selecionar tudo (Ctrl+A) + + + + 复制图片(Ctrl+X) + Imagem duplicada (Ctrl+X) + + + + 保存图片(Ctrl+S) + Guardar imagem (Ctrl+S) + + + + 显示/隐藏文字(Tab) + Mostrar/Ocultar Texto (Separador) + + + + 用默认应用打开图片 + Abra a imagem com o aplicativo padrão + + + + 删除图片(Ctrl+D) + Excluir imagem (Ctrl+D) + + + + MainWindowManager + + + 欢迎使用 Umi-OCR + Bem-vindo à Umi-OCR + + + + 已启用后台模式,可通过快捷键使用功能。 + O modo de fundo está ativado e as funções podem ser acedidas através de teclas de atalho. + + + + MarkdownView + + + 打开网页 + Abrir a página Web + + + + 链接 + ligação + + + + MessageBox + + + 取消 + Cancelar + + + + 确定 + Tens a certeza + + + + 警告 + advertir + + + + 发生了一点小问题 + Um pouco deu errado + + + + 复制 + cópia + + + + 已复制报错信息 %1 + A mensagem de erro %1 foi copiada + + + + 请前往 Issues 页面寻找解答或反馈 + Por favor, vá para a página questões para respostas ou comentários + + + + 问题反馈 + Feedback e pergunta + + + + MessageBoxWin + + + 确定 + Tens a certeza + + + + 不再提示 + Sem mais prompts + + + + 知道了 + Apanhei + + + + MissionCtrlPanel + + + 开始任务 + Iniciar a tarefa + + + + 暂停任务 +暂停后可以待机或休眠。 +但是关机或退出软件,将会丢弃任务内容。 + Pausar a tarefa +Após a pausa, pode ficar em espera ou hibernado. +No entanto, desligar ou sair do software descartará o conteúdo da tarefa. + + + + 继续任务 + Continuar a missão + + + + 终止任务 +放弃未完成的内容。 + Encerrar a tarefa +Descarte conteúdo inacabado. + + + + 正在运行 + Em execução + + + + 正在保存 + Poupança + + + + 已暂停 + Em pausa + + + + 任务停止 + A tarefa é interrompida + + + + 任务完成 + Missão cumprida + + + + ModalLayer + + + 保存并返回 + Guardar e devolver + + + + 返回 + regresso + + + + Navigation + + + 欢迎使用 Umi-OCR + Bem-vindo à Umi-OCR + + + + 请选择功能页 + Selecione uma página de recurso + + + + 当前版本 + Versão atual + + + + 项目链接 + Links de projetos + + + + 官方网站 + Site oficial + + + + 插件拓展 + Expansão de plug-in + + + + 问题反馈 + Feedback e pergunta + + + + 功能页 + Página de destaques + + + + 请打开对应标签页(如批量OCR、批量文档),再拖入文件。 + Abra a guia correspondente (por exemplo, OCR em lote, Documentos em lote) e arraste o arquivo. + + + + OcrManager + + + 文字识别 + Reconhecimento de texto + + + + 操作 + operar + + + + + 强制终止任务 + Forçar o encerramento da tarefa + + + + 应用修改 + Aplicar modificações + + + + 当前接口 + A interface atual + + + + 没有可用的 OCR 插件。 + + + + + OCR API 列表中不存在%1 + %1 não existe na lista de API OCR + + + + 当前已有%1组任务队列、共%2个任务正在执行。您可【强制终止任务】后修改API。 + Neste momento, %1 grupo de tarefas está em fila e %2 tarefas estão a ser executadas. Você pode modificar a API após [forçar o término da tarefa]. + + + + 无法修改 文字识别接口设置 + Não é possível modificar as configurações da interface de reconhecimento de texto + + + + 文字识别接口应用成功 + Interface de reconhecimento de palavras aplicada com sucesso + + + + 当前API为【%1】 + A API atual é [%1] + + + + 文字识别接口应用失败 + Falha na aplicação da interface de reconhecimento de palavras + + + + 当前没有运行中的任务 + Atualmente, não há tarefas em execução + + + + 当前已有%1组任务队列、共%2个任务正在执行。 +要强制终止全部任务吗? + Atualmente, há %1 fila de tarefas e %2 tarefas no total. +Deseja encerrar todas as tarefas à força? + + + + PagesManager + + + 新标签页 + Novo separador + + + + 截图OCR + Captura de tela OCR + + + + # 截图OCR + +屏幕截图,快捷转文字。也支持粘贴图片。 + # Captura de tela OCR + +Capturas de tela, rápido para texto. A colagem de imagens também é suportada. + + + + # 全局设置 + +调节全局设置项,对所有页面生效。 + # Configurações globais + +Ajuste as configurações globais para entrar em vigor em todas as páginas. + + + + 批量OCR + OCR em lote + + + + # 批量OCR + +导入本地图片或文件夹,批量转换文字。 + +支持格式: + # OCR em lote + +Importe imagens ou pastas locais para converter texto em lotes. + +Formatos suportados: + + + + 批量文档 + Documentação em massa + + + + # 批量文档识别 + +批量导入文档,提取文字,或生成双层可搜索PDF。 + +支持格式: + # Reconhecimento de Documentos em Lote + +Importe documentos em massa, extraia texto ou gere PDFs pesquisáveis em camadas duplas. + +Formatos suportados: + + + + 二维码 + Código QR + + + + # 二维码 + +识别或生成二维码、条形码。 + +支持协议: + # Código QR + +Reconhecer ou gerar códigos QR, códigos de barras. + +Acordos suportados: + + + + 全局设置 + Configurações globais + + + + 关于 + no que diz respeito + + + + # 关于 + # Sobre nós + + + + PopupManager + + + 确认 + Confirmar + + + + 取消 + cancelar + + + + PreviewDoc + + + 加密 + encriptação + + + + 打开文档失败 + Falha ao abrir o documento + + + + 请填写正确的密码 + Por favor, preencha a senha correta + + + + 密码正确 + A palavra-passe está correta + + + + 文档预览异常 + Exceção de visualização do documento + + + + 忽略区域说明: +右键拖拽,绘制矩形区域,包含在区域内的文字框将被忽略。可用于排除水印、页眉页脚。 +范围允许填写负数,表示倒数第x页。如-1表示最后一页,-2表示倒数第2页。 +忽略区域的设置对所有文档生效。 + Descrição da região ignorada: +Clique com o botão direito do mouse e arraste para desenhar uma área retangular e a caixa de texto contida na área será ignorada. Pode ser usado para excluir marcas d'água, cabeçalhos e rodapés. +O intervalo permite que números negativos sejam preenchidos, indicando a décima página a partir da parte inferior. Por exemplo, -1 indica a última página e -2 indica a penúltima página. +A configuração da zona de ignorar entra em vigor para todos os documentos. + + + + 密码: + Palavra-passe: + + + + 预览页面 + Pré-visualizar a página + + + + 预览PDF时,是否预览OCR结果 + Ao visualizar um PDF, se o resultado do OCR deve ser visualizado + + + + OCR页数 + OCR page count + + + + 忽略区域页数(全局) + Ignore Area page count (Global) + + + + + 范围 + gama + + + + 撤销 + Quash + + + + 清空 + vazio + + + + PreviewImage + + + 文字 + Redação + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + Sobrepor texto de reconhecimento na parte superior da imagem +Ele pode ser definido como padrão nas configurações globais + + + + QRCode + + + 获取剪贴板异常 + Obter exceções da área de transferência + + + + 剪贴板中为文本 + Texto na área de transferência + + + + 无有效图片 + Não existem imagens válidas + + + + 导入%1条图片路径 + Importar caminhos de imagem %1 + + + + 参数有误,或输入内容不合规定。请参照报错指示修改: + Os parâmetros estão incorretos ou o conteúdo de entrada não está de acordo com os regulamentos. Consulte as instruções de erro para modificar: + + + + + 生成二维码失败 + Falha ao gerar um código QR + + + + 已复制到剪贴板 + Copiado para a área de transferência + + + + 识图完成 + A leitura do mapa está concluída + + + + 无文字 + Sem texto + + + + 识别失败 + Falha na identificação + + + + 扫描二维码 + Digitalize o código QR + + + + 截图、拖入或粘贴二维码图片 + Screenshot, drag and drop, or paste QR code image + + + + + 设置 + Configurar + + + + 自动刷新 + Atualização automática + + + + 修改文字后,自动生成二维码/条形码 + Depois de modificar o texto, o código QR/código de barras é gerado automaticamente + + + + 刷新 + rubor + + + + 生成二维码/条形码 + Gerar códigos QR/códigos de barras + + + + 记录 + Gravação + + + + 生成 + gerar + + + + QRCodeConfigs + + + 快捷键 + tecla de atalho + + + + 屏幕截图 + Screenshot + + + + 粘贴图片 + Cole a imagem + + + + 预处理(一般无需改动) + Pré-tratamento (geralmente sem necessidade de alterações) + + + + 中值滤波 + Filtragem mediana + + + + 对图像进行平滑处理 +>0 时生效。可填1~9的奇数(不允许偶数) + Suavizar a imagem +>0. Você pode preencher o número ímpar de 1 ~ 9 (números pares não são permitidos) + + + + 锐度增强 + Melhoria da nitidez + + + + 增加图像的锐度 +>0.1 时生效。可填0.1~10的小数 + Aumentar a nitidez da imagem +> 0.1. Você pode preencher 0,1 ~ 10 decimais + + + + 对比度增强 + Melhoria do contraste + + + + 增加图像的对比度 +>0.1 时生效。可填0.1~10的小数 + Aumentar o contraste da imagem +> 0.1. Você pode preencher 0,1 ~ 10 decimais + + + + 转为灰度 + Vire para a escala de cinza + + + + 将图像像素转为灰度 + Converte pixels de imagem em escala de cinza + + + + 二值化 + Binarização + + + + 将图像像素转为纯黑和纯白 +启用了灰度,且二值化 >-1 时生效。可填0~255的整数 + Transforme os pixels da imagem em preto puro e branco puro +A escala de cinza é ativada e a binarização entra em vigor quando >-1. Você pode preencher o número inteiro de 0~255 + + + + 扫码后的操作 + O que fazer depois de digitalizar o código QR + + + + 复制结果 + Copiar os resultados + + + + 弹出主窗口 + A janela principal aparece + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + Depois que a imagem for reconhecida, se a janela principal estiver minimizada ou em segundo plano, ela aparecerá em primeiro plano + + + + 生成二维码/条形码 + Gerar códigos QR/códigos de barras + + + + 类型 + tipo + + + + 默认二维码: + Código QR padrão: + + + + 宽度 + Largura + + + + + 填0:自动选择 + Preencha 0: Seleção automática + + + + + + 像素 + píxel + + + + 高度 + altitude + + + + 边缘空白 + As margens estão em branco + + + + 填-1:自动选择 + Preenchimento -1: Seleção automática + + + + 纠错等级 + Nível de correção de erros + + + + 仅适用于: + Aplica-se apenas a: + + + + 自动 + Automático + + + + 其它 + Outros + + + + ResultTextContainer + + + 复制 + cópia + + + + ResultsTableView + + + 异常状态码:%1 +异常信息:%2 + Código de status da exceção: %1 +Informações de exceção: %2 + + + + 耗时 %1 + Tempo gasto %1 + + + + 置信度 %1 + Confiança %1 + + + + 记录:复制%1字 + Registo: Copiar %1 palavra + + + + 记录:无选中文字 + Registro: Nenhum caractere chinês selecionado + + + + 记录:复制全部%1字 + Gravar: Copie todas as palavras %1 + + + + 删除%1条记录 + Excluir registros %1 + + + + 清空记录 + Limpar o registo + + + + 复制    (Ctrl+C) + 复制 (Ctrl+C) + + + + 复制全部  (Ctrl+C 双击) + Copiar tudo (Ctrl+C duplo clique) + + + + 选中单个  (Ctrl+A) + Selecione um único (Chaleira + Ah) + + + + 选中全部记录(Ctrl+A 双击) + Selecionar todos os registros (Ctrl+A clique duas vezes) + + + + 删除选中记录 + Exclui os registros selecionados + + + + 清空全部记录(Ctrl+D 双击) + Limpar todos os registros (Ctrl+D clique duas vezes) + + + + 滚动 + rolar + + + + 自动滚动到底部 + Rolar automaticamente para a parte inferior + + + + 右键菜单 + Menu do botão direito do rato + + + + ScreenshotManager + + + 截图失败 + Falha ao tirar uma captura de tela + + + + 上次截图操作未结束,不能进行新的截图! + A última operação de captura de tela não terminou, e nenhuma nova captura de tela pode ser feita! + + + + 尚未记录截图区域 + A área da captura de tela não foi gravada + + + + 显示器: %1 +错误信息: %2 + Ecrã: %1 +Mensagem de erro: %2 + + + + 重复截图失败 + Falha ao repetir a captura de tela + + + + 未找到匹配的屏幕 + Nenhuma tela correspondente encontrada + + + + 屏幕设备名称不相同: +%1 +%2 + Os nomes dos dispositivos de tela não são os mesmos: +%1 +%2 + + + + ScreenshotOCR + + + 获取剪贴板异常 + Obter exceções da área de transferência + + + + 剪贴板中为文本 + Texto na área de transferência + + + + 无有效图片 + Não existem imagens válidas + + + + 导入%1条图片路径 + Importar caminhos de imagem %1 + + + + 依然关闭 + Ainda fechado + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + A missão está em curso. +Quer terminar uma tarefa e fechar a página? + + + + + 屏幕截图 + Screenshot + + + + + 粘贴图片 + Cole a imagem + + + + 截图识别任务异常 + As capturas de tela identificam exceções de tarefas + + + + 已复制到剪贴板 + Copiado para a área de transferência + + + + 识图完成 + A leitura do mapa está concluída + + + + 无文字 + Sem texto + + + + 识别失败 + Falha na identificação + + + + + 停止任务 + Parar a tarefa + + + + 文字 + texto + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + Sobreposição de imagens para mostrar texto de reconhecimento +Pode ser definido como desativado por padrão nas configurações globais + + + + 截图、拖入或粘贴图片 + Screenshot, drag and drop, or paste image + + + + 设置 + As configurações + + + + 记录 + registos + + + + ScreenshotOcrConfigs + + + OCR文本后处理 + Pós-processamento de texto OCR + + + + 快捷键 + tecla de atalho + + + + 屏幕截图 + Screenshot + + + + 粘贴图片 + Cole a imagem + + + + 重复截图 + Repetir capturas de ecrã + + + + 重新截取上一次截图的范围 + Recapture o alcance da última captura de tela + + + + 识图后的操作 + Operações após o reconhecimento de imagem + + + + 复制结果 + Copiar os resultados + + + + 弹出主窗口 + A janela principal aparece + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + Depois que a imagem for reconhecida, se a janela principal estiver minimizada ou em segundo plano, ela aparecerá em primeiro plano + + + + 其它 + Outros + + + + Size_ + + + 1.0 + 1.0 + + + + SystemTray + + + 打开主窗口 + Abrir a janela principal + + + + 退出 Umi-OCR + Saia do Umi-OCR + + + + Theme + + + 珍珠白 + Branco perolado + + + + 云墨黑 + Tinta de nuvem preta + + + + 赛博朋克 + Cyberpunk + + + + UtilsConfigDicts + + + 任务完成后的操作 + A ação após a conclusão da tarefa + + + + 系统 + sistema + + + + 无 + não + + + + 关机 + Desligamento + + + + 休眠 + dormência + + + + %1 系统暂不支持电源控制! + %1 O sistema não suporta controlo de energia neste momento! + + + + 继续%1 + Continuar %1 + + + + 取消%1 + Cancelar %1 + + + + 系统即将%1 + O sistema é de cerca de %1 + + + + 排版解析方案 + Esquema de análise tipográfica + + + + 按什么方式,解析和排序图片中的文字块 + De que forma analisar e ordenar os blocos de texto na imagem + + + + 多栏-按自然段换行 + Multi-coluna - linhas de enrolamento em segmentos naturais + + + + 多栏-总是换行 + Várias colunas - sempre quebras de linha + + + + 多栏-无换行 + Várias colunas - sem quebras de linha + + + + 单栏-按自然段换行 + Coluna única - envolve linhas em segmentos naturais + + + + 单栏-总是换行 + Coluna única - sempre quebra de linha + + + + 单栏-无换行 + Coluna única - sem quebras de linha + + + + 单栏-保留缩进 + Coluna única - retém o recuo + + + + 不做处理 + Não o faça + + + + 优先内部 + Prioridade interna + + + + 只允许内部 + Apenas interno é permitido + + + + 只允许外部 + Apenas externo é permitido + + + + 禁用所有通知 + Desativar todas as notificações + + + + 跟随全局设定 + Siga as configurações globais + + + + 通知弹窗类型 + O tipo de pop-up de notificação + + + + 通知类型已更改 + O tipo de notificação foi alterado + + + diff --git a/dev-tools/i18n/release/ru_RU.ts b/dev-tools/i18n/release/ru_RU.ts new file mode 100644 index 0000000..21c79a8 --- /dev/null +++ b/dev-tools/i18n/release/ru_RU.ts @@ -0,0 +1,2556 @@ + + + + + About + + + 开源、免费的离线OCR软件 + Бесплатное автономное ПО для оптического распознавания символов с открытым исходным кодом + + + + 当前版本 + Текущая версия + + + + 项目链接 + Ссылка на проект + + + + 官方网站 + Официальный сайт + + + + 插件拓展 + Подключаемые расширения + + + + 问题反馈 + Обратная связь + + + + 发布地址 + Релиз + + + + 许可协议 + Лицензионное соглашение + + + + 作者 + Автор + + + + 译者 + Переводчик + + + + 运行环境信息(如需请求协助,请提供给开发者) + Информация о среде запуска (если нужна помощь, предоставьте это разработчику) + + + + 复制 + Копировать + + + + AsynFilesLoader + + + 正在载入 %1 个文件: +%2 + Загрузка файла %1: +%2 + + + + BatchDOC + + + 加密 + Шифрование + + + + %1个加密文档 + %1 зашифрованный документ + + + + 请点击文件名填写密码 + Нажмите на имя файла, чтобы ввести пароль + + + + 文档已加密 + Документ зашифрован + + + + 【%1】 +请点击文档名,设置密码 + 【%1】 +Нажмите на название документа, чтобы установить пароль + + + + 失败 + Неудачно + + + + 排队 + Поставлено в очередь + + + + 依然关闭 + Всё ещё закрыто + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + Задача выполняется. +Завершить задание и закрыть страницу? + + + + + 文档 + Документ + + + + 状态 + Состояние + + + + 打开文档 + Открыть документ + + + + 拖入文档或文件夹 + Перетащите документ или папку + + + + 清空 + Очистить + + + + 文档识别异常 + Исключение для распознавания документа + + + + 批量识别完成 + Распознавание завершено + + + + 页数 + Количество страниц + + + + 请选择文档 + Выберите документ + + + + 设置 + Установки + + + + 记录 + Записать + + + + BatchDOCConfigs + + + OCR文本后处理 + Постобработка полученного текста + + + + 文档处理 + Обработка документов + + + + 内容提取模式 + Режим извлечения контента + + + + 若一页文档既存在图片又存在文本,如何进行处理 + Как поступить если на странице есть картинки и текст + + + + 混合OCR/原文本 + Смешанное распознавание текста/оригинальный текст + + + + 整页强制OCR + Принудительное распознавание текста на всю страницу + + + + 仅OCR图片 + Распознавание только картинок + + + + 仅拷贝原有文本 + Копировать только исходный текст + + + + 批量任务 + Пакетные задачи + + + + 保存到 + Сохранить в + + + + 文档原目录 + Каталог оригинальных документов + + + + + 指定目录 + Укажите каталог + + + + 必须先指定“保存到指定目录”才生效 + Для применения, сначала нужно указать "Сохранить в указанный каталог” + + + + OCR结果保存目录 + Каталог сохранения результатов распознавания + + + + 文件名格式 + Формат имени файла + + + + 日期时间格式 + Формат даты и времени + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + Формат даты для %date в имени файла. Поддерживается вставка следующих заполнителей: +%Y год, %m месяц, %d день, %H час, +%M минут, %S секунд, временная метка %unix +Пример: %Y год %m месяц %d день_%H-%M +Сгенерируется: 1 сентября 2023 г._12-13.txt + + + + 保存文件类型 + Сохранить тип файла + + + + layered.pdf 双层可搜索文档 + layered.pdf двухслойные документы с возможностью поиска + + + + text.pdf 单层纯文本文档 + text.pdf обычный текстовый документ + + + + 创建空白PDF文档,只写入识别文字,不含图片 + Создать пустой PDF-документ, записать только распознанный текст, без изображений + + + + txt 标准格式 + Стандартный формат txt + + + + 含识别文字和页数信息 + Содержит текст и информацию о номере страницы + + + + p.txt 纯文字格式 + p.txt текстовый формат + + + + 输出所有识别文字 + Вывод всего распознанного текста + + + + csv 表格文件(Excel) + csv файл формы(Excel) + + + + 将页数信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + Записать информацию о номере страницы и идентификационный контент в файл таблицы CSV. Его можно открыть в Excel и сохранить в формате xlsx. + + + + jsonl 原始信息 + jsonl исходная информация + + + + 每行为一条json数据,便于第三方程序读取操作 + Каждая строка представляет собой фрагмент данных json, который удобно использовать другим программам + + + + 忽略空白页 + Игнорировать пустые страницы + + + + 若某一页没有文字或识别失败,也不会输出错误提示信息 + Если на странице нет текста или распознавание не удалось, сообщение об ошибке не выводится + + + + 递归读取子文件夹 + Рекурсивное чтение вложенных папок + + + + 点击表格,可设置更多内容 + Нажмите на таблицу, чтобы установить больше контента + + + + 导入文件夹时,导入子文件夹中全部文档 + При импорте папки импортировать все документы из вложенной папки + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文档名 +%range 识别页数范围。只有识别页数小于总页数时才会显示。 +举例:[OCR]_%name%range_%date +生成:[OCR]_文档A(p2-10)_20230901_1213.txt +添加占位符可以避免旧文件被新文件覆盖。 + Нет необходимости заполнять расширение. Поддерживаются следующие заполнители: +%date datetime +%name Исходное название документа +%range определяет диапазон страниц. Он отображается только в том случае, если количество распознанных страниц меньше общего количества страниц. +Например: [OCR]_%name%range_%date +Сгенерировано: [OCR]_Document A(p2-10)_20230901_1213.txt +Добавление заполнителей предотвращает перезапись старых файлов новыми. + + + + 保留原有图片,叠加一层透明文字,可以搜索和复制 + Сохраните исходное изображение, наложите слой прозрачного текста, который можно искать и копировать + + + + 其它 + Другие + + + + BatchOCR + + + 依然关闭 + Всё ещё закрыто + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + Задача выполняется. +Завершить задание и закрыть страницу? + + + + 排队 + Поставлено в очередь + + + + 处理 + Обработать + + + + %1 张图片识别失败! + %1 распознавание изображения не удалось! + + + + 批量识别完成 + Распознавание пакетов завершено + + + + 批量识别任务异常 + Пакетная идентификация отклонений от выполнения задач + + + + + 图片 + фотография + + + + 耗时 + Тщательность + + + + 状态 + Состояние + + + + 打开图片 + Открыть изображение + + + + 拖入图片或文件夹 + Перетаскивание изображений или папок + + + + 清空 + Очистить + + + + 请选择图片 + Выберите изображение + + + + 设置 + Установить + + + + 记录 + Записать + + + + BatchOCRConfigs + + + OCR文本后处理 + Постобработка полученного текста + + + + 忽略区域 + Игнорировать область + + + + 进入设置 + Ввести настройки + + + + 批量任务 + Пакетные задачи + + + + 保存到 + Сохранить в + + + + 图片原目录 + Исходный каталог изображения + + + + + 指定目录 + Укажите каталог + + + + 必须先指定“保存到指定目录”才生效 + Для применения, сначала нужно указать "Сохранить в указанный каталог” + + + + OCR结果保存目录 + Каталог сохранения результатов распознавания + + + + 文件名格式 + Формат имени файла + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文件夹名/文件名 +举例:[OCR]_%name_%date +生成:[OCR]_我的图片_2023-09-01_12-13.txt +添加占位符可以避免旧文件被新文件覆盖。 + Никакого расширения не требуется. Поддерживается вставка следующих заполнителей: +%date дата и время +%name исходное имя папки/имя файла +Пример: [OCR]_%имя_%дата +Создать: [OCR]_My Picture_2023-09-01_12-13.txt +Добавление заполнителей предотвращает перезапись старых файлов новыми. + + + + 日期时间格式 + Формат даты и времени + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + Формат даты для %date в имени файла. Поддерживается вставка следующих заполнителей: +%Y год, %m месяц, %d день, %H час, +%M минут, %S секунд, временная метка %unix +Пример: %Y год %m месяц %d день_%H-%M +Сгенерируется: 1 сентября 2023 г._12-13.txt + + + + 保存文件类型 + Сохранить тип файла + + + + txt 标准格式 + Стандартный формат txt + + + + 含原图片文件名和识别文字 + Содержит имя исходного файла изображения и распознанный текст + + + + p.txt 纯文字格式 + p.txt текстовый формат + + + + 仅输出识别文字,不含图片标题 + Выводится только распознанный текст, без заголовка изображения + + + + txt 单独文件 + txt отдельный файл + + + + 对每张图片,生成同名txt文件,仅输出识别文字 + Для каждого изображения генерируется текстовый файл с тем же именем, и выводится только распознанный текст + + + + md 图文混排 + md смешанная графика и текст + + + + Markdown图文混排格式,可用Markdown阅读器浏览文件 + Графика и текст Markdown формата, вы можете использовать программу чтения Markdown для просмотра файлов + + + + csv 表格文件(Excel) + csv файл формы(Excel) + + + + 将图片信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + Запишисать информацию об изображении и содержимое распознавания в файл таблицы csv. Его можно открыть в Excel и сохранить в формате xlsx. + + + + jsonl 原始信息 + jsonl исходная информация + + + + 每行为一条json数据,便于第三方程序读取操作 + Каждая строка представляет собой фрагмент данных json, который удобно использовать другим программам + + + + 输出忽略空白图片 + Вывод игнорирует пустые изображения + + + + 若图片没有文字或识别失败,也不会输出错误提示信息 + Если на изображении нет текста или распознавание не удалось, сообщение об ошибке не выводится + + + + 递归读取子文件夹 + Рекурсивное чтение вложенных папок + + + + 导入文件夹时,导入子文件夹中全部图片 + При импорте папки импортируются все изображения в подпапке + + + + 其它 + Другие + + + + Configs + + + %1 处理配置项异常: +%2枚举列表为空。 + %1 Обрабатывает исключения из элементов конфигурации: +Список перечислений %2 пуст. + + + + 重置 + Перезагрузить + + + + 重置本页上的设定 + Сбросить настройки на этой странице + + + + 重置设定 + Сброс настроек + + + + 要重置本页的设定吗? + Сбросить настройки на этой странице? + + + + 高级 + Высший + + + + 显示更多高级选项。它们标有 * 号。 +请谨慎修改高级选项。 + Показать дополнительные параметры. Они отмечены *. +Изменяйте дополнительные параметры с осторожностью. + + + + 展开 + Расширять + + + + 折叠 + Сложить + + + + 必须为整数 + Должно быть целым числом + + + + 不能超过 + Не может превышать + + + + 不能低于 + Не может быть ниже, чем + + + + 必须为数字 + Должно быть, это число + + + + 已取消%1的快捷键。 + Комбинация клавиш для %1 была отменена. + + + + 更新热键成功 + Успешно обновление горячих клавиш + + + + %1的快捷键为 %2 + Сочетание клавиш для %1 соответствует %2 + + + + %1 快捷键%2已被注册,请尝试另外的按键组合。 + Комбинация клавиш %1 и %2 уже используется, пожалуйста, попробуйте другую комбинацию клавиш. + + + + %1 快捷键%2无法注册,请尝试另外的按键组合。 + %1 Сочетание клавиш %2 невозможно зарегистрировать, попробуйте другую комбинацию. + + + + 请按下快捷键组合。按【Esc】退出。 + Нажмите комбинацию клавиш быстрого доступа. Нажмите [Esc] для выхода. + + + + 当前快捷键录制已在进行,不能同时录制! + Запись сочетания клавиш в настоящее время выполняется и не может быть записана одновременно! + + + + 无法录制快捷键 + Не удается записать сочетания клавиш + + + + DropArea_ + + + 松手放入文件 + Отпустите и положите в файл + + + + FontPanel + + + 界面 + Интерфейс + + + + 内容 + Содержание + + + + 界面字体: +软件中大部分UI的字体。 + Шрифт интерфейса: +Шрифт большей части интерфейса в программном обеспечении. + + + + 内容字体: +识别结果内容的字体。 + Шрифт содержимого: +Распознать шрифт содержимого результата. + + + + GlobalConfigs + + + 快捷方式 + Ярлык + + + + 桌面 + Рабочий стол + + + + 开始菜单 + Меню "Пуск" + + + + 开机自启 + Автозапуск при включении + + + + 界面和外观 + Интерфейс и внешний вид + + + + 主题 + Тема + + + + 切换主题 + Переключить тему + + + + 字体 + Шрифт + + + + 修改字体 + Изменённый шрифт + + + + 界面与文字大小 + Интерфейс и размер текста + + + + 渲染器 + Средство визуализации + + + + 关闭硬件加速 + Отключить аппаратное ускорение + + + + 若出现界面闪烁、元素错位等界面异常,尝试切换渲染器或者关闭硬件加速 + Если есть проблемы с интерфейсом, такие как мерцание интерфейса, смещение элементов и т.д., попробуйте переключить средство визуализации или отключить аппаратное ускорение + + + + 禁用美化效果 + Отключить визуальные эффекты + + + + 在低配置机器上,禁用动画、阴影等效果可减少部分资源占用 + На компьютерах с низкой конфигурацией отключение таких эффектов, как анимация и тени, может снизить потребление ресурсов + + + + 图片预览默认显示叠加层 + Предпросмотр изображений по умолчанию показывает наложения + + + + 默认开启/关闭叠加层显示 +对所有图片预览组件生效 + Включить/выключить отображение наложения по умолчанию +Действительно для всех компонентов предварительного просмотра изображений + + + + 窗口 + Окно + + + + 启动时缩小到任务栏 + Уменьшить масштаб панели задач при запуске + + + + 软件启动时,不弹出主窗口 + При запуске программы главное окно не появляется + + + + 窗口置顶 + Верхняя часть окна + + + + 捷径:窗口左上角图钉 + Ярлык: кнопка в левом верхнем углу окна + + + + 锁定标签栏 + Заблокировать панель вкладок + + + + 捷径:窗口右上角小锁 + Ярлык: Маленький замок в правом верхнем углу окна + + + + 关闭主窗口时 + При закрытии главного окна + + + + 最小化到系统托盘 + Свернуть в системный трей + + + + 退出应用 + Выйти из приложения + + + + 隐藏托盘图标 + Скрыть значок в трее + + + + 若要弹出位于后台的软件窗口,请在文件管理器中重复启动软件。 +若要彻底退出软件,请从任务管理器中结束进程。 + Чтобы открыть окно программного обеспечения, расположенное в фоновом режиме, несколько раз запустите программное обеспечение в файловом менеджере. +Чтобы полностью выйти из программного обеспечения, завершите процесс из диспетчера задач. + + + + 截图 + скриншот (вычислительная техника) + + + + 截图前隐藏主窗口 + Скрыть главное окно перед тем, как сделать снимок экрана + + + + 截图前,如果主窗口处于前台,则隐藏主窗口 +将会延时等待主窗口关闭 + Прежде чем делать снимок экрана, если главное окно находится на переднем плане, скройте главное окно. +Ожидание закрытия главного окна будет с задержкой + + + + 隐藏等待时间 + Скрыть время ожидания + + + + 秒 + секунда + + + + 服务 + Служить + + + + 允许HTTP服务 + Разрешить HTTP-сервис + + + + Umi-OCR依赖HTTP接口进行本机跨进程通信。如果禁用,将无法使用命令行模式、多开检测等功能。 + Umi-OCR использует интерфейс HTTP для встроенного межпроцессного взаимодействия. Если этот параметр отключен, такие функции, как режим командной строки и обнаружение нескольких открытий, будут недоступны. + + + + + + 重启软件后生效 + Вступит в силу после перезапуска программы + + + + 主机 + Хозяин + + + + 仅本地 + Только местно + + + + 任何可用地址 + Любой доступный адрес + + + + 将允许局域网访问。请开启对应防火墙权限! + Доступ по локальной сети будет разрешен. Пожалуйста, включите соответствующие разрешения брандмауэра! + + + + 将禁止局域网访问。 + Доступ к локальной сети будет запрещен. + + + + 端口 + Порт + + + + 日志 + журнал (вычисления) + + + + 操作 + Действовать + + + + 打开日志保存目录 + Откройте каталог сохранения журнала + + + + 保存的日志级别 + Уровень сохранения журнала + + + + 已禁用日志文件保存 + Сохранение файлов журнала отключено + + + + 请尝试更换软件路径! + Попробуйте изменить путь к программному обеспечению! + + + + 配置文件读写异常 + Исключение для чтения и записи конфигурационного файла + + + + 插件加载失败 + Не удалось загрузить подключаемый модуль + + + + 成功添加快捷方式 + Успешно добавлен ярлык + + + + 添加快捷方式失败 + Не удалось добавить ярлык + + + + 成功移除 %1 个快捷方式 + Успешно удалено %1 ярлыков + + + + 提示 + Подсказка + + + + 没有找到可移除的快捷方式。 + Удаляемый ярлык не был найден. + + + + 渲染器变更 将在重启软件后生效 + Изменения в программе визуализации вступят в силу после перезапуска программы + + + + 端口号不合法 + Номер порта недопустим + + + + 端口号改为%1 + Изменить номер порта на %1 + + + + 原端口号%1被占用, +切换为新端口号%2。 + +若不想看到此通知,请在全局设置关闭高级模式。 + Исходный номер порта %1 занят. +Переключитесь на новый номер порта %2. + +Если вы не хотите видеть это уведомление, отключите расширенный режим в глобальных настройках. + + + + GlobalConfigsPage + + + 1. 关于快捷键、OCR语言等选项,请在各个功能页中进行设置。 +2. 勾选左下角的“高级”按钮,可以显示更多进阶选项。 + 1. Сочетаний клавиш, язык оптического распознавания символов и другие параметры установите их на каждой странице функций. +2. Нажмите кнопку «Дополнительно» в левом нижнем углу, чтобы отобразить дополнительные параметры. + + + + 小贴士 + Советы + + + + HTabBar + + + 窗口置顶 + Верхняя часть окна + + + + 锁定标签栏 + Заблокировать панель вкладок + + + + IconButtonBar + + + 右键菜单 + Меню правой кнопки мыши + + + + 保存图片 + Сохранить изображение + + + + 图片大小:适应窗口 + Размер изображения: адаптируется к размеру окна + + + + 图片大小:实际 + Размер изображения: фактический + + + + 截图 + скриншот (вычислительная техника) + + + + 粘贴 + Вставить + + + + 屏幕截图 + скриншот + + + + 粘贴图片 + Вставить изображение + + + + IgnoreArea + + + 拖入本地图片:OCR预览 +滚轮:缩放 +左键:拖拽 +右键:绘制忽略区域 + +可绘制一个或多个忽略区域矩形框。在执行批量OCR时,完全位于忽略区域内的文本块将被排除。 +比如批量处理影视截图时,可在右上角水印处添加忽略区域,避免输出水印文本。 + Перетащите локальные изображения: предварительный просмотр OCR +Колесо прокрутки: масштабирование +Щелчок левой кнопкой мыши: перетащить +Щелкните правой кнопкой мыши: нарисуйте игнорируемую область. + +Можно нарисовать один или несколько прямоугольников области игнорирования. При пакетном распознавании текстовые блоки, находящиеся полностью в игнорируемой области, будут исключены. +Например, при пакетной обработке снимков экрана фильмов и телепередач вы можете добавить область игнорирования возле водяного знака в правом верхнем углу, чтобы избежать вывода текста водяного знака. + + + + 图像尺寸: + Размер изображения: + + + + 区域数量: + Количество областей : + + + + 撤销 + отмена (вычислительная техника) + + + + 清空 + Очистить + + + + ImageScale + + + 复制图片 + Скопировать изображение + + + + 复制图片失败 + Не удалось скопировать изображение + + + + 打开图片 + Открыть изображение + + + + 打开图片失败 + Не удалось открыть изображение + + + + + 保存图片 + Сохранить изображение + + + + 保存图片失败 + Не удалось сохранить изображение + + + + ImageWithText + + + 图片:复制%1字 + Изображение: Копирование слова %1 + + + + 图片:无选中文字 + Изображение: текст не выделен + + + + 图片:复制全部%1字 + Изображение: скопируйте все слова %1 + + + + 复制  (Ctrl+C) + Копировать   (Ctrl+C) + + + + 全选  (Ctrl+A) + Выбрать все   (Ctrl+A) + + + + 复制图片(Ctrl+X) + Скопировать изображение(Ctrl+X) + + + + 保存图片(Ctrl+S) + Сохранить изображение(Ctrl+S) + + + + 显示/隐藏文字(Tab) + Показать/скрыть текст(Tab) + + + + 用默认应用打开图片 + Откройте изображение с помощью приложения по умолчанию + + + + 删除图片(Ctrl+D) + Удалить изображение(Ctrl+D) + + + + MainWindowManager + + + 欢迎使用 Umi-OCR + Добро пожаловать в Umi-OCR + + + + 已启用后台模式,可通过快捷键使用功能。 + Фоновый режим включен, а функции доступны с помощью сочетаний клавиш. + + + + MarkdownView + + + 打开网页 + Открыть веб-страницу + + + + 链接 + ссылка + + + + MessageBox + + + 取消 + отмены + + + + 确定 + определить + + + + 警告 + Предупреждение + + + + 发生了一点小问题 + Возникла небольшая проблема + + + + 复制 + Копировать + + + + 已复制报错信息 %1 + Сообщение об ошибке %1 было скопировано + + + + 请前往 Issues 页面寻找解答或反馈 + Пожалуйста, перейдите на страницу «Вопросы», чтобы получить ответы или отзывы + + + + 问题反馈 + Сообщить о проблеме + + + + MessageBoxWin + + + 确定 + Подтвердить + + + + 不再提示 + Больше не предлагать + + + + 知道了 + Понятно + + + + MissionCtrlPanel + + + 开始任务 + Запустить задание + + + + 暂停任务 +暂停后可以待机或休眠。 +但是关机或退出软件,将会丢弃任务内容。 + Приостановить задачу +После паузы вы можете перейти в режим ожидания или спящий режим. +Однако выключение или выход из программного обеспечения приведет к удалению содержимого задачи. + + + + 继续任务 + Продолжить задание + + + + 终止任务 +放弃未完成的内容。 + Завершить задание +Отказаться от незаконченного контента. + + + + 正在运行 + Бег + + + + 正在保存 + Сохранение + + + + 已暂停 + Приостановленно + + + + 任务停止 + Задача остановлена + + + + 任务完成 + Миссия выполнена + + + + ModalLayer + + + 保存并返回 + Сохранить и выйти + + + + 返回 + Выйти + + + + Navigation + + + 欢迎使用 Umi-OCR + Добро пожаловать в Umi-OCR + + + + 请选择功能页 + Выберите страницу функции + + + + 当前版本 + Текущая версия + + + + 项目链接 + Ссылка на проект + + + + 官方网站 + Официальный сайт + + + + 插件拓展 + Подключаемые расширения + + + + 问题反馈 + Обратная связь + + + + 功能页 + Страница функций + + + + 请打开对应标签页(如批量OCR、批量文档),再拖入文件。 + Откройте соответствующую вкладку (например, пакетное распознавание текста, пакетный документ), а затем перетащите файл. + + + + OcrManager + + + 文字识别 + Распознавание текста + + + + 操作 + Действовать + + + + + 强制终止任务 + Принудительно завершить выполнение задачи + + + + 应用修改 + Изменение приложения + + + + 当前接口 + Текущий интерфейс + + + + 没有可用的 OCR 插件。 + Плагины OCR отсутствуют. + + + + OCR API 列表中不存在%1 + %1 не существует в списке API OCR + + + + 当前已有%1组任务队列、共%2个任务正在执行。您可【强制终止任务】后修改API。 + В настоящее время существует %1 очередей задач, и всего выполняется %2 задач. Вы можете [принудительно завершить задачу], а затем изменить API. + + + + 无法修改 文字识别接口设置 + Невозможно изменить настройки интерфейса распознавания текста + + + + 文字识别接口应用成功 + Успешное применение интерфейса распознавания текста + + + + 当前API为【%1】 + Текущий API: 【%1 + + + + 文字识别接口应用失败 + Приложение интерфейса распознавания текста не удалось + + + + 当前没有运行中的任务 + На данный момент нет запущенных задач + + + + 当前已有%1组任务队列、共%2个任务正在执行。 +要强制终止全部任务吗? + В настоящее время существует %1 очередей задач, и всего выполняется %2 задач. +Завершить все задачи? + + + + PagesManager + + + 新标签页 + Новая вкладка + + + + 截图OCR + Распознавание снимков экрана + + + + # 截图OCR + +屏幕截图,快捷转文字。也支持粘贴图片。 + # Распознавание снимков экрана + +Скриншоты, быстрое преобразование текста.Также поддерживается вставка изображений. + + + + # 全局设置 + +调节全局设置项,对所有页面生效。 + # Глобальные настройки + +Настройте глобальные настройки, чтобы они вступили в силу на всех страницах. + + + + 批量OCR + Пакетное распознавание текста + + + + # 批量OCR + +导入本地图片或文件夹,批量转换文字。 + +支持格式: + # Пакетное распознавание текста + +Импортируйте локальные изображения или папки и конвертируйте текст в пакетном режиме. + +Поддерживаемые форматы: + + + + 批量文档 + Пакетные документы + + + + # 批量文档识别 + +批量导入文档,提取文字,或生成双层可搜索PDF。 + +支持格式: + # Пакетное распознавание документов + +Импортируйте документы в пакетном режиме, извлекайте текст или создавайте двухслойные PDF-файлы с возможностью поиска. + +Поддерживаемые форматы: + + + + 二维码 + QR-код + + + + # 二维码 + +识别或生成二维码、条形码。 + +支持协议: + # QR-код + +Распознавайте или создавайте QR-коды и штрих-коды. + +Соглашение о поддержке: + + + + 全局设置 + Глобальные настройки + + + + 关于 + Об + + + + # 关于 + # об + + + + PopupManager + + + 确认 + Подтвердить + + + + 取消 + Отмена + + + + PreviewDoc + + + 加密 + Шифрование + + + + 打开文档失败 + Не удалось открыть документ + + + + 请填写正确的密码 + Введите правильный пароль + + + + 密码正确 + Правильный пароль + + + + 文档预览异常 + Исключение для предварительного просмотра документа + + + + 忽略区域说明: +右键拖拽,绘制矩形区域,包含在区域内的文字框将被忽略。可用于排除水印、页眉页脚。 +范围允许填写负数,表示倒数第x页。如-1表示最后一页,-2表示倒数第2页。 +忽略区域的设置对所有文档生效。 + Игнорировать описание зоны: +Щелкните правой кнопкой мыши и перетащите, чтобы нарисовать прямоугольную область. Текстовое поле, содержащееся в этой области, будет игнорироваться. Может использоваться для исключения водяных знаков, верхних и нижних колонтитулов. +Диапазон позволяет заполнять отрицательные числа, указывая x-ю страницу снизу. Например, -1 представляет последнюю страницу, а -2 — предпоследнюю страницу. +Настройка зоны игнорирования действует для всех документов. + + + + 密码: + пароль: + + + + 预览页面 + Страница предварительного просмотра + + + + 预览PDF时,是否预览OCR结果 + При предварительном просмотре PDF следует ли просматривать результаты OCR + + + + OCR页数 + OCR Количество страниц + + + + 忽略区域页数(全局) + Игнорировать региональное количество страниц (глобальное) + + + + + 范围 + Диапазон + + + + 撤销 + Отмена + + + + 清空 + Очистить + + + + PreviewImage + + + 文字 + Язык + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + Отображение распознанного текста, наложенного на изображение +Можно отключить по умолчанию в глобальных настройках + + + + QRCode + + + 获取剪贴板异常 + Получить исключение буфера обмена + + + + 剪贴板中为文本 + Текст в буфере обмена + + + + 无有效图片 + Нет действительного изображения + + + + 导入%1条图片路径 + Импортировать %1 путей к изображениям + + + + 参数有误,或输入内容不合规定。请参照报错指示修改: + Параметры неверны, или входное содержимое является недопустимым. Обратитесь к инструкциям по устранению ошибок, чтобы изменить: + + + + + 生成二维码失败 + Не удалось сгенерировать QR-код + + + + 已复制到剪贴板 + Скопировано в буфер обмена + + + + 识图完成 + Распознавание изображений завершено + + + + 无文字 + Нет текста + + + + 识别失败 + Распознавание не удалось + + + + 扫描二维码 + Сканируйте QR-код + + + + 截图、拖入或粘贴二维码图片 + Сделайте снимок экрана, перетащите или вставьте изображение QR-кода + + + + + 设置 + настроить + + + + 自动刷新 + Автоматическое обновление + + + + 修改文字后,自动生成二维码/条形码 + После изменения текста QR-код/штрих-код генерируется автоматически + + + + 刷新 + Обновить + + + + 生成二维码/条形码 + Создать QR-код/штрих-код + + + + 记录 + Записать + + + + 生成 + Создать + + + + QRCodeConfigs + + + 快捷键 + Клавиатурные сочетания + + + + 屏幕截图 + Скриншот + + + + 粘贴图片 + Вставить изображение + + + + 预处理(一般无需改动) + Предварительная обработка (как правило, никаких изменений не требуется) + + + + 中值滤波 + Медианная фильтрация + + + + 对图像进行平滑处理 +>0 时生效。可填1~9的奇数(不允许偶数) + Сглаживание изображения +>0 вступает в силу. Вы можете вводить нечетные числа от 1 до 9 (четные числа не допускаются) + + + + 锐度增强 + Повышение резкости + + + + 增加图像的锐度 +>0.1 时生效。可填0.1~10的小数 + Увеличение резкости изображения +Оно вступает в силу, когда >0,1. Может заполнять десятичные дроби от 0,1 до 10 + + + + 对比度增强 + Повышение контрастности + + + + 增加图像的对比度 +>0.1 时生效。可填0.1~10的小数 + Увеличение контрастности изображения +Оно вступает в силу, когда >0,1. Может заполнять десятичные дроби от 0,1 до 10 + + + + 转为灰度 + Преобразовать в оттенки серого + + + + 将图像像素转为灰度 + Преобразование пикселей изображения в оттенки серого + + + + 二值化 + Бинаризация + + + + 将图像像素转为纯黑和纯白 +启用了灰度,且二值化 >-1 时生效。可填0~255的整数 + Преобразуйте пиксели изображения в чистый черный и чистый белый цвет. +Действует, когда включены оттенки серого и бинаризация >-1. Можно заполнять целые числа от 0 до 255 + + + + 扫码后的操作 + Действия после сканирования QR-кода + + + + 复制结果 + Копировать результаты + + + + 弹出主窗口 + Открыть главное окно + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + Если после распознавания изображения главное окно свернуто или находится на заднем плане, оно появится на переднем плане + + + + 生成二维码/条形码 + Создать QR-код/штрих-код + + + + 类型 + Тип + + + + 默认二维码: + QR-код по умолчанию: + + + + 宽度 + Ширина + + + + + 填0:自动选择 + Заполните 0: автоматический выбор + + + + + + 像素 + Изображение + + + + 高度 + Высота + + + + 边缘空白 + Поля пустые + + + + 填-1:自动选择 + Заполнение-1: Автоматический выбор + + + + 纠错等级 + Уровень исправления ошибок + + + + 仅适用于: + Доступно только для: + + + + 自动 + автоматический + + + + 其它 + Другие + + + + ResultTextContainer + + + 复制 + Копировать + + + + ResultsTableView + + + 异常状态码:%1 +异常信息:%2 + Код состояния исключения: %1 +Информация об исключении: %2 + + + + 耗时 %1 + Отнимающий много времени %1 + + + + 置信度 %1 + Достоверность %1 + + + + 记录:复制%1字 + Запись: скопировать %1 слов + + + + 记录:无选中文字 + Запись: Нет выделенного текста + + + + 记录:复制全部%1字 + Запись: скопировать все слова %1 + + + + 删除%1条记录 + Удалить %1 записей + + + + 清空记录 + Очистить запись + + + + 复制    (Ctrl+C) + Копировать    (Ctrl+C) + + + + 复制全部  (Ctrl+C 双击) + Скопировать всё (двойной щелчок Ctrl+C) + + + + 选中单个  (Ctrl+A) + Выбрать один  (Ctrl+A) + + + + 选中全部记录(Ctrl+A 双击) + Выбрать все записи (двойной щелчок Ctrl+A) + + + + 删除选中记录 + Удалить выбранные записи + + + + 清空全部记录(Ctrl+D 双击) + Очистить все записи (двойной щелчок Ctrl+D) + + + + 滚动 + Прокрутка + + + + 自动滚动到底部 + Автоматически прокручивать вниз + + + + 右键菜单 + Меню правой кнопки мыши + + + + ScreenshotManager + + + 截图失败 + Снимок экрана не выполнен + + + + 上次截图操作未结束,不能进行新的截图! + Последняя операция создания скриншота не завершена и новые снимки экрана сделать невозможно! + + + + 尚未记录截图区域 + Ни одна область скриншота еще не записана + + + + 显示器: %1 +错误信息: %2 + Монитор: %1 +Сообщение об ошибке: %2 + + + + 重复截图失败 + Повторный снимок экрана не удался + + + + 未找到匹配的屏幕 + Подходящего экрана не найдено + + + + 屏幕设备名称不相同: +%1 +%2 + Имена экранных устройств не совпадают: +%1 +%2 + + + + ScreenshotOCR + + + 获取剪贴板异常 + Получить исключение буфера обмена + + + + 剪贴板中为文本 + Текст в буфере обмена + + + + 无有效图片 + Нет действительного изображения + + + + 导入%1条图片路径 + Импортировать %1 путей к изображениям + + + + 依然关闭 + Всё ещё закрыто + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + Задача выполняется. +Завершить задание и закрыть страницу? + + + + + 屏幕截图 + Скриншот + + + + + 粘贴图片 + Вставить изображение + + + + 截图识别任务异常 + Исключение задачи идентификации скриншота + + + + 已复制到剪贴板 + Скопировано в буфер обмена + + + + 识图完成 + Распознавание изображений завершено + + + + 无文字 + Нет текста + + + + 识别失败 + Распознавание не удалось + + + + + 停止任务 + Остановить задачу + + + + 文字 + Язык + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + Отображение распознанного текста, наложенного на изображение +Можно отключить по умолчанию в глобальных настройках + + + + 截图、拖入或粘贴图片 + Сделайте снимок экрана, перетащите или вставьте изображение + + + + 设置 + Настроить + + + + 记录 + Записать + + + + ScreenshotOcrConfigs + + + OCR文本后处理 + Постобработка полученного текста + + + + 快捷键 + Клавиатурные сочетания + + + + 屏幕截图 + Скриншот + + + + 粘贴图片 + Вставить изображение + + + + 重复截图 + Дублировать скриншот + + + + 重新截取上一次截图的范围 + Восстановить диапазон последнего скриншота + + + + 识图后的操作 + Действия после распознавания картинок + + + + 复制结果 + Результат копирования + + + + 弹出主窗口 + Открыть главное окно + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + Если после распознавания изображения главное окно свернуто или находится на заднем плане, оно появится на переднем плане + + + + 其它 + Другие + + + + Size_ + + + 1.0 + 1.0 + + + + SystemTray + + + 打开主窗口 + Открыть главное окно + + + + 退出 Umi-OCR + Выйти из Umi-OCR + + + + Theme + + + 珍珠白 + жемчужно-белый + + + + 云墨黑 + Облако черное + + + + 赛博朋克 + киберпанк + + + + UtilsConfigDicts + + + 任务完成后的操作 + Операция после завершения задачи + + + + 系统 + Система + + + + 无 + Нет + + + + 关机 + Выключить + + + + 休眠 + Спящий режим + + + + %1 系统暂不支持电源控制! + %1 система не поддерживает управление питанием! + + + + 继续%1 + Продолжить %1 + + + + 取消%1 + Отмена %1 + + + + 系统即将%1 + Система скоро %1 + + + + 排版解析方案 + Схема анализа набора текста + + + + 按什么方式,解析和排序图片中的文字块 + Как анализировать и сортировать текстовые блоки на изображениях + + + + 多栏-按自然段换行 + Перенос нескольких столбцов в соответствии с естественными абзацами + + + + 多栏-总是换行 + Несколько столбцов – всегда переносить + + + + 多栏-无换行 + Несколько столбцов – без разрывов строк + + + + 单栏-按自然段换行 + Перенос в один столбец с помощью обычного абзаца + + + + 单栏-总是换行 + Один столбец – всегда переносить + + + + 单栏-无换行 + Один столбец – без разрывов строк + + + + 单栏-保留缩进 + Одна колонка - сохранять отступы + + + + 不做处理 + Без обработки + + + + 优先内部 + Расставить приоритеты внутри + + + + 只允许内部 + Разрешен только внутренний доступ + + + + 只允许外部 + Допускается только внешнее + + + + 禁用所有通知 + Отключить все уведомления + + + + 跟随全局设定 + Использовать глобальные настройки + + + + 通知弹窗类型 + Тип всплывающего окна уведомления + + + + 通知类型已更改 + Тип уведомления был изменен + + + diff --git a/dev-tools/i18n/release/ta.ts b/dev-tools/i18n/release/ta.ts new file mode 100644 index 0000000..03191d1 --- /dev/null +++ b/dev-tools/i18n/release/ta.ts @@ -0,0 +1,2556 @@ + + + + + About + + + 开源、免费的离线OCR软件 + திறந்த மூல, இலவச இணைப்பில்லாத OCR மென்பொருள் + + + + 当前版本 + தற்போதைய பதிப்பு + + + + 项目链接 + திட்ட இணைப்பு + + + + 官方网站 + அதிகாரப்பூர்வ வலைத்தளம் + + + + 插件拓展 + செருகுநிரல் விரிவாக்கம் + + + + 问题反馈 + கேள்வி கருத்து + + + + 发布地址 + முகவரியை வெளியிடுங்கள் + + + + 许可协议 + உரிம ஒப்பந்தம் + + + + 作者 + ஆசிரியர் + + + + 译者 + மொழிபெயர்ப்பாளர் + + + + 运行环境信息(如需请求协助,请提供给开发者) + இயக்க நேர சூழல் செய்தி (உங்களுக்கு உதவி தேவைப்பட்டால், அதை டெவலப்பருக்கு வழங்கவும்) + + + + 复制 + நகலெடு + + + + AsynFilesLoader + + + 正在载入 %1 个文件: +%2 + %1 கோப்புகளை ஏற்றுகிறது: + %2 + + + + BatchDOC + + + 加密 + குறியாக்கம் + + + + %1个加密文档 + %1 மறைகுறியாக்கப்பட்ட ஆவணம் + + + + 请点击文件名填写密码 + கடவுச்சொல்லை நிரப்ப கோப்பு பெயரைக் சொடுக்கு செய்க + + + + 文档已加密 + ஆவணங்கள் குறியாக்கம் செய்யப்பட்டுள்ளன + + + + 【%1】 +请点击文档名,设置密码 + 1%1 + ஆவண பெயரைக் சொடுக்கு செய்து கடவுச்சொல்லை அமைக்கவும் + + + + 失败 + தோல்வி + + + + 排队 + வரிசை + + + + 依然关闭 + இன்னும் மூடப்பட்டது + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + பணி நடந்து வருகிறது. + பணியை முடித்து பக்கத்தை மூட விரும்புகிறீர்களா? + + + + + 文档 + ஆவணம் + + + + 状态 + மாநிலம் + + + + 打开文档 + ஆவணத்தைத் திறக்கவும் + + + + 拖入文档或文件夹 + ஒரு ஆவணம் அல்லது கோப்புறையில் இழுக்கவும் + + + + 清空 + தெளிவான + + + + 文档识别异常 + ஆவண ஏற்பு விதிவிலக்கு + + + + 批量识别完成 + தொகுதி அடையாளம் முடிந்தது + + + + 页数 + பக்கங்களின் எண்ணிக்கை + + + + 请选择文档 + ஆவணத்தைத் தேர்ந்தெடுக்கவும் + + + + 设置 + அமைக்கவும் + + + + 记录 + பதிவு + + + + BatchDOCConfigs + + + OCR文本后处理 + OCR உரை பிந்தைய செயலாக்கம் + + + + 文档处理 + ஆவண செயலாக்கம் + + + + 内容提取模式 + உள்ளடக்க பிரித்தெடுத்தல் முறை + + + + 若一页文档既存在图片又存在文本,如何进行处理 + ஒரு பக்கத்தில் படங்கள் மற்றும் உரை இரண்டும் இருந்தால் ஆவணத்தை எவ்வாறு செயலாக்குவது + + + + 混合OCR/原文本 + கலப்பு OCR/அசல் உரை + + + + 整页强制OCR + முழு பக்கம் கட்டாய OCR + + + + 仅OCR图片 + OCR படங்கள் மட்டுமே + + + + 仅拷贝原有文本 + அசல் உரையை மட்டுமே நகலெடுக்கவும் + + + + 批量任务 + தொகுதி பணிகள் + + + + 保存到 + சேமிக்கவும் + + + + 文档原目录 + அசல் ஆவண அடைவு + + + + + 指定目录 + கோப்பகத்தைக் குறிப்பிடவும் + + + + 必须先指定“保存到指定目录”才生效 + "குறிப்பிட்ட கோப்பகத்திற்கு சேமி" இது நடைமுறைக்கு வருவதற்கு முன்பு நீங்கள் குறிப்பிட வேண்டும் + + + + OCR结果保存目录 + OCR முடிவுகள் கோப்பகத்தை சேமிக்கும் + + + + 文件名格式 + கோப்பு பெயர் வடிவம் + + + + 日期时间格式 + தேதி மற்றும் நேர வடிவம் + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + கோப்பு பெயரில் %தேதியின் தேதி வடிவம். பின்வரும் ஒதுக்கிடங்களை செருகுவதை ஆதரிக்கவும்: +%Y ஆண்டு, %m மாதம், %d நாள், %h மணிநேரம், +%மீ நிமிடங்கள், %கள் வினாடிகள், %யூனிக்ச் நேர முத்திரை +எடுத்துக்காட்டு: %y ஆண்டு %m மாதம் %d நாள்_ %h- %m +உருவாக்கப்பட்டது: செப்டம்பர் 1, 2023_12-13.txt + + + + 保存文件类型 + கோப்பு வகையைச் சேமிக்கவும் + + + + layered.pdf 双层可搜索文档 + அடுக்கு. பி.டி.எஃப் இரட்டை அடுக்கு தேடக்கூடிய ஆவணங்கள் + + + + text.pdf 单层纯文本文档 + text.pdf ஒற்றை அடுக்கு எளிய உரை ஆவணம் + + + + 创建空白PDF文档,只写入识别文字,不含图片 + ஒரு வெற்று PDF ஆவணத்தை உருவாக்கவும், உரையை மட்டுமே அங்கீகரிக்கவும், படங்கள் இல்லை + + + + txt 标准格式 + TXT நிலையான வடிவம் + + + + 含识别文字和页数信息 + உரை மற்றும் பக்க எண்ணிக்கையை அடையாளம் காண்பது பற்றிய தகவல்கள் உட்பட + + + + p.txt 纯文字格式 + p.txt எளிய உரை வடிவம் + + + + 输出所有识别文字 + அங்கீகரிக்கப்பட்ட அனைத்து உரையையும் வெளியீடு செய்யுங்கள் + + + + csv 表格文件(Excel) + சி.எச்.வி அட்டவணை கோப்பு (எக்செல்) + + + + 将页数信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + பக்க எண்ணிக்கை தகவல்களை எழுதுங்கள் மற்றும் காபிம அட்டவணை கோப்பில் உள்ளடக்கத்தை அடையாளம் காண்பது. இதை எக்செல் மூலம் திறந்து எக்ச்எல்எச்எக்ச் வடிவமாக சேமிக்க முடியும். + + + + jsonl 原始信息 + JSONL அசல் செய்தி + + + + 每行为一条json数据,便于第三方程序读取操作 + ஒவ்வொரு நடத்தையும் ஒரு சாதொபொகு தரவு, இது மூன்றாம் தரப்பு நிரல்களைப் படிக்கவும் செயல்படவும் வசதியானது + + + + 忽略空白页 + வெற்று பக்கங்களை புறக்கணிக்கவும் + + + + 若某一页没有文字或识别失败,也不会输出错误提示信息 + ஒரு குறிப்பிட்ட பக்கத்தில் உரை இல்லை அல்லது ஏற்பு தோல்வியுற்றால், பிழை செய்தி வெளியீடாக இருக்காது + + + + 递归读取子文件夹 + மறுநிகழ்வு துணை கோப்புறைகளைப் படியுங்கள் + + + + 点击表格,可设置更多内容 + மேலும் உள்ளடக்கத்தை அமைக்க அட்டவணையில் சொடுக்கு செய்க + + + + 导入文件夹时,导入子文件夹中全部文档 + கோப்புறைகளை இறக்குமதி செய்யும் போது, அனைத்து ஆவணங்களையும் துணைக் கோப்புறையில் இறக்குமதி செய்க + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文档名 +%range 识别页数范围。只有识别页数小于总页数时才会显示。 +举例:[OCR]_%name%range_%date +生成:[OCR]_文档A(p2-10)_20230901_1213.txt +添加占位符可以避免旧文件被新文件覆盖。 + நீட்டிப்பு பெயரை நிரப்பத் தேவையில்லை. பின்வரும் ஒதுக்கிடங்களை செருகுவதை ஆதரிக்கவும்: + %date தேதிநேரம் + %name அசல் ஆவண பெயர் + %range பக்க எண்ணிக்கை வரம்பை அடையாளம் காட்டுகிறது. பக்கங்களின் எண்ணிக்கை அடையாளம் காணப்பட்டால் மட்டுமே அது காண்பிக்கப்படும். + எடுத்துக்காட்டு: [OCR] _%பெயர்%வரம்பு_%தேதி + உருவாக்கு: [OCR] ஆவணம் (_D) A (P2-10) _20230901_1213.txt + பிளேச்ஓல்டர்களைச் சேர்ப்பது பழைய கோப்புகளைப் புதிய கோப்புகளால் மேலெழுதப்படுவதைத் தடுக்கலாம். + + + + 保留原有图片,叠加一层透明文字,可以搜索和复制 + அசல் படத்தை வைத்து, தேடவும் நகலெடுக்கவும் வெளிப்படையான உரையின் ஒரு அடுக்கை மிகைப்படுத்தவும் + + + + 其它 + மற்றொன்று + + + + BatchOCR + + + 依然关闭 + இன்னும் மூடப்பட்டது + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + பணி நடந்து வருகிறது. + பணியை முடித்து பக்கத்தை மூட விரும்புகிறீர்களா? + + + + 排队 + வரிசை + + + + 处理 + ஒப்பந்தம் + + + + %1 张图片识别失败! + %1 பட ஏற்பு தோல்வியடைந்தது! + + + + 批量识别完成 + தொகுதி அடையாளம் முடிந்தது + + + + 批量识别任务异常 + மொத்த அடையாள பணி விதிவிலக்குகள் + + + + + 图片 + படம் + + + + 耗时 + நேரம் எடுக்கும் + + + + 状态 + மாநிலம் + + + + 打开图片 + படத்தைத் திறக்கவும் + + + + 拖入图片或文件夹 + ஒரு படம் அல்லது கோப்புறையில் இழுக்கவும் + + + + 清空 + தெளிவான + + + + 请选择图片 + ஒரு படத்தைத் தேர்ந்தெடுக்கவும் + + + + 设置 + அமைக்கவும் + + + + 记录 + பதிவு + + + + BatchOCRConfigs + + + OCR文本后处理 + OCR உரை பிந்தைய செயலாக்கம் + + + + 忽略区域 + பகுதியை புறக்கணிக்கவும் + + + + 进入设置 + அமைப்புகளுக்குச் செல்லுங்கள் + + + + 批量任务 + தொகுதி பணிகள் + + + + 保存到 + சேமிக்கவும் + + + + 图片原目录 + அசல் பட அடைவு + + + + + 指定目录 + கோப்பகத்தைக் குறிப்பிடவும் + + + + 必须先指定“保存到指定目录”才生效 + "குறிப்பிட்ட கோப்பகத்திற்கு சேமி" இது நடைமுறைக்கு வருவதற்கு முன்பு நீங்கள் குறிப்பிட வேண்டும் + + + + OCR结果保存目录 + OCR முடிவுகள் கோப்பகத்தை சேமிக்கும் + + + + 文件名格式 + கோப்பு பெயர் வடிவம் + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文件夹名/文件名 +举例:[OCR]_%name_%date +生成:[OCR]_我的图片_2023-09-01_12-13.txt +添加占位符可以避免旧文件被新文件覆盖。 + நீட்டிப்பு பெயரை நிரப்பத் தேவையில்லை. பின்வரும் ஒதுக்கிடங்களை செருகுவதை ஆதரிக்கவும்: + %date தேதி நேரம் + %name அசல் கோப்புறை பெயர்/கோப்புப் பெயர் + எடுத்துக்காட்டு: [OCR] _%பெயர்_%தேதி + உருவாக்கு: [OCR] _my picture_2023-09-01_12-13.txt + பிளேச்ஓல்டர்களைச் சேர்ப்பது பழைய கோப்புகளைப் புதிய கோப்புகளால் மேலெழுதப்படுவதைத் தடுக்கலாம். + + + + 日期时间格式 + தேதி மற்றும் நேர வடிவம் + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + கோப்பு பெயரில் %தேதியின் தேதி வடிவம். பின்வரும் ஒதுக்கிடங்களை செருகுவதை ஆதரிக்கவும்: + %Y ஆண்டு, %m மாதம், %d நாள், %h மணிநேரம், + %மீ நிமிடங்கள், %கள் வினாடிகள், %யூனிக்ச் நேர முத்திரை + எடுத்துக்காட்டு: %y ஆண்டு %m மாதம் %d நாள்_ %h- %m + உருவாக்கப்பட்டது: செப்டம்பர் 1, 2023_12-13.txt + + + + 保存文件类型 + கோப்பு வகையைச் சேமிக்கவும் + + + + txt 标准格式 + TXT நிலையான வடிவம் + + + + 含原图片文件名和识别文字 + அசல் பட கோப்பு பெயர் மற்றும் அடையாள உரை ஆகியவை அடங்கும் + + + + p.txt 纯文字格式 + p.txt எளிய உரை வடிவம் + + + + 仅输出识别文字,不含图片标题 + வெளியீட்டு அங்கீகார உரை மட்டுமே, பட தலைப்பு உட்பட அல்ல + + + + txt 单独文件 + txt தனி கோப்பு + + + + 对每张图片,生成同名txt文件,仅输出识别文字 + ஒவ்வொரு படத்திற்கும், அதே பெயரின் TXT கோப்பை உருவாக்கவும், அடையாளம் காணும் உரை மட்டுமே வெளியீடு + + + + md 图文混排 + எம்.டி கலப்பு படம் மற்றும் உரை + + + + Markdown图文混排格式,可用Markdown阅读器浏览文件 + மார்க் பேரூர் கிராஃபிக் மற்றும் உரை கலப்பு வடிவம், கோப்புகளை உலவ மார்க்க்டவுன் ரீடரைப் பயன்படுத்தவும் + + + + csv 表格文件(Excel) + சி.எச்.வி அட்டவணை கோப்பு (எக்செல்) + + + + 将图片信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + பட செய்தி மற்றும் அடையாள உள்ளடக்கத்தை காபிம அட்டவணை கோப்பில் எழுதுங்கள். இதை எக்செல் மூலம் திறந்து எக்ச்எல்எச்எக்ச் வடிவமாக சேமிக்க முடியும். + + + + jsonl 原始信息 + JSONL அசல் செய்தி + + + + 每行为一条json数据,便于第三方程序读取操作 + ஒவ்வொரு நடத்தையும் ஒரு சாதொபொகு தரவு, இது மூன்றாம் தரப்பு நிரல்களைப் படிக்கவும் செயல்படவும் வசதியானது + + + + 输出忽略空白图片 + வெளியீடு வெற்று படங்களை புறக்கணிக்கிறது + + + + 若图片没有文字或识别失败,也不会输出错误提示信息 + படத்திற்கு உரை இல்லை அல்லது ஏற்பு தோல்வியுற்றால், பிழை செய்தி வெளியீடாக இருக்காது + + + + 递归读取子文件夹 + மீண்டும்மீண்டும் துணை கோப்புறைகளைப் படியுங்கள் + + + + 导入文件夹时,导入子文件夹中全部图片 + கோப்புறைகளை இறக்குமதி செய்யும் போது, அனைத்து படங்களையும் துணை கோப்புறையில் இறக்குமதி செய்க + + + + 其它 + மற்றொன்று + + + + Configs + + + %1 处理配置项异常: +%2枚举列表为空。 + %1 உள்ளமைவு உருப்படி விதிவிலக்கைக் கையாளுகிறது: + %2 கணக்கீட்டு பட்டியல் காலியாக உள்ளது. + + + + 重置 + மீட்டமை + + + + 重置本页上的设定 + இந்த பக்கத்தில் அமைப்புகளை மீட்டமைக்கவும் + + + + 重置设定 + அமைப்புகளை மீட்டமைக்கவும் + + + + 要重置本页的设定吗? + இந்த பக்கத்தில் உள்ள அமைப்புகளை மீட்டமைக்க விரும்புகிறீர்களா? + + + + 高级 + மேம்பட்டது + + + + 显示更多高级选项。它们标有 * 号。 +请谨慎修改高级选项。 + மேலும் மேம்பட்ட விருப்பங்களைக் காட்டு. அவை குறிக்கப்பட்டுள்ளன *. + மேம்பட்ட விருப்பங்களை எச்சரிக்கையுடன் மாற்றவும். + + + + 展开 + விரிவாக்கு + + + + 折叠 + மடிப்பு + + + + 必须为整数 + ஒரு முழு எண்ணாக இருக்க வேண்டும் + + + + 不能超过 + மீற முடியாது + + + + 不能低于 + விட குறைவாக இருக்க முடியாது + + + + 必须为数字 + ஒரு எண்ணாக இருக்க வேண்டும் + + + + 已取消%1的快捷键。 + %1 க்கான குறுக்குவழி விசை ரத்து செய்யப்பட்டுள்ளது. + + + + 更新热键成功 + ஆட்ச்கி வெற்றிகரமாக புதுப்பிக்கப்பட்டது + + + + %1的快捷键为 %2 + %1 க்கான குறுக்குவழி விசை %2 ஆகும் + + + + %1 快捷键%2已被注册,请尝试另外的按键组合。 + %1 குறுக்குவழி விசை %2 பதிவு செய்யப்பட்டுள்ளது, தயவுசெய்து மற்றொரு முக்கிய கலவையை முயற்சிக்கவும். + + + + %1 快捷键%2无法注册,请尝试另外的按键组合。 + %1 குறுக்குவழி விசை %2 ஐ பதிவு செய்ய முடியாது, தயவுசெய்து மற்றொரு முக்கிய கலவையை முயற்சிக்கவும். + + + + 请按下快捷键组合。按【Esc】退出。 + குறுக்குவழி விசை கலவையை அழுத்தவும். வெளியேற [ESC] ஐ அழுத்தவும். + + + + 当前快捷键录制已在进行,不能同时录制! + குறுக்குவழி விசைகளின் பதிவு தற்போது நடந்து வருகிறது, அதே நேரத்தில் பதிவு செய்ய முடியாது! + + + + 无法录制快捷键 + குறுக்குவழி விசைகளை பதிவு செய்ய முடியாது + + + + DropArea_ + + + 松手放入文件 + கோப்பை விட்டுவிடுங்கள் + + + + FontPanel + + + 界面 + இடைமுகம் + + + + 内容 + உள்ளடக்கம் + + + + 界面字体: +软件中大部分UI的字体。 + இடைமுக எழுத்துரு: + மென்பொருளில் பெரும்பாலான இடைமுகம் களுக்கான எழுத்துருக்கள். + + + + 内容字体: +识别结果内容的字体。 + உள்ளடக்க எழுத்துரு: +முடிவு உள்ளடக்கத்தை அடையாளம் காணும் எழுத்துரு. + + + + GlobalConfigs + + + 快捷方式 + குறுக்குவழிகள் + + + + 桌面 + டெச்க்டாப் + + + + 开始菜单 + தொடக்க பட்டியல் + + + + 开机自启 + ஆற்றல் மற்றும் தொடங்க + + + + 界面和外观 + இடைமுகம் மற்றும் தோற்றம் + + + + 主题 + கருப்பொருள் + + + + 切换主题 + கருப்பொருள் சுவிட்ச் + + + + 字体 + எழுத்துரு + + + + 修改字体 + எழுத்துருக்களை மாற்றவும் + + + + 界面与文字大小 + இடைமுகம் மற்றும் உரை அளவு + + + + 渲染器 + ரெண்டரர் + + + + 关闭硬件加速 + வன்பொருள் முடுக்கம் அணைக்கவும் + + + + 若出现界面闪烁、元素错位等界面异常,尝试切换渲染器或者关闭硬件加速 + இடைமுக ஒளிரும், உறுப்பு தவறாக வடிவமைத்தல் மற்றும் பிற இடைமுக அசாதாரணங்கள் ஏற்பட்டால், ரெண்டரரை மாற்ற முயற்சிக்கவும் அல்லது வன்பொருள் முடுக்கம் அணைக்கவும் + + + + 禁用美化效果 + அழகுபடுத்தும் விளைவை முடக்கு + + + + 在低配置机器上,禁用动画、阴影等效果可减少部分资源占用 + குறைந்த உள்ளமைக்கப்பட்ட இயந்திரங்களில், அனிமேசன்கள், நிழல்கள் மற்றும் பிற விளைவுகளை முடக்குவது சில வள பயன்பாட்டைக் குறைக்கும் + + + + 图片预览默认显示叠加层 + பட முன்னோட்டம் இயல்புநிலையாக மேலடுக்குகளைக் காட்டுகிறது + + + + 默认开启/关闭叠加层显示 +对所有图片预览组件生效 + இயல்புநிலையாக மேலடுக்கு காட்சியை ஆன்/ஆஃப் + அனைத்து பட முன்னோட்ட கூறுகளுக்கும் பயனுள்ளதாக இருக்கும் + + + + 窗口 + சாளரம் + + + + 启动时缩小到任务栏 + தொடக்கத்தில் பணிப்பட்டிக்கு சுருங்கவும் + + + + 软件启动时,不弹出主窗口 + மென்பொருள் தொடங்கும் போது, பிரதான சாளரம் பாப் அப் செய்யாது + + + + 窗口置顶 + மேல் சாளரம் + + + + 捷径:窗口左上角图钉 + குறுக்குவழி: சாளரத்தின் மேல் இடது மூலையில் புச்பின் + + + + 锁定标签栏 + டேக் பட்டியை பூட்டவும் + + + + 捷径:窗口右上角小锁 + குறுக்குவழி: சாளரத்தின் மேல் வலது மூலையில் சிறிய பூட்டு + + + + 关闭主窗口时 + முதன்மையான சாளரத்தை மூடும்போது + + + + 最小化到系统托盘 + கணினி தட்டில் குறைக்கவும் + + + + 退出应用 + பயன்பாட்டிலிருந்து வெளியேறவும் + + + + 隐藏托盘图标 + தட்டு ஐகானை மறைக்கவும் + + + + 若要弹出位于后台的软件窗口,请在文件管理器中重复启动软件。 +若要彻底退出软件,请从任务管理器中结束进程。 + பின்னணியில் அமைந்துள்ள மென்பொருள் சாளரத்தைப் பாப் செய்ய, கோப்பு மேலாளரில் மென்பொருளை மீண்டும்மீண்டும் மறுதொடக்கம் செய்யுங்கள். + மென்பொருளை முழுவதுமாக வெளியேற, பணி மேலாளரிடமிருந்து செயல்முறையை முடிக்கவும். + + + + 截图 + திரைகாட்சி + + + + 截图前隐藏主窗口 + திரைக்காட்சி முன் முதன்மையான சாளரத்தை மறைக்கவும் + + + + 截图前,如果主窗口处于前台,则隐藏主窗口 +将会延时等待主窗口关闭 + ச்கிரீன்சாட்டுக்கு முன், முதன்மையான சாளரம் முன்புறத்தில் இருந்தால், முதன்மையான சாளரத்தை மறைக்கவும் + முதன்மையான சாளரம் மூடப்படும் வரை காத்திருக்கும் + + + + 隐藏等待时间 + காத்திருப்பு நேரத்தை மறைக்கவும் + + + + 秒 + இரண்டாவது + + + + 服务 + பணி + + + + 允许HTTP服务 + HTTP சேவைகளை அனுமதிக்கவும் + + + + Umi-OCR依赖HTTP接口进行本机跨进程通信。如果禁用,将无法使用命令行模式、多开检测等功能。 + உமி-ஆக்யூட் சொந்த குறுக்கு-செயல்முறை தகவல்தொடர்புக்கான HTTP இடைமுகத்தை நம்பியுள்ளது. முடக்கப்பட்டால், நீங்கள் கட்டளை வரி பயன்முறை, மல்டி-ஆன் கண்டறிதல் மற்றும் பிற செயல்பாடுகளைப் பயன்படுத்த முடியாது. + + + + + + 重启软件后生效 + மென்பொருளை மறுதொடக்கம் செய்து நடைமுறைக்கு விடுங்கள் + + + + 主机 + புரவலன் + + + + 仅本地 + உள்ளக மட்டும் + + + + 任何可用地址 + கிடைக்கக்கூடிய எந்த முகவரி + + + + 将允许局域网访问。请开启对应防火墙权限! + லேன் அணுகல் அனுமதிக்கப்படும். தொடர்புடைய ஃபயர்வால் அனுமதிகளை இயக்கவும்! + + + + 将禁止局域网访问。 + உள்ளக பகுதி பிணையம் அணுகல் தடைசெய்யப்படும். + + + + 端口 + துறைமுகம் + + + + 日志 + பதிவு + + + + 操作 + செயல்படுங்கள் + + + + 打开日志保存目录 + பதிவு சேமி கோப்பகத்தைத் திறக்கவும் + + + + 保存的日志级别 + சேமித்த பதிவு நிலை + + + + 已禁用日志文件保存 + பதிவு கோப்பு சேமிப்பு முடக்கப்பட்டது + + + + 请尝试更换软件路径! + மென்பொருள் பாதையை மாற்ற முயற்சிக்கவும்! + + + + 配置文件读写异常 + உள்ளமைவு கோப்பு படித்து எழுது விதிவிலக்கு + + + + 插件加载失败 + சொருகி ஏற்றுதல் தோல்வியடைந்தது + + + + 成功添加快捷方式 + குறுக்குவழிகளை வெற்றிகரமாகச் சேர்த்தது + + + + 添加快捷方式失败 + குறுக்குவழிகளைச் சேர்க்கத் தவறிவிட்டது + + + + 成功移除 %1 个快捷方式 + %1 குறுக்குவழி வெற்றிகரமாக அகற்றப்பட்டது + + + + 提示 + குறிப்பு + + + + 没有找到可移除的快捷方式。 + நீக்கக்கூடிய குறுக்குவழி எதுவும் காணப்படவில்லை. + + + + 渲染器变更 将在重启软件后生效 + மென்பொருளை மறுதொடக்கம் செய்த பிறகு ரெண்டரர் மாற்றங்கள் நடைமுறைக்கு வரும் + + + + 端口号不合法 + சட்டவிரோத துறைமுகம் எண் + + + + 端口号改为%1 + துறைமுகம் எண்ணை %1 ஆக மாற்றவும் + + + + 原端口号%1被占用, +切换为新端口号%2。 + +若不想看到此通知,请在全局设置关闭高级模式。 + அசல் துறைமுகம் எண் %1 ஆக்கிரமிக்கப்பட்டுள்ளது. + புதிய துறைமுகம் எண் %2 க்கு மாறவும். + + இந்த அறிவிப்பைப் பார்க்க நீங்கள் விரும்பவில்லை என்றால், தயவுசெய்து உலகளாவிய அமைப்புகளில் மேம்பட்ட பயன்முறையை அணைக்கவும். + + + + GlobalConfigsPage + + + 1. 关于快捷键、OCR语言等选项,请在各个功能页中进行设置。 +2. 勾选左下角的“高级”按钮,可以显示更多进阶选项。 + 1. குறுக்குவழி விசைகள், OCR மொழி போன்ற விருப்பங்களுக்கு, தயவுசெய்து அவற்றை ஒவ்வொரு செயல்பாட்டு பக்கத்திலும் அமைக்கவும். + 2. மேலும் மேம்பட்ட விருப்பங்களைக் காண்பிக்க கீழ் இடது மூலையில் உள்ள "மேம்பட்ட" பொத்தானைச் சரிபார்க்கவும். + + + + 小贴士 + உதவிக்குறிப்புகள் + + + + HTabBar + + + 窗口置顶 + மேல் சாளரம் + + + + 锁定标签栏 + டேக் பட்டியை பூட்டவும் + + + + IconButtonBar + + + 右键菜单 + வலது சொடுக்கு பட்டியல் + + + + 保存图片 + படத்தை சேமிக்கவும் + + + + 图片大小:适应窗口 + பட அளவு: சாளரத்திற்கு ஏற்றவாறு + + + + 图片大小:实际 + பட அளவு: உண்மையானது + + + + 截图 + திரை காட்சி + + + + 粘贴 + ஒட்டவும் + + + + 屏幕截图 + திரை காட்சி + + + + 粘贴图片 + படத்தை ஒட்டவும் + + + + IgnoreArea + + + 拖入本地图片:OCR预览 +滚轮:缩放 +左键:拖拽 +右键:绘制忽略区域 + +可绘制一个或多个忽略区域矩形框。在执行批量OCR时,完全位于忽略区域内的文本块将被排除。 +比如批量处理影视截图时,可在右上角水印处添加忽略区域,避免输出水印文本。 + உள்ளக படத்தில் இழுக்கவும்: OCR முன்னோட்டம் + உருள்: பெரிதாக்கு + இடது பொத்தான்: இழுத்து விடுங்கள் + வலது கிளிக்: புறக்கணிக்கப்பட்ட பகுதியை வரையவும் + + ஒன்று அல்லது அதற்கு மேற்பட்ட புறக்கணிக்கப்பட்ட பகுதி செவ்வக பெட்டிகளை வரையலாம். தொகுதி OCR ஐ நிகழ்த்தும்போது, புறக்கணிக்கப்பட்ட பகுதிக்குள் இருக்கும் உரை தொகுதிகள் விலக்கப்படும். + எடுத்துக்காட்டாக, திரைப்படம் மற்றும் தொலைக்காட்சி திரை சாட்களின் தொகுதி செயலாக்கம் போது, வாட்டர்மார்க் உரையை வெளியிடுவதைத் தவிர்க்க மேல் வலது மூலையில் உள்ள வாட்டர்மார்க்கில் புறக்கணிக்கப்பட்ட பகுதியைச் சேர்க்கலாம். + + + + 图像尺寸: + பட அளவு: + + + + 区域数量: + பகுதிகளின் எண்ணிக்கை: + + + + 撤销 + ரத்து செய்யுங்கள் + + + + 清空 + தெளிவான + + + + ImageScale + + + 复制图片 + படத்தை நகலெடுக்கவும் + + + + 复制图片失败 + படத்தை நகலெடுப்பதில் தோல்வி + + + + 打开图片 + படத்தைத் திறக்கவும் + + + + 打开图片失败 + படத்தைத் திறக்கத் தவறிவிட்டது + + + + + 保存图片 + படத்தை சேமிக்கவும் + + + + 保存图片失败 + படத்தை சேமிப்பதில் தோல்வி + + + + ImageWithText + + + 图片:复制%1字 + படம்: %1 வார்த்தையை நகலெடுக்கவும் + + + + 图片:无选中文字 + படம்: தேர்வு இல்லாமல் சீன எழுத்துக்கள் + + + + 图片:复制全部%1字 + படம்: அனைத்து %1 வார்த்தையையும் நகலெடுக்கவும் + + + + 复制  (Ctrl+C) + நகலெடு  (Ctrl+C) + + + + 全选  (Ctrl+A) + அனைத்தையும் தேர்ந்தெடுக்கவும் (Ctrl+A) + + + + 复制图片(Ctrl+X) + படத்தை நகலெடுக்கவும் (Ctrl+x) + + + + 保存图片(Ctrl+S) + படத்தை சேமிக்கவும் (Ctrl+S) + + + + 显示/隐藏文字(Tab) + உரையைக் காட்டவும்/மறைக்கவும் (தாவல்) + + + + 用默认应用打开图片 + இயல்புநிலை பயன்பாட்டுடன் படங்களைத் திறக்கவும் + + + + 删除图片(Ctrl+D) + படத்தை நீக்கு (Ctrl+D) + + + + MainWindowManager + + + 欢迎使用 Umi-OCR + உமி-ஆக்ராவுக்கு வருக + + + + 已启用后台模式,可通过快捷键使用功能。 + பின்னணி பயன்முறை இயக்கப்பட்டது, மேலும் செயல்பாடுகளை குறுக்குவழி விசைகள் மூலம் பயன்படுத்தலாம். + + + + MarkdownView + + + 打开网页 + வலைப்பக்கத்தைத் திறக்கவும் + + + + 链接 + இணைப்பு + + + + MessageBox + + + 取消 + ரத்துசெய் + + + + 确定 + நிச்சயமாக + + + + 警告 + எச்சரிக்கை + + + + 发生了一点小问题 + சிறிய ஒன்று நடந்தது + + + + 复制 + நகலெடு + + + + 已复制报错信息 %1 + பிழை செய்தி %1 ஐ நகலெடுக்கவும் + + + + 请前往 Issues 页面寻找解答或反馈 + பதில்கள் அல்லது பின்னூட்டங்களுக்காக சிக்கல்கள் பக்கத்திற்குச் செல்லவும் + + + + 问题反馈 + கேள்வி கருத்து + + + + MessageBoxWin + + + 确定 + நிச்சயமாக + + + + 不再提示 + இனி தூண்டுகிறது + + + + 知道了 + தெரியும் + + + + MissionCtrlPanel + + + 开始任务 + பணியைத் தொடங்கவும் + + + + 暂停任务 +暂停后可以待机或休眠。 +但是关机或退出软件,将会丢弃任务内容。 + பணியை இடைநிறுத்துங்கள் +இடைநிறுத்தப்பட்ட பிறகு நீங்கள் காத்திருப்பு அல்லது தூங்கலாம். +இருப்பினும், மென்பொருளை மூடுவது அல்லது வெளியேறுவது பணி உள்ளடக்கத்தை நிராகரிக்கும். + + + + 继续任务 + பணியைத் தொடரவும் + + + + 终止任务 +放弃未完成的内容。 + பணியை நிறுத்தவும் + முடிக்கப்படாத உள்ளடக்கத்தை விட்டுவிடுங்கள். + + + + 正在运行 + இயங்கும் + + + + 正在保存 + சேமிப்பு + + + + 已暂停 + இடைநிறுத்தப்பட்டது + + + + 任务停止 + பணி நிறுத்தம் + + + + 任务完成 + பணி நிறைவு + + + + ModalLayer + + + 保存并返回 + சேமித்து திரும்பவும் + + + + 返回 + திரும்ப + + + + Navigation + + + 欢迎使用 Umi-OCR + உமி-ஆக்ராவுக்கு வருக + + + + 请选择功能页 + செயல்பாட்டு பக்கத்தைத் தேர்ந்தெடுக்கவும் + + + + 当前版本 + தற்போதைய பதிப்பு + + + + 项目链接 + திட்ட இணைப்பு + + + + 官方网站 + அதிகாரப்பூர்வ வலைத்தளம் + + + + 插件拓展 + செருகுநிரல் விரிவாக்கம் + + + + 问题反馈 + கேள்வி கருத்து + + + + 功能页 + செயல்பாடு பக்கம் + + + + 请打开对应标签页(如批量OCR、批量文档),再拖入文件。 + தொடர்புடைய தாவல் பக்கத்தைத் திறந்து (தொகுதி OCR, தொகுதி ஆவணம் போன்றவை) மற்றும் கோப்பில் இழுக்கவும். + + + + OcrManager + + + 文字识别 + உரை ஏற்பு + + + + 操作 + செயல்படுங்கள் + + + + + 强制终止任务 + கட்டாயமாக பணிநீக்கம் + + + + 应用修改 + மாற்றத்தைப் பயன்படுத்துங்கள் + + + + 当前接口 + தற்போதைய இடைமுகம் + + + + 没有可用的 OCR 插件。 + OCR செருகுநிரல் எதுவும் கிடைக்கவில்லை. + + + + OCR API 列表中不存在%1 + OCR பநிஇ பட்டியலில் %1 இல்லை + + + + 当前已有%1组任务队列、共%2个任务正在执行。您可【强制终止任务】后修改API。 + தற்போது, %1 பணி வரிசைகள் மற்றும் மொத்தம் %2 பணிகள் செயல்படுத்தப்படுகின்றன. [பணியை கட்டாயப்படுத்துதல்] பிறகு நீங்கள் பநிஇ ஐ மாற்றலாம். + + + + 无法修改 文字识别接口设置 + உரை அங்கீகார இடைமுக அமைப்புகளை மாற்ற முடியவில்லை + + + + 文字识别接口应用成功 + உரை அங்கீகார இடைமுகம் வெற்றிகரமாக பயன்படுத்தப்படுகிறது + + + + 当前API为【%1】 + தற்போதைய பநிஇ [%1] + + + + 文字识别接口应用失败 + உரை அங்கீகார இடைமுக பயன்பாடு தோல்வியுற்றது + + + + 当前没有运行中的任务 + தற்போது இயங்கும் பணிகள் எதுவும் இல்லை + + + + 当前已有%1组任务队列、共%2个任务正在执行。 +要强制终止全部任务吗? + தற்போது, %1 பணி வரிசைகள் மற்றும் மொத்தம் %2 பணிகள் செயல்படுத்தப்படுகின்றன. + எல்லா பணிகளையும் நிறுத்த கட்டாயப்படுத்த விரும்புகிறீர்களா? + + + + PagesManager + + + 新标签页 + புதிய தாவல் பக்கம் + + + + 截图OCR + திரைக்காட்சி OCR + + + + # 截图OCR + +屏幕截图,快捷转文字。也支持粘贴图片。 + # திரைக்காட்சி OCR + + திரைக்காட்சி, விரைவான உரை மாற்றம். இது ஒட்டும் படங்களையும் ஆதரிக்கிறது. + + + + # 全局设置 + +调节全局设置项,对所有页面生效。 + # உலகளாவிய அமைப்புகள் + + அனைத்து பக்கங்களிலும் நடைமுறைக்கு வர உலகளாவிய அமைப்புகளை சரிசெய்யவும். + + + + 批量OCR + தொகுதி OCR + + + + # 批量OCR + +导入本地图片或文件夹,批量转换文字。 + +支持格式: + # தொகுதி OCR + + உள்ளக படங்கள் அல்லது கோப்புறைகளை இறக்குமதி செய்து உரையை தொகுதிகளில் மாற்றவும். + + உதவி வடிவங்கள்: + + + + 批量文档 + தொகுதி ஆவணங்கள் + + + + # 批量文档识别 + +批量导入文档,提取文字,或生成双层可搜索PDF。 + +支持格式: + # தொகுதி ஆவண அடையாளம் + + மொத்த இறக்குமதி ஆவணங்கள், உரையை பிரித்தெடுக்கவும் அல்லது இரட்டை அடுக்கு தேடக்கூடிய PDF களை உருவாக்கவும். + + உதவி வடிவங்கள்: + + + + 二维码 + QR குறியீடு + + + + # 二维码 + +识别或生成二维码、条形码。 + +支持协议: + # QR குறியீடு + + QR குறியீடுகள் மற்றும் பார்கோடுகளை அடையாளம் காணவும் அல்லது உருவாக்கவும். + + உதவி ஒப்பந்தம்: + + + + 全局设置 + உலகளாவிய அமைப்புகள் + + + + 关于 + பற்றி + + + + # 关于 + # பற்றி + + + + PopupManager + + + 确认 + உறுதிப்படுத்தவும் + + + + 取消 + ரத்துசெய் + + + + PreviewDoc + + + 加密 + குறியாக்கம் + + + + 打开文档失败 + ஆவணத்தைத் திறக்கத் தவறிவிட்டது + + + + 请填写正确的密码 + சரியான கடவுச்சொல்லை நிரப்பவும் + + + + 密码正确 + கடவுச்சொல் சரியானது + + + + 文档预览异常 + ஆவண முன்னோட்டம் விதிவிலக்கு + + + + 忽略区域说明: +右键拖拽,绘制矩形区域,包含在区域内的文字框将被忽略。可用于排除水印、页眉页脚。 +范围允许填写负数,表示倒数第x页。如-1表示最后一页,-2表示倒数第2页。 +忽略区域的设置对所有文档生效。 + பகுதி விளக்கத்தை புறக்கணிக்கவும்: + ஒரு செவ்வக பகுதியை வரைய வலது சொடுக்கு செய்யவும், மற்றும் இப்பகுதியில் உள்ள உரை பெட்டிகள் புறக்கணிக்கப்படும். வாட்டர்மார்க்ச், தலைப்புகள் மற்றும் அடிக்குறிப்புகளை விலக்க பயன்படுத்தலாம். + எக்ச்-லாச்ட் பக்கத்தைக் குறிக்கும் எதிர்மறை எண்களை நிரப்ப வரம்பு அனுமதிக்கிறது. -1 கடைசி பக்கத்தைக் குறிக்கிறது என்றால், -2 இரண்டாவது முதல் கடைசி பக்கத்தைக் குறிக்கிறது. + புறக்கணிப்பு பகுதியின் அமைப்புகள் அனைத்து ஆவணங்களுக்கும் பயனுள்ளதாக இருக்கும். + + + + 密码: + கடவுச்சொல்: + + + + 预览页面 + முன்னோட்டம் பக்கம் + + + + 预览PDF时,是否预览OCR结果 + PDFS ஐ முன்னோட்டமிடும்போது OCR முடிவுகளை முன்னோட்டமிட வேண்டுமா + + + + OCR页数 + OCR பக்க எண்ணிக்கை + + + + 忽略区域页数(全局) + பகுதி பக்கங்களின் எண்ணிக்கையை (உலகளாவிய) புறக்கணிக்கவும் + + + + + 范围 + நோக்கம் + + + + 撤销 + ரத்து செய்யுங்கள் + + + + 清空 + தெளிவான + + + + PreviewImage + + + 文字 + எழுத்து + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + படத்தில் அங்கீகார உரையை மேலெழுதவும் + உலகளாவிய அமைப்புகளில் இயல்புநிலையாக அமைக்கலாம் + + + + QRCode + + + 获取剪贴板异常 + இடைநிலைப்பலகை விதிவிலக்கைப் பெறுங்கள் + + + + 剪贴板中为文本 + கிளிப்போர்டில் உரை + + + + 无有效图片 + செல்லுபடியாகும் படங்கள் இல்லை + + + + 导入%1条图片路径 + %1 பட பாதையை இறக்குமதி செய்க + + + + 参数有误,或输入内容不合规定。请参照报错指示修改: + அளவுருக்கள் தவறானவை, அல்லது உள்ளீட்டு உள்ளடக்கம் சீரற்றது. மாற்றுவதற்கு பிழை வழிமுறைகளைப் பார்க்கவும்: + + + + + 生成二维码失败 + QR குறியீட்டை உருவாக்குவதில் தோல்வி + + + + 已复制到剪贴板 + இடைநிலைப்பலகைக்கு நகலெடுக்கப்பட்டது + + + + 识图完成 + முழுமையான பட ஏற்பு + + + + 无文字 + உரை இல்லை + + + + 识别失败 + ஏற்பு தோல்வியடைந்தது + + + + 扫描二维码 + QR குறியீட்டை ச்கேன் செய்யுங்கள் + + + + 截图、拖入或粘贴二维码图片 + திரை காட்சி, QR குறியீடு படங்களை இழுக்கவும் அல்லது ஒட்டவும் + + + + + 设置 + அமைக்கவும் + + + + 自动刷新 + தானியங்கி புதுப்பிப்பு + + + + 修改文字后,自动生成二维码/条形码 + உரையை மாற்றிய பின், QR குறியீடு/பார் குறியீடு தானாக உருவாக்கப்படும் + + + + 刷新 + புதுப்பிக்கவும் + + + + 生成二维码/条形码 + QR குறியீடு/பார்கோடு உருவாக்கவும் + + + + 记录 + பதிவு + + + + 生成 + உருவாக்கு + + + + QRCodeConfigs + + + 快捷键 + குறுக்குவழி விசை + + + + 屏幕截图 + திரை காட்சி + + + + 粘贴图片 + படத்தை ஒட்டவும் + + + + 预处理(一般无需改动) + முன் செயலாக்கம் (பொதுவாக எந்த மாற்றங்களும் தேவையில்லை) + + + + 中值滤波 + சராசரி வடிகட்டுதல் + + + + 对图像进行平滑处理 +>0 时生效。可填1~9的奇数(不允许偶数) + படத்தை மென்மையாக்கவும் + > 0 போது பயனுள்ளதாக இருக்கும். 1 முதல் 9 வரையிலான ஒற்றைப்படை எண்களை நிரப்பலாம் (எண்கள் கூட அனுமதிக்கப்படாது) + + + + 锐度增强 + கூர்மையான விரிவாக்கம் + + + + 增加图像的锐度 +>0.1 时生效。可填0.1~10的小数 + படத்தின் கூர்மையை அதிகரிக்கவும் + பயனளிக்கும் போது> 0.1. 0.1 ~ 10 தசமங்களை நிரப்ப முடியும் + + + + 对比度增强 + மாறுபட்ட விரிவாக்கம் + + + + 增加图像的对比度 +>0.1 时生效。可填0.1~10的小数 + படத்தின் மாறுபாட்டை அதிகரிக்கவும் + பயனளிக்கும் போது> 0.1. 0.1 ~ 10 தசமங்களை நிரப்ப முடியும் + + + + 转为灰度 + கிரேச்கேலுக்கு திரும்பவும் + + + + 将图像像素转为灰度 + பட பிக்சல்களை கிரேச்கேலுக்கு மாற்றவும் + + + + 二值化 + இரும + + + + 将图像像素转为纯黑和纯白 +启用了灰度,且二值化 >-1 时生效。可填0~255的整数 + பட பிக்சல்களை தூய கருப்பு மற்றும் தூய வெள்ளை நிறமாக மாற்றவும் +பைனரைசேசன்> -1 பயன்படுத்தப்படும்போது கிரேச்கேல் இயக்கப்பட்டது மற்றும் பயனுள்ளதாக இருக்கும். 0 ~ 255 இன் முழு எண்களை நிரப்பலாம் + + + + 扫码后的操作 + குறியீட்டை ச்கேன் செய்த பிறகு செயல்பாடு + + + + 复制结果 + முடிவுகளை நகலெடுக்கவும் + + + + 弹出主窗口 + முதன்மையான சாளரத்தை பாப் அப் செய்யுங்கள் + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + படத்தை அங்கீகரித்தபிறகு, முதன்மையான சாளரம் குறைக்கப்பட்டால் அல்லது பின்னணியில் இருந்தால், அது முன்புறத்தில் தோன்றும் + + + + 生成二维码/条形码 + QR குறியீடு/பார்கோடு உருவாக்கவும் + + + + 类型 + தட்டச்சு செய்க + + + + 默认二维码: + இயல்புநிலை QR குறியீடு: + + + + 宽度 + அகலம் + + + + + 填0:自动选择 + 0 இல் நிரப்பவும்: தானாகவே தேர்ந்தெடுக்கவும் + + + + + + 像素 + படப்புள்ளிகள் + + + + 高度 + உயர்ந்த + + + + 边缘空白 + வெற்று விளிம்பு + + + + 填-1:自动选择 + -1 இல் நிரப்பவும்: தானாகவே தேர்ந்தெடுக்கவும் + + + + 纠错等级 + பிழை திருத்தம் நிலை + + + + 仅适用于: + இதற்கு மட்டுமே பொருந்தும்: + + + + 自动 + தானியங்கி + + + + 其它 + மற்றொன்று + + + + ResultTextContainer + + + 复制 + நகலெடு + + + + ResultsTableView + + + 异常状态码:%1 +异常信息:%2 + விதிவிலக்கு நிலை குறியீடு: %1 + விதிவிலக்கு தகவல்: %2 + + + + 耗时 %1 + எடுக்கப்பட்ட நேரம் %1 + + + + 置信度 %1 + நம்பிக்கை %1 + + + + 记录:复制%1字 + பதிவு: %1 வார்த்தையை நகலெடுக்கவும் + + + + 记录:无选中文字 + பதிவு: தேர்வு இல்லாமல் சீன எழுத்துக்கள் + + + + 记录:复制全部%1字 + பதிவு: அனைத்து %1 வார்த்தையையும் நகலெடுக்கவும் + + + + 删除%1条记录 + %1 பதிவை நீக்கு + + + + 清空记录 + பதிவை அழிக்கவும் + + + + 复制    (Ctrl+C) + நகலெடு    (Ctrl+C) + + + + 复制全部  (Ctrl+C 双击) + அனைத்தையும் நகலெடுக்கவும் (Ctrl+C இரட்டை சொடுக்கவும்) + + + + 选中单个  (Ctrl+A) + ஒற்றை (Ctrl+A) ஐத் தேர்ந்தெடுக்கவும் + + + + 选中全部记录(Ctrl+A 双击) + எல்லா பதிவுகளையும் தேர்ந்தெடுக்கவும் (Ctrl+இரட்டை சொடுக்கவும்) + + + + 删除选中记录 + தேர்ந்தெடுக்கப்பட்ட பதிவுகளை நீக்கு + + + + 清空全部记录(Ctrl+D 双击) + எல்லா பதிவுகளையும் அழிக்கவும் (Ctrl+D இரட்டை சொடுக்கவும்) + + + + 滚动 + சுருள் + + + + 自动滚动到底部 + தானாகவே கீழே உருட்டவும் + + + + 右键菜单 + வலது சொடுக்கு பட்டியல் + + + + ScreenshotManager + + + 截图失败 + திரைக்காட்சி தோல்வியடைந்தது + + + + 上次截图操作未结束,不能进行新的截图! + கடைசி திரைக்காட்சி செயல்பாடு முடிவடையவில்லை, மேலும் புதிய திரை சாட்களை எடுக்க முடியாது! + + + + 尚未记录截图区域 + திரைக்காட்சி பகுதி எதுவும் பதிவு செய்யப்படவில்லை + + + + 显示器: %1 +错误信息: %2 + மானிட்டர்: %1 + பிழை செய்தி: %2 + + + + 重复截图失败 + மீண்டும்மீண்டும் திரைக்காட்சி தோல்வியடைந்தது + + + + 未找到匹配的屏幕 + பொருந்தக்கூடிய திரை எதுவும் கிடைக்கவில்லை + + + + 屏幕设备名称不相同: +%1 +%2 + திரை சாதன பெயர்கள் வேறுபட்டவை: + %1 + %2 + + + + ScreenshotOCR + + + 获取剪贴板异常 + இடைநிலைப்பலகை விதிவிலக்கைப் பெறுங்கள் + + + + 剪贴板中为文本 + கிளிப்போர்டில் உரை + + + + 无有效图片 + செல்லுபடியாகும் படங்கள் இல்லை + + + + 导入%1条图片路径 + %1 பட பாதையை இறக்குமதி செய்க + + + + 依然关闭 + இன்னும் மூடப்பட்டது + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + பணி நடந்து வருகிறது. + பணியை முடித்து பக்கத்தை மூட விரும்புகிறீர்களா? + + + + + 屏幕截图 + திரை காட்சி + + + + + 粘贴图片 + படத்தை ஒட்டவும் + + + + 截图识别任务异常 + பணி விதிவிலக்குகளை அடையாளம் காண திரை காட்சி + + + + 已复制到剪贴板 + இடைநிலைப்பலகைக்கு நகலெடுக்கப்பட்டது + + + + 识图完成 + முழுமையான பட ஏற்பு + + + + 无文字 + உரை இல்லை + + + + 识别失败 + ஏற்பு தோல்வியடைந்தது + + + + + 停止任务 + பணியை நிறுத்துங்கள் + + + + 文字 + எழுத்து + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + படத்தில் அங்கீகார உரையை மேலெழுதவும் + உலகளாவிய அமைப்புகளில் இயல்புநிலையாக அமைக்கலாம் + + + + 截图、拖入或粘贴图片 + திரை காட்சி, படத்தை இழுக்கவும் அல்லது ஒட்டவும் + + + + 设置 + அமைக்கவும் + + + + 记录 + பதிவு + + + + ScreenshotOcrConfigs + + + OCR文本后处理 + OCR உரை பிந்தைய செயலாக்கம் + + + + 快捷键 + குறுக்குவழி விசை + + + + 屏幕截图 + திரை காட்சி + + + + 粘贴图片 + படத்தை ஒட்டவும் + + + + 重复截图 + திரை சாட்களை மீண்டும் செய்யவும் + + + + 重新截取上一次截图的范围 + கடைசி திரை சாட்டின் நோக்கத்தை மீண்டும் ச்னாப் செய்யுங்கள் + + + + 识图后的操作 + படத்தை அங்கீகரித்த பிறகு செயல்பாடு + + + + 复制结果 + முடிவுகளை நகலெடுக்கவும் + + + + 弹出主窗口 + முதன்மையான சாளரத்தை பாப் அப் செய்யுங்கள் + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + படத்தை அங்கீகரித்தபிறகு, முதன்மையான சாளரம் குறைக்கப்பட்டால் அல்லது பின்னணியில் இருந்தால், அது முன்புறத்தில் தோன்றும் + + + + 其它 + மற்றொன்று + + + + Size_ + + + 1.0 + 1.0 + + + + SystemTray + + + 打开主窗口 + முதன்மையான சாளரத்தைத் திறக்கவும் + + + + 退出 Umi-OCR + உமி-ஆக்ராவிலிருந்து வெளியேறவும் + + + + Theme + + + 珍珠白 + முத்து வெள்ளை + + + + 云墨黑 + மேகங்கள் மற்றும் கருப்பு + + + + 赛博朋克 + சைபர்பங்க் + + + + UtilsConfigDicts + + + 任务完成后的操作 + பணி முடிந்ததும் செயல்பாடு + + + + 系统 + அமைப்பு + + + + 无 + எதுவுமில்லை + + + + 关机 + மூடு + + + + 休眠 + தூங்கு + + + + %1 系统暂不支持电源控制! + %1 கணினி இன்னும் மின் கட்டுப்பாட்டை ஆதரிக்கவில்லை! + + + + 继续%1 + 继续%1 + + + + 取消%1 + %1 ஐ ரத்துசெய் + + + + 系统即将%1 + கணினி %1 ஆக உள்ளது + + + + 排版解析方案 + தட்டச்சு பகுப்பாய்வு தீர்வு + + + + 按什么方式,解析和排序图片中的文字块 + எந்த வகையில், படங்களில் உரைப்பகுதியை பாகுபடுத்தி வரிசைப்படுத்துங்கள் + + + + 多栏-按自然段换行 + பல நெடுவரிசைகள் - இயற்கை பத்தி மூலம் வரியை மடிக்கவும் + + + + 多栏-总是换行 + பல நெடுவரிசைகள் - எப்போதும் மடக்கு + + + + 多栏-无换行 + பல நெடுவரிசைகள் - வரி முறிவுகள் இல்லை + + + + 单栏-按自然段换行 + ஒற்றை நெடுவரிசை - இயற்கை பத்தி மூலம் கோட்டை மடிக்கவும் + + + + 单栏-总是换行 + ஒற்றை நெடுவரிசை - எப்போதும் வரி -காற்று + + + + 单栏-无换行 + ஒற்றை நெடுவரிசை-இல்லை வரி இடைவெளி + + + + 单栏-保留缩进 + ஒற்றை நெடுவரிசை - உள்தள்ளலை வைத்திருங்கள் + + + + 不做处理 + செயலாக்கம் இல்லை + + + + 优先内部 + உள் முன்னுரிமை + + + + 只允许内部 + உள் மட்டுமே + + + + 只允许外部 + வெளிப்புறம் மட்டுமே + + + + 禁用所有通知 + அனைத்து அறிவிப்புகளையும் முடக்கு + + + + 跟随全局设定 + உலகளாவிய அமைப்புகளைப் பின்பற்றவும் + + + + 通知弹窗类型 + அறிவிப்பு பாப்-அப் வகை + + + + 通知类型已更改 + அறிவிப்பு வகை மாற்றப்பட்டது + + + diff --git a/dev-tools/i18n/release/uz.ts b/dev-tools/i18n/release/uz.ts new file mode 100644 index 0000000..2a86a68 --- /dev/null +++ b/dev-tools/i18n/release/uz.ts @@ -0,0 +1,2482 @@ + + + + + About + + + 开源、免费的离线OCR软件 + + + + + 当前版本 + + + + + 项目链接 + + + + + 官方网站 + + + + + 插件拓展 + + + + + 问题反馈 + + + + + 发布地址 + + + + + 许可协议 + + + + + 作者 + + + + + 译者 + + + + + 运行环境信息(如需请求协助,请提供给开发者) + + + + + 复制 + + + + + AsynFilesLoader + + + 正在载入 %1 个文件: +%2 + + + + + BatchDOC + + + 加密 + + + + + %1个加密文档 + + + + + 请点击文件名填写密码 + + + + + 文档已加密 + + + + + 【%1】 +请点击文档名,设置密码 + + + + + 失败 + + + + + 排队 + + + + + 依然关闭 + + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + + + + + + 文档 + + + + + 状态 + + + + + 打开文档 + + + + + 拖入文档或文件夹 + + + + + 清空 + + + + + 文档识别异常 + + + + + 批量识别完成 + + + + + 页数 + + + + + 请选择文档 + + + + + 设置 + + + + + 记录 + + + + + BatchDOCConfigs + + + OCR文本后处理 + + + + + 文档处理 + + + + + 内容提取模式 + + + + + 若一页文档既存在图片又存在文本,如何进行处理 + + + + + 混合OCR/原文本 + + + + + 整页强制OCR + + + + + 仅OCR图片 + + + + + 仅拷贝原有文本 + + + + + 批量任务 + + + + + 保存到 + + + + + 文档原目录 + + + + + + 指定目录 + + + + + 必须先指定“保存到指定目录”才生效 + + + + + OCR结果保存目录 + + + + + 文件名格式 + + + + + 日期时间格式 + + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + + + + + 保存文件类型 + + + + + layered.pdf 双层可搜索文档 + + + + + text.pdf 单层纯文本文档 + + + + + 创建空白PDF文档,只写入识别文字,不含图片 + + + + + txt 标准格式 + + + + + 含识别文字和页数信息 + + + + + p.txt 纯文字格式 + + + + + 输出所有识别文字 + + + + + csv 表格文件(Excel) + + + + + 将页数信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + + + + + jsonl 原始信息 + + + + + 每行为一条json数据,便于第三方程序读取操作 + + + + + 忽略空白页 + + + + + 若某一页没有文字或识别失败,也不会输出错误提示信息 + + + + + 递归读取子文件夹 + + + + + 点击表格,可设置更多内容 + + + + + 导入文件夹时,导入子文件夹中全部文档 + + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文档名 +%range 识别页数范围。只有识别页数小于总页数时才会显示。 +举例:[OCR]_%name%range_%date +生成:[OCR]_文档A(p2-10)_20230901_1213.txt +添加占位符可以避免旧文件被新文件覆盖。 + + + + + 保留原有图片,叠加一层透明文字,可以搜索和复制 + + + + + 其它 + + + + + BatchOCR + + + 依然关闭 + + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + + + + + 排队 + + + + + 处理 + + + + + %1 张图片识别失败! + + + + + 批量识别完成 + + + + + 批量识别任务异常 + + + + + + 图片 + + + + + 耗时 + + + + + 状态 + + + + + 打开图片 + + + + + 拖入图片或文件夹 + + + + + 清空 + + + + + 请选择图片 + + + + + 设置 + + + + + 记录 + + + + + BatchOCRConfigs + + + OCR文本后处理 + + + + + 忽略区域 + + + + + 进入设置 + + + + + 批量任务 + + + + + 保存到 + + + + + 图片原目录 + + + + + + 指定目录 + + + + + 必须先指定“保存到指定目录”才生效 + + + + + OCR结果保存目录 + + + + + 文件名格式 + + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文件夹名/文件名 +举例:[OCR]_%name_%date +生成:[OCR]_我的图片_2023-09-01_12-13.txt +添加占位符可以避免旧文件被新文件覆盖。 + + + + + 日期时间格式 + + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + + + + + 保存文件类型 + + + + + txt 标准格式 + + + + + 含原图片文件名和识别文字 + + + + + p.txt 纯文字格式 + + + + + 仅输出识别文字,不含图片标题 + + + + + txt 单独文件 + + + + + 对每张图片,生成同名txt文件,仅输出识别文字 + + + + + md 图文混排 + + + + + Markdown图文混排格式,可用Markdown阅读器浏览文件 + + + + + csv 表格文件(Excel) + + + + + 将图片信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + + + + + jsonl 原始信息 + + + + + 每行为一条json数据,便于第三方程序读取操作 + + + + + 输出忽略空白图片 + + + + + 若图片没有文字或识别失败,也不会输出错误提示信息 + + + + + 递归读取子文件夹 + + + + + 导入文件夹时,导入子文件夹中全部图片 + + + + + 其它 + + + + + Configs + + + %1 处理配置项异常: +%2枚举列表为空。 + + + + + 重置 + + + + + 重置本页上的设定 + + + + + 重置设定 + + + + + 要重置本页的设定吗? + + + + + 高级 + + + + + 显示更多高级选项。它们标有 * 号。 +请谨慎修改高级选项。 + + + + + 展开 + + + + + 折叠 + + + + + 必须为整数 + + + + + 不能超过 + + + + + 不能低于 + + + + + 必须为数字 + + + + + 已取消%1的快捷键。 + + + + + 更新热键成功 + + + + + %1的快捷键为 %2 + + + + + %1 快捷键%2已被注册,请尝试另外的按键组合。 + + + + + %1 快捷键%2无法注册,请尝试另外的按键组合。 + + + + + 请按下快捷键组合。按【Esc】退出。 + + + + + 当前快捷键录制已在进行,不能同时录制! + + + + + 无法录制快捷键 + + + + + DropArea_ + + + 松手放入文件 + + + + + FontPanel + + + 界面 + + + + + 内容 + + + + + 界面字体: +软件中大部分UI的字体。 + + + + + 内容字体: +识别结果内容的字体。 + + + + + GlobalConfigs + + + 快捷方式 + + + + + 桌面 + + + + + 开始菜单 + + + + + 开机自启 + + + + + 界面和外观 + + + + + 主题 + + + + + 切换主题 + + + + + 字体 + + + + + 修改字体 + + + + + 界面与文字大小 + + + + + 渲染器 + + + + + 关闭硬件加速 + + + + + 若出现界面闪烁、元素错位等界面异常,尝试切换渲染器或者关闭硬件加速 + + + + + 禁用美化效果 + + + + + 在低配置机器上,禁用动画、阴影等效果可减少部分资源占用 + + + + + 图片预览默认显示叠加层 + + + + + 默认开启/关闭叠加层显示 +对所有图片预览组件生效 + + + + + 窗口 + + + + + 启动时缩小到任务栏 + + + + + 软件启动时,不弹出主窗口 + + + + + 窗口置顶 + + + + + 捷径:窗口左上角图钉 + + + + + 锁定标签栏 + + + + + 捷径:窗口右上角小锁 + + + + + 关闭主窗口时 + + + + + 最小化到系统托盘 + + + + + 退出应用 + + + + + 隐藏托盘图标 + + + + + 若要弹出位于后台的软件窗口,请在文件管理器中重复启动软件。 +若要彻底退出软件,请从任务管理器中结束进程。 + + + + + 截图 + + + + + 截图前隐藏主窗口 + + + + + 截图前,如果主窗口处于前台,则隐藏主窗口 +将会延时等待主窗口关闭 + + + + + 隐藏等待时间 + + + + + 秒 + + + + + 服务 + + + + + 允许HTTP服务 + + + + + Umi-OCR依赖HTTP接口进行本机跨进程通信。如果禁用,将无法使用命令行模式、多开检测等功能。 + + + + + + + 重启软件后生效 + + + + + 主机 + + + + + 仅本地 + + + + + 任何可用地址 + + + + + 将允许局域网访问。请开启对应防火墙权限! + + + + + 将禁止局域网访问。 + + + + + 端口 + + + + + 日志 + + + + + 操作 + + + + + 打开日志保存目录 + + + + + 保存的日志级别 + + + + + 已禁用日志文件保存 + + + + + 请尝试更换软件路径! + + + + + 配置文件读写异常 + + + + + 插件加载失败 + + + + + 成功添加快捷方式 + + + + + 添加快捷方式失败 + + + + + 成功移除 %1 个快捷方式 + + + + + 提示 + + + + + 没有找到可移除的快捷方式。 + + + + + 渲染器变更 将在重启软件后生效 + + + + + 端口号不合法 + + + + + 端口号改为%1 + + + + + 原端口号%1被占用, +切换为新端口号%2。 + +若不想看到此通知,请在全局设置关闭高级模式。 + + + + + GlobalConfigsPage + + + 1. 关于快捷键、OCR语言等选项,请在各个功能页中进行设置。 +2. 勾选左下角的“高级”按钮,可以显示更多进阶选项。 + + + + + 小贴士 + + + + + HTabBar + + + 窗口置顶 + + + + + 锁定标签栏 + + + + + IconButtonBar + + + 右键菜单 + + + + + 保存图片 + + + + + 图片大小:适应窗口 + + + + + 图片大小:实际 + + + + + 截图 + + + + + 粘贴 + + + + + 屏幕截图 + Screenshot + + + + 粘贴图片 + + + + + IgnoreArea + + + 拖入本地图片:OCR预览 +滚轮:缩放 +左键:拖拽 +右键:绘制忽略区域 + +可绘制一个或多个忽略区域矩形框。在执行批量OCR时,完全位于忽略区域内的文本块将被排除。 +比如批量处理影视截图时,可在右上角水印处添加忽略区域,避免输出水印文本。 + + + + + 图像尺寸: + + + + + 区域数量: + + + + + 撤销 + + + + + 清空 + + + + + ImageScale + + + 复制图片 + + + + + 复制图片失败 + + + + + 打开图片 + + + + + 打开图片失败 + + + + + + 保存图片 + + + + + 保存图片失败 + + + + + ImageWithText + + + 图片:复制%1字 + + + + + 图片:无选中文字 + + + + + 图片:复制全部%1字 + + + + + 复制  (Ctrl+C) + + + + + 全选  (Ctrl+A) + + + + + 复制图片(Ctrl+X) + + + + + 保存图片(Ctrl+S) + + + + + 显示/隐藏文字(Tab) + + + + + 用默认应用打开图片 + + + + + 删除图片(Ctrl+D) + + + + + MainWindowManager + + + 欢迎使用 Umi-OCR + + + + + 已启用后台模式,可通过快捷键使用功能。 + + + + + MarkdownView + + + 打开网页 + + + + + 链接 + + + + + MessageBox + + + 取消 + + + + + 确定 + + + + + 警告 + + + + + 发生了一点小问题 + + + + + 复制 + + + + + 已复制报错信息 %1 + + + + + 请前往 Issues 页面寻找解答或反馈 + + + + + 问题反馈 + + + + + MessageBoxWin + + + 确定 + + + + + 不再提示 + + + + + 知道了 + + + + + MissionCtrlPanel + + + 开始任务 + + + + + 暂停任务 +暂停后可以待机或休眠。 +但是关机或退出软件,将会丢弃任务内容。 + + + + + 继续任务 + + + + + 终止任务 +放弃未完成的内容。 + + + + + 正在运行 + + + + + 正在保存 + + + + + 已暂停 + + + + + 任务停止 + + + + + 任务完成 + + + + + ModalLayer + + + 保存并返回 + + + + + 返回 + + + + + Navigation + + + 欢迎使用 Umi-OCR + + + + + 请选择功能页 + + + + + 当前版本 + + + + + 项目链接 + + + + + 官方网站 + + + + + 插件拓展 + + + + + 问题反馈 + + + + + 功能页 + + + + + 请打开对应标签页(如批量OCR、批量文档),再拖入文件。 + + + + + OcrManager + + + 文字识别 + + + + + 操作 + + + + + + 强制终止任务 + + + + + 应用修改 + + + + + 当前接口 + + + + + 没有可用的 OCR 插件。 + + + + + OCR API 列表中不存在%1 + + + + + 当前已有%1组任务队列、共%2个任务正在执行。您可【强制终止任务】后修改API。 + + + + + 无法修改 文字识别接口设置 + + + + + 文字识别接口应用成功 + + + + + 当前API为【%1】 + + + + + 文字识别接口应用失败 + + + + + 当前没有运行中的任务 + + + + + 当前已有%1组任务队列、共%2个任务正在执行。 +要强制终止全部任务吗? + + + + + PagesManager + + + 新标签页 + + + + + 截图OCR + + + + + # 截图OCR + +屏幕截图,快捷转文字。也支持粘贴图片。 + + + + + # 全局设置 + +调节全局设置项,对所有页面生效。 + + + + + 批量OCR + + + + + # 批量OCR + +导入本地图片或文件夹,批量转换文字。 + +支持格式: + + + + + 批量文档 + + + + + # 批量文档识别 + +批量导入文档,提取文字,或生成双层可搜索PDF。 + +支持格式: + + + + + 二维码 + + + + + # 二维码 + +识别或生成二维码、条形码。 + +支持协议: + + + + + 全局设置 + + + + + 关于 + + + + + # 关于 + + + + + PopupManager + + + 确认 + + + + + 取消 + + + + + PreviewDoc + + + 加密 + + + + + 打开文档失败 + + + + + 请填写正确的密码 + + + + + 密码正确 + + + + + 文档预览异常 + + + + + 忽略区域说明: +右键拖拽,绘制矩形区域,包含在区域内的文字框将被忽略。可用于排除水印、页眉页脚。 +范围允许填写负数,表示倒数第x页。如-1表示最后一页,-2表示倒数第2页。 +忽略区域的设置对所有文档生效。 + + + + + 密码: + + + + + 预览页面 + + + + + 预览PDF时,是否预览OCR结果 + + + + + OCR页数 + OCR page count + + + + 忽略区域页数(全局) + Ignore Area page count (Global) + + + + + 范围 + + + + + 撤销 + + + + + 清空 + + + + + PreviewImage + + + 文字 + + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + + + + + QRCode + + + 获取剪贴板异常 + + + + + 剪贴板中为文本 + + + + + 无有效图片 + + + + + 导入%1条图片路径 + + + + + 参数有误,或输入内容不合规定。请参照报错指示修改: + + + + + + 生成二维码失败 + + + + + 已复制到剪贴板 + + + + + 识图完成 + + + + + 无文字 + + + + + 识别失败 + + + + + 扫描二维码 + + + + + 截图、拖入或粘贴二维码图片 + Screenshot, drag and drop, or paste QR code image + + + + + 设置 + + + + + 自动刷新 + + + + + 修改文字后,自动生成二维码/条形码 + + + + + 刷新 + + + + + 生成二维码/条形码 + + + + + 记录 + + + + + 生成 + + + + + QRCodeConfigs + + + 快捷键 + + + + + 屏幕截图 + Screenshot + + + + 粘贴图片 + + + + + 预处理(一般无需改动) + + + + + 中值滤波 + + + + + 对图像进行平滑处理 +>0 时生效。可填1~9的奇数(不允许偶数) + + + + + 锐度增强 + + + + + 增加图像的锐度 +>0.1 时生效。可填0.1~10的小数 + + + + + 对比度增强 + + + + + 增加图像的对比度 +>0.1 时生效。可填0.1~10的小数 + + + + + 转为灰度 + + + + + 将图像像素转为灰度 + + + + + 二值化 + + + + + 将图像像素转为纯黑和纯白 +启用了灰度,且二值化 >-1 时生效。可填0~255的整数 + + + + + 扫码后的操作 + + + + + 复制结果 + + + + + 弹出主窗口 + + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + + + + + 生成二维码/条形码 + + + + + 类型 + + + + + 默认二维码: + + + + + 宽度 + + + + + + 填0:自动选择 + + + + + + + 像素 + + + + + 高度 + + + + + 边缘空白 + + + + + 填-1:自动选择 + + + + + 纠错等级 + + + + + 仅适用于: + + + + + 自动 + + + + + 其它 + + + + + ResultTextContainer + + + 复制 + + + + + ResultsTableView + + + 异常状态码:%1 +异常信息:%2 + + + + + 耗时 %1 + + + + + 置信度 %1 + + + + + 记录:复制%1字 + + + + + 记录:无选中文字 + + + + + 记录:复制全部%1字 + + + + + 删除%1条记录 + + + + + 清空记录 + + + + + 复制    (Ctrl+C) + + + + + 复制全部  (Ctrl+C 双击) + + + + + 选中单个  (Ctrl+A) + + + + + 选中全部记录(Ctrl+A 双击) + + + + + 删除选中记录 + + + + + 清空全部记录(Ctrl+D 双击) + + + + + 滚动 + + + + + 自动滚动到底部 + + + + + 右键菜单 + + + + + ScreenshotManager + + + 截图失败 + + + + + 上次截图操作未结束,不能进行新的截图! + + + + + 尚未记录截图区域 + + + + + 显示器: %1 +错误信息: %2 + + + + + 重复截图失败 + + + + + 未找到匹配的屏幕 + + + + + 屏幕设备名称不相同: +%1 +%2 + + + + + ScreenshotOCR + + + 获取剪贴板异常 + + + + + 剪贴板中为文本 + + + + + 无有效图片 + + + + + 导入%1条图片路径 + + + + + 依然关闭 + + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + + + + + + 屏幕截图 + Screenshot + + + + + 粘贴图片 + + + + + 截图识别任务异常 + + + + + 已复制到剪贴板 + + + + + 识图完成 + + + + + 无文字 + + + + + 识别失败 + + + + + + 停止任务 + + + + + 文字 + + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + + + + + 截图、拖入或粘贴图片 + Screenshot, drag and drop, or paste image + + + + 设置 + + + + + 记录 + + + + + ScreenshotOcrConfigs + + + OCR文本后处理 + + + + + 快捷键 + + + + + 屏幕截图 + Screenshot + + + + 粘贴图片 + + + + + 重复截图 + + + + + 重新截取上一次截图的范围 + + + + + 识图后的操作 + + + + + 复制结果 + + + + + 弹出主窗口 + + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + + + + + 其它 + + + + + Size_ + + + 1.0 + + + + + SystemTray + + + 打开主窗口 + + + + + 退出 Umi-OCR + + + + + Theme + + + 珍珠白 + + + + + 云墨黑 + + + + + 赛博朋克 + + + + + UtilsConfigDicts + + + 任务完成后的操作 + + + + + 系统 + + + + + 无 + + + + + 关机 + + + + + 休眠 + + + + + %1 系统暂不支持电源控制! + %1 Linux system currently does not support power control! + + + + 继续%1 + + + + + 取消%1 + + + + + 系统即将%1 + + + + + 排版解析方案 + + + + + 按什么方式,解析和排序图片中的文字块 + + + + + 多栏-按自然段换行 + + + + + 多栏-总是换行 + + + + + 多栏-无换行 + + + + + 单栏-按自然段换行 + + + + + 单栏-总是换行 + + + + + 单栏-无换行 + + + + + 单栏-保留缩进 + + + + + 不做处理 + + + + + 优先内部 + + + + + 只允许内部 + + + + + 只允许外部 + + + + + 禁用所有通知 + + + + + 跟随全局设定 + + + + + 通知弹窗类型 + + + + + 通知类型已更改 + + + + diff --git a/dev-tools/i18n/release/vi.ts b/dev-tools/i18n/release/vi.ts new file mode 100644 index 0000000..feeb424 --- /dev/null +++ b/dev-tools/i18n/release/vi.ts @@ -0,0 +1,2484 @@ + + + + + About + + + 开源、免费的离线OCR软件 + Phần mềm OCR ngoại tuyến miễn phí, mã nguồn mở + + + + 当前版本 + Phiên bản hiện tại + + + + 项目链接 + Liên kết dự án + + + + 官方网站 + Trang web chính thức + + + + 插件拓展 + Mở rộng plugin + + + + 问题反馈 + Phản hồi vấn đề + + + + 发布地址 + Địa chỉ phát hành + + + + 许可协议 + Thỏa thuận cấp phép + + + + 作者 + tác giả + + + + 译者 + Người dịch + + + + 运行环境信息(如需请求协助,请提供给开发者) + Thông tin về môi trường hoạt động (nếu cần hỗ trợ, vui lòng cung cấp cho nhà phát triển) + + + + 复制 + sao chép + + + + AsynFilesLoader + + + 正在载入 %1 个文件: +%2 + Đang tải %1 tệp: +%2 + + + + BatchDOC + + + 加密 + mã hóa + + + + %1个加密文档 + %1 tài liệu được mã hóa + + + + 请点击文件名填写密码 + Vui lòng nhấp vào tên tệp để nhập mật khẩu + + + + 文档已加密 + Tài liệu đã được mã hóa + + + + 【%1】 +请点击文档名,设置密码 + 【%1】 + + + + 失败 + thất bại + + + + 排队 + xếp hàng + + + + 依然关闭 + Vẫn đóng + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + Nhiệm vụ đang được tiến hành. +Bạn có muốn kết thúc tác vụ và đóng trang không? + + + + + 文档 + tài liệu + + + + 状态 + tình trạng + + + + 打开文档 + Mở Tài liệu + + + + 拖入文档或文件夹 + Kéo vào một tài liệu hoặc thư mục + + + + 清空 + Thông thoáng + + + + 文档识别异常 + Sự bất thường trong nhận dạng tài liệu + + + + 批量识别完成 + + + + + 页数 + + + + + 请选择文档 + + + + + 设置 + + + + + 记录 + + + + + BatchDOCConfigs + + + OCR文本后处理 + + + + + 文档处理 + + + + + 内容提取模式 + + + + + 若一页文档既存在图片又存在文本,如何进行处理 + + + + + 混合OCR/原文本 + + + + + 整页强制OCR + + + + + 仅OCR图片 + + + + + 仅拷贝原有文本 + + + + + 批量任务 + + + + + 保存到 + + + + + 文档原目录 + + + + + + 指定目录 + + + + + 必须先指定“保存到指定目录”才生效 + + + + + OCR结果保存目录 + + + + + 文件名格式 + + + + + 日期时间格式 + + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + + + + + 保存文件类型 + + + + + layered.pdf 双层可搜索文档 + + + + + text.pdf 单层纯文本文档 + + + + + 创建空白PDF文档,只写入识别文字,不含图片 + + + + + txt 标准格式 + + + + + 含识别文字和页数信息 + + + + + p.txt 纯文字格式 + + + + + 输出所有识别文字 + + + + + csv 表格文件(Excel) + + + + + 将页数信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + + + + + jsonl 原始信息 + + + + + 每行为一条json数据,便于第三方程序读取操作 + + + + + 忽略空白页 + + + + + 若某一页没有文字或识别失败,也不会输出错误提示信息 + + + + + 递归读取子文件夹 + + + + + 点击表格,可设置更多内容 + + + + + 导入文件夹时,导入子文件夹中全部文档 + + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文档名 +%range 识别页数范围。只有识别页数小于总页数时才会显示。 +举例:[OCR]_%name%range_%date +生成:[OCR]_文档A(p2-10)_20230901_1213.txt +添加占位符可以避免旧文件被新文件覆盖。 + + + + + 保留原有图片,叠加一层透明文字,可以搜索和复制 + + + + + 其它 + + + + + BatchOCR + + + 依然关闭 + + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + + + + + 排队 + + + + + 处理 + + + + + %1 张图片识别失败! + + + + + 批量识别完成 + + + + + 批量识别任务异常 + + + + + + 图片 + + + + + 耗时 + + + + + 状态 + + + + + 打开图片 + + + + + 拖入图片或文件夹 + + + + + 清空 + + + + + 请选择图片 + + + + + 设置 + + + + + 记录 + + + + + BatchOCRConfigs + + + OCR文本后处理 + + + + + 忽略区域 + + + + + 进入设置 + + + + + 批量任务 + + + + + 保存到 + + + + + 图片原目录 + + + + + + 指定目录 + + + + + 必须先指定“保存到指定目录”才生效 + + + + + OCR结果保存目录 + + + + + 文件名格式 + + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文件夹名/文件名 +举例:[OCR]_%name_%date +生成:[OCR]_我的图片_2023-09-01_12-13.txt +添加占位符可以避免旧文件被新文件覆盖。 + + + + + 日期时间格式 + + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + + + + + 保存文件类型 + + + + + txt 标准格式 + + + + + 含原图片文件名和识别文字 + + + + + p.txt 纯文字格式 + + + + + 仅输出识别文字,不含图片标题 + + + + + txt 单独文件 + + + + + 对每张图片,生成同名txt文件,仅输出识别文字 + + + + + md 图文混排 + + + + + Markdown图文混排格式,可用Markdown阅读器浏览文件 + + + + + csv 表格文件(Excel) + + + + + 将图片信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + + + + + jsonl 原始信息 + + + + + 每行为一条json数据,便于第三方程序读取操作 + + + + + 输出忽略空白图片 + + + + + 若图片没有文字或识别失败,也不会输出错误提示信息 + + + + + 递归读取子文件夹 + + + + + 导入文件夹时,导入子文件夹中全部图片 + + + + + 其它 + + + + + Configs + + + %1 处理配置项异常: +%2枚举列表为空。 + + + + + 重置 + + + + + 重置本页上的设定 + + + + + 重置设定 + + + + + 要重置本页的设定吗? + + + + + 高级 + + + + + 显示更多高级选项。它们标有 * 号。 +请谨慎修改高级选项。 + + + + + 展开 + + + + + 折叠 + + + + + 必须为整数 + + + + + 不能超过 + + + + + 不能低于 + + + + + 必须为数字 + + + + + 已取消%1的快捷键。 + + + + + 更新热键成功 + + + + + %1的快捷键为 %2 + + + + + %1 快捷键%2已被注册,请尝试另外的按键组合。 + + + + + %1 快捷键%2无法注册,请尝试另外的按键组合。 + + + + + 请按下快捷键组合。按【Esc】退出。 + + + + + 当前快捷键录制已在进行,不能同时录制! + + + + + 无法录制快捷键 + + + + + DropArea_ + + + 松手放入文件 + + + + + FontPanel + + + 界面 + + + + + 内容 + + + + + 界面字体: +软件中大部分UI的字体。 + + + + + 内容字体: +识别结果内容的字体。 + + + + + GlobalConfigs + + + 快捷方式 + + + + + 桌面 + + + + + 开始菜单 + + + + + 开机自启 + + + + + 界面和外观 + + + + + 主题 + + + + + 切换主题 + + + + + 字体 + + + + + 修改字体 + + + + + 界面与文字大小 + + + + + 渲染器 + + + + + 关闭硬件加速 + + + + + 若出现界面闪烁、元素错位等界面异常,尝试切换渲染器或者关闭硬件加速 + + + + + 禁用美化效果 + + + + + 在低配置机器上,禁用动画、阴影等效果可减少部分资源占用 + + + + + 图片预览默认显示叠加层 + + + + + 默认开启/关闭叠加层显示 +对所有图片预览组件生效 + + + + + 窗口 + + + + + 启动时缩小到任务栏 + + + + + 软件启动时,不弹出主窗口 + + + + + 窗口置顶 + + + + + 捷径:窗口左上角图钉 + + + + + 锁定标签栏 + + + + + 捷径:窗口右上角小锁 + + + + + 关闭主窗口时 + + + + + 最小化到系统托盘 + + + + + 退出应用 + + + + + 隐藏托盘图标 + + + + + 若要弹出位于后台的软件窗口,请在文件管理器中重复启动软件。 +若要彻底退出软件,请从任务管理器中结束进程。 + + + + + 截图 + + + + + 截图前隐藏主窗口 + + + + + 截图前,如果主窗口处于前台,则隐藏主窗口 +将会延时等待主窗口关闭 + + + + + 隐藏等待时间 + + + + + 秒 + + + + + 服务 + + + + + 允许HTTP服务 + + + + + Umi-OCR依赖HTTP接口进行本机跨进程通信。如果禁用,将无法使用命令行模式、多开检测等功能。 + + + + + + + 重启软件后生效 + + + + + 主机 + + + + + 仅本地 + + + + + 任何可用地址 + + + + + 将允许局域网访问。请开启对应防火墙权限! + + + + + 将禁止局域网访问。 + + + + + 端口 + + + + + 日志 + + + + + 操作 + + + + + 打开日志保存目录 + + + + + 保存的日志级别 + + + + + 已禁用日志文件保存 + + + + + 请尝试更换软件路径! + + + + + 配置文件读写异常 + + + + + 插件加载失败 + + + + + 成功添加快捷方式 + + + + + 添加快捷方式失败 + + + + + 成功移除 %1 个快捷方式 + + + + + 提示 + + + + + 没有找到可移除的快捷方式。 + + + + + 渲染器变更 将在重启软件后生效 + + + + + 端口号不合法 + + + + + 端口号改为%1 + + + + + 原端口号%1被占用, +切换为新端口号%2。 + +若不想看到此通知,请在全局设置关闭高级模式。 + + + + + GlobalConfigsPage + + + 1. 关于快捷键、OCR语言等选项,请在各个功能页中进行设置。 +2. 勾选左下角的“高级”按钮,可以显示更多进阶选项。 + + + + + 小贴士 + + + + + HTabBar + + + 窗口置顶 + + + + + 锁定标签栏 + + + + + IconButtonBar + + + 右键菜单 + + + + + 保存图片 + + + + + 图片大小:适应窗口 + + + + + 图片大小:实际 + + + + + 截图 + + + + + 粘贴 + + + + + 屏幕截图 + Screenshot + + + + 粘贴图片 + + + + + IgnoreArea + + + 拖入本地图片:OCR预览 +滚轮:缩放 +左键:拖拽 +右键:绘制忽略区域 + +可绘制一个或多个忽略区域矩形框。在执行批量OCR时,完全位于忽略区域内的文本块将被排除。 +比如批量处理影视截图时,可在右上角水印处添加忽略区域,避免输出水印文本。 + + + + + 图像尺寸: + + + + + 区域数量: + + + + + 撤销 + + + + + 清空 + + + + + ImageScale + + + 复制图片 + + + + + 复制图片失败 + + + + + 打开图片 + + + + + 打开图片失败 + + + + + + 保存图片 + + + + + 保存图片失败 + + + + + ImageWithText + + + 图片:复制%1字 + + + + + 图片:无选中文字 + + + + + 图片:复制全部%1字 + + + + + 复制  (Ctrl+C) + + + + + 全选  (Ctrl+A) + + + + + 复制图片(Ctrl+X) + + + + + 保存图片(Ctrl+S) + + + + + 显示/隐藏文字(Tab) + + + + + 用默认应用打开图片 + + + + + 删除图片(Ctrl+D) + + + + + MainWindowManager + + + 欢迎使用 Umi-OCR + + + + + 已启用后台模式,可通过快捷键使用功能。 + + + + + MarkdownView + + + 打开网页 + + + + + 链接 + + + + + MessageBox + + + 取消 + + + + + 确定 + + + + + 警告 + + + + + 发生了一点小问题 + + + + + 复制 + + + + + 已复制报错信息 %1 + + + + + 请前往 Issues 页面寻找解答或反馈 + + + + + 问题反馈 + + + + + MessageBoxWin + + + 确定 + + + + + 不再提示 + + + + + 知道了 + + + + + MissionCtrlPanel + + + 开始任务 + + + + + 暂停任务 +暂停后可以待机或休眠。 +但是关机或退出软件,将会丢弃任务内容。 + + + + + 继续任务 + + + + + 终止任务 +放弃未完成的内容。 + + + + + 正在运行 + + + + + 正在保存 + + + + + 已暂停 + + + + + 任务停止 + + + + + 任务完成 + + + + + ModalLayer + + + 保存并返回 + + + + + 返回 + + + + + Navigation + + + 欢迎使用 Umi-OCR + + + + + 请选择功能页 + + + + + 当前版本 + + + + + 项目链接 + + + + + 官方网站 + + + + + 插件拓展 + + + + + 问题反馈 + + + + + 功能页 + + + + + 请打开对应标签页(如批量OCR、批量文档),再拖入文件。 + + + + + OcrManager + + + 文字识别 + + + + + 操作 + + + + + + 强制终止任务 + + + + + 应用修改 + + + + + 当前接口 + + + + + 没有可用的 OCR 插件。 + + + + + OCR API 列表中不存在%1 + + + + + 当前已有%1组任务队列、共%2个任务正在执行。您可【强制终止任务】后修改API。 + + + + + 无法修改 文字识别接口设置 + + + + + 文字识别接口应用成功 + + + + + 当前API为【%1】 + + + + + 文字识别接口应用失败 + + + + + 当前没有运行中的任务 + + + + + 当前已有%1组任务队列、共%2个任务正在执行。 +要强制终止全部任务吗? + + + + + PagesManager + + + 新标签页 + + + + + 截图OCR + + + + + # 截图OCR + +屏幕截图,快捷转文字。也支持粘贴图片。 + + + + + # 全局设置 + +调节全局设置项,对所有页面生效。 + + + + + 批量OCR + + + + + # 批量OCR + +导入本地图片或文件夹,批量转换文字。 + +支持格式: + + + + + 批量文档 + + + + + # 批量文档识别 + +批量导入文档,提取文字,或生成双层可搜索PDF。 + +支持格式: + + + + + 二维码 + + + + + # 二维码 + +识别或生成二维码、条形码。 + +支持协议: + + + + + 全局设置 + + + + + 关于 + + + + + # 关于 + + + + + PopupManager + + + 确认 + + + + + 取消 + + + + + PreviewDoc + + + 加密 + + + + + 打开文档失败 + + + + + 请填写正确的密码 + + + + + 密码正确 + + + + + 文档预览异常 + + + + + 忽略区域说明: +右键拖拽,绘制矩形区域,包含在区域内的文字框将被忽略。可用于排除水印、页眉页脚。 +范围允许填写负数,表示倒数第x页。如-1表示最后一页,-2表示倒数第2页。 +忽略区域的设置对所有文档生效。 + + + + + 密码: + + + + + 预览页面 + + + + + 预览PDF时,是否预览OCR结果 + + + + + OCR页数 + OCR page count + + + + 忽略区域页数(全局) + Ignore Area page count (Global) + + + + + 范围 + + + + + 撤销 + + + + + 清空 + + + + + PreviewImage + + + 文字 + + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + + + + + QRCode + + + 获取剪贴板异常 + + + + + 剪贴板中为文本 + + + + + 无有效图片 + + + + + 导入%1条图片路径 + + + + + 参数有误,或输入内容不合规定。请参照报错指示修改: + + + + + + 生成二维码失败 + + + + + 已复制到剪贴板 + + + + + 识图完成 + + + + + 无文字 + + + + + 识别失败 + + + + + 扫描二维码 + + + + + 截图、拖入或粘贴二维码图片 + Screenshot, drag and drop, or paste QR code image + + + + + 设置 + + + + + 自动刷新 + + + + + 修改文字后,自动生成二维码/条形码 + + + + + 刷新 + + + + + 生成二维码/条形码 + + + + + 记录 + + + + + 生成 + + + + + QRCodeConfigs + + + 快捷键 + + + + + 屏幕截图 + Screenshot + + + + 粘贴图片 + + + + + 预处理(一般无需改动) + + + + + 中值滤波 + + + + + 对图像进行平滑处理 +>0 时生效。可填1~9的奇数(不允许偶数) + + + + + 锐度增强 + + + + + 增加图像的锐度 +>0.1 时生效。可填0.1~10的小数 + + + + + 对比度增强 + + + + + 增加图像的对比度 +>0.1 时生效。可填0.1~10的小数 + + + + + 转为灰度 + + + + + 将图像像素转为灰度 + + + + + 二值化 + + + + + 将图像像素转为纯黑和纯白 +启用了灰度,且二值化 >-1 时生效。可填0~255的整数 + + + + + 扫码后的操作 + + + + + 复制结果 + + + + + 弹出主窗口 + + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + + + + + 生成二维码/条形码 + + + + + 类型 + + + + + 默认二维码: + + + + + 宽度 + + + + + + 填0:自动选择 + + + + + + + 像素 + + + + + 高度 + + + + + 边缘空白 + + + + + 填-1:自动选择 + + + + + 纠错等级 + + + + + 仅适用于: + + + + + 自动 + + + + + 其它 + + + + + ResultTextContainer + + + 复制 + + + + + ResultsTableView + + + 异常状态码:%1 +异常信息:%2 + + + + + 耗时 %1 + + + + + 置信度 %1 + + + + + 记录:复制%1字 + + + + + 记录:无选中文字 + + + + + 记录:复制全部%1字 + + + + + 删除%1条记录 + + + + + 清空记录 + + + + + 复制    (Ctrl+C) + + + + + 复制全部  (Ctrl+C 双击) + + + + + 选中单个  (Ctrl+A) + + + + + 选中全部记录(Ctrl+A 双击) + + + + + 删除选中记录 + + + + + 清空全部记录(Ctrl+D 双击) + + + + + 滚动 + + + + + 自动滚动到底部 + + + + + 右键菜单 + + + + + ScreenshotManager + + + 截图失败 + + + + + 上次截图操作未结束,不能进行新的截图! + + + + + 尚未记录截图区域 + + + + + 显示器: %1 +错误信息: %2 + + + + + 重复截图失败 + + + + + 未找到匹配的屏幕 + + + + + 屏幕设备名称不相同: +%1 +%2 + + + + + ScreenshotOCR + + + 获取剪贴板异常 + + + + + 剪贴板中为文本 + + + + + 无有效图片 + + + + + 导入%1条图片路径 + + + + + 依然关闭 + + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + + + + + + 屏幕截图 + Screenshot + + + + + 粘贴图片 + + + + + 截图识别任务异常 + + + + + 已复制到剪贴板 + + + + + 识图完成 + + + + + 无文字 + + + + + 识别失败 + + + + + + 停止任务 + + + + + 文字 + + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + + + + + 截图、拖入或粘贴图片 + Screenshot, drag and drop, or paste image + + + + 设置 + + + + + 记录 + + + + + ScreenshotOcrConfigs + + + OCR文本后处理 + + + + + 快捷键 + + + + + 屏幕截图 + Screenshot + + + + 粘贴图片 + + + + + 重复截图 + + + + + 重新截取上一次截图的范围 + + + + + 识图后的操作 + + + + + 复制结果 + + + + + 弹出主窗口 + + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + + + + + 其它 + + + + + Size_ + + + 1.0 + + + + + SystemTray + + + 打开主窗口 + + + + + 退出 Umi-OCR + + + + + Theme + + + 珍珠白 + + + + + 云墨黑 + + + + + 赛博朋克 + + + + + UtilsConfigDicts + + + 任务完成后的操作 + + + + + 系统 + + + + + 无 + + + + + 关机 + + + + + 休眠 + + + + + %1 系统暂不支持电源控制! + %1 Linux system currently does not support power control! + + + + 继续%1 + + + + + 取消%1 + + + + + 系统即将%1 + + + + + 排版解析方案 + + + + + 按什么方式,解析和排序图片中的文字块 + + + + + 多栏-按自然段换行 + + + + + 多栏-总是换行 + + + + + 多栏-无换行 + + + + + 单栏-按自然段换行 + + + + + 单栏-总是换行 + + + + + 单栏-无换行 + + + + + 单栏-保留缩进 + + + + + 不做处理 + + + + + 优先内部 + + + + + 只允许内部 + + + + + 只允许外部 + + + + + 禁用所有通知 + + + + + 跟随全局设定 + + + + + 通知弹窗类型 + + + + + 通知类型已更改 + + + + diff --git a/dev-tools/i18n/release/zh_TW.ts b/dev-tools/i18n/release/zh_TW.ts new file mode 100644 index 0000000..54679fb --- /dev/null +++ b/dev-tools/i18n/release/zh_TW.ts @@ -0,0 +1,2556 @@ + + + + + About + + + 开源、免费的离线OCR软件 + 開源、免費的離線 OCR 軟體 + + + + 当前版本 + 目前版本 + + + + 项目链接 + 專案網址 + + + + 官方网站 + 官方網站 + + + + 插件拓展 + 外掛程式擴充 + + + + 问题反馈 + 提出問題 + + + + 发布地址 + 發行網址 + + + + 许可协议 + 許可證 + + + + 作者 + 作者 + + + + 译者 + 譯者 + + + + 运行环境信息(如需请求协助,请提供给开发者) + 執行環境資訊(如需請求協助,請提供給開發者) + + + + 复制 + 複製 + + + + AsynFilesLoader + + + 正在载入 %1 个文件: +%2 + 正在載入 %1 個檔案: +%2 + + + + BatchDOC + + + 加密 + 加密 + + + + %1个加密文档 + %1 個加密文件 + + + + 请点击文件名填写密码 + 請點擊文件名填寫密碼 + + + + 文档已加密 + 文件已加密 + + + + 【%1】 +请点击文档名,设置密码 + 【%1】 +請點擊文件檔名,設定密碼 + + + + 失败 + 失敗 + + + + 排队 + 排隊 + + + + 依然关闭 + 依然關閉 + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + 任務正在進行中。 +要結束任務並關閉頁面嗎? + + + + + 文档 + 文件 + + + + 状态 + 狀態 + + + + 打开文档 + 開啟檔案 + + + + 拖入文档或文件夹 + 拖入檔案或資料夾 + + + + 清空 + 清空 + + + + 文档识别异常 + 文件識別異常 + + + + 批量识别完成 + 批量識別已完成 + + + + 页数 + 頁數 + + + + 请选择文档 + 請選擇文件 + + + + 设置 + 設定 + + + + 记录 + 記錄 + + + + BatchDOCConfigs + + + OCR文本后处理 + OCR文本後處理 + + + + 文档处理 + 文件處理 + + + + 内容提取模式 + 內容提取模式 + + + + 若一页文档既存在图片又存在文本,如何进行处理 + 若一頁文件既存在圖片又存在文本,如何進行處理 + + + + 混合OCR/原文本 + 混合OCR/原文本 + + + + 整页强制OCR + 整頁強制OCR + + + + 仅OCR图片 + 僅OCR圖片 + + + + 仅拷贝原有文本 + 僅複製原有文本 + + + + 批量任务 + 批次任務 + + + + 保存到 + 儲存到 + + + + 文档原目录 + 文件原目錄 + + + + + 指定目录 + 指定目錄 + + + + 必须先指定“保存到指定目录”才生效 + 必須先指定「儲存到指定目錄」才生效 + + + + OCR结果保存目录 + OCR結果儲存目錄 + + + + 文件名格式 + 檔案名稱格式 + + + + 日期时间格式 + 日期時間格式 + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + 檔案名稱中 %date 的日期格式。支援插入以下佔位符: +%Y 年、 %m 月、 %d 日、 %H 小時、 +%M 分鐘、 %S 秒 、 %unix 時間戳 +舉例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + + + + 保存文件类型 + 儲存檔案類型 + + + + layered.pdf 双层可搜索文档 + layered.pdf 雙層可搜尋文件 + + + + text.pdf 单层纯文本文档 + text.pdf 單層純文本文檔 + + + + 创建空白PDF文档,只写入识别文字,不含图片 + 創建空白PDF文件,只寫入識別文字,不含圖片 + + + + txt 标准格式 + txt 標準格式 + + + + 含识别文字和页数信息 + 含辨識文字和頁數資訊 + + + + p.txt 纯文字格式 + p.txt 純文字格式 + + + + 输出所有识别文字 + 輸出所有辨識文字 + + + + csv 表格文件(Excel) + csv 表格檔案(Excel) + + + + 将页数信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + 將頁數資訊和辨識內容寫入csv表格檔案。可用Excel開啟,另存為xlsx格式。 + + + + jsonl 原始信息 + jsonl 原始資訊 + + + + 每行为一条json数据,便于第三方程序读取操作 + 每行為一條json資料,便於第三方程式讀取操作 + + + + 忽略空白页 + 忽略空白頁 + + + + 若某一页没有文字或识别失败,也不会输出错误提示信息 + 若某一頁沒有文字或辨識失敗,也不會輸出錯誤提示訊息 + + + + 递归读取子文件夹 + 遞迴讀取子資料夾 + + + + 点击表格,可设置更多内容 + 點擊表格以設定更多內容 + + + + 导入文件夹时,导入子文件夹中全部文档 + 匯入資料夾時,匯入子資料夾中全部文件 + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文档名 +%range 识别页数范围。只有识别页数小于总页数时才会显示。 +举例:[OCR]_%name%range_%date +生成:[OCR]_文档A(p2-10)_20230901_1213.txt +添加占位符可以避免旧文件被新文件覆盖。 + 無需填寫副檔名。支援插入以下佔位符: +%date 日期時間 +%name 原文件名稱 +%range 識別頁數範圍。只有在識別頁數小於總頁數時才會顯示。 +舉例:[OCR]_%name%range_%date +產生:[OCR]_文件A(p2-10)_20230901_1213.txt +增加佔位符可以避免舊文件被新文件覆寫。 + + + + 保留原有图片,叠加一层透明文字,可以搜索和复制 + 保留原有圖片,疊加一層透明文字,可以搜尋和複製 + + + + 其它 + 其它 + + + + BatchOCR + + + 依然关闭 + 依然關閉 + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + 任務正在進行中。 +要結束任務並關閉頁面嗎? + + + + 排队 + 排隊 + + + + 处理 + 處理 + + + + %1 张图片识别失败! + %1 張圖片識別失敗! + + + + 批量识别完成 + 批次辨識完成 + + + + 批量识别任务异常 + 批次辨識任務異常 + + + + + 图片 + 圖片 + + + + 耗时 + 耗時 + + + + 状态 + 狀態 + + + + 打开图片 + 開啟圖片 + + + + 拖入图片或文件夹 + 拖入圖片或資料夾 + + + + 清空 + 清空 + + + + 请选择图片 + 請選擇圖片 + + + + 设置 + 設定 + + + + 记录 + 記錄 + + + + BatchOCRConfigs + + + OCR文本后处理 + OCR文本後處理 + + + + 忽略区域 + 忽略區域 + + + + 进入设置 + 進入設定 + + + + 批量任务 + 批次任務 + + + + 保存到 + 儲存到 + + + + 图片原目录 + 圖片原目錄 + + + + + 指定目录 + 指定目錄 + + + + 必须先指定“保存到指定目录”才生效 + 必須先指定「儲存到指定目錄」才生效 + + + + OCR结果保存目录 + OCR結果儲存目錄 + + + + 文件名格式 + 檔案名稱格式 + + + + 无需填写拓展名。支持插入以下占位符: +%date 日期时间 +%name 原文件夹名/文件名 +举例:[OCR]_%name_%date +生成:[OCR]_我的图片_2023-09-01_12-13.txt +添加占位符可以避免旧文件被新文件覆盖。 + 無需填寫副檔名。支援插入以下佔位符: +%date 日期時間 +%name 原資料夾名/檔案名稱 +舉例:[OCR]_%name_%date +生成:[OCR]_我的圖片_2023-09-01_12-13.txt +添加占位符可以避免舊文件被新檔案覆蓋。 + + + + 日期时间格式 + 日期時間格式 + + + + 文件名中 %date 的日期格式。支持插入以下占位符: +%Y 年、 %m 月、 %d 日、 %H 小时、 +%M 分钟、 %S 秒 、 %unix 时间戳 +举例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + 檔案名稱中 %date 的日期格式。支援插入以下佔位符: +%Y 年、 %m 月、 %d 日、 %H 小時、 +%M 分鐘、 %S 秒 、 %unix 時間戳 +舉例:%Y年%m月%d日_%H-%M +生成:2023年09月01日_12-13.txt + + + + 保存文件类型 + 儲存檔案類型 + + + + txt 标准格式 + txt 標準格式 + + + + 含原图片文件名和识别文字 + 含原圖片檔案名稱和辨識文字 + + + + p.txt 纯文字格式 + p.txt 純文字格式 + + + + 仅输出识别文字,不含图片标题 + 僅輸出辨識文字,不含圖片標題 + + + + txt 单独文件 + txt 單獨文件 + + + + 对每张图片,生成同名txt文件,仅输出识别文字 + 對每張圖片,生成同名txt檔案,僅輸出辨識文字 + + + + md 图文混排 + md 圖文混排 + + + + Markdown图文混排格式,可用Markdown阅读器浏览文件 + Markdown圖文混排格式,可用Markdown閱讀器瀏覽檔案 + + + + csv 表格文件(Excel) + csv 表格文件(Excel) + + + + 将图片信息和识别内容写入csv表格文件。可用Excel打开,另存为xlsx格式。 + 將圖片資訊和辨識內容寫入csv表格檔案。可用Excel開啟,另存為xlsx格式。 + + + + jsonl 原始信息 + jsonl 原始資訊 + + + + 每行为一条json数据,便于第三方程序读取操作 + 每行為一條json資料,便於第三方程式讀取操作 + + + + 输出忽略空白图片 + 輸出忽略空白圖片 + + + + 若图片没有文字或识别失败,也不会输出错误提示信息 + 若圖片沒有文字或辨識失敗,也不會輸出錯誤提示訊息 + + + + 递归读取子文件夹 + 遞迴讀取子資料夾 + + + + 导入文件夹时,导入子文件夹中全部图片 + 匯入資料夾時,匯出子資料夾中全部圖片 + + + + 其它 + 其它 + + + + Configs + + + %1 处理配置项异常: +%2枚举列表为空。 + %1 處理組態項目異常: +%2枚舉列表為空。 + + + + 重置 + 重置 + + + + 重置本页上的设定 + 重置本頁上的設定 + + + + 重置设定 + 重置設定 + + + + 要重置本页的设定吗? + 要重置本頁的設定嗎? + + + + 高级 + 進階 + + + + 显示更多高级选项。它们标有 * 号。 +请谨慎修改高级选项。 + 顯示更多進階選項。它們標有 * 號。 +請謹慎修改進階選項。 + + + + 展开 + 展開 + + + + 折叠 + 折疊 + + + + 必须为整数 + 必須為整數 + + + + 不能超过 + 不能超過 + + + + 不能低于 + 不能低於 + + + + 必须为数字 + 必須為數字 + + + + 已取消%1的快捷键。 + 已取消%1的快速鍵。 + + + + 更新热键成功 + 更新熱鍵成功 + + + + %1的快捷键为 %2 + %1的快速鍵為 %2 + + + + %1 快捷键%2已被注册,请尝试另外的按键组合。 + %1 快速鍵%2已被註冊,請嘗試另外的按鍵組合。 + + + + %1 快捷键%2无法注册,请尝试另外的按键组合。 + %1 快速鍵%2無法註冊,請嘗試另外的按鍵組合。 + + + + 请按下快捷键组合。按【Esc】退出。 + 請按下快速鍵組合。按【Esc】退出。 + + + + 当前快捷键录制已在进行,不能同时录制! + 目前快速鍵錄制已在進行,不能同時錄制! + + + + 无法录制快捷键 + 無法錄制快速鍵 + + + + DropArea_ + + + 松手放入文件 + 鬆手放入檔案 + + + + FontPanel + + + 界面 + 介面 + + + + 内容 + 內容 + + + + 界面字体: +软件中大部分UI的字体。 + 介面字體: +軟體中大部分UI的字體。 + + + + 内容字体: +识别结果内容的字体。 + 內容字體: +辨識結果內容的字體。 + + + + GlobalConfigs + + + 快捷方式 + 捷徑 + + + + 桌面 + 桌面 + + + + 开始菜单 + 開始功能表 + + + + 开机自启 + 開機自啟 + + + + 界面和外观 + 介面和外觀 + + + + 主题 + 主題 + + + + 切换主题 + 切換主題 + + + + 字体 + 字體 + + + + 修改字体 + 修改字體 + + + + 界面与文字大小 + 介面與文字大小 + + + + 渲染器 + 彩現器 + + + + 关闭硬件加速 + 關閉硬體加速 + + + + 若出现界面闪烁、元素错位等界面异常,尝试切换渲染器或者关闭硬件加速 + 若出現介面閃爍、元素錯位等介面異常,嘗試切換彩現器或者關閉硬體加速 + + + + 禁用美化效果 + 禁用美化效果 + + + + 在低配置机器上,禁用动画、阴影等效果可减少部分资源占用 + 在低配備機器上,禁用動畫、陰影等效果可減少部分資源佔用 + + + + 图片预览默认显示叠加层 + 圖片預覽預設顯示疊加層 + + + + 默认开启/关闭叠加层显示 +对所有图片预览组件生效 + 預設開啟/關閉疊加顯示 +對所有圖片預覽元素生效 + + + + 窗口 + 視窗 + + + + 启动时缩小到任务栏 + 啟動時縮小到工作列 + + + + 软件启动时,不弹出主窗口 + 軟體啟動時,不彈出主視窗 + + + + 窗口置顶 + 視窗置頂 + + + + 捷径:窗口左上角图钉 + 捷徑:視窗左上角圖釘 + + + + 锁定标签栏 + 鎖定分頁列 + + + + 捷径:窗口右上角小锁 + 捷徑:視窗右上角小鎖 + + + + 关闭主窗口时 + 關閉主視窗時 + + + + 最小化到系统托盘 + 最小化到系統匣 + + + + 退出应用 + 退出應用 + + + + 隐藏托盘图标 + 隱藏系統匣圖示 + + + + 若要弹出位于后台的软件窗口,请在文件管理器中重复启动软件。 +若要彻底退出软件,请从任务管理器中结束进程。 + 若要彈出在後台執行的的程式視窗,請在檔案管理器中重複開啟程式。 +若要徹底退出程式,請從工作管理員中選擇處理程序並結束工作。 + + + + 截图 + 截圖 + + + + 截图前隐藏主窗口 + 截圖前隱藏主視窗 + + + + 截图前,如果主窗口处于前台,则隐藏主窗口 +将会延时等待主窗口关闭 + 截圖前,如果主視窗處於前臺,則隱藏主視窗 +將會延時等待主視窗關閉 + + + + 隐藏等待时间 + 隱藏等待時間 + + + + 秒 + + + + + 服务 + 服務 + + + + 允许HTTP服务 + 允許HTTP服務 + + + + Umi-OCR依赖HTTP接口进行本机跨进程通信。如果禁用,将无法使用命令行模式、多开检测等功能。 + Umi-OCR依賴HTTP介面進行本機跨行程通訊。如果禁用,將無法使用命令列模式、多開檢測等功能。 + + + + + + 重启软件后生效 + 重啟軟體後生效 + + + + 主机 + 主機 + + + + 仅本地 + 僅本機 + + + + 任何可用地址 + 任何可用位址 + + + + 将允许局域网访问。请开启对应防火墙权限! + 將允許區域網路存取。請開啟對應防火牆權限! + + + + 将禁止局域网访问。 + 將禁止區域網路存取。 + + + + 端口 + 通訊埠 + + + + 日志 + 日誌 + + + + 操作 + 操作 + + + + 打开日志保存目录 + 開啟日誌儲存目錄 + + + + 保存的日志级别 + 儲存的日誌層級 + + + + 已禁用日志文件保存 + 已停用日誌檔案儲存 + + + + 请尝试更换软件路径! + 請嘗試更換軟體路徑! + + + + 配置文件读写异常 + 組態檔案讀寫异常 + + + + 插件加载失败 + 插件加載失敗 + + + + 成功添加快捷方式 + 成功新增捷徑 + + + + 添加快捷方式失败 + 新增捷徑失敗 + + + + 成功移除 %1 个快捷方式 + 成功移除 %1 個捷徑 + + + + 提示 + 提示 + + + + 没有找到可移除的快捷方式。 + 沒有找到可移除的捷徑。 + + + + 渲染器变更 将在重启软件后生效 + 彩現器變更 將在重啟軟體後生效 + + + + 端口号不合法 + 通訊埠號不合法 + + + + 端口号改为%1 + 通訊埠號改為%1 + + + + 原端口号%1被占用, +切换为新端口号%2。 + +若不想看到此通知,请在全局设置关闭高级模式。 + 原通訊埠號%1被佔用, +切換為新通訊埠號%2。 + +若不想看到此通知,請在全域設定關閉進階模式。 + + + + GlobalConfigsPage + + + 1. 关于快捷键、OCR语言等选项,请在各个功能页中进行设置。 +2. 勾选左下角的“高级”按钮,可以显示更多进阶选项。 + 1. 關於快捷鍵、OCR語言等選項,請在各功能頁中進行設定。 +2. 勾選左下角的「進階」按鈕,可以顯示更多進階選項。 + + + + 小贴士 + 常用提示 + + + + HTabBar + + + 窗口置顶 + 視窗置頂 + + + + 锁定标签栏 + 鎖定分頁列 + + + + IconButtonBar + + + 右键菜单 + 右鍵選單 + + + + 保存图片 + 儲存圖片 + + + + 图片大小:适应窗口 + 圖片大小:調整成視窗大小 + + + + 图片大小:实际 + 圖片大小:實際 + + + + 截图 + 螢幕截圖 + + + + 粘贴 + 貼上 + + + + 屏幕截图 + 熒幕截圖 + + + + 粘贴图片 + 貼上圖片 + + + + IgnoreArea + + + 拖入本地图片:OCR预览 +滚轮:缩放 +左键:拖拽 +右键:绘制忽略区域 + +可绘制一个或多个忽略区域矩形框。在执行批量OCR时,完全位于忽略区域内的文本块将被排除。 +比如批量处理影视截图时,可在右上角水印处添加忽略区域,避免输出水印文本。 + 拖入本機圖片:OCR預覽 +滾輪:縮放 +左鍵:拖拽 +右鍵:繪製忽略區域 + +可繪製一個或多個忽略區域矩形框。在執行批次OCR時,完全位於忽略區域內的文字區塊將被排除。 +比如批次處理影像截圖時,可在右上角水印處添加忽略區域,避免輸出水印文字。 + + + + 图像尺寸: + 圖像尺寸: + + + + 区域数量: + 區域數量: + + + + 撤销 + 復原 + + + + 清空 + 清空 + + + + ImageScale + + + 复制图片 + 複製圖片 + + + + 复制图片失败 + 複製圖片失敗 + + + + 打开图片 + 開啟圖片 + + + + 打开图片失败 + 開啟圖片失敗 + + + + + 保存图片 + 儲存圖片 + + + + 保存图片失败 + 儲存圖片失敗 + + + + ImageWithText + + + 图片:复制%1字 + 圖片:複製%1字 + + + + 图片:无选中文字 + 圖片:無選取文字 + + + + 图片:复制全部%1字 + 圖片:複製全部%1字 + + + + 复制  (Ctrl+C) + 複製  (Ctrl+C) + + + + 全选  (Ctrl+A) + 全選  (Ctrl+A) + + + + 复制图片(Ctrl+X) + 複製圖片(Ctrl+X) + + + + 保存图片(Ctrl+S) + 儲存圖片(Ctrl+S) + + + + 显示/隐藏文字(Tab) + 顯示/隱藏文字(Tab) + + + + 用默认应用打开图片 + 使用預設應用程式開啟圖片 + + + + 删除图片(Ctrl+D) + 删除圖片(Ctrl+D) + + + + MainWindowManager + + + 欢迎使用 Umi-OCR + 歡迎使用 Umi-OCR + + + + 已启用后台模式,可通过快捷键使用功能。 + 已啟用後台模式,可通過快速鍵使用功能。 + + + + MarkdownView + + + 打开网页 + 打開網頁 + + + + 链接 + 網址 + + + + MessageBox + + + 取消 + 取消 + + + + 确定 + 確定 + + + + 警告 + 警告 + + + + 发生了一点小问题 + 發生了一點小問題 + + + + 复制 + 複製 + + + + 已复制报错信息 %1 + 已複製報錯資訊 %1 + + + + 请前往 Issues 页面寻找解答或反馈 + 請前往Issues頁面尋找解答或回饋意見 + + + + 问题反馈 + 提出問題 + + + + MessageBoxWin + + + 确定 + 確定 + + + + 不再提示 + 不再提示 + + + + 知道了 + 知道了 + + + + MissionCtrlPanel + + + 开始任务 + 開始任務 + + + + 暂停任务 +暂停后可以待机或休眠。 +但是关机或退出软件,将会丢弃任务内容。 + 暫停任務 +暫停後可以待機或休眠。 +但是關機或退出軟體,將會丟棄任務內容。 + + + + 继续任务 + 繼續任務 + + + + 终止任务 +放弃未完成的内容。 + 終止任務 +放棄未完成的內容。 + + + + 正在运行 + 正在運行 + + + + 正在保存 + 正在儲存 + + + + 已暂停 + 已暫停 + + + + 任务停止 + 任務停止 + + + + 任务完成 + 任務完成 + + + + ModalLayer + + + 保存并返回 + 儲存並返回 + + + + 返回 + 返回 + + + + Navigation + + + 欢迎使用 Umi-OCR + 歡迎使用 Umi-OCR + + + + 请选择功能页 + 請選擇功能頁 + + + + 当前版本 + 當前版本 + + + + 项目链接 + 項目網址 + + + + 官方网站 + 官方網站 + + + + 插件拓展 + 外掛程式擴充 + + + + 问题反馈 + 提出問題 + + + + 功能页 + 功能頁 + + + + 请打开对应标签页(如批量OCR、批量文档),再拖入文件。 + 請打開對應標籤頁(如批量OCR、批量文件),再拖入文件。 + + + + OcrManager + + + 文字识别 + 文字辨識 + + + + 操作 + 操作 + + + + + 强制终止任务 + 強制終止任務 + + + + 应用修改 + 應用修改 + + + + 当前接口 + 目前介面 + + + + 没有可用的 OCR 插件。 + 沒有可用的 OCR 外掛程式。 + + + + OCR API 列表中不存在%1 + OCR API 列表中不存在%1 + + + + 当前已有%1组任务队列、共%2个任务正在执行。您可【强制终止任务】后修改API。 + 目前已有%1組任務隊列、共%2個任務正在執行。您可【強制終止任務】後修改API。 + + + + 无法修改 文字识别接口设置 + 無法修改 文字辨識介面設定 + + + + 文字识别接口应用成功 + 文字辨識介面應用成功 + + + + 当前API为【%1】 + 目前API為【%1】 + + + + 文字识别接口应用失败 + 文字辨識介面應用失敗 + + + + 当前没有运行中的任务 + 目前沒有執行中的任務 + + + + 当前已有%1组任务队列、共%2个任务正在执行。 +要强制终止全部任务吗? + 目前已有%1組任務隊列、共%2個任務正在執行。 +要強制終止全部任務嗎? + + + + PagesManager + + + 新标签页 + 新分頁 + + + + 截图OCR + 截圖OCR + + + + # 截图OCR + +屏幕截图,快捷转文字。也支持粘贴图片。 + # 截圖OCR + +熒幕截圖,快捷轉文字。也支援貼上圖片。 + + + + # 全局设置 + +调节全局设置项,对所有页面生效。 + # 全域設定 + +調節全域設定項,對所有頁面生效。 + + + + 批量OCR + 批次OCR + + + + # 批量OCR + +导入本地图片或文件夹,批量转换文字。 + +支持格式: + # 批次OCR + +匯入本機圖片或資料夾,批次轉換文字。 + +支援格式: + + + + 批量文档 + 批次文件 + + + + # 批量文档识别 + +批量导入文档,提取文字,或生成双层可搜索PDF。 + +支持格式: + # 批次文件辨識 + +批次匯入文件,提取文字,或生成雙層可搜尋PDF。 + +支援格式: + + + + 二维码 + 二維碼 + + + + # 二维码 + +识别或生成二维码、条形码。 + +支持协议: + # 二維碼 + +辨識或產生二維碼、條碼。 + +支援的協定: + + + + 全局设置 + 全域設定 + + + + 关于 + 關於 + + + + # 关于 + # 關於 + + + + PopupManager + + + 确认 + 確認 + + + + 取消 + 取消 + + + + PreviewDoc + + + 加密 + 加密 + + + + 打开文档失败 + 開啟文件失敗 + + + + 请填写正确的密码 + 請填寫正確的密碼 + + + + 密码正确 + 密碼正確 + + + + 文档预览异常 + 文件預覽異常 + + + + 忽略区域说明: +右键拖拽,绘制矩形区域,包含在区域内的文字框将被忽略。可用于排除水印、页眉页脚。 +范围允许填写负数,表示倒数第x页。如-1表示最后一页,-2表示倒数第2页。 +忽略区域的设置对所有文档生效。 + 忽略區域說明: +右鍵拖拽,繪製矩形區域,包含在區域內的文字框將被忽略。 可用於排除浮水印、頁首頁尾。 +範圍允許填寫負數,表示倒數第x頁。 如-1表示最後一頁,-2表示倒數第2頁。 +忽略區域的設定對所有檔案生效。 + + + + 密码: + 密碼: + + + + 预览页面 + 預覽頁面 + + + + 预览PDF时,是否预览OCR结果 + 預覽PDF時,是否預覽OCR結果 + + + + OCR页数 + OCR頁數 + + + + 忽略区域页数(全局) + 忽略區域頁數(全域) + + + + + 范围 + 範圍 + + + + 撤销 + 復原 + + + + 清空 + 清空 + + + + PreviewImage + + + 文字 + 文字 + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + 在圖片上疊加顯示辨識文字 +可在全域設定中設為預設關閉 + + + + QRCode + + + 获取剪贴板异常 + 獲取剪貼簿異常 + + + + 剪贴板中为文本 + 剪貼簿中為文本 + + + + 无有效图片 + 無有效圖片 + + + + 导入%1条图片路径 + 匯入%1條圖片路徑 + + + + 参数有误,或输入内容不合规定。请参照报错指示修改: + 參數有誤,或輸入內容不合規定。請參照報錯訓示修改: + + + + + 生成二维码失败 + 生成二維碼失敗 + + + + 已复制到剪贴板 + 已複製到剪貼簿 + + + + 识图完成 + 識圖完成 + + + + 无文字 + 無文字 + + + + 识别失败 + 辨識失敗 + + + + 扫描二维码 + 掃描二維碼 + + + + 截图、拖入或粘贴二维码图片 + 截圖、拖入或貼上二維碼圖片 + + + + + 设置 + 設定 + + + + 自动刷新 + 自動重新整理 + + + + 修改文字后,自动生成二维码/条形码 + 修改文字後,自動生成二維碼/條碼 + + + + 刷新 + 重新整理 + + + + 生成二维码/条形码 + 生成二維碼/條碼 + + + + 记录 + 記錄 + + + + 生成 + 生成 + + + + QRCodeConfigs + + + 快捷键 + 快速鍵 + + + + 屏幕截图 + 熒幕截圖 + + + + 粘贴图片 + 貼上圖片 + + + + 预处理(一般无需改动) + 預處理(一般無需改動) + + + + 中值滤波 + 中值濾波 + + + + 对图像进行平滑处理 +>0 时生效。可填1~9的奇数(不允许偶数) + 對圖像進行平滑處理 +>0 時生效。可填1~9的奇數(不允許偶數) + + + + 锐度增强 + 銳度增強 + + + + 增加图像的锐度 +>0.1 时生效。可填0.1~10的小数 + 增加圖像的銳度 +>0.1 時生效。可填0.1~10的小數 + + + + 对比度增强 + 對比度增強 + + + + 增加图像的对比度 +>0.1 时生效。可填0.1~10的小数 + 增加圖像的對比度 +>0.1 時生效。可填0.1~10的小數 + + + + 转为灰度 + 轉為灰度 + + + + 将图像像素转为灰度 + 將圖像畫素轉為灰度 + + + + 二值化 + 二值化 + + + + 将图像像素转为纯黑和纯白 +启用了灰度,且二值化 >-1 时生效。可填0~255的整数 + 將圖像畫素轉為純黑和純白 +啟用了灰度,且二值化 >-1 時生效。可填0~255的整數 + + + + 扫码后的操作 + 掃碼後的操作 + + + + 复制结果 + 複製結果 + + + + 弹出主窗口 + 彈出主視窗 + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + 識圖後,如果主視窗最小化或處於背景,則彈到前景 + + + + 生成二维码/条形码 + 生成 QR Code/條碼 + + + + 类型 + 類型 + + + + 默认二维码: + 預設 QR Code: + + + + 宽度 + 寬度 + + + + + 填0:自动选择 + 填0:自動選擇 + + + + + + 像素 + 像素 + + + + 高度 + 高度 + + + + 边缘空白 + 邊緣空白 + + + + 填-1:自动选择 + 填-1:自動選擇 + + + + 纠错等级 + 糾錯等級 + + + + 仅适用于: + 僅適用於: + + + + 自动 + 自動 + + + + 其它 + 其它 + + + + ResultTextContainer + + + 复制 + 複製 + + + + ResultsTableView + + + 异常状态码:%1 +异常信息:%2 + 異常狀態碼:%1 +異常訊息:%2 + + + + 耗时 %1 + 耗時 %1 + + + + 置信度 %1 + 信賴度 %1 + + + + 记录:复制%1字 + 記錄:複製%1字 + + + + 记录:无选中文字 + 記錄:無選取文字 + + + + 记录:复制全部%1字 + 記錄:複製全部%1字 + + + + 删除%1条记录 + 刪除%1條記錄 + + + + 清空记录 + 清空記錄 + + + + 复制    (Ctrl+C) + 複製    (Ctrl+C) + + + + 复制全部  (Ctrl+C 双击) + 複製全部  (Ctrl+C 雙擊) + + + + 选中单个  (Ctrl+A) + 選取單個  (Ctrl+A) + + + + 选中全部记录(Ctrl+A 双击) + 選取全部記錄(Ctrl+A 雙擊) + + + + 删除选中记录 + 刪除選取記錄 + + + + 清空全部记录(Ctrl+D 双击) + 清空全部記錄(Ctrl+D 雙擊) + + + + 滚动 + 捲動 + + + + 自动滚动到底部 + 自動捲動到底部 + + + + 右键菜单 + 右鍵選單 + + + + ScreenshotManager + + + 截图失败 + 截圖失敗 + + + + 上次截图操作未结束,不能进行新的截图! + 上次截圖操作未結束,不能進行新的截圖! + + + + 尚未记录截图区域 + 尚未記錄截圖區域 + + + + 显示器: %1 +错误信息: %2 + 顯示器: %1 +錯誤資訊: %2 + + + + 重复截图失败 + 重複截圖失敗 + + + + 未找到匹配的屏幕 + 未找到可供配對的螢幕 + + + + 屏幕设备名称不相同: +%1 +%2 + 螢幕設備名稱不相同: +%1 +%2 + + + + ScreenshotOCR + + + 获取剪贴板异常 + 獲取剪貼簿異常 + + + + 剪贴板中为文本 + 剪貼簿中為文本 + + + + 无有效图片 + 無有效圖片 + + + + 导入%1条图片路径 + 匯入%1條圖片路徑 + + + + 依然关闭 + 依然關閉 + + + + 任务正在进行中。 +要结束任务并关闭页面吗? + 任務正在進行中。 +要結束任務並關閉頁面嗎? + + + + + 屏幕截图 + 熒幕截圖 + + + + + 粘贴图片 + 貼上圖片 + + + + 截图识别任务异常 + 截圖辨識任務異常 + + + + 已复制到剪贴板 + 已複製到剪貼簿 + + + + 识图完成 + 識圖完成 + + + + 无文字 + 無文字 + + + + 识别失败 + 辨識失敗 + + + + + 停止任务 + 停止任務 + + + + 文字 + 文字 + + + + 在图片上叠加显示识别文字 +可在全局设置中设为默认关闭 + 在圖片上疊加顯示辨識文字 +可在全域設定中設為預設關閉 + + + + 截图、拖入或粘贴图片 + 截圖、拖入或貼上圖片 + + + + 设置 + 設定 + + + + 记录 + 記錄 + + + + ScreenshotOcrConfigs + + + OCR文本后处理 + OCR文本後處理 + + + + 快捷键 + 快速鍵 + + + + 屏幕截图 + 熒幕截圖 + + + + 粘贴图片 + 貼上圖片 + + + + 重复截图 + 重複截圖 + + + + 重新截取上一次截图的范围 + 重新擷取上一次截圖的範圍 + + + + 识图后的操作 + 識圖後的操作 + + + + 复制结果 + 複製結果 + + + + 弹出主窗口 + 彈出主視窗 + + + + 识图后,如果主窗口最小化或处于后台,则弹到前台 + 識圖後,如果主視窗最小化或處於背景,則彈到前景 + + + + 其它 + 其它 + + + + Size_ + + + 1.0 + 1.0 + + + + SystemTray + + + 打开主窗口 + 打開主視窗 + + + + 退出 Umi-OCR + 退出 Umi-OCR + + + + Theme + + + 珍珠白 + 珍珠白 + + + + 云墨黑 + 雲墨黑 + + + + 赛博朋克 + 賽博龐克 + + + + UtilsConfigDicts + + + 任务完成后的操作 + 任務完成後的操作 + + + + 系统 + 系統 + + + + 无 + + + + + 关机 + 關機 + + + + 休眠 + 休眠 + + + + %1 系统暂不支持电源控制! + %1 系統暫不支持電源控制! + + + + 继续%1 + 繼續%1 + + + + 取消%1 + 取消%1 + + + + 系统即将%1 + 系統即將%1 + + + + 排版解析方案 + 排版方式 + + + + 按什么方式,解析和排序图片中的文字块 + 按照什麼方式,解析和排序圖片中的文字區塊 + + + + 多栏-按自然段换行 + 多欄 ─ 按自然段落換行 + + + + 多栏-总是换行 + 多欄 ─ 總是換行 + + + + 多栏-无换行 + 多欄 ─ 不換行 + + + + 单栏-按自然段换行 + 單欄 ─ 按自然段落換行 + + + + 单栏-总是换行 + 單欄 ─ 總是換行 + + + + 单栏-无换行 + 單欄 ─ 不換行 + + + + 单栏-保留缩进 + 單欄 ─ 保留縮排 + + + + 不做处理 + 不做處理 + + + + 优先内部 + 優先內部 + + + + 只允许内部 + 只允許內部 + + + + 只允许外部 + 只允許外部 + + + + 禁用所有通知 + 禁用所有通知 + + + + 跟随全局设定 + 跟隨全域設定 + + + + 通知弹窗类型 + 通知彈窗類型 + + + + 通知类型已更改 + 通知類型已更改 + + + diff --git a/dev-tools/i18n/翻译.code-workspace b/dev-tools/i18n/翻译.code-workspace new file mode 100644 index 0000000..541a25c --- /dev/null +++ b/dev-tools/i18n/翻译.code-workspace @@ -0,0 +1,13 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + }, + "python.formatting.provider": "none" + } +} \ No newline at end of file diff --git a/dev-tools/i18n/翻译步骤(完整).md b/dev-tools/i18n/翻译步骤(完整).md new file mode 100644 index 0000000..4d29412 --- /dev/null +++ b/dev-tools/i18n/翻译步骤(完整).md @@ -0,0 +1,139 @@ +# 翻译步骤(完整) + + + +## 准备 + +本文将介绍 Umi-OCR 代码文件翻译的完整流程。 + +请先fork或clone本仓库。强烈建议只 clone 主分支,因为某些分支含有体积很大的二进制库,会让你花费很长时间下载。 + +``` + git clone --branch main --single-branch https://github.com/hiroi-sora/Umi-OCR_v2.git +``` + +要求:Windows系统。 + +本文中的代码指令,如果没有特别说明,则是在当前工作目录(`dev-tools/i18n`)中输入到命令行。 + +例,你可以:按Win+R,输入cmd,打开命令行,然后输入下述命令跳转到工作目录(假设工程放在`D:\Projects\`) + +``` +cd /d D:\Projects\Umi-OCR_v2\dev-tools\i18n +``` + +本文假设当前要翻译的目标语言为英语 `en_US` 。如果是其它目标语言,请将下文所有 `en_US` 替换为目标语言标识符。所有标识符如下: + +| 标识符 | 语言 | +| ------- | --------- | +| `zh_CN` | 简体中文 | +| `zh_TW` | 繁體中文 | +| `en_US` | English | +| `es_ES` | Español | +| `fr_FR` | Français | +| `de_DE` | Deutsch | +| `ja_JP` | 日本語 | +| `ko_KR` | 한국어 | +| `ru_RU` | Русский | +| `pt_BR` | Português | +| `it_IT` | Italiano | + +与上述语言语种相同,但地区不同的语言,将应用最接近的翻译文件。如: +(`zh_HK`,`en_GB`,`en_CA`,`es_MX`,`fr_CA`,`de_AT`,`de_CH`,`pt_PT`) + +如果想添加一个不属于上述任何语种的翻译,需要在源代码中添加对应的标识符映射(`UmiOCR-data\py_src\utils\i18n_configs.py`)。或者,请寻求项目作者的帮助。 + +## 简介 + +Qt-Qml框架的翻译工作的流程分为3步:(假设当前要翻译为英语`en_US`) + +1. 从源代码中提取出所有待翻译字符串,生成 `en_US.ts` 工程文件。 +2. 编辑 `en_US.ts` 文件,将其中的文本进行翻译。 +3. 将写好的 `en_US.ts` 文件,编译为 `en_US.qm` 二进制文件。 + +完成上述工作后,就能将`en_US.qm`加载进Umi-OCR中,在Umi中切换为`English`语言了。 + +## 1. 提取待翻译字符串 + +#### 方法一:生成/刷新单个ts工程文件 + +使用Qt工具 `lupdate.exe` + +```cmd +lupdate.exe "../../UmiOCR-data/qt_res/qml" -recursive -ts "en_US.ts" +``` + +如果提示`无法将“lupdate.exe”项识别……`,则在指令最前面加上`./`,如: + +```cmd +./lupdate.exe "../../UmiOCR-data/qt_res/qml" -recursive -ts "en_US.ts" +``` + +执行成功时,应该输出如下: + +```cmd +Scanning directory '../../UmiOCR-data/qt_res/qml'... +Updating 'en_US.ts'... + Found 307 source text(s) (140 new and 167 already existing) + Kept 42 obsolete entries + Same-text heuristic provided 21 translation(s) +``` + +如果已经开始编辑ts文件,重新调用`lupdate.exe`是安全的,不会丢失已经编辑的进度(当然,建议多备份)。 + +#### 方法二:生成/刷新所有语言的ts工程文件 + +比如修改了源代码,往源代码中添加了新的文本后,要刷新所有ts文件。 + +运行本目录下的 `lupdate_all.py` + +## 2. 进行翻译工作 + +使用Qt工具 `linguist.exe` 可视化进行翻译工作。 + +```cmd +linguist.exe "en_US.ts" +``` + +或者,文件管理器中,将 `en_US.ts` 拖到 `linguist.exe` 图标上。 + +`linguist.exe`会打开一个可视化窗口,在上面进行翻译或检查。 + +如果是首次打开该ts文件,`linguist`会弹窗询问源语言和目标语言。源语言`Source language`选择`Chinese (简体中文)`,目标语言自定。`Country`一般选`Any Country`即可。 + +`linguist` 使用很简单,具体用法可在网上找,下面介绍基础用法: + +1. 左侧`Context`栏表示待翻译的源代码文件,及每个文件的文本条数。如`BatchOCR 18/19`表示源代码`BatchOCR`一共有19条文本,其中18条已完成翻译。 +2. 中上`Strings`栏表示当前源代码文件的每一条文本。图标`?`/`!`表示未翻译或未检查,`√`表示已翻译。每翻译一条文本,记得点击该图标,将它转为`√`。 +3. 右侧`Sorces and Forms`栏表示该文本在源代码中的位置。可以结合代码中的注释来进行翻译。 +4. 中心栏进行翻译。`Sorces text`为原文本,`Translattion to XXX`填写翻译文本,`Translator comments for XXXX` 可以不用写。 + +进行工作前,请务必阅读 [翻译注意事项](翻译注意事项.md) 。 + +## 3. 编译二进制包 + +方法一:使用`linguist.exe` + +点击菜单 `File` → `Release` ,即可在本目录下生成 `en_US.qm`。 + +方法二:使用指令 + +```cmd +lrelease.exe "en_US.ts" +``` + +## 放置翻译文件 + +将编译好的二进制文件 `en_US.qm` 拷贝到下述目录: + +``` +Umi-OCR_v2/UmiOCR-data/i18n +``` + +启动 Umi-OCR ,即可在全局设置中切换该语言。请仔细检查翻译文本是否正确,尺寸是否合适。可按照`翻译注意事项`的说明调整字体缩放系数。 + +## 提交贡献 + +将翻译好的 `.ts` 文件放在本目录下, `.qm` 文件放在 `Umi-OCR_v2/UmiOCR-data/i18n` 。不要包含其它无关文件。 + +然后提交PR。 \ No newline at end of file diff --git a/dev-tools/i18n/翻译步骤(简易).md b/dev-tools/i18n/翻译步骤(简易).md new file mode 100644 index 0000000..2c1a6f8 --- /dev/null +++ b/dev-tools/i18n/翻译步骤(简易).md @@ -0,0 +1,81 @@ +# 翻译步骤(简易) + +本文将介绍 Umi-OCR 代码文件翻译的简易流程,供非程序员的译者快速进入工作。 + +## 会用Git + +请先fork或clone本仓库。强烈建议只 clone 主分支,因为某些分支含有体积很大的二进制库,会让你花费很长时间下载。 + +``` + git clone --branch main --single-branch https://github.com/hiroi-sora/Umi-OCR_v2.git +``` + +## 不会用Git + +1. fork本项目。 +2. 下载本项目的zip包。步骤:本项目的GitHub首页 → 绿色Code按钮 → `Download ZIP` +3. 进行翻译工作。 + +## 翻译工作 + +当前目录下有多个翻译工程文件,下面是文件名与对应语言。 + +这些文件中已经记录了部分工作进度(机器翻译,或者别的译者的工作)。 + +你擅长哪种语言,就按下文方法打开对应的文件,修改原有翻译或补充新的翻译。 + +如果没有找到对应文件,请参考 [翻译步骤(完整)](翻译步骤(完整).md) 生成。 + +| 文件 | 语言 | +| ---------- | --------- | +| `zh_CN.ts` | 简体中文 | +| `zh_TW.ts` | 繁體中文 | +| `en_US.ts` | English | +| `es_ES.ts` | Español | +| `fr_FR.ts` | Français | +| `de_DE.ts` | Deutsch | +| `ja_JP.ts` | 日本語 | +| `ko_KR.ts` | 한국어 | +| `ru_RU.ts` | Русский | +| `pt_BR.ts` | Português | +| `it_IT.ts` | Italiano | + +将对应的文件,拖入本目录的 `linguist.exe` 图标上即可打开翻译窗口。 + +`linguist` 使用很简单,具体用法可在网上找,下面介绍基础用法: + +1. 左侧`Context`栏表示待翻译的源代码文件,及每个文件的文本条数。如`BatchOCR 18/19`表示源代码`BatchOCR`一共有19条文本,其中18条已完成翻译。 +2. 中上`Strings`栏表示当前源代码文件的每一条文本。图标`?`/`!`表示未翻译或未检查,`√`表示已翻译。每翻译一条文本,记得点击该图标,将它转为`√`。 +3. 右侧`Sorces and Forms`栏表示该文本在源代码中的位置。可以结合代码中的注释来进行翻译。 +4. 中心栏进行翻译。`Sorces text`为原文本,`Translattion to XXX`填写翻译文本,`Translator comments for XXXX` 可以不用写。 + +进行工作前,请务必阅读 [翻译注意事项](翻译注意事项.md) 。 + + +完成翻译和校验后,在`linguist`内,点击菜单 `File` → `Release` ,即可在本目录下编译生成 `en_US.qm`(假设工程文件为`en_US.ts`)。 + +将它拷贝到下述目录: + +``` +Umi-OCR_v2/UmiOCR-data/i18n +``` + +启动 Umi-OCR ,即可在全局设置中切换该语言。请仔细检查翻译文本是否正确,尺寸是否合适。可按照`翻译注意事项`的说明调整字体缩放系数。 + +## 提交贡献 + +### 会用Git + +将翻译好的 `.ts` 文件放在本目录下, `.qm` 文件放在 `Umi-OCR_v2/UmiOCR-data/i18n` 。不要包含其它无关文件。 + +然后提交PR。 + +### 不会用Git + +1. 找到你翻译好的`xxx.ts`文件,用记事本打开,复制全部文本。 +2. 在GitHub找到你fork出来的仓库(你账号下的仓库),打开相同的`xxx.ts`。 +3. 点击右上角的铅笔图标`Edit`。清空原有内容,将第4步复制的文本粘贴进去。 +4. 点击右上角 Commit changes 。 +5. 找一找 `Create pull request` 按钮,向本仓库提交PR。 + +#### 感谢各位译者大佬的贡献! \ No newline at end of file diff --git a/dev-tools/i18n/翻译注意事项.md b/dev-tools/i18n/翻译注意事项.md new file mode 100644 index 0000000..8e93cf5 --- /dev/null +++ b/dev-tools/i18n/翻译注意事项.md @@ -0,0 +1,113 @@ +# 翻译注意事项 + +请译者务必注意本文提出的事项。 + +## 关于换行`\n` + +源代码中以`\n`表示的换行符,在翻译工具 `linguist` 中转换直接换行(结尾符号类似音符)。 + +翻译时,也应该用回车来输入直接换行,而不是插入`\n`。例: + +源代码文本: +``` +这是第一行\n这是第二行 +``` + +错误的翻译:`\n`换行 +``` +This is the first line\nThis is the second line +``` + +正确的翻译格式:直接换行 +``` +This is the first line +This is the second line +``` + +## 关于占位符`%1` + +待翻译文本中可能含有`%数字`格式的占位符。翻译时,也应该保留所有占位符。例: + +源代码文本: +``` +原端口号%1被占用,\n切换为新端口号%2。 +``` + +正确的翻译格式: +``` +The original port number %1 is occupied. +Switching to the new port number %2. +``` + +## 关于MarkDown格式`#` + +有一些源文件,如`PagesManager`中,会含有一些markdown格式的长字符串。 + +请务必遵照原文本的格式来编写翻译文本,保留如`#`、`  `这样的特殊字符。 + +(`  `为1个全角空格+2个半角空格,用于让qml的md解析器空出一行。直接复制这坨东西即可。) + +例:源代码 +``` +# 截图OCR + +  + +屏幕截图,快捷转文字。也支持粘贴图片。 +``` + +正确的翻译格式: +``` +# Screenshot OCR + +  + +Screenshot, quick text conversion. Also supports pasting images. +``` + +## 关于字体缩放系数 + +在 Umi-OCR 界面设计中,很多UI组件的尺寸是和行高绑定的。比如定义一个按钮,宽度为5倍行高,高度为1.5倍行高。这些宽高系数是以中文为基准设定的。 + +相同单词或句子,有些语言的占地面积与中文并不相同。比如`截屏`-`Screenshot`,英文的长度更长。因此,当使用英文UI语言时,要适当缩小文字尺寸以适应UI。即行高不变,调整文字大小。 + +在 `Size_` 中,有一个待翻译的文本 `1.0` 。对应到代码的这一行: +``` +property string languageScale: qsTr("1.0") +``` + +修改这个数字,如改为 `0.92` ,就能调整当前翻译的字体缩放系数。 + +为了方便测试,可以先打开Umi-OCR`全局设置`标签页,勾选`高级`。拉到最底下,`developer tools` → `languageScale (textScale)` 。调整该数字,可以实时预览字体缩放。在这里调整好后,再将数字填入翻译文件中。 + + +## 转换为纯文本 / 使用机器翻译 + +如果本地已安装Python,将ts文件拖动到 `convert_ts_txt.py` 可转为纯文本txt文件,每行对应一条原文本。 + +或者命令行调用: + +``` +convert_ts_txt.py en_US.ts +``` + +结果:`en_US.ts` → `en_US.txt` + +(在这个步骤,所有换行暂时以`\n`表示。后期将txt转回ts时,脚本会自动处理换行。) + +然后可以用自动化手段处理 `en_US.txt` 了。比如问GPT: + +``` +请你充当翻译器,协助完成一个软件的UI翻译工作。 +下面将输入多行文本。每行是一个原文。请你将其翻译为译文。 +请保持原文的特殊格式,原文中的换行、%开头的标识符、mardown等格式予以保留。 +首先,将简体中文翻译为英语。 + +【放文本】 +``` + +接下来,将翻译好的文本覆盖写回`en_US.txt`(写之前先备份一下txt)。使用有行号的文本编辑器,检查每行对应的译文是否和原文一致。 + +然后,将写好的`en_US.txt`拖入 `convert_txt_ts.py` ,这将覆盖写入 `en_US.txt.ts` 。(先备份一下ts) + +最后,用 `linguist` 打开 `en_US.txt.ts` ,检查、校对翻译,编译为`en_US.txt.qm`。重命名为`en_US.qm`,即可导入Umi-OCR测试。 \ No newline at end of file diff --git a/docs/README_CLI.md b/docs/README_CLI.md new file mode 100644 index 0000000..d47e692 --- /dev/null +++ b/docs/README_CLI.md @@ -0,0 +1,257 @@ +[Translate to English](https://github-com.translate.goog/hiroi-sora/Umi-OCR/blob/main/docs/README_CLI.md?_x_tr_sl=zh-CN&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=wapp) + +- 命令行手册 +- [HTTP接口手册](http/README.md) + +# 命令行手册 + +### 基础说明 + +命令行调用入口就是主程序 `Umi-OCR.exe` 。如果你使用的是备用启动器(如`UmiOCR-data/RUN_GUI.bat`),可能无法使用命令行。 + +![Umi-OCR-全局页-服务.png](https://tupian.li/images/2023/10/25/653907e9bac06.png) + +如上图,必须允许HTTP服务才能使用命令行(默认开启)。主机选择 **仅本地** 就行了。 + +> Umi-OCR 依赖HTTP接口进行跨进程通信,将你输入的命令行指令传递给后台的Umi-OCR处理进程。通信过程仅在系统内部的本地环回进行,不会泄露到外部(不经过物理网卡),请放心使用。 + +**获取说明**:`umi-ocr --help` + +### 软件操控指令 + +**弹出主窗口**:`umi-ocr --show` + +**隐藏主窗口**:`umi-ocr --hide` + +**关闭软件**:`umi-ocr --quit` + +**重新加载配置文件**:`umi-ocr --reload` (`v2.1.5`以上版本支持) + +> Umi-OCR 的配置文件是 `./UmiOCR-data/.settings` ,ini格式;软件界面上设置的参数将会保存到此文件。允许手动修改配置文件,然后使用 `--reload` 指令来重新加载该配置文件并刷新软件设置界面。 + +### OCR指令 + +**鼠标截屏**:`umi-ocr --screenshot` + +
    +范围截屏(无需鼠标划选) +
    +自动对指定屏幕、指定区域进行截屏。 + +**范围截屏** 指令: +```bash +umi-ocr --screenshot screen=0 rect=x,y,w,h +``` + +范围截屏控制参数: +- `screen`: 要截图的显示器编号(多个显示器时有效),从0开始。缺省为0。 +- `rect`: 截图范围矩形框,`x坐标,y坐标,w宽度,h高度`。缺省为全屏。 + +注意: +- 这两个参数的前面无需加`--`。 +- 这两个参数至少要填一个,才能触发范围截图。没有任一参数时,执行鼠标截屏。 + +示例1:截取第1个显示器的全屏 +```bash +umi-ocr --screenshot screen=0 +``` + +示例2:截取第2个显示器,从左上角 (50,100) 开始,大小为 300x200 的矩形区域 +```bash +umi-ocr --screenshot screen=1 rect=50,100,300,200 +``` + +示例3:与 [HotkeysCMD](https://github.com/hiroi-sora/HotkeysCMD) 工具配合,实现点击 **快捷键** 进行范围截图。 + +向 HotkeysCMD 的配置文件中添加这一行,表示点击 `F10` 时进行范围截图: + +```bash +F10 umi-ocr --screenshot screen=0 rect=50,100,300,200 +``` + +更多快捷键定义方式,详见 [HotkeysCMD](https://github.com/hiroi-sora/HotkeysCMD) 文档。 + +

    + +**粘贴图片**:`umi-ocr --clipboard` + +**指定路径**:`umi-ocr --path "D:/xxx.png"` + +- 可传入文件夹的路径。将搜索文件夹中所有图片(包括嵌套子文件夹),并输出所有识别结果。 +- 可传入多个路径。请用双引号`""`包裹单个路径,不同路径间用空格 ` ` 隔开。 + +**指定多个路径** 示例:`umi-ocr --path "D:/img1.png" "D:/img2.png" "D:/image/test"` + +提示: + +- 多图识别时,耗时较长;一次命令结束前不要输入下一个命令。 +- 对于截屏、粘贴、路径指令,OCR参数(如识别语言,是否复制到剪贴板、是否弹出主窗口)采用`截图OCR`标签页的设定。如果不希望命令行任务弹出主窗口,请在`截图OCR`标签页中关闭该选项。 + +### 二维码指令 + +**识别二维码**:`umi-ocr --qrcode_read "D:/xxx.png"` + +- 与OCR指令一致,二维码识别的指令也支持传入多个图片&文件夹路径。 + +**生成二维码**:`umi-ocr --qrcode_create "文本内容" "D:/输出图片.jpeg"` + +- 默认的图片宽高为最小适配长度。也可以在指令后方加上数字,手动指定图片宽高: + +例,同时指定宽高为128像素:`umi-ocr --qrcode_create "文本内容" "D:/输出图片.jpeg" 128` + +例,宽128,高256像素:`umi-ocr --qrcode_create "文本内容" "D:/输出图片.jpeg" 128 256` + +### 关于指令简写 + +- 所有指令支持用前几个字母替代,如`--screenshot`、`--clipboard`可以分别简写为`--sc`、`--clipbo`。具体可自己尝试。 +- 对于大部分系统,支持使用小写文件名+省略`.exe`来调用程序。即 `umi-ocr --sc` 等价于 `Umi-OCR.exe --sc` 。 + +--- + +### 命令行结果输出 + +- **复制到剪贴板** ` --clip` +- **输出到文件(覆盖)** ` --output "file.txt"` +- **输出到文件(追加)** ` --output_append "file.txt"` + +也可以使用箭头符号: + +- `"-->"` 等价于 `--output` +- `"-->>"` 等价于 `--output_append` + +例: +```bash +umi-ocr --screenshot --clip +umi-ocr --screenshot --output test.txt +umi-ocr --screenshot "-->" test.txt +``` + +> 注:由于运行环境的一些限制,Umi-OCR 暂时无法重定向输出流,系统管道重定向符`>`、管道操作符`|`可能失效。 +> 如果需要用程序 调用命令行指令,但是发现无法收到回传,可使用 [HTTP转发命令行](http/argv.md#/argv) 代替。 + +--- + +## 高级指令 + +(仅供有经验的开发者使用) + +高级指令代表了一种无限的可能性(笑),允许通过命令行调用任意标签页(模块)上的任意函数。但是用法比较复杂,你需要在一定程度上阅读本项目源码才能知道该调用哪个函数、传入什么参数。 + +### 页面指令 + +> “页面模板”相当于收藏夹,可以从收藏夹中打开一个新页面。 +> “已打开的页面”可以关闭。 + +查询当前已打开的页面,及所有页面模板:可以获取 [index] +``` +umi-ocr --all_pages +``` + +创建新标签页:[index] 为页面模板序号 +``` +umi-ocr --add_page [index] +``` + +删除已创建的标签页:[index] 为现有页面序号 +``` +umi-ocr --del_page [index] +``` + +### 模块指令 + +> 每个标签页,通常会具有两个模块,一个是py,一个是qml。还可能会有一些不依附于标签页的独立py或qml模块。 +> 每个模块上都有一些函数可以被调用。 +> 模块名 [name] 允许简写,如一个模块全称是 "ScreenshotOCR_1" ,那么可用 "ScreenshotOCR" 来代替。 +> 每次程序运行,模块名(的后缀)不一定相同。请使用简写来忽略后缀。 + +查询当前存在的py和qml模块:可获取 [name] +``` +umi-ocr --all_modules +``` + +### 函数指令 + +查询某个py模块上有什么可调用的函数: [name] 为模块名 +``` +umi-ocr --call_py [name] +``` + +查询某个qml模块上有什么可调用的函数: [name] 为模块名 +``` +umi-ocr --call_qml [name] +``` + +调用py模块上的函数: + +- [name] 为模块名,[function] 为函数名。 [..paras] 为任意个参数。 +- paras 输入字符串。会根据文本结构,自动转为4种变量类型: `int`、`float`、`list`、`dict` 。 +``` +umi-ocr --call_py [name] --func [function] [..paras] +``` + +调用qml模块上的函数: +``` +umi-ocr --call_qml [name] --func [function] [..paras] +``` + +示例,调用二维码页qml模块的路径扫码函数,传入路径列表: + +``` +umi-ocr --call_qml QRCode --func scanPaths '[\"D:/Pictures/Screenshots/test/二维码/1111.png\",\"D:/Pictures/Screenshots/test/二维码/2222.png\"]' +``` + +### 同步调用函数 + +> 命令行解析器运行在子线程。为了确保线程安全,默认转到主线程执行命令。所以对于你来说就是异步执行的了,即无法取得函数的返回值。 +> 如果要获取函数返回值,可传入 --thread 指令,同步执行命令。 +> 这种操作较不安全,可能导致功能不正常甚至程序崩溃。 + +``` +umi-ocr --call_qml [name] --func [function] --thread [..paras] +``` + +--- + +### 高级指令示例: + +示例目标:将一些PDF文档添加到软件,生成双层可搜索PDF。 + +(提示:这个例子只是用来演示高级指令能做到什么事情。PDF文档识别可以直接调用 [HTTP接口](http/api_doc.md) 。) + +做法: + +##### 1. (可选)如果当前没有打开`批量文档`标签页,那么打开它: +- 1.1. 查询当前所有页面模板: +```bash +umi-ocr --all_pages +``` +- 1.2. 已知`BatchDOC`标签页的 `template_index` 为`3`。创建该标签页: +```bash +umi-ocr --add_page 3 +``` +- 1.3. 检查 `BatchDOC` 模块是否已存在: +```bash +umi-ocr --all_modules +``` +- 发现在`Qml modules`中,已存在 `BatchDOC_1` ,那么就是正确的。 + +##### 2. 将多个文档的路径,输入软件: +- 假设想要添加以下的文件: +```bash +C:\Users\My\Desktop\111.epub +C:\Users\My\Desktop\222.pdf +``` +- 使用以下指令,输入文档路径:(路径中`\`需要改为`/`) +```bash +umi-ocr --call_qml BatchDOC --func addDocs '[ \"C:/Users/My/Desktop/111.epub\", \"C:/Users/My/Desktop/222.pdf\"]' +``` +关于`addDocs`后面路径参数的格式: +- 在 Powershell 中,最外层为单引号`'`,且左双引号前面必须有空格。即:`'[■\"path_1\",■\"path_2\",■\"path_3\"]'` (将`■`替换为空格` `)。单个路径为`'[■\"路径1\"]'`。 +- 在 Terminal (终端)中,最外层为双引号`"`。即:`"[\"path_1\",\"path_2\",\"path_3\"]"`。 +- 这是 Windows 解析命令行参数的规则限制,与 Umi 自身的设计无关。 + +##### 3. 启动任务: +```bash +umi-ocr --call_qml BatchDOC --func docStart +``` +- 暂时无法通过CLI更改保存文件的类型(默认为 双层可搜索PDF )。想要添加其他保存类型,必须在软件界面中勾选。 diff --git a/docs/http/README.md b/docs/http/README.md new file mode 100644 index 0000000..176b0e1 --- /dev/null +++ b/docs/http/README.md @@ -0,0 +1,44 @@ +[Translate to English](https://github-com.translate.goog/hiroi-sora/Umi-OCR/blob/main/docs/http/README.md?_x_tr_sl=zh-CN&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=wapp) + +- [命令行手册](../README_CLI.md) +- HTTP接口手册 + +# HTTP接口手册 + +(本文档仅适用于 Umi-OCR 最新版本。旧版本请查看 [Github备份分支](https://github.com/hiroi-sora/Umi-OCR/branches) 中对应版本的文档。) + +#### 基础说明 + +![Umi-OCR-全局页-服务.png](https://tupian.li/images/2023/10/25/653907e9bac06.png) + +如上图,必须允许HTTP服务才能使用HTTP接口(默认开启)。如果需要允许被局域网访问,请将主机切换到**任何可用地址**。 + +在全局设置页中勾选**高级**才会显示。 + +##### 注意事项: + +1. 关闭 Umi-OCR 软件时,如果仍有用户未断开HTTP接口连接,可能导致Umi-OCR关闭不完全(UI线程结束了,但负责网络的子线程未被关闭)。这时只能等待所有用户关闭连接,或者进任务管理器强制结束进程。 +2. 由于后端组件的性能限制,对并发支持较差,尽量不要并发调用。 +3. 由于后端组件的性能限制,在长时间、大批量、连续调用时,有小几率出现 `Error: connect ECONNREFUSED` 之类的HTTP报错。此时重新发起请求即可。只要后台工作线程没有崩,这些小问题不会持续影响调用。 + +--- + +## 目录 + +### 图片识别 + +1. [图片OCR:参数查询](api_ocr.md#/api/ocr/get_options) +2. [图片OCR:Base64 识别](api_ocr.md#/api/ocr) + +### 文档识别(PDF识别) + +- [文档识别流程](api_doc.md#/api/doc) + +### 二维码识别 + +1. [二维码:Base64 识别](api_qrcode.md#/api/qrcode) +2. [二维码:从文本生成图片](api_qrcode.md#/api/qrcode/text) + +### 命令行 + +- [命令行接口](argv.md#/argv) diff --git a/docs/http/api_doc.md b/docs/http/api_doc.md new file mode 100644 index 0000000..d024c2a --- /dev/null +++ b/docs/http/api_doc.md @@ -0,0 +1,450 @@ +## 目录 + +### 图片识别 + +1. [图片OCR:参数查询](api_ocr.md#/api/ocr/get_options) +2. [图片OCR:Base64 识别](api_ocr.md#/api/ocr) + +### 文档识别(PDF识别) + +- [文档识别流程](#/api/doc) + +### 二维码识别 + +1. [二维码:Base64 识别](api_qrcode.md#/api/qrcode) +2. [二维码:从文本生成图片](api_qrcode.md#/api/qrcode/text) + +### 命令行 + +- [命令行接口](argv.md#/argv) + +--- + +> [!TIP] +> `v2.1.4` 及以上的版本,才具有文档识别功能。 + + + +--- + +## 文档识别流程 + +调用文档识别的流程为: + +- 准备工作:开发之前,先查询确认一下参数。👉 [说明](#/api/doc/get_options) +- 第1步:上传要识别的文件,获取任务ID。👉 [说明](#/api/doc/upload) +- 第2步:通过ID,轮询任务状态,直到OCR任务结束。👉 [说明](#/api/doc/result) +- 第3步:生成目标文件(如双层可搜索PDF),获取下载链接。👉 [说明](#/api/doc/download) +- 第4步:下载目标文件。👉 [说明](#/api/doc/download/id) +- 第5步:清理任务。👉 [说明](#/api/doc/clear) + +建议参考下述示例代码: + +- Python - [api_doc_demo.py](api_doc_demo.py) +- Web - [api_doc_demo.html](api_doc_demo.html) + + + +--- + +## 准备工作:参数查询 + +> 在不同的情况下(比如使用不同的OCR引擎插件), **第1步-上传文件** 时可以传入不同的参数。 +> 通过 **参数查询接口** ,可以获取所有参数的定义、默认值、可选值等信息。 +> 你可以调用查询接口来确认信息,也可以通过查询接口返回的字典来自动化生成前端UI。 + + +URL:`/api/doc/get_options` + +例:`http://127.0.0.1:1224/api/doc/get_options` + +### 0.1. 请求格式 + +方法:`GET` + +### 0.2. 响应格式 + +返回一个json字符串,记录 **[文档上传接口](#/api/doc/upload)** 的参数定义。 + +
    +以PaddleOCR引擎插件为例,返回值格式化后为:(点击展开) + +```json +{ + "ocr.language": { + "title": "语言/模型库", + "optionsList": [ + ["models/config_chinese.txt","简体中文"], + ["models/config_en.txt","English"], + ["models/config_chinese_cht(v2).txt","繁體中文"], + ["models/config_japan.txt","日本語"], + ["models/config_korean.txt","한국어"], + ["models/config_cyrillic.txt","Русский"] + ], + "type": "enum", + "default": "models/config_chinese.txt" + }, + "ocr.cls": { + "title": "纠正文本方向", + "default": false, + "toolTip": "启用方向分类,识别倾斜或倒置的文本。可能降低识别速度。", + "type": "boolean" + }, + "ocr.limit_side_len": { + "title": "限制图像边长", + "optionsList": [ + [960,"960 (默认)"], + [2880,"2880"], + [4320,"4320"], + [999999,"无限制"] + ], + "toolTip": "将边长大于该值的图片进行压缩,可以提高识别速度。可能降低识别精度。", + "type": "enum", + "default": 960 + }, + "tbpu.parser": { + "title": "排版解析方案", + "toolTip": "按什么方式,解析和排序图片中的文字块", + "default": "multi_para", + "optionsList": [ + ["multi_para","多栏-按自然段换行"], + ["multi_line","多栏-总是换行"], + ["multi_none","多栏-无换行"], + ["single_para","单栏-按自然段换行"], + ["single_line","单栏-总是换行"], + ["single_none","单栏-无换行"], + ["single_code","单栏-保留缩进"], + ["none","不做处理"] + ], + "type": "enum" + }, + "tbpu.ignoreArea": { + "title": "忽略区域", + "toolTip": "数组,每一项为[[左上角x,y],[右下角x,y]]。", + "default": [], + "type": "var" + }, + "tbpu.ignoreRangeStart": { + "title": "忽略区域起始", + "toolTip": "忽略区域生效的页数范围起始。从1开始。", + "default": 1, + "type": "number", + "isInt": true + }, + "tbpu.ignoreRangeEnd": { + "title": "忽略区域结束", + "toolTip": "忽略区域生效的页数范围结束。可以用负数表示倒数第X页。", + "default": -1, + "type": "number", + "isInt": true + }, + "pageRangeStart": { + "title": "OCR页数起始", + "toolTip": "OCR的页数范围起始。从1开始。", + "default": 1, + "type": "number", + "isInt": true + }, + "pageRangeEnd": { + "title": "OCR页数结束", + "toolTip": "OCR的页数范围结束。可以用负数表示倒数第X页。", + "default": -1, + "type": "number", + "isInt": true + }, + "pageList": { + "title": "OCR页数列表", + "toolTip": "数组,可指定单个或多个页数。例:[1,2,5]表示对第1、2、5页进行OCR。如果与页数范围同时填写,则 pageList 优先。", + "default": [], + "type": "var" + }, + "password": { + "title": "密码", + "toolTip": "如果文档已加密,则填写文档密码。", + "default": "", + "type": "text" + }, + "doc.extractionMode": { + "title": "内容提取模式", + "toolTip": "若一页文档既存在图片又存在文本,如何进行处理。", + "default": "mixed", + "optionsList": [ + ["mixed","混合OCR/原文本"], + ["fullPage","整页强制OCR"], + ["imageOnly","仅OCR图片"], + ["textOnly","仅拷贝原有文本"] + ], + "type": "enum" + } +} +``` + +

    + +上述返回值中,每个参数有这些属性: + +- `title`:参数名称。 +- `toolTip`:参数说明。 +- `default`:默认值。 +- `type`:参数值的类型,具体如下: + - `enum`:枚举。参数值必须为 `optionsList` 中某一项的 `[0]` 。 + - `boolean`:布尔。参数值必须为 `true/false` 。 + - `text`:字符串。 + - `number`:数字。如果属性`isInt==true`,那么必须为整数。 + - `var`:特殊类型,具体见 `toolTip` 的说明。 + +所有参数都是可选的。任一参数不填时,将被设为默认值。 + + +对上述参数的完整解释: + +| 键 | 默认值 | 类型 | 说明 | +| ----------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ocr.language` | `"models/config_chinese.txt"` | 枚举,可选值为字符串:`"models/config_chinese.txt"`、 `"models/config_en.txt"`、 `"models/config_chinese_cht(v2).txt"`、 `"models/config_japan.txt"`、 `"models/config_korean.txt"`、 `"models/config_cyrillic.txt"` | 语言/模型库:加载 `./UmiOCR-data/plugins/PaddleOCR-json/models`目录中的引擎配置文件,可切换不同语言的配置。**注意,此参数仅适用于PaddleOCR引擎插件!其他OCR引擎请自行调用参数查询接口获取。** | +| `ocr.cls` | `false` | 布尔,可选`true`/`false` | 纠正文本方向:填`true`时启用方向分类,识别倾斜或倒置的文本。可能降低识别速度。**注意,仅适用于PaddleOCR!** | +| `ocr.limit_side_len` | `960` | 枚举,可选值为整数: `960`、 `2880`、 `4320`、 `999999` | 限制图像边长:将边长大于该值的图片进行压缩。较低的限制值可以提高识别速度,较高的限制可以提高大图的识别精度。**注意,仅适用于PaddleOCR!** | +| `tbpu.parser` | `"multi_para"` | 枚举,可选值为字符串:`"multi_para"`、 `"multi_line"`、 `"multi_none"`、 `"single_para"`、 `"single_line"`、 `"single_none"`、 `"single_code"`、 `"none"` | 排版解析方案:按什么方式,解析和排序图片中的文字块。可选值的含义请见上方折叠内容`tbpu.parser`块的`optionsList`。 | +| `tbpu.ignoreArea` | `[]` | 嵌套整数列表 | 忽略区域:处于任意一个忽略区域内的OCR文本块将被舍弃。每个忽略区域用矩形坐标`[[左上角x,y],[右下角x,y]]`表示。总列表中可存放多个忽略区域,示例:`[[[0,0],[100,50]], [[10,100],[110,150]]]` | +| `tbpu.ignoreRangeStart` | `1` | 整数 | 忽略区域生效的页数范围起始。从1开始。 | +| `tbpu.ignoreRangeEnd` | `-1` | 整数 | 忽略区域生效的页数范围结束。可以用负数`-X`表示倒数第X页。 | +| `pageRangeStart` | `1` | 整数 | OCR的页数范围起始。从1开始。 | +| `pageRangeEnd` | `-1` | 整数 | OCR的页数范围结束。可以用负数`-X`表示倒数第X页。 | +| `pageList` | `[]` | 整数列表 | 页数列表:可指定单个或多个页数。例:`[1,2,5]`表示仅对第1、2、5页进行OCR。如果与页数范围(pageRangeStart、pageRangeEnd)同时填写,则 pageList 优先。 | +| `password` | `""` | 字符串 | 若要识别加密的文档,则需填写文档密码。 | +| `doc.extractionMode` | `"mixed"` | 枚举,可选值为字符串: `mixed`、 `fullPage`、 `imageOnly`、 `textOnly` | 内容提取模式:若一页文档既存在图片又存在文本,如何进行处理。可选值的含义依次为:`混合OCR/原文本`、`整页强制OCR`、`仅OCR图片`、`仅拷贝原有文本` | + + +对于上述参数示例,可以组装出以下的设置字典,最终这些设置将在 **第1步:上传待识别文档** 时发送给服务器。**注意,以下部分参数仅适用于PaddleOCR插件!别的OCR插件请自行调用查询接口获取参数规则。** + +```json +{ + "ocr.language": "models/config_chinese.txt", + "ocr.cls": true, + "ocr.limit_side_len": 4320, + "tbpu.parser": "multi_none", + "tbpu.ignoreArea": [[[0,0],[100,50]], [[10,100],[110,150]], [[200,50],[300,80]]], + "pageRangeStart": 1, + "pageRangeEnd": 10, + "doc.extractionMode": "fullPage", +} +``` + +### 0.3. 参数查询 示例代码 + +
    +JavaScript 示例:(点击展开) + +```javascript +const url = "http://127.0.0.1:1224/api/doc/get_options"; +fetch(url, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }) + .then(response => response.json()) + .then(data => { console.log(data); }) + .catch(error => { console.error(error); }); +``` + +
    + +
    +Python 示例:(点击展开) + +```python +import json, requests + +response = requests.get("http://127.0.0.1:1224/api/doc/get_options") +res_dict = json.loads(response.text) +print(json.dumps(res_dict, indent=4, ensure_ascii=False)) +``` + +

    + +手动调用: +- 确保 Umi-OCR 已在运行。 +- 浏览器访问 http://127.0.0.1:1224/api/doc/get_options +- 复制全部内容,粘贴到 [在线JSON解析工具](https://www.x-json.cn/) 里转换为可读文本。 + + + +--- + +## 第1步:上传待识别文档 + +上传一个文档文件(及配置参数),启动识别任务,返回任务ID。 + +URL:`/api/doc/upload` + +例:`http://127.0.0.1:1224/api/doc/upload` + +### 1.1. 请求格式 + +方法:`POST` + +参数:表单 `formData` ,具有两个键值: + +- **file** :必填。要上传的文件。 +- **json** :可选。设置参数字典(json字符串),详情见 [查询接口](#/api/doc/get_options/table) 。 + + + +
    +JavaScript 示例:(点击展开) + +```JavaScript + const fileInput = document.getElementById('file_path').files[0]; // 文件对象 + const missionOptions = { // 配置参数字典 + "doc.extractionMode": "mixed", + }; + // 必须要将字典转换为json字符串 + const missionOptionsJSON = JSON.stringify(missionOptions) + // 组装表单 + const formData = new FormData(); + formData.append('file', fileInput); + formData.append('json', missionOptionsJSON); + + let response = await fetch("http://127.0.0.1:1224/api/doc/upload", { + method: 'POST', + body: formData + }); +``` + +
    + +### 1.2. 响应格式 + +返回json字符串,内容为一个字典,键值为: + +- **code** :(int)任务状态码。`100`为上传成功,其余为失败。 +- **data** :(string)如果上传成功,则为任务ID。失败,则为失败原因。 + + + +--- + +## 第2步:查询任务状态 + +传入任务ID,返回任务的当前执行状态(进行中/已完成)和识别文本。 + +URL:`/api/doc/result` + +例:`http://127.0.0.1:1224/api/doc/result` + +### 2.1. 请求格式 + +方法:`POST` + +参数: json字符串,内容为一个字典,键值为: + +- **id** :必填,字符串。文件上传接口执行成功后,返回的任务ID。 +- **is_data** :布尔值。非必填。 + - `true` :返回值中包含识别结果。 + - `false` (默认):返回值中不包含识别结果,只有简略的任务状态信息。 +- **is_unread** :布尔值。非必填。 + - `true` (默认):返回未读过的识别结果条目。 + - `false` :返回全部识别结果条目。 +- **format** :字符串。非必填。 + - `"dict"` (默认):以字典形式返回详细的识别结果。 + - `"text"` :以字符串形式返回识别结果文本。 + +### 2.2. 响应格式 + +返回json字符串,内容为一个字典,键值为: + +- **code** :(int)任务状态码。`100`为查询成功,其余为失败。 +- **data** :(string)如果查询失败,则为失败原因。如果查询成功且`is_data=true`,则为识别内容。如果查询成功且`is_data=false`,则为空数组。 + +以下内容只有 `code==100` 时才存在: + +- **processed_count** :(int)已经识别完的页数。 +- **pages_count** :(int)总页数。 +- **is_done** :(boolean)`true`表示任务已结束(state可能为`success`或`failure`),`false`表示任务仍在进行中。 +- **state** :(string)任务状态。值的含义:`waiting` 任务排队中,`running` 任务进行中,`success` 任务成功,`failure` 任务失败。 +- **message** :(string)只有 `state=="failure"` 才存在此项,表示任务失败的原因。注意,就算任务失败,仍可能通过 `data` 获取已完成的部分任务结果。 + + + +--- + +## 第3步:获取结果下载链接 + +必须在任务成功结束后才能调用,即第2步查询得知 `is_done==true && state=="success"` 。 + +传入任务ID和目标文件类型,生成目标文件,返回目标文件的下载链接。 + +URL:`/api/doc/download` + +例:`http://127.0.0.1:1224/api/doc/download` + +### 3.1. 请求格式 + +方法:`POST` + +参数: json字符串,内容为一个字典,键值为: + +- **id** :必填,字符串。任务ID。 +- **file_types** :数组,每一项为字符串。只填写一个值时,返回单个文件的下载链接。填写多个值时,返回单个zip压缩包下载链接,其中打包了多个文件。 + - `"pdfLayered"` (默认):双层可搜索PDF。 + - `"pdfOneLayer"` :单层纯文本PDF。 + - `"txt"` :带页数等信息的txt文件。 + - `"txtPlain"` :只含识别文本的txt文件。 + - `"jsonl"` :与 `result` 接口, `format="dict"` 的格式类似。每行为一个json对象。 + - `"csv"` :表格,每行为一页的识别文本。 +- **ignore_blank** :布尔值。是否忽略空页(没有文字的页数)。 + - `true` (默认):如 txt、csv 等文件中,会跳过空页。 + - `false` :不跳过空页,文件中空页的内容记为空字符串。 +> 注: Umi-OCR `v2.1.4` 及以前的版本, `ignore_blank` 参数存在问题,请使用最新版本。 + +### 3.2. 响应格式 + +返回json字符串,内容为一个字典,键值为: + +- **code** :(int)任务状态码。`100`为成功生成目标文件,其余为无法生成目标文件。 +- **data** :(string)如果成功,则为下载链接。如果失败,则为失败原因。 +- **name** :(string)只有成功时才存在此项。下载链接对应的文件名。 + + + +--- + +## 第4步:通过链接下载结果文件 + +第3步获取的下载链接,可通过get请求下载,或者直接用浏览器打开下载。 + +链接中类似id的部分, **不是** 任务ID。 **不能** 通过任务ID拼接出下载链接。 + + + +--- + +## 第5步:任务清理 + +在URL中拼接任务ID,清理对应的任务。 + +URL:`/api/doc/clear/` + +例:`http://127.0.0.1:1224/api/doc/clear/cbe2f874-84a9-48b4-a6c0-9157245f7bae` + +### 5.1. 请求格式 + +方法:`GET` + +### 5.2. 响应格式 + +返回json字符串,内容为一个字典,键值为: + +- **code** :(int)状态码。`100`为清理成功,其余为清理失败(或者不存在对应任务)。 +- **data** :(string)原因。 + +### 5.3. 关于清理的说明 + +任务清理将删除此任务存放在服务器上的所有临时文件(包括上传的文件)。如果任务进行中时执行清理,将强制终止任务。 + +一个任务被清理后,无法再获取任务状态、访问下载链接。 + +建议调用者在每次完成任务后手动清理任务,以便及时释放服务器资源。如果不手动清理,**任务会在24小时后自动清理**。 + +- 如果任务上传后一直在进行,那么最长持续运行24小时。 +- 如果任务已完成,那么从完成的时候开始算,最长保留24小时。 + +如果 Umi-OCR 被意外关闭,那么重新启动时自动清理上次遗留的所有任务。 diff --git a/docs/http/api_doc_demo.html b/docs/http/api_doc_demo.html new file mode 100644 index 0000000..a8b7623 --- /dev/null +++ b/docs/http/api_doc_demo.html @@ -0,0 +1,213 @@ + + + + + + + Document OCR Demo + + + + +

    Document OCR Demo

    +
    + + +

    + + +

    + + +

    + + +
    +
    +
    + + + + + \ No newline at end of file diff --git a/docs/http/api_doc_demo.py b/docs/http/api_doc_demo.py new file mode 100644 index 0000000..7da0619 --- /dev/null +++ b/docs/http/api_doc_demo.py @@ -0,0 +1,164 @@ +# https://github.com/hiroi-sora/Umi-OCR/blob/main/docs/http/api_doc.md#/api/doc + +import os +import json +import time +import requests + +base_url = "http://127.0.0.1:1224" + +url = "{}/api/doc/upload".format(base_url) + +print("=======================================") +print("===== 1. Upload file, get task ID =====") +print("== URL:", url) + +# File to be recognized +file_path = r"XXXXX.pdf" +# Task parameters +options_json = json.dumps( + { + "doc.extractionMode": "mixed", + } +) +with open(file_path, "rb") as file: + response = requests.post(url, files={"file": file}, data={"json": options_json}) +response.raise_for_status() +res_data = json.loads(response.text) +if res_data["code"] == 101: + # If code == 101, it indicates that the server did not receive the uploaded file. + # On some Linux systems, if file_name contains non-ASCII characters, this error might occur. + # In this case, we can specify a temp_name containing only ASCII characters to construct the upload request. + + file_name = os.path.basename(file_path) + file_prefix, file_suffix = os.path.splitext(file_name) + temp_name = "temp" + file_suffix + print("[Warning] Detected file upload failure: code == 101") + print( + "Attempting to use temp_name", + temp_name, + "instead of the original file_name", + file_name, + ) + with open(file_path, "rb") as file: + response = requests.post( + url, + # use temp_name to construct the upload request + files={"file": (temp_name, file)}, + data={"json": options_json}, + ) + response.raise_for_status() + res_data = json.loads(response.text) +assert res_data["code"] == 100, "Task submission failed: {}".format(res_data) + +id = res_data["data"] +print("Task ID:", id) + +url = "{}/api/doc/result".format(base_url) +print("===================================================") +print("===== 2. Poll task status until OCR task ends =====") +print("== URL:", url) + +headers = {"Content-Type": "application/json"} +data_str = json.dumps( + { + "id": id, + "is_data": True, + "format": "text", + "is_unread": True, + } +) +while True: + time.sleep(1) + response = requests.post(url, data=data_str, headers=headers) + response.raise_for_status() + res_data = json.loads(response.text) + assert res_data["code"] == 100, "Failed to get task status: {}".format(res_data) + + print( + " Progress: {}/{}".format( + res_data["processed_count"], res_data["pages_count"] + ) + ) + if res_data["data"]: + print("{}\n========================".format(res_data["data"])) + if res_data["is_done"]: + state = res_data["state"] + assert state == "success", "Task execution failed: {}".format( + res_data["message"] + ) + print("OCR task completed.") + break + +url = "{}/api/doc/download".format(base_url) +print("======================================================") +print("===== 3. Generate target file, get download link =====") +print("== URL:", url) + +# Download file parameters +download_options = { + "file_types": [ + "txt", + "txtPlain", + "jsonl", + "csv", + "pdfLayered", + "pdfOneLayer", + ], + # ↓ `ingore_blank` is a typo. If you are using Umi-OCR version 2.1.4 or earlier, please use this incorrect spelling. + # ↓ If you are using the latest code-built version of Umi-OCR, please use the corrected spelling `ignore_blank`. + "ingore_blank": False, # Do not ignore blank pages +} +download_options["id"] = id +data_str = json.dumps(download_options) +response = requests.post(url, data=data_str, headers=headers) +response.raise_for_status() +res_data = json.loads(response.text) +assert res_data["code"] == 100, "Failed to get download URL: {}".format(res_data) + +url = res_data["data"] +name = res_data["name"] + +print("===================================") +print("===== 4. Download target file =====") +print("== URL:", url) + +# Save location for downloaded files +download_dir = "./download" +if not os.path.exists(download_dir): + os.makedirs(download_dir) +download_path = os.path.join(download_dir, name) +response = requests.get(url, stream=True) +response.raise_for_status() +# Download file size +total_size = int(response.headers.get("content-length", 0)) +downloaded_size = 0 +log_size = 10485760 # Print progress every 10MB + +with open(download_path, "wb") as file: + for chunk in response.iter_content(chunk_size=8192): + if chunk: + file.write(chunk) + downloaded_size += len(chunk) + if downloaded_size >= log_size: + log_size = downloaded_size + 10485760 + progress = (downloaded_size / total_size) * 100 + print( + " Downloading file: {}MB | Progress: {:.2f}%".format( + int(downloaded_size / 1048576), progress + ) + ) +print("Target file downloaded successfully: ", download_path) + +url = "{}/api/doc/clear/{}".format(base_url, id) +print("============================") +print("===== 5. Clean up task =====") +print("== URL:", url) + +response = requests.get(url) +response.raise_for_status() +res_data = json.loads(response.text) +assert res_data["code"] == 100, "Task cleanup failed: {}".format(res_data) +print("Task cleaned up successfully.") + +print("======================\nProcess completed.") diff --git a/docs/http/api_ocr.md b/docs/http/api_ocr.md new file mode 100644 index 0000000..cf43d9d --- /dev/null +++ b/docs/http/api_ocr.md @@ -0,0 +1,362 @@ +## 目录 + +### 图片识别 + +1. [图片OCR:参数查询](#/api/ocr/get_options) +2. [图片OCR:Base64 识别](#/api/ocr) + +### 文档识别(PDF识别) + +- [文档识别流程](api_doc.md#/api/doc) + +### 二维码识别 + +1. [二维码:Base64 识别](api_qrcode.md#/api/qrcode) +2. [二维码:从文本生成图片](api_qrcode.md#/api/qrcode/text) + +### 命令行 + +- [命令行接口](argv.md#/argv) + + + +--- + +## 1. 图片OCR:参数查询 + +> 在不同的情况下(比如使用不同的OCR引擎插件), **图片识别接口** 可以传入不同的参数。 +> 通过 **参数查询接口** ,可以获取所有参数的定义、默认值、可选值等信息。 +> 你可以手动调用查询接口来确认信息,也可以通过查询接口返回的字典来自动化生成前端UI。 + + +URL:`/api/ocr/get_options` + +例:`http://127.0.0.1:1224/api/ocr/get_options` + +(默认端口为`1224`,可以在 Umi-OCR 全局设置中更改。) + +### 1.1. 请求格式 + +方法:`GET` + +### 1.2. 响应格式 + +返回一个json字符串,记录 **图片OCR接口** 的参数定义。 + +
    +以PaddleOCR引擎插件为例,返回值格式化后为:(点击展开) + +```json +{ + "ocr.language": { + "title": "语言/模型库", + "optionsList": [ + ["models/config_chinese.txt","简体中文"], + ["models/config_en.txt","English"], + ["models/config_chinese_cht(v2).txt","繁體中文"], + ["models/config_japan.txt","日本語"], + ["models/config_korean.txt","한국어"], + ["models/config_cyrillic.txt","Русский"] + ], + "type": "enum", + "default": "models/config_chinese.txt" + }, + "ocr.cls": { + "title": "纠正文本方向", + "default": false, + "toolTip": "启用方向分类,识别倾斜或倒置的文本。可能降低识别速度。", + "type": "boolean" + }, + "ocr.limit_side_len": { + "title": "限制图像边长", + "optionsList": [ + [960,"960 (默认)"], + [2880,"2880"], + [4320,"4320"], + [999999,"无限制"] + ], + "toolTip": "将边长大于该值的图片进行压缩,可以提高识别速度。可能降低识别精度。", + "type": "enum", + "default": 960 + }, + "tbpu.parser": { + "title": "排版解析方案", + "toolTip": "按什么方式,解析和排序图片中的文字块", + "default": "multi_para", + "optionsList": [ + ["multi_para","多栏-按自然段换行"], + ["multi_line","多栏-总是换行"], + ["multi_none","多栏-无换行"], + ["single_para","单栏-按自然段换行"], + ["single_line","单栏-总是换行"], + ["single_none","单栏-无换行"], + ["single_code","单栏-保留缩进"], + ["none","不做处理"] + ], + "type": "enum" + }, + "tbpu.ignoreArea": { + "title": "忽略区域", + "toolTip": "数组,每一项为[[左上角x,y],[右下角x,y]]。", + "default": [], + "type": "var" + }, + "data.format": { + "title": "数据返回格式", + "toolTip": "返回值字典中,[\"data\"] 按什么格式表示OCR结果数据", + "default": "dict", + "optionsList": [ + ["dict","含有位置等信息的原始字典"], + ["text","纯文本"] + ], + "type": "enum" + } +} +``` + +

    + +返回值中,每个参数有这些属性: + +- `title`:参数名称。 +- `toolTip`:参数说明。 +- `default`:默认值。 +- `type`:参数值的类型,具体如下: + - `enum`:枚举。参数值必须为 `optionsList` 中某一项的 `[0]` 。 + - `boolean`:布尔。参数值必须为 `true/false` 。 + - `text`:字符串。 + - `number`:数字。如何属性`isInt==true`,那么必须为整数。 + - `var`:特殊类型,具体见 `toolTip` 的说明。 + +所有参数都是可选的。任一参数不填时,将被设为默认值。 + + +对上述参数的完整解释: + +| 键 | 默认值 | 类型 | 说明 | +| -------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ocr.language` | `"models/config_chinese.txt"` | 枚举,可选值为字符串:`"models/config_chinese.txt"`、 `"models/config_en.txt"`、 `"models/config_chinese_cht(v2).txt"`、 `"models/config_japan.txt"`、 `"models/config_korean.txt"`、 `"models/config_cyrillic.txt"` | 语言/模型库:加载 `./UmiOCR-data/plugins/PaddleOCR-json/models`目录中的引擎配置文件,可切换不同语言的配置。**注意,此参数仅适用于PaddleOCR引擎插件!其他OCR引擎请自行调用参数查询接口获取。** | +| `ocr.cls` | `false` | 布尔,可选`true`/`false` | 纠正文本方向:填`true`时启用方向分类,识别倾斜或倒置的文本。可能降低识别速度。**注意,仅适用于PaddleOCR!** | +| `ocr.limit_side_len` | `960` | 枚举,可选值为整数: `960`、 `2880`、 `4320`、 `999999` | 限制图像边长:将边长大于该值的图片进行压缩。较低的限制值可以提高识别速度,较高的限制可以提高大图的识别精度。**注意,仅适用于PaddleOCR!** | +| `tbpu.parser` | `"multi_para"` | 枚举,可选值为字符串:`"multi_para"`、 `"multi_line"`、 `"multi_none"`、 `"single_para"`、 `"single_line"`、 `"single_none"`、 `"single_code"`、 `"none"` | 排版解析方案:按什么方式,解析和排序图片中的文字块。可选值的含义请见上方折叠内容`tbpu.parser`块的`optionsList`。 | +| `tbpu.ignoreArea` | `[]` | 嵌套整数列表 | 忽略区域:处于任意一个忽略区域内的OCR文本块将被舍弃。每个忽略区域用矩形坐标`[[左上角x,y],[右下角x,y]]`表示,详细见下个段落。 | +| `data.format` | `"dict"` | 枚举,可选值为字符串: `dict`、 `text` | 数据返回格式:返回值字典中,`["data"]` 按什么格式表示OCR结果数据。`dict`表示含有位置等信息的详细字典,`text`表示仅返回识别文本。 | + +- 关于忽略区域 `tbpu.ignoreArea` : + - 示例:假设忽略区域包含3个矩形框,那么 `tbpu.ignoreArea` 的格式类似: + ```javascript + [ + [[0,0],[100,50]], // 第1个框,左上角(0,0),右下角(100,50) + [[0,60],[200,120]], // 第2个 + [[400,0],[500,30]] // 第3个 + ] + ``` + - 注意,完全处于忽略区域框内部的整个文本块(而不是单个字符)会被忽略。如下图所示,黄色边框的深色矩形是一个忽略区域。那么只有`key_mouse`才会被忽略。`pubsub_connector.py`、`pubsub_service.py` 这两个文本块得以保留。 + +

    忽略区域范围示例.png

    + + +对于上述返回值示例,可以组装出这样的参数字典: + +```json +{ + "ocr.language": "models/config_chinese.txt", + "ocr.cls": true, + "ocr.limit_side_len": 4320, + "tbpu.parser": "multi_none", + "data.format": "text", + "tbpu.ignoreArea": [[[0,0],[100,50]], [[0,60],[200,120]]] +} +``` + +### 1.3. 参数查询 示例代码 + +
    +JavaScript 示例:(点击展开) + +```javascript +const url = "http://127.0.0.1:1224/api/ocr/get_options"; +fetch(url, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }) + .then(response => response.json()) + .then(data => { console.log(data); }) + .catch(error => { console.error(error); }); +``` + +
    + +
    +Python 示例:(点击展开) + +```python +import json, requests + +response = requests.get("http://127.0.0.1:1224/api/ocr/get_options") +res_dict = json.loads(response.text) +print(json.dumps(res_dict, indent=4, ensure_ascii=False)) +``` + +
    + +手动调用: +- 确保 Umi-OCR 已在运行。 +- 浏览器访问 http://127.0.0.1:1224/api/ocr/get_options +- 复制全部内容,粘贴到 [在线JSON解析工具](https://www.x-json.cn/) 里转换为可读文本。 + + + +--- + +## 2. 图片OCR:Base64 识别接口 + + +传入一个base64编码的图片,返回OCR识别结果。 + +URL:`/api/ocr` + +例:`http://127.0.0.1:1224/api/ocr` + +### 2.1. 请求格式 + +方法:`POST` + +参数:json字符串,内容为一个字典,键值为: + +- **base64** : 必填。待识别图像的 Base64 编码字符串,无需 `data:image/png;base64,` 等前缀。 +- **options** :可选。参数字典,见 [查询接口](#/api/ocr/get_options/table) 。 + +POST 参数示例: + +```json +{ + "base64": "iVBORw0KGgoAAAAN……", + "options": { + "ocr.language": "models/config_chinese.txt", + "ocr.cls": true, + "ocr.limit_side_len": 4320, + "tbpu.parser": "multi_none", + "data.format": "text" + } +} +``` + +### 2.2. 响应格式 + +返回json字符串,内容为一个字典,键值为:: + +| 字段 | 类型 | 描述 | +| --------- | ----------- | -------------------------------------------------- | +| code | int | 任务状态码。`100`为成功,`101`为无文本,其余为失败 | +| data | list/string | 识别结果,格式见下 | +| time | double | 识别耗时(秒) | +| timestamp | double | 任务开始时间戳(秒) | + +#### `data` 格式 + +图片中无文本(`code==101`),或识别失败(`code!=100 and code!=101`)时: + +- `["data"]`为string,内容为错误原因。例: `{"code": 902, "data": "向识别器进程传入指令失败,疑似子进程已崩溃"}` + +识别成功(`code==100`)时,如果options中`data.format`为`dict`(默认值): + +- `["data"]`为list,每一项元素为dict,包含以下子元素: + +| 参数名 | 类型 | 描述 | +| ------ | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| text | string | 文本 | +| score | double | 置信度 (0~1) | +| box | list | 文本框顺时针四个角的xy坐标:`[左上,右上,右下,左下]` | +| end | string | 表示本行文字结尾的结束符,根据排版解析得出。可能为空、空格` `、换行`\n`。将所有OCR文本块拼接为完整段落时,按照 `本行文字+本行结束符+下一行文字+下一行结束符+……`的形式,就能恢复段落结构。 | + + +结果示例: +```json + +{ + "code": 100, + "data": [ + { + "text": "第一行的文本,", + "score": 0.99800001, + "box": [[x1,y1], [x2,y2], [x3,y3], [x4,y4]], + "end": "\n", + }, + { + "text": "第二行的文本", + "score": 0.97513333, + "box": [[x1,y1], [x2,y2], [x3,y3], [x4,y4]], + "end": "", + }, + ] +} +``` + +识别成功(`code==100`)时,如果options中`data.format`为`text`: + +- `["data"]`为string,即所有OCR结果的拼接。例: +```json +"data": "第一行的文本,\n第二行的文本" +``` + +#### 返回json形式的说明 + +1. 为了确保兼容性,返回值json字符串经过了转义,非英文字符被转换为 `\uXXXX` 形式的Unicode码点。使用任意编程语言的json库将其解析后,即可得到可读原文。 +2. 返回值json字符串中可能存在转义后的换行符`\\n`(即`\`+`n`)来表达OCR段落结构。在某些语言的http库中,可能会自动将请求结果字符串中的转义换行符转换为真实换行,这会导致后续json解析失败。如果遇到这种情况,可以尝试先获取返回结果字符串,将其中所有真实换行`\n`替换为转义换行`\\n`,确保整个字符串中不存在真实换行;再交给json解析。 + +### 2.3. 调用接口 示例代码 + +
    +JavaScript 示例:(点击展开) + +```javascript +const url = 'http://127.0.0.1:1224/api/ocr'; +const data = { + base64: "iVBORw0KGgoAAAANSUhEUgAAAC4AAAAXCAIAAAD7ruoFAAAACXBIWXMAABnWAAAZ1gEY0crtAAAAEXRFWHRTb2Z0d2FyZQBTbmlwYXN0ZV0Xzt0AAAHjSURBVEiJ7ZYrcsMwEEBXnR7FLuj0BPIJHJOi0DAZ2qSsMCxEgjYrDQqJdALrBJ2ASndRgeNI8ledutOCLrLl1e7T/mRkjIG/IXe/DWBldRTNEoQSpgNURe5puiiaJehrMuJSXSTgbaby0A1WzLrCCQCmyn0FwoN0V06QONWAt1nUxfnjHYA8p65GjhDKxcjedVH6JOejBPwYh21eE0Wzfe0tqIsEkGXcVcpoMH4CRZ+P0lsQp/pWJ4ripf1XFDFe8GHSHlYcSo9Es31t60RdFlN1RUmrma5oTzTVB8ZUaeeYEC9GmL6kNkDw9BANAQYo3xTNdqUkvHq+rYhDKW0Bj3RSEIpmyWyBaZaMTCrCK+tJ5Jsa07fs3E7esE66HzralRLgJKp0/BD6fJRSxvmDsb6joqkcFXGqMVVFFEHDL2gTxwCAaTabnkFUWhDCHTd9iYrGcAL1ZnqIp5Vpiqh7bCfua7FA4qN0INMcN1+cgCzj+UFxtbmvwdZvGIrI41JiqhZBWhhF8WxorkYPpQwJiWYJeA3rXE4hzcwJ+B96F9zCFHC0FcVegghvFul7oeEE8PvHeJqC0w0AUbbFIT8JnEwGbPKcS2OxU3HMTqD0r4wgEIuiKJ7i4MS16+og8/+bPZRPLa+6Ld2DSzcAAAAASUVORK5CYII=", + // 可选参数示例 + "options": { + "data.format": "text", + } +}; + +fetch(url, { + method: "POST", body: JSON.stringify(data), + headers: {"Content-Type": "application/json"}, + }) + .then(response => response.json()) + .then(data => { + console.log(data); + }) + .catch(error => { + console.error(error); + }); +``` + +
    + +
    +Python 示例:(点击展开) + +```python +import requests +import json + +url = "http://127.0.0.1:1224/api/ocr" +data = { + "base64": "iVBORw0KGgoAAAANSUhEUgAAAC4AAAAXCAIAAAD7ruoFAAAACXBIWXMAABnWAAAZ1gEY0crtAAAAEXRFWHRTb2Z0d2FyZQBTbmlwYXN0ZV0Xzt0AAAHjSURBVEiJ7ZYrcsMwEEBXnR7FLuj0BPIJHJOi0DAZ2qSsMCxEgjYrDQqJdALrBJ2ASndRgeNI8ledutOCLrLl1e7T/mRkjIG/IXe/DWBldRTNEoQSpgNURe5puiiaJehrMuJSXSTgbaby0A1WzLrCCQCmyn0FwoN0V06QONWAt1nUxfnjHYA8p65GjhDKxcjedVH6JOejBPwYh21eE0Wzfe0tqIsEkGXcVcpoMH4CRZ+P0lsQp/pWJ4ripf1XFDFe8GHSHlYcSo9Es31t60RdFlN1RUmrma5oTzTVB8ZUaeeYEC9GmL6kNkDw9BANAQYo3xTNdqUkvHq+rYhDKW0Bj3RSEIpmyWyBaZaMTCrCK+tJ5Jsa07fs3E7esE66HzralRLgJKp0/BD6fJRSxvmDsb6joqkcFXGqMVVFFEHDL2gTxwCAaTabnkFUWhDCHTd9iYrGcAL1ZnqIp5Vpiqh7bCfua7FA4qN0INMcN1+cgCzj+UFxtbmvwdZvGIrI41JiqhZBWhhF8WxorkYPpQwJiWYJeA3rXE4hzcwJ+B96F9zCFHC0FcVegghvFul7oeEE8PvHeJqC0w0AUbbFIT8JnEwGbPKcS2OxU3HMTqD0r4wgEIuiKJ7i4MS16+og8/+bPZRPLa+6Ld2DSzcAAAAASUVORK5CYII=", + # 可选参数示例 + "options": { + "data.format": "text", + } +} +headers = {"Content-Type": "application/json"} +data_str = json.dumps(data) +response = requests.post(url, data=data_str, headers=headers) +response.raise_for_status() +res_dict = json.loads(response.text) +print(res_dict) +``` + +
    + diff --git a/docs/http/api_qrcode.md b/docs/http/api_qrcode.md new file mode 100644 index 0000000..b27294c --- /dev/null +++ b/docs/http/api_qrcode.md @@ -0,0 +1,234 @@ +## 目录 + +### 图片识别 + +1. [图片OCR:参数查询](api_ocr.md#/api/ocr/get_options) +2. [图片OCR:Base64 识别](api_ocr.md#/api/ocr) + +### 文档识别(PDF识别) + +- [文档识别流程](api_doc.md#/api/doc) + +### 二维码识别 + +1. [二维码:Base64 识别](#/api/qrcode) +2. [二维码:从文本生成图片](#/api/qrcode/text) + +### 命令行 + +- [命令行接口](argv.md#/argv) + +--- + + + +## 1. 二维码:Base64 识别接口 + +URL:`/api/qrcode` + +例:`http://127.0.0.1:1224/api/qrcode` + +### 1.1. 请求格式 + +方法:`POST` + +参数:`json` 字典,值为: + +- **base64** : 必填。待识别图像的 Base64 编码字符串,无需 `data:image/png;base64,` 等前缀。 +- **options** :可选。参数字典,可选项为: + - **preprocessing.median_filter_size** :中值滤波器的大小。取值范围:1~9的奇数。默认不进行滤波。 + - **preprocessing.sharpness_factor** :锐度增强因子。取值范围:0.1~10.0。默认不调整锐度。 + - **preprocessing.contrast_factor** :对比度增强因子。取值范围:0.1~10.0。大于1增强对比度,小于1但大于0减少对比度,1保持原样。默认不调整对比度。 + - **preprocessing.grayscale** :是否将图像转换为灰度图像。true为转换,false为不转换。默认为false。 + - **preprocessing.threshold** :二值化阈值,用于灰度图像的二值化处理。取值范围:0~255 整数。只有当 `"preprocessing.grayscale"=true` 时,此参数才生效。默认为false。 + + +参数示例: + +```json +{ + "base64": "iVBORw0KGgoAAAAN……", + "options": { + "preprocessing.sharpness_factor": 1.0, + "preprocessing.contrast_factor": 1.0, + "preprocessing.grayscale": false, + "preprocessing.threshold": false, + } +} +``` + +### 1.2. 响应格式 + +返回 `json` ,与 OCR 结果的格式非常相似。 + +| 字段 | 类型 | 描述 | +| --------- | ------ | -------------------------------------------------- | +| code | int | 任务状态码。`100`为成功,`101`为无文本,其余为失败 | +| data | list | 识别结果,格式见下 | +| time | double | 识别耗时(秒) | +| timestamp | double | 任务开始时间戳(秒) | + +#### `data` 格式 + +图片中无文本(`code==101`),或识别失败(`code!=100 and code!=101`)时: + +- `["data"]` 为string,内容为错误原因。例: `{"code": 204, "data": "【Error】zxingcpp 二维码解析失败。\n[Error] zxingcpp read_bar……"}` + +识别成功(`code==100`)时: + +- `["data"]` 为list,记录图片中每个二维码的结果(一张图片可能含多个码)。每项结果的子元素为: + +| 参数名 | 类型 | 描述 | +| ----------- | ------ | --------------------------------------------------- | +| text | string | 二维码文本。 | +| format | string | 二维码格式,如 `"QRCode"` 等,具体见下。 | +| box | list | 文本框顺时针四个角的xy坐标:`[左上,右上,右下,左下]` | +| orientation | int | 二维码方向。0为正上。 | +| score | int | 为了与OCR格式兼容而设,永远为1,无意义。 | + + + +##### 二维码格式 format: + +`"Aztec","Codabar","Code128","Code39","Code93","DataBar","DataBarExpanded","DataMatrix","EAN13","EAN8","ITF","LinearCodes","MatrixCodes","MaxiCode","MicroQRCode","PDF417","QRCode","UPCA","UPCE"` + +结果示例: +```json +{ + "code": 100, + "data": [ { + "orientation": 0, + "box": [[4,4],[25,4],[25,25],[4,25]], + "score": 1, + "format": "QRCode", + "text": "abc" + } ], + "time": 0, + "timestamp": 1711521012.625574 +} +``` + +### 1.3. 调用接口 示例代码 + +
    +JavaScript 示例:(点击展开) + +```javascript +const url = "http://127.0.0.1:1224/api/qrcode"; +const base64 = "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAALCAAdAB0BAREA/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/9oACAEBAAA/APU/GfjM+EjAzW9o0DW8txNPdXEkSxKkkMYAEcUjMS069hjBrn3+K0yi3B0/RozO52y3OtG3gaPy7WRWV5IVJO27DFSoIEbYycCrF18Sb2z1a20u70rTbO8uLiKzigutRl3NcNDBIyAxW7rhTcIu4sAcE8Cu00LU/wC2/D2mat5Pk/brSK58rdu2b0Dbc4GcZxnAri/iSdPGs6AuqySW+nzpcW11dg27xwIzQspkimikDIZUiG/5QhK5PzCuPI1qz8ISalajUtNu1czLGsxnt7tHhhhiijNmkSF22W8aFeFWZ2RjIjeVXvrq0t/EWmaTpq3d9rTXFpCqpa2iRW92sCJOUP2WZYjEsNszrG7Bd/GNhr2zQtP/ALI8PaZpuMfY7SK3x5nmY2IF+9tXd067Vz6DpXH/ABK1LVrN7SLTIr6622k159isYYnknkjuLVUI8yGXGzzWfhc5UHPFeeSyav4dtI9R8O+Ho5dYS4WNrSK1EV2sb29ncFJY7aOPzIkkYhjhSGaME7WdHy72y8NWthbfDxrrfDDdpdXH2eVvtIu/IcStcOUaCGFMqGKNKUELZDEsU+g/DUcMXhXSI7cRrAllCsYjIKhQgxgh3BGP9t/95upk1PQtH1vyv7W0qxv/ACc+X9rt0l2ZxnG4HGcDp6Co7Xw1oNiipaaJptuiPvVYbVEAbcjZGB13RxnPqin+EYksdC0fTIo4rDSrG0jjlM6JBbpGFkKlC4AHDFSVz1wcdKuQQQ2tvFb28UcMESBI441CqigYAAHAAHGK/9k=" +const data = { "base64": base64 }; + +fetch(url, { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify(data) + }) + .then(response => response.json()) + .then(data => { + if(data.code === 100) { + console.log("QRCode count:", data.data.length); + for (let d of data.data) { + console.log(" text: ", d.text); + console.log(" format: ", d.format); + console.log(" orientation: ", d.orientation); + console.log(" ===="); + } + } + else { + console.log("Error! Code", data.code, " Msg: ", data.data); + } + }) + .catch(error => { console.error(error); }); +``` + +
    + + + +--- + +## 2. 二维码:从文本生成图片 + +传入文本,根据文本生成二维码图片,返回图片base64。 + +URL:`/api/qrcode` (与二维码识别接口一致,只是参数不同) + +例:`http://127.0.0.1:1224/api/qrcode` + +### 2.1. 请求格式 + +方法:`POST` + +参数:`json` 字典,值为: + +- **text** : 必填。要写入二维码的文本。 +- **options** :可选。参数字典,可选项为: + - **format** :二维码码格式,可选值 [见上文](#qrcode_format)。默认为 `"QRCode"` 。 + - **w** :生成图像宽度,整数。默认 `0` 为自动设为最小宽度。 + - **h** :生成图像高度,整数。默认 `0` 为自动设为最小高度。 + - **quiet_zone** :二维码四周的空白边缘宽度,整数。默认 `-1` 为自动调节。 + - **ec_level** :纠错等级,整数。默认 `-1` 。可选值: `-1`:自动, `1`:7%,`0`:15%,`3`:25%, `2`:30%。仅在这些格式下生效:`Aztec`、`PDF417`、`QRCode` 。 + +参数示例: + +```json +{ + "text": "要写入二维码的文本", + "options": { + "format": "QRCode", + "w": 0, + "h": 0, + "quiet_zone": -1, + "ec_level": -1, + } +} +``` + + +### 2.2. 响应格式 + +`json` + +| 字段名 | 类型 | 描述 | +| ------ | ------ | ------------------------------- | +| code | int | 任务状态。`100`成功,其余为失败 | +| data | string | 生成结果,格式见下 | + +- 生成图片成功(`code==100`)时:`"data"` 为图片的base64字符串,图片编码为`jpeg`。 +- 生成图片失败(`code!=100`)时:`"data"` 为错误信息字符串。 + +### 2.3. 调用接口 示例代码 + +
    +JavaScript 示例:(点击展开) + +```javascript +const url = "http://127.0.0.1:1224/api/qrcode"; +const data = { + "text": "test abc 123 !!!", + // "options": { + // "format": "QRCode", + // "w": 0, + // "h": 0, + // "quiet_zone": -1, + // "ec_level": -1, + // } +}; +fetch(url, { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify(data) + }) + .then(response => response.json()) + .then(data => { + if(data.code === 100) { + console.log("Image base64: \n", data.data); + } + else { + console.log("Error! Code", data.code, " Msg: ", data.data); + } + }) + .catch(error => { console.error(error); }); +``` + +
    \ No newline at end of file diff --git a/docs/http/argv.md b/docs/http/argv.md new file mode 100644 index 0000000..88f7a51 --- /dev/null +++ b/docs/http/argv.md @@ -0,0 +1,71 @@ +## 目录 + +### 图片识别 + +1. [图片OCR:参数查询](api_ocr.md#/api/ocr/get_options) +2. [图片OCR:Base64 识别](api_ocr.md#/api/ocr) + +### 文档识别(PDF识别) + +- [文档识别流程](api_doc.md#/api/doc) + +### 二维码识别 + +1. [二维码:Base64 识别](api_qrcode.md#/api/qrcode) +2. [二维码:从文本生成图片](api_qrcode.md#/api/qrcode/text) + +### 命令行 + +- [命令行接口](#/argv) + + + +--- + +## 命令行 接口 + +此接口用于命令行参数的跨进程传输,一般由程序内部自动调用。开发者也可手动调用。 + +由于此接口较敏感(如允许访问本机图片、关闭软件等),故只允许本地环回 `127.0.0.1` 调用。局域网或外网无法访问此接口。 + +URL:`/argv` + +例:`http://127.0.0.1:1224/argv` + +### 请求 + +方法:`POST` + +参数:`json` `list` + +传入一个 **列表** ,列表中记录命令行参数。 + +- 如:命令行调用 `Umi-OCR.exe --path "D:/xxx.png"` +- 等价于向argv接口发送: `["--path", "D:/xxx.png"]` + +具体命令行规则请见 [README_CLI.md](README_CLI.md) 。 + +### 调用接口 示例代码 + +
    +JavaScript 示例:(点击展开) + +```javascript +const url = "http://127.0.0.1:1224/argv"; +// 等价于命令行指令 Umi-OCR --screenshot +const data = ["--screenshot"]; +fetch(url, { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify(data) + }) + .then(response => response.text()) // 返回值是字符串 + .then(data => { + console.log("screenshot text:\n", data) + }) + .catch(error => { + console.error(error); + }); +``` + +
    \ No newline at end of file diff --git a/docs/images/Preview1.png b/docs/images/Preview1.png new file mode 100644 index 0000000..496e862 Binary files /dev/null and b/docs/images/Preview1.png differ diff --git a/docs/images/Umi-OCR-全局页1.png b/docs/images/Umi-OCR-全局页1.png new file mode 100644 index 0000000..89a551a Binary files /dev/null and b/docs/images/Umi-OCR-全局页1.png differ diff --git a/docs/images/Umi-OCR-截图页1.png b/docs/images/Umi-OCR-截图页1.png new file mode 100644 index 0000000..7fca598 Binary files /dev/null and b/docs/images/Umi-OCR-截图页1.png differ diff --git a/docs/images/Umi-OCR-截图页2.png b/docs/images/Umi-OCR-截图页2.png new file mode 100644 index 0000000..0a3966a Binary files /dev/null and b/docs/images/Umi-OCR-截图页2.png differ diff --git a/docs/images/Umi-OCR-批量页1.png b/docs/images/Umi-OCR-批量页1.png new file mode 100644 index 0000000..2eeba8e Binary files /dev/null and b/docs/images/Umi-OCR-批量页1.png differ diff --git a/docs/images/i18n.png b/docs/images/i18n.png new file mode 100644 index 0000000..57aea91 Binary files /dev/null and b/docs/images/i18n.png differ diff --git a/docs/images/icon-256.png b/docs/images/icon-256.png new file mode 100644 index 0000000..df49df7 Binary files /dev/null and b/docs/images/icon-256.png differ