├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── app.properties ├── models └── readme.md ├── pom.xml ├── readme-cn.md ├── readme.md ├── readme_files ├── 1.jpg ├── 2.jpg ├── 3.jpg ├── 4.jpg └── 5.jpg └── src ├── main ├── deploy │ └── package │ │ ├── macosx │ │ └── treehole.icns │ │ └── windows │ │ └── treehole.ico ├── java │ └── com │ │ ├── litongjava │ │ └── project │ │ │ └── config │ │ │ ├── ConfigKeys.java │ │ │ └── ProjectConfig.java │ │ └── luooqi │ │ └── ocr │ │ ├── OcrApp.java │ │ ├── config │ │ └── InitConfig.java │ │ ├── constants │ │ └── ImagesConstants.java │ │ ├── controller │ │ └── ProcessController.java │ │ ├── local │ │ └── PaddlePaddleOCRV4.java │ │ ├── model │ │ ├── CaptureInfo.java │ │ ├── StageInfo.java │ │ └── TextBlock.java │ │ ├── snap │ │ └── ScreenCapture.java │ │ ├── utils │ │ ├── BufferedImageUtils.java │ │ ├── CommUtils.java │ │ ├── GlobalKeyListener.java │ │ ├── LibraryUtils.java │ │ ├── OcrUtils.java │ │ ├── VoidDispatchService.java │ │ └── WebUtils.java │ │ └── windows │ │ └── MainForm.java └── resources │ ├── css │ └── main.css │ ├── fonts │ └── icomoon.svg │ ├── images │ └── 01.png │ ├── img │ ├── add-image.png │ ├── clear.png │ ├── copy.png │ ├── logo.png │ ├── paste.png │ ├── screenshot.png │ └── wrap.png │ └── logback.xml └── test ├── java └── com │ ├── litongjava │ ├── RapidOcrTest.java │ └── project │ │ └── config │ │ └── ProjectConfigTest.java │ └── luooqi │ └── ocr │ └── utils │ ├── OcrUtilsTest.java │ └── PdfTest.java └── resources ├── 03.png └── 2.jpg /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Docker JavaFX 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build_windows: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Download and Extract Models 17 | run: | 18 | wget https://github.com/litongjava/tools-ocr/releases/download/model-ppocr-v4/ch_PP-OCRv4_det_infer-onnx.zip 19 | wget https://github.com/litongjava/tools-ocr/releases/download/model-ppocr-v4/ch_PP-OCRv4_rec_infer-onnx.zip 20 | mkdir -p models/ch_PP-OCRv4_det_infer 21 | mkdir -p models/ch_PP-OCRv4_rec_infer 22 | unzip ch_PP-OCRv4_det_infer-onnx.zip -d models/ch_PP-OCRv4_det_infer 23 | unzip ch_PP-OCRv4_rec_infer-onnx.zip -d models/ch_PP-OCRv4_rec_infer 24 | 25 | - name: Copy Models 26 | run: | 27 | mkdir -p target/jfx/app 28 | cp -r models target/jfx/app/ 29 | 30 | - name: Build with Docker 31 | run: | 32 | docker run --rm \ 33 | -v ${{ github.workspace }}:/workspace \ 34 | -w /workspace \ 35 | litongjava/centos-7-maven:3.8.8 \ 36 | mvn jfx:native -DskipTests 37 | 38 | - name: Show Native Files 39 | run: ls target/jfx/native 40 | 41 | - name: Upload package 42 | uses: actions/upload-artifact@v3 43 | with: 44 | name: target-jfx-native-linux-x64 45 | path: target/jfx/native/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Eclipse template 2 | *.pydevproject 3 | .metadata 4 | .gradle* 5 | classes/ 6 | bin/ 7 | tmp/ 8 | *.tmp 9 | *.bak 10 | *.swp 11 | *~.nib 12 | local.properties 13 | .settings/ 14 | .loadpath 15 | rebel.xml 16 | 17 | # Eclipse Core 18 | .project 19 | 20 | generatedsources 21 | 22 | # External tool builders 23 | .externalToolBuilders/ 24 | 25 | # Locally stored "Eclipse launch configurations" 26 | *.launch 27 | 28 | # CDT-specific 29 | .cproject 30 | 31 | # JDT-specific (Eclipse Java Development Tools) 32 | .classpath 33 | 34 | # PDT-specific 35 | .buildpath 36 | 37 | # sbteclipse plugin 38 | .target 39 | 40 | # TeXlipse plugin 41 | .texlipse 42 | 43 | 44 | 45 | ### JetBrains template 46 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 47 | 48 | *.iml 49 | .flattened-pom.xml 50 | ## Directory-based project format: 51 | .idea/ 52 | # if you remove the above rule, at least ignore the following: 53 | 54 | # User-specific stuff: 55 | # .idea/workspace.xml 56 | # .idea/tasks.xml 57 | # .idea/dictionaries 58 | 59 | # Sensitive or high-churn files: 60 | # .idea/dataSources.ids 61 | # .idea/dataSources.xml 62 | # .idea/sqlDataSources.xml 63 | # .idea/dynamic.xml 64 | # .idea/uiDesigner.xml 65 | 66 | # Gradle: 67 | # .idea/gradle.xml 68 | # .idea/libraries 69 | 70 | # Mongo Explorer plugin: 71 | # .idea/mongoSettings.xml 72 | 73 | ## File-based project format: 74 | *.ipr 75 | *.iws 76 | 77 | ## Plugin-specific files: 78 | 79 | # IntelliJ 80 | /out/ 81 | 82 | # mpeltonen/sbt-idea plugin 83 | .idea_modules/ 84 | 85 | # JIRA plugin 86 | atlassian-ide-plugin.xml 87 | 88 | # Crashlytics plugin (for Android Studio and IntelliJ) 89 | com_crashlytics_export_strings.xml 90 | crashlytics.properties 91 | crashlytics-build.properties 92 | 93 | build/ 94 | 95 | # Ignore Gradle GUI config 96 | gradle-app.setting 97 | 98 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 99 | !gradle-wrapper.jar 100 | 101 | db 102 | 103 | ### Java template 104 | *.class 105 | 106 | # Mobile Tools for Java (J2ME) 107 | .mtj.tmp/ 108 | 109 | # Package Files # 110 | #*.jar 111 | 112 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 113 | hs_err_pid* 114 | 115 | 116 | ### Leiningen template 117 | classes/ 118 | target/ 119 | logs/ 120 | checkouts/ 121 | .lein-deps-sum 122 | .lein-repl-history 123 | .lein-plugins/ 124 | .lein-failures 125 | .nrepl-port 126 | 127 | querydsl/ 128 | 129 | .DS_Store 130 | 131 | *.exe 132 | *.out 133 | 134 | *.log 135 | node_modules/ 136 | dist/ 137 | dist.zip 138 | package-lock.json 139 | *.lock 140 | local.properties 141 | .cxx 142 | .externalNativeBuild 143 | /captures 144 | /build 145 | __pycache__/ 146 | *.pyc 147 | 148 | 149 | cmake-build-debug/ 150 | cmake-build-debug-mingw/ 151 | venv/ 152 | .idea/ 153 | ch_PP-OCRv4_det_infer/ 154 | ch_PP-OCRv4_rec_infer/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /app.properties: -------------------------------------------------------------------------------- 1 | #Sun Apr 28 04:53:24 HST 2024 2 | recName=ch_PP-OCRv3_rec_infer 3 | model=model 4 | keysName=ppocr_keys_v1.txt 5 | libPath=D\:\\lib\\ocr-lib\\win64\\bin 6 | clsName=ch_ppocr_mobile_v2.0_cls_infer 7 | modelsDir=D\:\\model\\ppocr-v3-NCNN-models 8 | detName=ch_PP-OCRv3_det_infer 9 | -------------------------------------------------------------------------------- /models/readme.md: -------------------------------------------------------------------------------- 1 | models path 2 | ```shell 3 | wget https://github.com/litongjava/tools-ocr/releases/download/model-ppocr-v4/ch_PP-OCRv4_det_infer-onnx.zip 4 | wget https://github.com/litongjava/tools-ocr/releases/download/model-ppocr-v4/ch_PP-OCRv4_rec_infer-onnx.zip 5 | mkdir -p models/ch_PP-OCRv4_det_infer 6 | mkdir -p models/ch_PP-OCRv4_rec_infer 7 | unzip ch_PP-OCRv4_det_infer-onnx.zip -d models/ch_PP-OCRv4_det_infer 8 | unzip ch_PP-OCRv4_rec_infer-onnx.zip -d models/ch_PP-OCRv4_rec_infer 9 | ``` -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.luooqi 8 | tools-ocr 9 | 2.3.0 10 | 11 | 12 | UTF-8 13 | 1.8 14 | ${java.version} 15 | ${java.version} 16 | com.luooqi.ocr.OcrApp 17 | 1.18.20 18 | 1.2.3 19 | 0.25.0 20 | 21 | 22 | 23 | 24 | 25 | com.1stleg 26 | jnativehook 27 | 2.1.0 28 | 29 | 30 | 31 | cn.hutool 32 | hutool-all 33 | 5.8.11 34 | 35 | 36 | 37 | org.imgscalr 38 | imgscalr-lib 39 | 4.2 40 | 41 | 42 | 43 | org.apache.pdfbox 44 | pdfbox 45 | 2.0.24 46 | 47 | 48 | 49 | org.projectlombok 50 | lombok 51 | ${lombok.version} 52 | provided 53 | 54 | 55 | ch.qos.logback 56 | logback-classic 57 | ${logback.version} 58 | 59 | 60 | 61 | 62 | io.github.mymonstercat 63 | rapidocr 64 | 0.0.7 65 | 66 | 67 | 68 | io.github.mymonstercat 69 | rapidocr-onnx-platform 70 | 0.0.7 71 | 72 | 73 | 74 | junit 75 | junit 76 | 4.13.2 77 | test 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | org.apache.maven.plugins 121 | maven-dependency-plugin 122 | 2.10 123 | 124 | 125 | copy-dependencies 126 | package 127 | 128 | false 129 | false 130 | true 131 | 132 | 133 | copy-dependencies 134 | 135 | 136 | 137 | 138 | 139 | org.openjfx 140 | javafx-maven-plugin 141 | 0.0.8 142 | 143 | ${main.class} 144 | 145 | 146 | 147 | com.zenjava 148 | javafx-maven-plugin 149 | 8.8.3 150 | 151 | ${main.class} 152 | treehole 153 | com.luooqi 154 | true 155 | 156 | 157 | luooqi@2020 158 | true 159 | 160 | ${project.version} 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /readme-cn.md: -------------------------------------------------------------------------------- 1 | # 树洞 OCR 2 | 3 | [English](./readme.md) | [中文](./readme-cn.md) 4 | 5 | ## 介绍 6 | 7 | - 本地 OCR 识别 8 | : 树洞 OCR 文字识别工具无需联网,通过调用本地 OCR 技术,基于 Paddle OCR 模型和深度学习框架如 PyTorch、DJL,提供快速准确的文字识别。 9 | - 跨平台兼容 10 | : 基于 java 1.8 和 JavaFX 开发,支持在不同操作系统上运行,包括 Mac OS X 12.6 及以上版本。 11 | - 强大的功能支持 12 | : 除了基础的文字识别,还包括 PDF 识别、图片文字识别、快捷键截图识别等功能. 13 | 14 | ## 主要依赖库 15 | 16 | - jdk 1.8 17 | - javafx 18 | - djl 19 | - pytorch 20 | - onnx 21 | - paddle ocr 22 | - opencv 23 | 24 | ## 开源地址 25 | 26 | [gitee](https://gitee.com/ppnt/tools-ocr) | [github](https://github.com/litongjava/tools-ocr) 27 | 28 | ## document 29 | 30 | https://tree-hole-ocr-docs.vercel.app/ 31 | 32 | ## required 33 | 34 | - Mac OS X 12.6 因为依赖 djl 0.25.0 35 | 36 | ## 安装 37 | 38 | > - **安装路径请勿包含中文字符**; 39 | > - 本程序使用 JavaFX 开发,提供的安装包中已经包含了 Java 40 | > - 从[release](https://github.com/litongjava/tools-ocr/releases/)下载最新版本解压安装即可 41 | 42 | ## 程序使用 43 | 44 | ### 截图 45 | 46 | - 方法一:在程序主界面点击截图按钮; 47 | - 方法二:点击截图快捷键 F4。 48 | 49 | ### 圈选区域 50 | 51 | 进入截图界面后,按下鼠标左键,然后拖动即可圈选所要截取的区域; 52 | 圈选结束后,可以对圈选的区域进行微调: 53 | 54 | - 使用 **方向键**,可以对所选区域的右边界和上边界进行微调; 55 | - 使用 **Shift+方向键**,可以对所选区域的左边界和下边界进行微调; 56 | - 使用 **Ctrl+A**,可以全选整个屏幕。 57 | 58 | ### 确定圈选 59 | 60 | 圈选完成后,点击 `Enter` 或者 `Space` 键,或者鼠标左键双击即可确认圈选;确认圈选后,会自动对所选区域进行 OCR 文字识别。 61 | 62 | ![](readme_files/3.jpg) 63 | ![](readme_files/4.jpg) 64 | 65 | ## 本地构建 66 | 67 | ### 下载模型并解压 68 | 69 | ``` 70 | wget https://github.com/litongjava/tools-ocr/releases/download/model-ppocr-v4/ch_PP-OCRv4_rec_infer-onnx.zip 71 | wget https://github.com/litongjava/tools-ocr/releases/download/model-ppocr-v4/ch_PP-OCRv4_det_infer-onnx.zip 72 | ``` 73 | 74 | 解压模型 75 | 76 | ``` 77 | mkdir models/ch_PP-OCRv4_rec_infer 78 | mkdir models/ch_PP-OCRv4_det_infer 79 | unzip /Users/mac/Downloads/ch_PP-OCRv4_rec_infer-onnx.zip -d models/ch_PP-OCRv4_rec_infer 80 | unzip /Users/mac/Downloads/ch_PP-OCRv4_det_infer-onnx.zip -d models/ch_PP-OCRv4_det_infer 81 | ``` 82 | 83 | ### 构建程序 84 | 85 | 你下载代码在本地进行构建,构建命令如下 86 | windows 87 | 88 | ``` 89 | mkdir target\jfx\app 90 | cp -r models target\jfx\app 91 | mvn jfx:native -DskipTests -f pom.xml 92 | ``` 93 | 94 | macos 95 | 96 | ```shell script 97 | rm -rf target/jfx/app 98 | mkdir -p target/jfx/app 99 | cp -r models target/jfx/app 100 | mvn jfx:native -DskipTests -f pom.xml 101 | ``` 102 | 103 | ## 查看系统运行日志 104 | 105 | cd treehole.app/Contents/java/logs 106 | 107 | ## 注意事项 108 | 109 | ### MAC 权限设置 110 | 111 | 由于监控了截图快捷键,因此 MAC 需要开启相应的权限,请见下图: 112 | 笔者设置如下 113 | 114 | - Settings-->Security and Privacy-->Accessbility 115 | ![MAC权限设置](readme_files/5.jpg) 116 | - Settings-->Security and Privacy-->Screen Recording 117 | ![2](readme_files/2.jpg) 118 | 119 | ## 常用目录 120 | 121 | - 日志目录/Applications/treehole.app/Contents/Java/logs 122 | - 临时图片保存目录 /Applications/treehole.app/Contents/Java 123 | 124 | ## TODO 125 | 126 | - [x] PDF 识别 127 | - [x] 图片文字识别 128 | - [x] 识别结果文本对齐(暂未实现多分栏) 129 | - [x] 全屏模式下截图 130 | - [x] 添加正在识别动画 131 | - [x] 多屏支持 132 | - [ ] 文本翻译 133 | - [ ] 公式识别 134 | - [ ] 表格识别 135 | - [ ] 软件设置 136 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Tree Hole OCR 2 | 3 | [English](./readme.md) | [中文](./readme-cn.md) 4 | 5 | ## Introduction 6 | 7 | - Local OCR Recognition: Tree Hole OCR text recognition tool does not require internet connection. It leverages local OCR technology, based on Paddle OCR model and deep learning frameworks such as PyTorch, DJL, to provide fast and accurate text recognition. 8 | - Cross-platform compatibility: Developed with Java 1.8 and JavaFX, it supports operation on different operating systems, including Mac OS X 12.6 and above. 9 | - Powerful functionality: In addition to basic text recognition, it also includes PDF recognition, image text recognition, shortcut key screenshot recognition, and more. 10 | 11 | ## Dependencies Library 12 | 13 | - JDK 1.8 14 | - JavaFX 15 | - DJL 16 | - PyTorch 17 | - ONNX 18 | - Paddle OCR 19 | - OpenCV 20 | 21 | ## Open Source Address 22 | 23 | [gitee](https://gitee.com/ppnt/tools-ocr) | [github](https://github.com/litongjava/tools-ocr) 24 | 25 | ## Documentation 26 | 27 | https://tree-hole-ocr-docs.vercel.app/ 28 | 29 | ## Requirements 30 | 31 | - Mac OS X 12.6 due to dependency on DJL 0.25.0 32 | 33 | ## Installation 34 | 35 | > - **Please do not include Chinese characters in the installation path**; 36 | > - This program is developed with JavaFX, and the installation package provided already includes Java. 37 | > - Download the latest version from [release](https://github.com/litongjava/tools-ocr/releases/) and unzip it for installation. 38 | 39 | ## Using the Program 40 | 41 | ### Screenshot 42 | 43 | - Method one: Click the screenshot button on the main interface of the program; 44 | - Method two: Press the screenshot shortcut key F4. 45 | 46 | ### Selecting Area 47 | 48 | After entering the screenshot interface, press and hold the left mouse button, then drag to select the area you want to capture; 49 | After completing the selection, you can fine-tune the selected area: 50 | 51 | - Use **arrow keys** to adjust the right and top borders of the selected area; 52 | - Use **Shift + arrow keys** to adjust the left and bottom borders of the selected area; 53 | - Use **Ctrl + A** to select the entire screen. 54 | 55 | ### Confirm Selection 56 | 57 | After completing the selection, press `Enter` or `Space` key, or double-click the left mouse button to confirm the selection; Once confirmed, the program will automatically perform OCR text recognition on the selected area. 58 | 59 | - image 60 | 61 | ![](readme_files/3.jpg) 62 | 63 | - result: 64 | 65 | ![](readme_files/4.jpg) 66 | 67 | ## Local Build 68 | 69 | ### Download and Unzip the Models 70 | 71 | ``` 72 | wget https://github.com/litongjava/tools-ocr/releases/download/model-ppocr-v4/ch_PP-OCRv4_rec_infer-onnx.zip 73 | wget https://github.com/litongjava/tools-ocr/releases/download/model-ppocr-v4/ch_PP-OCRv4_det_infer-onnx.zip 74 | ``` 75 | 76 | Unzip the models 77 | 78 | ``` 79 | mkdir models/ch_PP-OCRv4_rec_infer 80 | mkdir models/ch_PP-OCRv4_det_infer 81 | unzip /Users/mac/Downloads/ch_PP-OCRv4_rec_infer-onnx.zip -d models/ch_PP-OCRv4_rec_infer 82 | unzip /Users/mac/Downloads/ch_PP-OCRv4_det_infer-onnx.zip -d models/ch_PP-OCRv4_det_infer 83 | ``` 84 | 85 | ### Build the Program 86 | 87 | You can download the code and build it locally. The build commands are as follows: 88 | windows 89 | 90 | ``` 91 | mkdir target\jfx\app 92 | cp -r models target\jfx\app 93 | mvn jfx:native -DskipTests -f pom.xml 94 | ``` 95 | 96 | macos 97 | 98 | ```shell script 99 | rm -rf target/jfx/app 100 | mkdir -p target/jfx/app 101 | cp -r models target/jfx/app 102 | mvn jfx:native -DskipTests -f pom.xml 103 | ``` 104 | 105 | ## View System Operating Log 106 | 107 | cd treehole.app/Contents/java/logs 108 | 109 | ## Notices 110 | 111 | ### MAC Permission Settings 112 | 113 | Since screenshot shortcuts are monitored, MAC needs appropriate permissions settings, as shown below: 114 | 115 | - Settings --> Security and Privacy --> Accessibility 116 | ![MAC Permission Settings](readme_files/5.jpg) 117 | - Settings --> Security and Privacy --> Screen Recording 118 | ![2](readme_files/2.jpg) 119 | 120 | ## Common Directories 121 | 122 | - Log directory /Applications/treehole.app/Contents/Java/logs 123 | - Temporary image saving directory /Applications/treehole.app/Contents/Java 124 | 125 | ## TODO 126 | 127 | - [x] PDF Recognition 128 | - [x] Image Text Recognition 129 | - [x] Recognition result text alignment (multi-column yet to be implemented) 130 | - [x] Full screen mode screenshot 131 | - [x] Adding recognition animation 132 | - [x] Multi-screen support 133 | - [ ] Text Translation 134 | - [ ] Formula Recognition 135 | - [ ] Table Recognition 136 | - [ ] Software Settings 137 | -------------------------------------------------------------------------------- /readme_files/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/tools-ocr/98c12ae01df864a81c89f3f2bd4ff97cefa4c381/readme_files/1.jpg -------------------------------------------------------------------------------- /readme_files/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/tools-ocr/98c12ae01df864a81c89f3f2bd4ff97cefa4c381/readme_files/2.jpg -------------------------------------------------------------------------------- /readme_files/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/tools-ocr/98c12ae01df864a81c89f3f2bd4ff97cefa4c381/readme_files/3.jpg -------------------------------------------------------------------------------- /readme_files/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/tools-ocr/98c12ae01df864a81c89f3f2bd4ff97cefa4c381/readme_files/4.jpg -------------------------------------------------------------------------------- /readme_files/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/tools-ocr/98c12ae01df864a81c89f3f2bd4ff97cefa4c381/readme_files/5.jpg -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/treehole.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/tools-ocr/98c12ae01df864a81c89f3f2bd4ff97cefa4c381/src/main/deploy/package/macosx/treehole.icns -------------------------------------------------------------------------------- /src/main/deploy/package/windows/treehole.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/tools-ocr/98c12ae01df864a81c89f3f2bd4ff97cefa4c381/src/main/deploy/package/windows/treehole.ico -------------------------------------------------------------------------------- /src/main/java/com/litongjava/project/config/ConfigKeys.java: -------------------------------------------------------------------------------- 1 | package com.litongjava.project.config; 2 | 3 | /** 4 | * Created by litonglinux@qq.com on 10/11/2023_3:39 PM 5 | */ 6 | public class ConfigKeys { 7 | public static final String libPath = "libPath"; 8 | public static final String modelsDir = "modelsDir"; 9 | public static final String detName = "detName"; 10 | public static final String clsName = "clsName"; 11 | public static final String recName = "recName"; 12 | public static final String keysName = "keysName"; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/litongjava/project/config/ProjectConfig.java: -------------------------------------------------------------------------------- 1 | package com.litongjava.project.config; 2 | 3 | import java.io.FileInputStream; 4 | import java.io.FileNotFoundException; 5 | import java.io.FileOutputStream; 6 | import java.io.IOException; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | import java.util.Properties; 10 | import java.util.concurrent.ConcurrentHashMap; 11 | 12 | /** 13 | * Created by litonglinux@qq.com on 10/11/2023_3:22 PM 14 | * 内部维护一个Map,将配置写入文件 15 | */ 16 | public class ProjectConfig { 17 | private Map configs = new ConcurrentHashMap<>(); 18 | private String configFileName = "app.properties"; 19 | 20 | public ProjectConfig() { 21 | this.configs = readConfig(); 22 | } 23 | 24 | public ProjectConfig(String configFileName) { 25 | this.configFileName = configFileName; 26 | this.configs = readConfig(); 27 | } 28 | 29 | 30 | public String getConfigFileName() { 31 | return configFileName; 32 | } 33 | 34 | public Boolean getBool(String key) { 35 | return (Boolean) configs.get(key); 36 | } 37 | 38 | public Integer getInt(String key) { 39 | return (Integer) configs.get(key); 40 | } 41 | 42 | public String getStr(String key) { 43 | return (String) configs.get(key); 44 | } 45 | 46 | public void put(String key, Object value) { 47 | configs.put(key, value); 48 | saveConfig(); 49 | } 50 | 51 | 52 | public void batchPut(Map map) { 53 | configs.putAll(map); 54 | saveConfig(); 55 | } 56 | 57 | // 将configs保持到文件文件 58 | private void saveConfig() { 59 | Properties properties = new Properties(); 60 | 61 | // Convert configs to properties 62 | for (Map.Entry entry : configs.entrySet()) { 63 | properties.setProperty(entry.getKey(), String.valueOf(entry.getValue())); 64 | } 65 | 66 | try (FileOutputStream out = new FileOutputStream(configFileName)) { 67 | properties.store(out, null); 68 | } catch (IOException e) { 69 | e.printStackTrace(); 70 | } 71 | } 72 | 73 | private Map readConfig() { 74 | Properties properties = new Properties(); 75 | Map resultMap = new HashMap<>(); 76 | 77 | try (FileInputStream in = new FileInputStream(configFileName)) { 78 | properties.load(in); 79 | for (String key : properties.stringPropertyNames()) { 80 | resultMap.put(key, properties.getProperty(key)); 81 | } 82 | } catch (FileNotFoundException e) { 83 | try (FileOutputStream out = new FileOutputStream(configFileName)) { 84 | properties.store(out, null); 85 | } catch (IOException ioException) { 86 | ioException.printStackTrace(); 87 | } 88 | } catch (IOException e) { 89 | e.printStackTrace(); 90 | } 91 | 92 | return resultMap; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/luooqi/ocr/OcrApp.java: -------------------------------------------------------------------------------- 1 | package com.luooqi.ocr; 2 | 3 | import cn.hutool.core.thread.GlobalThreadPool; 4 | import com.luooqi.ocr.config.InitConfig; 5 | import com.luooqi.ocr.local.PaddlePaddleOCRV4; 6 | import com.luooqi.ocr.windows.MainForm; 7 | import javafx.application.Application; 8 | import javafx.stage.Stage; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.jnativehook.GlobalScreen; 11 | 12 | @Slf4j 13 | public class OcrApp extends Application { 14 | 15 | public static void main(String[] args) { 16 | launch(args); 17 | } 18 | 19 | @Override 20 | public void init() throws Exception { 21 | super.init(); 22 | //InitConfig.init(); 23 | } 24 | 25 | 26 | @Override 27 | public void start(Stage primaryStage) { 28 | MainForm mainForm = new MainForm(); 29 | mainForm.init(primaryStage); 30 | primaryStage.show(); 31 | } 32 | 33 | @Override 34 | public void stop() throws Exception { 35 | log.info("close"); 36 | GlobalScreen.unregisterNativeHook(); 37 | PaddlePaddleOCRV4.INSTANCE.close(); 38 | GlobalThreadPool.shutdown(true); 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/java/com/luooqi/ocr/config/InitConfig.java: -------------------------------------------------------------------------------- 1 | package com.luooqi.ocr.config; 2 | 3 | import com.luooqi.ocr.utils.GlobalKeyListener; 4 | import com.luooqi.ocr.utils.VoidDispatchService; 5 | import org.jnativehook.GlobalScreen; 6 | 7 | import java.util.logging.Level; 8 | import java.util.logging.Logger; 9 | 10 | /** 11 | * Created by litonglinux@qq.com on 10/11/2023_12:53 AM 12 | */ 13 | public class InitConfig { 14 | 15 | public static void init() { 16 | // ProjectConfig projectConfig = Aop.get(ProjectConfig.class); 17 | // Map map = new HashMap<>(); 18 | // map.put(ConfigKeys.libPath, "D:\\lib\\ocr-lib\\win64\\bin"); 19 | // map.put(ConfigKeys.modelsDir, "D:\\model\\ppocr-v3-NCNN-models"); 20 | // map.put(ConfigKeys.detName, "ch_PP-OCRv3_det_infer"); 21 | // map.put(ConfigKeys.clsName, "ch_ppocr_mobile_v2.0_cls_infer"); 22 | // map.put(ConfigKeys.recName, "ch_PP-OCRv3_rec_infer"); 23 | // map.put(ConfigKeys.keysName, "ppocr_keys_v1.txt"); 24 | // projectConfig.batchPut(map); 25 | 26 | 27 | } 28 | 29 | 30 | public static void initKeyHook() { 31 | try { 32 | Logger logger = Logger.getLogger(GlobalScreen.class.getPackage().getName()); 33 | logger.setLevel(Level.WARNING); 34 | logger.setUseParentHandlers(false); 35 | GlobalScreen.setEventDispatcher(new VoidDispatchService()); 36 | GlobalScreen.registerNativeHook(); 37 | GlobalScreen.addNativeKeyListener(new GlobalKeyListener()); 38 | } catch (Exception ex) { 39 | ex.printStackTrace(); 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/luooqi/ocr/constants/ImagesConstants.java: -------------------------------------------------------------------------------- 1 | package com.luooqi.ocr.constants; 2 | 3 | /** 4 | * Created by litonglinux@qq.com on 12/9/2023_7:14 PM 5 | */ 6 | public class ImagesConstants { 7 | public static final String LOGO = "img/logo.png"; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/luooqi/ocr/controller/ProcessController.java: -------------------------------------------------------------------------------- 1 | package com.luooqi.ocr.controller; 2 | 3 | import com.luooqi.ocr.utils.CommUtils; 4 | 5 | import javafx.geometry.Insets; 6 | import javafx.geometry.Pos; 7 | import javafx.scene.Scene; 8 | import javafx.scene.control.Label; 9 | import javafx.scene.control.ProgressIndicator; 10 | import javafx.scene.layout.Background; 11 | import javafx.scene.layout.BackgroundFill; 12 | import javafx.scene.layout.CornerRadii; 13 | import javafx.scene.layout.VBox; 14 | import javafx.scene.paint.Color; 15 | import javafx.scene.text.Font; 16 | import javafx.stage.Stage; 17 | import javafx.stage.StageStyle; 18 | 19 | public class ProcessController extends Stage { 20 | 21 | public ProcessController() { 22 | VBox vBox = new VBox(); 23 | vBox.setAlignment(Pos.BASELINE_CENTER); 24 | vBox.setMinWidth(300); 25 | vBox.setBackground(new Background(new BackgroundFill(Color.rgb(250, 250, 250), CornerRadii.EMPTY, Insets.EMPTY))); 26 | ProgressIndicator progressIndicator = new ProgressIndicator(); 27 | progressIndicator.setStyle(CommUtils.STYLE_TRANSPARENT); 28 | int circleSize = 75; 29 | progressIndicator.setMinWidth(circleSize); 30 | progressIndicator.setMinHeight(circleSize); 31 | Label topLab = new Label("正在识别图片,请稍等....."); 32 | topLab.setFont(Font.font(18)); 33 | vBox.setSpacing(10); 34 | vBox.setPadding(new Insets(20, 0, 20, 0)); 35 | vBox.getChildren().add(progressIndicator); 36 | vBox.getChildren().add(topLab); 37 | Scene scene = new Scene(vBox, Color.TRANSPARENT); 38 | setScene(scene); 39 | initStyle(StageStyle.TRANSPARENT); 40 | CommUtils.initStage(this); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/luooqi/ocr/local/PaddlePaddleOCRV4.java: -------------------------------------------------------------------------------- 1 | package com.luooqi.ocr.local; 2 | 3 | import java.io.File; 4 | 5 | import com.benjaminwan.ocrlibrary.OcrResult; 6 | 7 | import io.github.mymonstercat.Model; 8 | import io.github.mymonstercat.ocr.InferenceEngine; 9 | import io.github.mymonstercat.ocr.config.HardwareConfig; 10 | 11 | /** 12 | * Created by litonglinux@qq.com on 11/23/2023_2:09 AM 13 | */ 14 | public enum PaddlePaddleOCRV4 { 15 | INSTANCE; 16 | 17 | static InferenceEngine engine = null; 18 | 19 | PaddlePaddleOCRV4() { 20 | 21 | } 22 | 23 | // noting not to do.but init 24 | public static void init() { 25 | HardwareConfig onnxConfig = HardwareConfig.getOnnxConfig(); 26 | onnxConfig.setNumThread(2); 27 | engine = InferenceEngine.getInstance(Model.ONNX_PPOCR_V4_SERVER, onnxConfig); 28 | } 29 | 30 | public OcrResult ocr(File imageFile) { 31 | return engine.runOcr(imageFile.getAbsolutePath()); 32 | } 33 | 34 | public void close() { 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/luooqi/ocr/model/CaptureInfo.java: -------------------------------------------------------------------------------- 1 | 2 | package com.luooqi.ocr.model; 3 | 4 | import cn.hutool.core.swing.ScreenUtil; 5 | import javafx.beans.binding.BooleanBinding; 6 | import javafx.beans.property.BooleanProperty; 7 | import javafx.beans.property.SimpleBooleanProperty; 8 | import javafx.scene.text.Font; 9 | import javafx.scene.text.FontWeight; 10 | import javafx.stage.Screen; 11 | 12 | /** 13 | * @author GOXR3PLUS 14 | */ 15 | public class CaptureInfo { 16 | 17 | /** 18 | * The x pressed. 19 | */ 20 | public int mouseXPressed = 0; 21 | 22 | /** 23 | * The y pressed. 24 | */ 25 | public int mouseYPressed = 0; 26 | 27 | /** 28 | * The x now. 29 | */ 30 | public int mouseXNow = 0; 31 | 32 | /** 33 | * The y now. 34 | */ 35 | public int mouseYNow = 0; 36 | 37 | /** 38 | * The upper left X. 39 | */ 40 | public int rectUpperLeftX = 0; 41 | 42 | /** 43 | * The upper left Y. 44 | */ 45 | public int rectUpperLeftY = 0; 46 | 47 | /** 48 | * The rectangle width. 49 | */ 50 | public int rectWidth; 51 | 52 | /** 53 | * The rectangle height. 54 | */ 55 | public int rectHeight; 56 | 57 | // ---------------- 58 | 59 | /** 60 | * The font. 61 | */ 62 | public Font font = Font.font("", FontWeight.BOLD, 14); 63 | 64 | // --------------- 65 | 66 | /** 67 | * The shift pressed. 68 | */ 69 | public BooleanProperty shiftPressed = new SimpleBooleanProperty(); 70 | 71 | /** 72 | * The up pressed. 73 | */ 74 | public BooleanProperty upPressed = new SimpleBooleanProperty(); 75 | 76 | /** 77 | * The right pressed. 78 | */ 79 | public BooleanProperty rightPressed = new SimpleBooleanProperty(); 80 | 81 | /** 82 | * The down pressed. 83 | */ 84 | public BooleanProperty downPressed = new SimpleBooleanProperty(); 85 | 86 | /** 87 | * The left pressed. 88 | */ 89 | public BooleanProperty leftPressed = new SimpleBooleanProperty(); 90 | 91 | /** 92 | * The any pressed. 93 | */ 94 | public BooleanBinding anyPressed = upPressed.or(downPressed).or(leftPressed).or(rightPressed); 95 | 96 | /** 97 | * The hide extra features. 98 | */ 99 | public BooleanProperty hideExtraFeatures = new SimpleBooleanProperty(); 100 | 101 | // ------------ 102 | 103 | /** 104 | * The screen width. 105 | */ 106 | public static int ScreenWidth = ScreenUtil.getWidth(); 107 | 108 | /** 109 | * The screen height. 110 | */ 111 | public static int ScreenHeight = ScreenUtil.getHeight(); 112 | 113 | public static int ScreenMinX = 0; 114 | public static int ScreenMaxX = 0; 115 | 116 | public void reset() { 117 | mouseXNow = 0; 118 | mouseXPressed = 0; 119 | mouseYNow = 0; 120 | mouseYPressed = 0; 121 | rectUpperLeftY = 0; 122 | rectUpperLeftX = 0; 123 | rectWidth = 0; 124 | rectHeight = 0; 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/com/luooqi/ocr/model/StageInfo.java: -------------------------------------------------------------------------------- 1 | package com.luooqi.ocr.model; 2 | 3 | public class StageInfo { 4 | private double x; 5 | private double y; 6 | private double width; 7 | private double height; 8 | private boolean fullScreenState; 9 | 10 | public StageInfo() { 11 | } 12 | 13 | public StageInfo(double x, double y, double width, double height, boolean fullScreenState) { 14 | this.x = x; 15 | this.y = y; 16 | this.width = width; 17 | this.height = height; 18 | this.fullScreenState = fullScreenState; 19 | } 20 | 21 | public double getX() { 22 | return x; 23 | } 24 | 25 | public void setX(double x) { 26 | this.x = x; 27 | } 28 | 29 | public double getY() { 30 | return y; 31 | } 32 | 33 | public void setY(double y) { 34 | this.y = y; 35 | } 36 | 37 | public double getWidth() { 38 | return width; 39 | } 40 | 41 | public void setWidth(double width) { 42 | this.width = width; 43 | } 44 | 45 | public double getHeight() { 46 | return height; 47 | } 48 | 49 | public void setHeight(double height) { 50 | this.height = height; 51 | } 52 | 53 | public boolean isFullScreenState() { 54 | return fullScreenState; 55 | } 56 | 57 | public void setFullScreenState(boolean fullScreenState) { 58 | this.fullScreenState = fullScreenState; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/luooqi/ocr/model/TextBlock.java: -------------------------------------------------------------------------------- 1 | package com.luooqi.ocr.model; 2 | 3 | import java.awt.*; 4 | 5 | public class TextBlock { 6 | private Point topLeft; 7 | private Point topRight; 8 | private Point bottomLeft; 9 | private Point bottomRight; 10 | private double angle; 11 | private double fontSize; 12 | private String text; 13 | 14 | public TextBlock() { 15 | } 16 | 17 | public TextBlock(Point topLeft, Point topRight, Point bottomLeft, Point bottomRight, String text) { 18 | this.topLeft = topLeft; 19 | this.topRight = topRight; 20 | this.bottomLeft = bottomLeft; 21 | this.bottomRight = bottomRight; 22 | this.text = text; 23 | calcAngle(); 24 | } 25 | 26 | public Point getTopLeft() { 27 | return topLeft; 28 | } 29 | 30 | public void setTopLeft(Point topLeft) { 31 | this.topLeft = topLeft; 32 | calcAngle(); 33 | } 34 | 35 | public Point getTopRight() { 36 | return topRight; 37 | } 38 | 39 | public void setTopRight(Point topRight) { 40 | this.topRight = topRight; 41 | calcAngle(); 42 | } 43 | 44 | public Point getBottomLeft() { 45 | return bottomLeft; 46 | } 47 | 48 | public void setBottomLeft(Point bottomLeft) { 49 | this.bottomLeft = bottomLeft; 50 | calcAngle(); 51 | } 52 | 53 | public Point getBottomRight() { 54 | return bottomRight; 55 | } 56 | 57 | public void setBottomRight(Point bottomRight) { 58 | this.bottomRight = bottomRight; 59 | calcAngle(); 60 | } 61 | 62 | public String getText() { 63 | return text; 64 | } 65 | 66 | public void setText(String text) { 67 | this.text = text; 68 | } 69 | 70 | public double getFontSize() { 71 | return fontSize; 72 | } 73 | 74 | private void setFontSize(double fontSize) { 75 | this.fontSize = fontSize; 76 | } 77 | 78 | private void calcAngle() { 79 | if (this.topLeft != null && this.bottomLeft != null) { 80 | int x = this.topLeft.x - this.bottomLeft.x; 81 | int y = this.bottomLeft.y - this.topLeft.y; 82 | setAngle(x * 1.0 / y); 83 | setFontSize(Math.sqrt(x * x + y * y)); 84 | } 85 | } 86 | 87 | public double getAngle() { 88 | return angle; 89 | } 90 | 91 | private void setAngle(double angle) { 92 | this.angle = angle; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/luooqi/ocr/snap/ScreenCapture.java: -------------------------------------------------------------------------------- 1 | 2 | package com.luooqi.ocr.snap; 3 | 4 | import java.awt.AWTException; 5 | import java.awt.Rectangle; 6 | import java.awt.Robot; 7 | import java.awt.image.BufferedImage; 8 | 9 | import com.luooqi.ocr.model.CaptureInfo; 10 | import com.luooqi.ocr.utils.CommUtils; 11 | import com.luooqi.ocr.windows.MainForm; 12 | 13 | import cn.hutool.core.swing.ScreenUtil; 14 | import cn.hutool.log.StaticLog; 15 | import javafx.animation.AnimationTimer; 16 | import javafx.application.Platform; 17 | import javafx.embed.swing.SwingFXUtils; 18 | import javafx.scene.Cursor; 19 | import javafx.scene.Scene; 20 | import javafx.scene.canvas.Canvas; 21 | import javafx.scene.canvas.GraphicsContext; 22 | import javafx.scene.image.WritableImage; 23 | import javafx.scene.input.KeyCode; 24 | import javafx.scene.input.KeyEvent; 25 | import javafx.scene.input.MouseButton; 26 | import javafx.scene.layout.Background; 27 | import javafx.scene.layout.BackgroundImage; 28 | import javafx.scene.layout.BackgroundPosition; 29 | import javafx.scene.layout.BackgroundRepeat; 30 | import javafx.scene.layout.BackgroundSize; 31 | import javafx.scene.layout.BorderPane; 32 | import javafx.scene.layout.Pane; 33 | import javafx.scene.paint.Color; 34 | import javafx.scene.text.Font; 35 | import javafx.scene.text.FontWeight; 36 | import javafx.stage.Stage; 37 | 38 | /** 39 | * This is the Window which is used from the user to draw the rectangle representing an area on the screen to be captured. 40 | * 41 | * @author GOXR3PLUS 42 | */ 43 | public class ScreenCapture { 44 | 45 | private final BorderPane rootPane; 46 | private final Canvas mainCanvas; 47 | private final CaptureInfo data; 48 | private GraphicsContext gc; 49 | private Scene scene; 50 | private final Stage stage; 51 | public static boolean isSnapping = false; 52 | 53 | /** 54 | * When a key is being pressed into the capture window then this Animation Timer is doing it's magic. 55 | */ 56 | private final AnimationTimer yPressedAnimation = new AnimationTimer() { 57 | private long nextSecond = 0L; 58 | private long precisionLevel; 59 | 60 | @Override 61 | public void start() { 62 | nextSecond = 0L; 63 | precisionLevel = 0L; 64 | super.start(); 65 | } 66 | 67 | @Override 68 | public void handle(long nanos) { 69 | if (nanos >= nextSecond) { 70 | nextSecond = nanos + precisionLevel; 71 | 72 | // With special key pressed 73 | // (we want [LEFT] and [DOWN] side of the rectangle to be 74 | // movable) 75 | 76 | // No Special Key is Pressed 77 | // (we want [RIGHT] and [UP] side of the rectangle to be 78 | // movable) 79 | 80 | // ------------------------------ 81 | if (data.rightPressed.get()) { 82 | if (data.shiftPressed.get()) { // Special Key? 83 | if (data.mouseXNow > data.mouseXPressed) { // Mouse gone Right? 84 | data.mouseXPressed += 1; 85 | } else { 86 | data.mouseXNow += 1; 87 | } 88 | } else { 89 | if (data.mouseXNow > data.mouseXPressed) { // Mouse gone Right? 90 | data.mouseXNow += 1; 91 | } else { 92 | data.mouseXPressed += 1; 93 | } 94 | } 95 | } 96 | 97 | if (data.leftPressed.get()) { 98 | if (data.shiftPressed.get()) { // Special Key? 99 | if (data.mouseXNow > data.mouseXPressed) { // Mouse gone Right? 100 | data.mouseXPressed -= 1; 101 | } else { 102 | data.mouseXNow -= 1; 103 | } 104 | } else { 105 | if (data.mouseXNow > data.mouseXPressed) { // Mouse gone Right? 106 | data.mouseXNow -= 1; 107 | } else { 108 | data.mouseXPressed -= 1; 109 | } 110 | } 111 | } 112 | 113 | if (data.upPressed.get()) { 114 | if (data.shiftPressed.get()) { // Special Key? 115 | if (data.mouseYNow > data.mouseYPressed) { // Mouse gone UP? 116 | data.mouseYNow -= 1; 117 | } else { 118 | data.mouseYPressed -= 1; 119 | } 120 | } else { 121 | if (data.mouseYNow > data.mouseYPressed) { // Mouse gone UP? 122 | data.mouseYPressed -= 1; 123 | } else { 124 | data.mouseYNow -= 1; 125 | } 126 | } 127 | } 128 | 129 | if (data.downPressed.get()) { 130 | if (data.shiftPressed.get()) { // Special Key? 131 | if (data.mouseYNow > data.mouseYPressed) { // Mouse gone UP? 132 | data.mouseYNow += 1; 133 | } else { 134 | data.mouseYPressed += 1; 135 | } 136 | } else { 137 | if (data.mouseYNow > data.mouseYPressed) { // Mouse gone UP? 138 | data.mouseYPressed += 1; 139 | } else { 140 | data.mouseYNow += 1; 141 | } 142 | } 143 | } 144 | 145 | if (data.mouseXPressed < 0) { 146 | data.mouseXPressed = 0; 147 | } 148 | if (data.mouseXNow < 0) { 149 | data.mouseXNow = 0; 150 | } 151 | if (data.mouseXPressed > CaptureInfo.ScreenWidth) { 152 | data.mouseXPressed = CaptureInfo.ScreenWidth; 153 | } 154 | if (data.mouseXNow > CaptureInfo.ScreenWidth) { 155 | data.mouseXNow = CaptureInfo.ScreenWidth; 156 | } 157 | repaintCanvas(); 158 | } 159 | } 160 | }; 161 | 162 | /** 163 | * Constructor. 164 | */ 165 | public ScreenCapture(Stage mainStage) { 166 | data = new CaptureInfo(); 167 | stage = mainStage; 168 | rootPane = new BorderPane(); 169 | mainCanvas = new Canvas(); 170 | mainCanvas.setCursor(Cursor.CROSSHAIR); 171 | mainCanvas.setStyle(CommUtils.STYLE_TRANSPARENT); 172 | rootPane.getChildren().add(mainCanvas); 173 | 174 | // Scene 175 | scene = new Scene(rootPane, CaptureInfo.ScreenWidth, CaptureInfo.ScreenHeight, Color.TRANSPARENT); 176 | scene.setCursor(Cursor.NONE); 177 | 178 | addKeyHandlers(); 179 | 180 | // Canvas 181 | mainCanvas.setWidth(CaptureInfo.ScreenWidth); 182 | mainCanvas.setHeight(CaptureInfo.ScreenHeight); 183 | mainCanvas.setOnMousePressed(m -> { 184 | if (m.getButton() == MouseButton.PRIMARY) { 185 | data.mouseXPressed = (int) m.getX(); 186 | data.mouseYPressed = (int) m.getY(); 187 | } 188 | }); 189 | 190 | mainCanvas.setOnMouseDragged(m -> { 191 | if (m.getButton() == MouseButton.PRIMARY) { 192 | if (m.getScreenX() >= CaptureInfo.ScreenMinX && m.getScreenX() <= CaptureInfo.ScreenMaxX) { 193 | data.mouseXNow = (int) m.getX(); 194 | } else if (m.getScreenX() > CaptureInfo.ScreenMaxX) { 195 | data.mouseXNow = CaptureInfo.ScreenWidth; 196 | } 197 | 198 | if (m.getScreenY() <= CaptureInfo.ScreenHeight) { 199 | data.mouseYNow = (int) m.getY(); 200 | } else { 201 | data.mouseYNow = CaptureInfo.ScreenHeight; 202 | } 203 | repaintCanvas(); 204 | } 205 | }); 206 | 207 | // graphics context 2D 208 | initGraphContent(); 209 | // HideFeaturesPressed 210 | data.hideExtraFeatures.addListener((observable, oldValue, newValue) -> repaintCanvas()); 211 | } 212 | 213 | private void initGraphContent() { 214 | gc = mainCanvas.getGraphicsContext2D(); 215 | gc.setLineDashes(6); 216 | gc.setFont(Font.font("null", FontWeight.BOLD, 14)); 217 | } 218 | 219 | /** 220 | * Adds the KeyHandlers to the Scene. 221 | */ 222 | private void addKeyHandlers() { 223 | 224 | // -------------Read the below to understand the Code------------------- 225 | 226 | // the default prototype of the below code is 227 | // 1->when the user is pressing RIGHT ARROW -> The rectangle is 228 | // increasing from the RIGHT side 229 | // 2->when the user is pressing LEFT ARROW -> The rectangle is 230 | // decreasing from the RIGHT side 231 | // 3->when the user is pressing UP ARROW -> The rectangle is increasing 232 | // from the UP side 233 | // 4->when the user is pressing DOWN ARROW -> The rectangle is 234 | // decreasing from the UP side 235 | 236 | // when ->LEFT KEY <- is pressed 237 | // 1->when the user is pressing RIGHT ARROW -> The rectangle is 238 | // increasing from the LEFT side 239 | // 2->when the user is pressing LEFT ARROW -> The rectangle is 240 | // decreasing from the LEFT side 241 | // 3->when the user is pressing UP ARROW -> The rectangle is increasing 242 | // from the DOWN side 243 | // 4->when the user is pressing DOWN ARROW -> The rectangle is 244 | // decreasing from the DOWN side 245 | 246 | scene.setOnKeyPressed(key -> { 247 | if (key.isShiftDown()) 248 | data.shiftPressed.set(true); 249 | 250 | if (key.getCode() == KeyCode.LEFT) 251 | data.leftPressed.set(true); 252 | 253 | if (key.getCode() == KeyCode.RIGHT) 254 | data.rightPressed.set(true); 255 | 256 | if (key.getCode() == KeyCode.UP) 257 | data.upPressed.set(true); 258 | 259 | if (key.getCode() == KeyCode.DOWN) 260 | data.downPressed.set(true); 261 | 262 | if (key.getCode() == KeyCode.H) 263 | data.hideExtraFeatures.set(true); 264 | }); 265 | 266 | // keyReleased 267 | scene.setOnKeyReleased(key -> { 268 | if (key.getCode() == KeyCode.SHIFT) { 269 | data.shiftPressed.set(false); 270 | } 271 | 272 | if (key.getCode() == KeyCode.RIGHT) { 273 | if (key.isControlDown()) { 274 | data.mouseXNow = (int) stage.getWidth(); 275 | repaintCanvas(); 276 | } 277 | data.rightPressed.set(false); 278 | } 279 | 280 | if (key.getCode() == KeyCode.LEFT) { 281 | if (key.isControlDown()) { 282 | data.mouseXPressed = 0; 283 | repaintCanvas(); 284 | } 285 | data.leftPressed.set(false); 286 | } 287 | 288 | if (key.getCode() == KeyCode.UP) { 289 | if (key.isControlDown()) { 290 | data.mouseYPressed = 0; 291 | repaintCanvas(); 292 | } 293 | data.upPressed.set(false); 294 | } 295 | 296 | if (key.getCode() == KeyCode.DOWN) { 297 | if (key.isControlDown()) { 298 | data.mouseYNow = (int) stage.getHeight(); 299 | repaintCanvas(); 300 | } 301 | data.downPressed.set(false); 302 | } 303 | 304 | if (key.getCode() == KeyCode.A && key.isControlDown()) { 305 | selectWholeScreen(); 306 | } 307 | 308 | if (key.getCode() == KeyCode.H) { 309 | data.hideExtraFeatures.set(false); 310 | } 311 | 312 | if (key.getCode() == KeyCode.ESCAPE || key.getCode() == KeyCode.BACK_SPACE) { 313 | cancelSnap(); 314 | isSnapping = false; 315 | } else if (key.getCode() == KeyCode.ENTER || key.getCode() == KeyCode.SPACE) { 316 | deActivateAllKeys(); 317 | isSnapping = false; 318 | prepareImage(); 319 | } 320 | }); 321 | 322 | data.anyPressed.addListener((obs, wasPressed, isNowPressed) -> { 323 | if (isNowPressed) { 324 | yPressedAnimation.start(); 325 | } else { 326 | yPressedAnimation.stop(); 327 | } 328 | }); 329 | 330 | rootPane.setOnMouseClicked(event -> { 331 | if (event.getClickCount() > 1) { 332 | if (data.rectWidth * data.rectHeight > 0) { 333 | rootPane.fireEvent(new KeyEvent(KeyEvent.KEY_RELEASED, "", "", KeyCode.ENTER, false, false, false, false)); 334 | } 335 | } 336 | }); 337 | } 338 | 339 | /** 340 | * Deactivates the keys contained into this method. 341 | */ 342 | private void deActivateAllKeys() { 343 | data.shiftPressed.set(false); 344 | data.upPressed.set(false); 345 | data.rightPressed.set(false); 346 | data.downPressed.set(false); 347 | data.leftPressed.set(false); 348 | data.hideExtraFeatures.set(false); 349 | } 350 | 351 | /** 352 | * Repaint the canvas of the capture window. 353 | */ 354 | private void repaintCanvas() { 355 | gc.clearRect(0, 0, CaptureInfo.ScreenWidth, CaptureInfo.ScreenHeight); 356 | gc.setFill(CommUtils.MASK_COLOR); 357 | gc.fillRect(0, 0, CaptureInfo.ScreenWidth, CaptureInfo.ScreenHeight); 358 | 359 | gc.setFont(data.font); 360 | gc.setStroke(Color.RED); 361 | gc.setLineWidth(1); 362 | 363 | // smart calculation of where the mouse has been dragged 364 | data.rectWidth = (data.mouseXNow > data.mouseXPressed) ? data.mouseXNow - data.mouseXPressed // RIGHT 365 | : data.mouseXPressed - data.mouseXNow // LEFT 366 | ; 367 | data.rectHeight = (data.mouseYNow > data.mouseYPressed) ? data.mouseYNow - data.mouseYPressed // DOWN 368 | : data.mouseYPressed - data.mouseYNow // UP 369 | ; 370 | 371 | data.rectUpperLeftX = // -------->UPPER_LEFT_X 372 | (data.mouseXNow > data.mouseXPressed) ? data.mouseXPressed // RIGHT 373 | : data.mouseXNow// LEFT 374 | ; 375 | data.rectUpperLeftY = // -------->UPPER_LEFT_Y 376 | (data.mouseYNow > data.mouseYPressed) ? data.mouseYPressed // DOWN 377 | : data.mouseYNow // UP 378 | ; 379 | 380 | gc.strokeRect(data.rectUpperLeftX - 1.00, data.rectUpperLeftY - 1.00, data.rectWidth + 2.00, 381 | data.rectHeight + 2.00); 382 | gc.clearRect(data.rectUpperLeftX, data.rectUpperLeftY, data.rectWidth, data.rectHeight); 383 | 384 | // draw the text 385 | if (!data.hideExtraFeatures.getValue() && (data.rectWidth > 0 || data.rectHeight > 0)) { 386 | double middle = data.rectUpperLeftX + data.rectWidth / 2.00; 387 | gc.setLineWidth(1); 388 | gc.setFill(Color.FIREBRICK); 389 | gc.fillRect(middle - 77, data.rectUpperLeftY < 50 ? data.rectUpperLeftY + 2 : data.rectUpperLeftY - 18.00, 100, 390 | 18); 391 | gc.setFill(Color.WHITE); 392 | gc.fillText(data.rectWidth + " * " + data.rectHeight, middle - 77 + 9, 393 | data.rectUpperLeftY < 50 ? data.rectUpperLeftY + 17.00 : data.rectUpperLeftY - 4.00); 394 | } 395 | } 396 | 397 | /** 398 | * Selects whole Screen. 399 | */ 400 | private void selectWholeScreen() { 401 | data.mouseXPressed = 0; 402 | data.mouseYPressed = 0; 403 | data.mouseXNow = (int) stage.getWidth(); 404 | data.mouseYNow = (int) stage.getHeight(); 405 | repaintCanvas(); 406 | } 407 | 408 | public void prepareForCapture() { 409 | isSnapping = true; 410 | MainForm.stage.setOpacity(0.0f); 411 | Platform.runLater(() -> { 412 | Rectangle rectangle = CommUtils.getDisplayScreen(MainForm.stage); 413 | data.reset(); 414 | CaptureInfo.ScreenMinX = rectangle.x; 415 | CaptureInfo.ScreenMaxX = rectangle.x + rectangle.width; 416 | CaptureInfo.ScreenWidth = rectangle.width; 417 | CaptureInfo.ScreenHeight = rectangle.height; 418 | BufferedImage bufferedImage = ScreenUtil.captureScreen(rectangle); 419 | // bufferedImage = Scalr.resize(bufferedImage, Scalr.Method.QUALITY, Scalr.Mode.AUTOMATIC, CaptureInfo.ScreenWidth * 2, CaptureInfo.ScreenHeight * 2); 420 | WritableImage fxImage = SwingFXUtils.toFXImage(bufferedImage, null); 421 | deActivateAllKeys(); 422 | scene.setRoot(new Pane()); 423 | scene = new Scene(rootPane, CaptureInfo.ScreenWidth, CaptureInfo.ScreenHeight, Color.TRANSPARENT); 424 | addKeyHandlers(); 425 | mainCanvas.setWidth(CaptureInfo.ScreenWidth); 426 | mainCanvas.setHeight(CaptureInfo.ScreenHeight); 427 | mainCanvas.setCursor(Cursor.CROSSHAIR); 428 | initGraphContent(); 429 | rootPane.setBackground(new Background(new BackgroundImage(fxImage, BackgroundRepeat.NO_REPEAT, 430 | BackgroundRepeat.NO_REPEAT, BackgroundPosition.CENTER, 431 | new BackgroundSize(CaptureInfo.ScreenWidth, CaptureInfo.ScreenHeight, false, false, true, true)))); 432 | repaintCanvas(); 433 | stage.setScene(scene); 434 | stage.setFullScreenExitHint(""); 435 | if (stage.isIconified()) { 436 | stage.setIconified(false); 437 | } 438 | stage.setFullScreen(true); 439 | stage.setAlwaysOnTop(true); 440 | stage.setOpacity(1.0f); 441 | stage.requestFocus(); 442 | }); 443 | } 444 | 445 | private void prepareImage() { 446 | gc.clearRect(0, 0, stage.getWidth(), stage.getHeight()); 447 | BufferedImage image; 448 | try { 449 | mainCanvas.setDisable(true); 450 | image = new Robot().createScreenCapture(new Rectangle(data.rectUpperLeftX + CaptureInfo.ScreenMinX, 451 | data.rectUpperLeftY + (int) CommUtils.getCrtScreen(stage).getVisualBounds().getMinY(), data.rectWidth, 452 | data.rectHeight)); 453 | } catch (AWTException ex) { 454 | StaticLog.error(ex); 455 | return; 456 | } finally { 457 | mainCanvas.setDisable(false); 458 | MainForm.restore(false); 459 | } 460 | MainForm.doOcr(image); 461 | } 462 | 463 | public void cancelSnap() { 464 | deActivateAllKeys(); 465 | MainForm.restore(true); 466 | } 467 | } 468 | -------------------------------------------------------------------------------- /src/main/java/com/luooqi/ocr/utils/BufferedImageUtils.java: -------------------------------------------------------------------------------- 1 | package com.luooqi.ocr.utils; 2 | 3 | import javax.imageio.ImageIO; 4 | import java.awt.image.BufferedImage; 5 | import java.io.ByteArrayInputStream; 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | 10 | /** 11 | * Created by litonglinux@qq.com on 12/9/2023_6:28 PM 12 | */ 13 | public class BufferedImageUtils { 14 | public static InputStream toInputStream(BufferedImage bufferedImage) { 15 | // 将BufferedImage写入到一个ByteArrayOutputStream 16 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 17 | try { 18 | ImageIO.write(bufferedImage, "png", baos); // 选择合适的格式,如 "png" 或 "jpg" 19 | } catch (IOException e) { 20 | e.printStackTrace(); 21 | } 22 | 23 | // 使用输出流的字节数组来创建一个InputStream 24 | byte[] imageBytes = baos.toByteArray(); 25 | InputStream inputStream = new ByteArrayInputStream(imageBytes); 26 | return inputStream; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/luooqi/ocr/utils/CommUtils.java: -------------------------------------------------------------------------------- 1 | package com.luooqi.ocr.utils; 2 | 3 | import java.awt.Point; 4 | import java.awt.Rectangle; 5 | import java.awt.image.BufferedImage; 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.IOException; 8 | import java.lang.reflect.Method; 9 | import java.net.URL; 10 | import java.util.ArrayList; 11 | import java.util.Comparator; 12 | import java.util.Iterator; 13 | import java.util.List; 14 | import java.util.regex.Matcher; 15 | import java.util.regex.Pattern; 16 | 17 | import javax.imageio.IIOImage; 18 | import javax.imageio.ImageIO; 19 | import javax.imageio.ImageWriteParam; 20 | import javax.imageio.ImageWriter; 21 | import javax.imageio.stream.MemoryCacheImageOutputStream; 22 | import javax.swing.ImageIcon; 23 | 24 | import cn.hutool.core.util.ClassUtil; 25 | import com.luooqi.ocr.OcrApp; 26 | import com.luooqi.ocr.constants.ImagesConstants; 27 | import com.luooqi.ocr.model.TextBlock; 28 | 29 | import cn.hutool.core.util.CharUtil; 30 | import cn.hutool.core.util.StrUtil; 31 | import cn.hutool.http.HttpRequest; 32 | import cn.hutool.http.HttpResponse; 33 | import cn.hutool.http.HttpUtil; 34 | import cn.hutool.log.StaticLog; 35 | import javafx.geometry.Insets; 36 | import javafx.geometry.Orientation; 37 | import javafx.geometry.Rectangle2D; 38 | import javafx.scene.control.Button; 39 | import javafx.scene.control.ButtonBase; 40 | import javafx.scene.control.Separator; 41 | import javafx.scene.control.ToggleButton; 42 | import javafx.scene.control.ToggleGroup; 43 | import javafx.scene.control.Tooltip; 44 | import javafx.scene.layout.Background; 45 | import javafx.scene.layout.BackgroundFill; 46 | import javafx.scene.layout.CornerRadii; 47 | import javafx.scene.paint.Color; 48 | import javafx.scene.paint.Paint; 49 | import javafx.stage.Screen; 50 | import javafx.stage.Stage; 51 | 52 | public class CommUtils { 53 | 54 | public static final Paint MASK_COLOR = Color.rgb(0, 0, 0, 0.4); 55 | public static final int BUTTON_SIZE = 28; 56 | public static Background BG_TRANSPARENT = new Background( 57 | new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)); 58 | private static Pattern NORMAL_CHAR = Pattern.compile("[\\u4e00-\\u9fa5\\w、-,/|_]"); 59 | public static Separator SEPARATOR = new Separator(Orientation.VERTICAL); 60 | private static final float IMAGE_QUALITY = 0.5f; 61 | private static final int SAME_LINE_LIMIT = 8; 62 | private static final int CHAR_WIDTH = 12; 63 | public static final String STYLE_TRANSPARENT = "-fx-background-color: transparent;"; 64 | public static final String SPECIAL_CHARS = "[\\s`~!@#$%^&*()_\\-+=|{}':;,\\[\\].<>/?!¥…()【】‘;:”“’。,、?]+"; 65 | public static boolean IS_MAC_OS = false; 66 | 67 | static { 68 | String osName = System.getProperty("os.name", "generic").toLowerCase(); 69 | if ((osName.contains("mac")) || (osName.contains("darwin"))) { 70 | IS_MAC_OS = true; 71 | } 72 | } 73 | 74 | public static byte[] imageToBytes(BufferedImage img) { 75 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); 76 | MemoryCacheImageOutputStream outputStream = new MemoryCacheImageOutputStream(byteArrayOutputStream); 77 | try { 78 | Iterator iter = ImageIO.getImageWritersByFormatName("jpeg"); 79 | ImageWriter writer = (ImageWriter) iter.next(); 80 | ImageWriteParam iwp = writer.getDefaultWriteParam(); 81 | iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); 82 | iwp.setCompressionQuality(IMAGE_QUALITY); 83 | writer.setOutput(outputStream); 84 | IIOImage image = new IIOImage(img, null, null); 85 | writer.write(null, image, iwp); 86 | writer.dispose(); 87 | byte[] result = byteArrayOutputStream.toByteArray(); 88 | byteArrayOutputStream.close(); 89 | outputStream.close(); 90 | return result; 91 | } catch (IOException e) { 92 | StaticLog.error(e); 93 | return new byte[0]; 94 | } 95 | } 96 | 97 | static String combineTextBlocks(List textBlocks, boolean isEng) { 98 | textBlocks.sort(Comparator.comparingInt(o -> o.getTopLeft().y)); 99 | List> lineBlocks = new ArrayList<>(); 100 | int lastY = -1; 101 | List lineBlock = new ArrayList<>(); 102 | boolean sameLine = true; 103 | int minX = Integer.MAX_VALUE; 104 | TextBlock minBlock = null; 105 | TextBlock maxBlock = null; 106 | int maxX = -1; 107 | double maxAngle = -100; 108 | for (TextBlock textBlock : textBlocks) { 109 | // System.out.println(textBlock.getAngle()+ "\t" + textBlock.getFontSize()); 110 | if (textBlock.getTopLeft().x < minX) { 111 | minX = textBlock.getTopLeft().x; 112 | minBlock = textBlock; 113 | } 114 | if (textBlock.getTopRight().x > maxX) { 115 | maxX = textBlock.getTopRight().x; 116 | maxBlock = textBlock; 117 | } 118 | if (Math.abs(textBlock.getAngle()) > maxAngle) { 119 | maxAngle = Math.abs(textBlock.getAngle()); 120 | } 121 | if (lastY == -1) { 122 | lastY = textBlock.getTopLeft().y; 123 | } else { 124 | sameLine = textBlock.getTopLeft().y - lastY <= SAME_LINE_LIMIT; 125 | } 126 | if (!sameLine) { 127 | lineBlock.sort(Comparator.comparingInt(o -> o.getTopLeft().x)); 128 | lineBlocks.add(lineBlock); 129 | lineBlock = new ArrayList<>(); 130 | sameLine = true; 131 | lastY = textBlock.getTopLeft().y; 132 | } 133 | lineBlock.add(textBlock); 134 | } 135 | 136 | if (lineBlock.size() > 0) { 137 | lineBlock.sort(Comparator.comparingInt(o -> o.getTopLeft().x)); 138 | lineBlocks.add(lineBlock); 139 | } 140 | StringBuilder sb = new StringBuilder(); 141 | TextBlock lastBlock = null; 142 | for (List line : lineBlocks) { 143 | TextBlock firstBlock = line.get(0); 144 | if (lastBlock != null) { 145 | String blockTxt = lastBlock.getText().trim(); 146 | if (StrUtil.isBlank(blockTxt)) { 147 | continue; 148 | } 149 | String endTxt = blockTxt.substring(blockTxt.length() - 1); 150 | if (maxX - lastBlock.getTopRight().x >= CHAR_WIDTH * 2 || !NORMAL_CHAR.matcher(endTxt).find() 151 | || (NORMAL_CHAR.matcher(endTxt).find() && (firstBlock.getTopLeft().x - minX) > CHAR_WIDTH * 2)) { 152 | sb.append("\n"); 153 | for (int i = 0, ln = (firstBlock.getTopLeft().x - minX) / CHAR_WIDTH; i < ln; i++) { 154 | if (i % 2 == 0) { 155 | sb.append(" "); 156 | } 157 | } 158 | } else { 159 | if (CharUtil.isLetterOrNumber(endTxt.charAt(0)) 160 | && CharUtil.isLetterOrNumber(firstBlock.getText().charAt(0))) { 161 | sb.append(" "); 162 | } 163 | } 164 | } else { 165 | for (int i = 0, ln = (firstBlock.getTopLeft().x - minX) / CHAR_WIDTH; i < ln; i++) { 166 | if (i % 2 == 0) { 167 | sb.append(" "); 168 | } 169 | } 170 | } 171 | 172 | for (int i = 0; i < line.size(); i++) { 173 | TextBlock text = line.get(i); 174 | String ocrText = text.getText(); 175 | if (i > 0) { 176 | for (int a = 0, 177 | ln = (text.getTopLeft().x - line.get(i - 1).getTopRight().x) / (CHAR_WIDTH * 2); a < ln; a++) { 178 | sb.append(" "); 179 | } 180 | } 181 | sb.append(ocrText); 182 | } 183 | lastBlock = line.get(line.size() - 1); 184 | } 185 | return sb.toString(); 186 | } 187 | 188 | static Point frameToPoint(String text) { 189 | String[] arr = text.split(","); 190 | return new Point(Integer.valueOf(arr[0].trim()), Integer.valueOf(arr[1].trim())); 191 | } 192 | 193 | static String postMultiData(String url, byte[] data, String boundary) { 194 | return postMultiData(url, data, boundary, "", ""); 195 | } 196 | 197 | private static String postMultiData(String url, byte[] data, String boundary, String cookie, String referer) { 198 | try { 199 | HttpRequest request = HttpUtil.createPost(url).timeout(15000); 200 | request.contentType("multipart/form-data; boundary=" + boundary); 201 | request.body(data); 202 | if (StrUtil.isNotBlank(referer)) { 203 | request.header("Referer", referer); 204 | } 205 | if (StrUtil.isNotBlank(cookie)) { 206 | request.cookie(cookie); 207 | } 208 | HttpResponse response = request.execute(); 209 | return WebUtils.getSafeHtml(response); 210 | } catch (Exception ex) { 211 | StaticLog.error(ex); 212 | return null; 213 | } 214 | } 215 | 216 | static byte[] mergeByte(byte[]... bytes) { 217 | int length = 0; 218 | for (byte[] b : bytes) { 219 | length += b.length; 220 | } 221 | byte[] resultBytes = new byte[length]; 222 | int offset = 0; 223 | for (byte[] arr : bytes) { 224 | System.arraycopy(arr, 0, resultBytes, offset, arr.length); 225 | offset += arr.length; 226 | } 227 | return resultBytes; 228 | } 229 | 230 | public static Button createButton(String id, Runnable action, String toolTip) { 231 | return createButton(id, BUTTON_SIZE, action, toolTip); 232 | } 233 | 234 | public static Button createButton(String id, int size, Runnable action, String toolTip) { 235 | javafx.scene.control.Button button = new Button(); 236 | initButton(button, id, size, action, toolTip); 237 | return button; 238 | } 239 | 240 | public static ToggleButton createToggleButton(ToggleGroup grp, String id, Runnable action, String toolTip) { 241 | return createToggleButton(grp, id, BUTTON_SIZE, action, toolTip); 242 | } 243 | 244 | public static ToggleButton createToggleButton(ToggleGroup grp, String id, int size, Runnable action, String toolTip) { 245 | ToggleButton button = new ToggleButton(); 246 | button.setToggleGroup(grp); 247 | initButton(button, id, size, action, toolTip); 248 | return button; 249 | } 250 | 251 | private static void initButton(ButtonBase button, String id, int size, Runnable action, String toolTip) { 252 | button.setId(id); 253 | button.setOnAction(evt -> action.run()); 254 | button.setMinSize(size, size); 255 | if (toolTip != null) { 256 | button.setTooltip(new Tooltip(toolTip)); 257 | } 258 | } 259 | 260 | public static void initStage(Stage stage) { 261 | 262 | try { 263 | if (CommUtils.IS_MAC_OS) { 264 | URL iconURL = ClassUtil.getClassLoader().getResource(ImagesConstants.LOGO); 265 | java.awt.Image image = new ImageIcon(iconURL).getImage(); 266 | Class appleApp = Class.forName("com.apple.eawt.Application"); 267 | // noinspection unchecked 268 | Method getApplication = appleApp.getMethod("getApplication"); 269 | Object application = getApplication.invoke(appleApp); 270 | Class[] params = new Class[1]; 271 | params[0] = java.awt.Image.class; 272 | // noinspection unchecked 273 | Method setDockIconImage = appleApp.getMethod("setDockIconImage", params); 274 | setDockIconImage.invoke(application, image); 275 | } 276 | } catch (Exception e) { 277 | StaticLog.error(e); 278 | } 279 | stage.setTitle("树洞OCR文字识别"); 280 | URL iconURL = ClassUtil.getClassLoader().getResource(ImagesConstants.LOGO); 281 | stage.getIcons().add(new javafx.scene.image.Image(iconURL.toExternalForm())); 282 | } 283 | 284 | private static final Pattern SCALE_PATTERN = Pattern.compile("renderScale:([\\d.]+)"); 285 | 286 | public static Rectangle getDisplayScreen(Stage stage) { 287 | Screen crtScreen = getCrtScreen(stage); 288 | Rectangle2D rectangle2D = crtScreen.getBounds(); 289 | return new Rectangle((int) rectangle2D.getMinX(), (int) rectangle2D.getMinY(), (int) rectangle2D.getWidth(), 290 | (int) rectangle2D.getHeight()); 291 | } 292 | 293 | public static float getScale(Stage stage) { 294 | Screen crtScreen = getCrtScreen(stage); 295 | float scale = 1.0f; 296 | assert crtScreen != null; 297 | String str = crtScreen.toString(); 298 | Matcher matcher = SCALE_PATTERN.matcher(str); 299 | if (matcher.find()) { 300 | scale = Float.parseFloat(matcher.group(1)); 301 | } 302 | return scale; 303 | } 304 | 305 | public static Screen getCrtScreen(Stage stage) { 306 | double x = stage.getX(); 307 | Screen crtScreen = null; 308 | for (Screen screen : Screen.getScreens()) { 309 | crtScreen = screen; 310 | Rectangle2D bounds = screen.getBounds(); 311 | if (bounds.getMaxX() > x) { 312 | break; 313 | } 314 | } 315 | return crtScreen; 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /src/main/java/com/luooqi/ocr/utils/GlobalKeyListener.java: -------------------------------------------------------------------------------- 1 | package com.luooqi.ocr.utils; 2 | 3 | import cn.hutool.log.StaticLog; 4 | import com.luooqi.ocr.OcrApp; 5 | import com.luooqi.ocr.snap.ScreenCapture; 6 | import com.luooqi.ocr.windows.MainForm; 7 | import org.jnativehook.NativeInputEvent; 8 | import org.jnativehook.keyboard.NativeKeyEvent; 9 | import org.jnativehook.keyboard.NativeKeyListener; 10 | 11 | import java.lang.reflect.Field; 12 | 13 | public class GlobalKeyListener implements NativeKeyListener { 14 | @Override 15 | public void nativeKeyTyped(NativeKeyEvent nativeKeyEvent) { 16 | 17 | } 18 | 19 | @Override 20 | public void nativeKeyPressed(NativeKeyEvent e) { 21 | if (e.getKeyCode() == NativeKeyEvent.VC_F4) { 22 | preventEvent(e); 23 | MainForm.screenShotOcr(); 24 | } else if (e.getKeyCode() == NativeKeyEvent.VC_ESCAPE) { 25 | if (ScreenCapture.isSnapping) { 26 | preventEvent(e); 27 | MainForm.cancelSnap(); 28 | } 29 | } 30 | } 31 | 32 | @Override 33 | public void nativeKeyReleased(NativeKeyEvent e) { 34 | // if (e.getKeyCode() == NativeKeyEvent.VC_F4){ 35 | // preventEvent(e); 36 | // } 37 | // GlobalScreen.addNativeKeyListener(new GlobalKeyListener()); 38 | } 39 | 40 | private void preventEvent(NativeKeyEvent e) { 41 | try { 42 | Field f = NativeInputEvent.class.getDeclaredField("reserved"); 43 | f.setAccessible(true); 44 | f.setShort(e, (short) 0x01); 45 | } catch (Exception ex) { 46 | StaticLog.error(ex); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/luooqi/ocr/utils/LibraryUtils.java: -------------------------------------------------------------------------------- 1 | package com.luooqi.ocr.utils; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.lang.reflect.Field; 6 | 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | @Slf4j 10 | public class LibraryUtils { 11 | 12 | public static void addLibary(String path) { 13 | File file = new File(path); 14 | String absolutePath = file.getAbsolutePath(); 15 | log.info("add lib:{}",absolutePath); 16 | if (!file.exists()) { 17 | file.mkdirs(); 18 | } 19 | try { 20 | addLibDir(absolutePath); 21 | } catch (IOException e1) { 22 | e1.printStackTrace(); 23 | } 24 | } 25 | 26 | public static void addLibDir(String s) throws IOException { 27 | try { 28 | Field field = ClassLoader.class.getDeclaredField("usr_paths"); 29 | field.setAccessible(true); 30 | String[] paths = (String[]) field.get(null); 31 | for (int i = 0; i < paths.length; i++) { 32 | if (s.equals(paths[i])) { 33 | return; 34 | } 35 | } 36 | String[] tmp = new String[paths.length + 1]; 37 | System.arraycopy(paths, 0, tmp, 0, paths.length); 38 | tmp[paths.length] = s; 39 | field.set(null, tmp); 40 | } catch (IllegalAccessException e) { 41 | throw new IOException("Failed to get permissions to set library path"); 42 | } catch (NoSuchFieldException e) { 43 | throw new IOException("Failed to get field handle to set library path"); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/java/com/luooqi/ocr/utils/OcrUtils.java: -------------------------------------------------------------------------------- 1 | package com.luooqi.ocr.utils; 2 | 3 | import java.awt.Point; 4 | import java.awt.image.BufferedImage; 5 | import java.io.File; 6 | import java.io.FileOutputStream; 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | import javax.imageio.ImageIO; 14 | 15 | import org.apache.pdfbox.pdmodel.PDDocument; 16 | import org.apache.pdfbox.rendering.PDFRenderer; 17 | 18 | import com.benjaminwan.ocrlibrary.OcrResult; 19 | import com.luooqi.ocr.local.PaddlePaddleOCRV4; 20 | import com.luooqi.ocr.model.TextBlock; 21 | 22 | import cn.hutool.core.codec.Base64; 23 | import cn.hutool.core.io.FileUtil; 24 | import cn.hutool.core.lang.UUID; 25 | import cn.hutool.core.util.CharsetUtil; 26 | import cn.hutool.core.util.HashUtil; 27 | import cn.hutool.core.util.StrUtil; 28 | import cn.hutool.core.util.URLUtil; 29 | import cn.hutool.crypto.SecureUtil; 30 | import cn.hutool.http.HttpRequest; 31 | import cn.hutool.http.HttpResponse; 32 | import cn.hutool.http.HttpUtil; 33 | import cn.hutool.json.JSONArray; 34 | import cn.hutool.json.JSONObject; 35 | import cn.hutool.json.JSONUtil; 36 | import cn.hutool.log.StaticLog; 37 | 38 | /** 39 | * tools-ocr 40 | * Created by 何志龙 on 2019-03-22. 41 | */ 42 | public class OcrUtils { 43 | 44 | public static String recImgLocal(byte[] imgData) { 45 | String path = "tmp_" + Math.abs(Arrays.hashCode(imgData)) + ".png"; 46 | File file = FileUtil.writeBytes(imgData, path); 47 | return recImgLocal(file); 48 | } 49 | 50 | public static String recImgLocal(BufferedImage image) { 51 | byte[] bytes = CommUtils.imageToBytes(image); 52 | return recImgLocal(bytes); 53 | } 54 | 55 | public static String recImgLocal(File file) { 56 | if (file.exists()) { 57 | try { 58 | return extractLocalResult(PaddlePaddleOCRV4.INSTANCE.ocr(file)); 59 | } catch (Exception e) { 60 | e.printStackTrace(); 61 | return e.getMessage(); 62 | } 63 | } 64 | return "文件不存在"; 65 | } 66 | 67 | public static String recPdfLocal(File pdfFile) { 68 | if (pdfFile.exists()) { 69 | try (PDDocument document = PDDocument.load(pdfFile)) { 70 | PDFRenderer renderer = new PDFRenderer(document); 71 | List ocrResults = new ArrayList<>(); 72 | 73 | for (int i = 0; i < document.getNumberOfPages(); ++i) { 74 | BufferedImage bufferedImage = renderer.renderImageWithDPI(i, 300); 75 | long hashCode = HashUtil.hfHash(pdfFile.getName()); 76 | String filename = "temp_" + hashCode + "_" + i + ".png"; 77 | FileOutputStream fileOutputStream = new FileOutputStream(filename); 78 | ImageIO.write(bufferedImage, "png", fileOutputStream); // 选择合适的格式,如 "png" 或 "jpg" 79 | String text = recImgLocal(new File(filename)); 80 | ocrResults.add(text); 81 | } 82 | // 将所有页面的OCR结果合并为一个字符串 83 | return String.join("\n", ocrResults); 84 | 85 | } catch (Exception e) { 86 | e.printStackTrace(); 87 | } 88 | return "文件不存在"; 89 | } 90 | return null; 91 | } 92 | 93 | public static String ocrImg(byte[] imgData) { 94 | int i = Math.abs(UUID.randomUUID().hashCode()) % 4; 95 | StaticLog.info("OCR Engine: " + i); 96 | switch (i) { 97 | case 0: 98 | return bdGeneralOcr(imgData); 99 | case 1: 100 | return bdAccurateOcr(imgData); 101 | case 2: 102 | return sogouMobileOcr(imgData); 103 | default: 104 | return sogouWebOcr(imgData); 105 | } 106 | } 107 | 108 | private static String bdGeneralOcr(byte[] imgData) { 109 | return bdBaseOcr(imgData, "general_location"); 110 | } 111 | 112 | private static String bdAccurateOcr(byte[] imgData) { 113 | return bdBaseOcr(imgData, "https://aip.baidubce.com/rest/2.0/ocr/v1/accurate"); 114 | } 115 | 116 | private static String bdBaseOcr(byte[] imgData, String type) { 117 | String[] urlArr = new String[] { "http://ai.baidu.com/tech/ocr/general", 118 | "http://ai.baidu.com/index/seccode?action=show" }; 119 | StringBuilder cookie = new StringBuilder(); 120 | for (String url : urlArr) { 121 | HttpResponse cookieResp = WebUtils.get(url); 122 | List ckList = cookieResp.headerList("Set-Cookie"); 123 | if (ckList != null) { 124 | for (String s : ckList) { 125 | cookie.append(s.replaceAll("expires[\\S\\s]+", "")); 126 | } 127 | } 128 | } 129 | HashMap header = new HashMap<>(); 130 | header.put("Referer", "http://ai.baidu.com/tech/ocr/general"); 131 | header.put("Cookie", cookie.toString()); 132 | String data = "type=" + URLUtil.encodeQuery(type) + "&detect_direction=false&image_url&image=" 133 | + URLUtil.encodeQuery("data:image/jpeg;base64," + Base64.encode(imgData)) + "&language_type=CHN_ENG"; 134 | HttpResponse response = WebUtils.postRaw("http://ai.baidu.com/aidemo", data, 0, header); 135 | return extractBdResult(WebUtils.getSafeHtml(response)); 136 | } 137 | 138 | public static String sogouMobileOcr(byte[] imgData) { 139 | String boundary = "------WebKitFormBoundary8orYTmcj8BHvQpVU"; 140 | String url = "http://ocr.shouji.sogou.com/v2/ocr/json"; 141 | String header = boundary 142 | + "\r\nContent-Disposition: form-data; name=\"pic\"; filename=\"pic.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n"; 143 | String footer = "\r\n" + boundary + "--\r\n"; 144 | byte[] postData = CommUtils.mergeByte(header.getBytes(CharsetUtil.CHARSET_ISO_8859_1), imgData, 145 | footer.getBytes(CharsetUtil.CHARSET_ISO_8859_1)); 146 | return extractSogouResult(CommUtils.postMultiData(url, postData, boundary.substring(2))); 147 | } 148 | 149 | public static String sogouWebOcr(byte[] imgData) { 150 | String url = "https://deepi.sogou.com/api/sogouService"; 151 | String referer = "https://deepi.sogou.com/?from=picsearch&tdsourcetag=s_pctim_aiomsg"; 152 | String imageData = Base64.encode(imgData); 153 | long t = System.currentTimeMillis(); 154 | String sign = SecureUtil.md5("sogou_ocr_just_for_deepibasicOpenOcr" + t 155 | + imageData.substring(0, Math.min(1024, imageData.length())) + "4b66a37108dab018ace616c4ae07e644"); 156 | Map data = new HashMap<>(); 157 | data.put("image", imageData); 158 | data.put("lang", "zh-Chs"); 159 | data.put("pid", "sogou_ocr_just_for_deepi"); 160 | data.put("salt", t); 161 | data.put("service", "basicOpenOcr"); 162 | data.put("sign", sign); 163 | HttpRequest request = HttpUtil.createPost(url).timeout(15000); 164 | request.form(data); 165 | request.header("Referer", referer); 166 | HttpResponse response = request.execute(); 167 | return extractSogouResult(WebUtils.getSafeHtml(response)); 168 | } 169 | 170 | private static String extractSogouResult(String html) { 171 | if (StrUtil.isBlank(html)) { 172 | return ""; 173 | } 174 | JSONObject jsonObject = JSONUtil.parseObj(html); 175 | if (jsonObject.getInt("success", 0) != 1) { 176 | return ""; 177 | } 178 | JSONArray jsonArray = jsonObject.getJSONArray("result"); 179 | List textBlocks = new ArrayList<>(); 180 | boolean isEng; 181 | for (int i = 0; i < jsonArray.size(); i++) { 182 | JSONObject jObj = jsonArray.getJSONObject(i); 183 | TextBlock textBlock = new TextBlock(); 184 | textBlock.setText(jObj.getStr("content").trim()); 185 | // noinspection SuspiciousToArrayCall 186 | String[] frames = jObj.getJSONArray("frame").toArray(new String[0]); 187 | textBlock.setTopLeft(CommUtils.frameToPoint(frames[0])); 188 | textBlock.setTopRight(CommUtils.frameToPoint(frames[1])); 189 | textBlock.setBottomRight(CommUtils.frameToPoint(frames[2])); 190 | textBlock.setBottomLeft(CommUtils.frameToPoint(frames[3])); 191 | textBlocks.add(textBlock); 192 | } 193 | isEng = jsonObject.getStr("lang", "zh-Chs").equals("zh-Chs"); 194 | return CommUtils.combineTextBlocks(textBlocks, isEng); 195 | } 196 | 197 | private static String extractBdResult(String html) { 198 | if (StrUtil.isBlank(html)) { 199 | return ""; 200 | } 201 | JSONObject jsonObject = JSONUtil.parseObj(html); 202 | if (jsonObject.getInt("errno", 0) != 0) { 203 | return ""; 204 | } 205 | JSONArray jsonArray = jsonObject.getJSONObject("data").getJSONArray("words_result"); 206 | List textBlocks = new ArrayList<>(); 207 | boolean isEng = false; 208 | for (int i = 0; i < jsonArray.size(); i++) { 209 | JSONObject jObj = jsonArray.getJSONObject(i); 210 | TextBlock textBlock = new TextBlock(); 211 | textBlock.setText(jObj.getStr("words").trim()); 212 | // noinspection SuspiciousToArrayCall 213 | JSONObject location = jObj.getJSONObject("location"); 214 | int top = location.getInt("top"); 215 | int left = location.getInt("left"); 216 | int width = location.getInt("width"); 217 | int height = location.getInt("height"); 218 | textBlock.setTopLeft(new Point(top, left)); 219 | textBlock.setTopRight(new Point(top, left + width)); 220 | textBlock.setBottomLeft(new Point(top + height, left)); 221 | textBlock.setBottomRight(new Point(top + height, left + width)); 222 | textBlocks.add(textBlock); 223 | } 224 | return CommUtils.combineTextBlocks(textBlocks, isEng); 225 | } 226 | 227 | private static String extractLocalResult(OcrResult ocrResult) { 228 | if (ocrResult == null) { 229 | return ""; 230 | } 231 | ArrayList blocks = ocrResult.getTextBlocks(); 232 | List textBlocks = new ArrayList<>(); 233 | boolean isEng = false; 234 | for (com.benjaminwan.ocrlibrary.TextBlock block : blocks) { 235 | TextBlock textBlock = new TextBlock(); 236 | textBlock.setText(block.getText()); 237 | textBlock.setTopLeft(new Point(block.getBoxPoint().get(0).getX(), block.getBoxPoint().get(0).getY())); 238 | textBlock.setTopRight(new Point(block.getBoxPoint().get(1).getX(), block.getBoxPoint().get(1).getY())); 239 | textBlock.setBottomLeft(new Point(block.getBoxPoint().get(2).getX(), block.getBoxPoint().get(2).getY())); 240 | textBlock.setBottomRight(new Point(block.getBoxPoint().get(3).getX(), block.getBoxPoint().get(3).getY())); 241 | textBlocks.add(textBlock); 242 | } 243 | return CommUtils.combineTextBlocks(textBlocks, isEng); 244 | } 245 | 246 | } 247 | -------------------------------------------------------------------------------- /src/main/java/com/luooqi/ocr/utils/VoidDispatchService.java: -------------------------------------------------------------------------------- 1 | package com.luooqi.ocr.utils; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.concurrent.AbstractExecutorService; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | public class VoidDispatchService extends AbstractExecutorService { 9 | private boolean running = false; 10 | 11 | public VoidDispatchService() { 12 | running = true; 13 | } 14 | 15 | public void shutdown() { 16 | running = false; 17 | } 18 | 19 | public List shutdownNow() { 20 | running = false; 21 | return new ArrayList(0); 22 | } 23 | 24 | public boolean isShutdown() { 25 | return !running; 26 | } 27 | 28 | public boolean isTerminated() { 29 | return !running; 30 | } 31 | 32 | public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { 33 | return true; 34 | } 35 | 36 | public void execute(Runnable r) { 37 | r.run(); 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/java/com/luooqi/ocr/utils/WebUtils.java: -------------------------------------------------------------------------------- 1 | package com.luooqi.ocr.utils; 2 | 3 | import cn.hutool.http.Header; 4 | import cn.hutool.http.HttpRequest; 5 | import cn.hutool.http.HttpResponse; 6 | import cn.hutool.http.HttpUtil; 7 | import cn.hutool.log.StaticLog; 8 | 9 | import java.util.Hashtable; 10 | import java.util.Map; 11 | 12 | /** 13 | * fish-web 14 | * Created by 何志龙 on 2018-03-25. 15 | */ 16 | @SuppressWarnings("SpellCheckingInspection") 17 | public class WebUtils { 18 | 19 | static { 20 | HttpRequest.closeCookie(); 21 | } 22 | 23 | public static String getSafeHtml(HttpResponse response) { 24 | if (response == null) { 25 | return ""; 26 | } 27 | return response.body(); 28 | } 29 | 30 | public static String getHtml(String url) { 31 | HttpResponse response = get(url); 32 | String html = getSafeHtml(response); 33 | if (response != null) { 34 | response.close(); 35 | } 36 | return html; 37 | } 38 | 39 | public static HttpResponse get(String url) { 40 | return get(url, 0, null, true); 41 | } 42 | 43 | public static String getLocation(String url, String cookie) { 44 | try { 45 | HttpResponse response = get(url, 0, new Hashtable() {{ 46 | put("Cookie", cookie); 47 | }}, false); 48 | if (response == null) { 49 | return url; 50 | } 51 | String location = response.header(Header.LOCATION); 52 | response.close(); 53 | return location; 54 | } catch (Exception ex) { 55 | return ""; 56 | } 57 | } 58 | 59 | public static HttpResponse get(String url, String cookie) { 60 | return get(url, 0, new Hashtable() {{ 61 | put("Cookie", cookie); 62 | }}, true); 63 | } 64 | 65 | public static HttpResponse get(String url, int userAgent, String cookie) { 66 | return get(url, userAgent, new Hashtable() {{ 67 | put("Cookie", cookie); 68 | }}, true); 69 | } 70 | 71 | public static HttpResponse get(String url, int userAgent, Map headers) { 72 | return get(url, userAgent, headers, true); 73 | } 74 | 75 | public static HttpResponse get(String url, int userAgent, Map headers, boolean allowRedirct) { 76 | try { 77 | HttpRequest request = HttpUtil.createGet(url).timeout(10000).setFollowRedirects(allowRedirct); 78 | if (headers == null) { 79 | headers = new Hashtable<>(); 80 | } 81 | switch (userAgent) { 82 | case 1: 83 | headers.put("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 9_3_2 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Mobile/13F69 MicroMessenger/6.3.16 NetType/WIFI Language/zh_CN"); 84 | break; 85 | case 2: 86 | headers.put("User-Agent", "Mozilla/5.0 (Linux; U; Android 2.2; en-gb; GT-P1000 Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1"); 87 | break; 88 | case 3: 89 | headers.put("User-Agent", "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; NOKIA; Lumia 930) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/13.10586"); 90 | break; 91 | case 4: 92 | headers.put("User-Agent", "NativeHost"); 93 | break; 94 | case 5: 95 | headers.put("User-Agent", "Dalvik/1.6.0 (Linux; U; Android 4.4.2; NoxW Build/KOT49H) ITV_5.7.1.46583"); 96 | break; 97 | case 6: 98 | headers.put("User-Agent", "qqlive"); 99 | break; 100 | case 7: 101 | headers.put("User-Agent", "Dalvik/1.6.0 (Linux; U; Android 4.2.2; 6S Build/JDQ39E)"); 102 | break; 103 | case 8: 104 | headers.put("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) XIAMI-MUSIC/3.0.9 Chrome/56.0.2924.87 Electron/1.6.11 Safari/537.36"); 105 | break; 106 | case 9: 107 | headers.put("User-Agent", "okhttp/2.7.5"); 108 | break; 109 | case 10: 110 | headers.put("User-Agent", "Mozilla/5.0 (Linux; Android 5.1.1; oppo r11 plus Build/LMY48Z) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/39.0.0.0 Mobile Safari/537.36 SogouSearch Android1.0 version3.0"); 111 | break; 112 | default: 113 | headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36"); 114 | break; 115 | } 116 | request.addHeaders(headers); 117 | return request.execute(); 118 | } catch (Exception ex) { 119 | StaticLog.error(ex); 120 | return null; 121 | } 122 | } 123 | 124 | public static HttpResponse postRaw(String url, String data) { 125 | return postRaw(url, data, 0, null); 126 | } 127 | 128 | public static HttpResponse postRaw(String url, String data, int userAgent, Map headers) { 129 | return postData(url, new Hashtable() {{ 130 | put("FORM", data); 131 | }}, 2, userAgent, headers); 132 | } 133 | 134 | public static HttpResponse postJson(String url, String data, int userAgent, Map headers) { 135 | return postData(url, new Hashtable() {{ 136 | put("JSON", data); 137 | }}, 1, userAgent, headers); 138 | } 139 | 140 | public static HttpResponse postForm(String url, Map data, int userAgent, Map headers) { 141 | return postData(url, data, 0, userAgent, headers); 142 | } 143 | 144 | private static HttpResponse postData(String url, Map data, int contentType, int userAgent, Map headers) { 145 | try { 146 | HttpRequest request = HttpUtil.createPost(url).timeout(10000); 147 | if (contentType == 0) { 148 | request.contentType("application/x-www-form-urlencoded"); 149 | request.form(data); 150 | } else if (contentType == 1) { 151 | request.body(data.values().iterator().next().toString(), "application/json;charset=UTF-8"); 152 | } else { 153 | request.contentType("application/x-www-form-urlencoded"); 154 | request.body(data.values().iterator().next().toString()); 155 | } 156 | if (headers == null) { 157 | headers = new Hashtable<>(); 158 | } 159 | switch (userAgent) { 160 | case 1: 161 | headers.put("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3"); 162 | break; 163 | case 2: 164 | headers.put("User-Agent", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); 165 | break; 166 | case 3: 167 | headers.put("User-Agent", "Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 920)"); 168 | break; 169 | case 4: 170 | headers.put("User-Agent", "NativeHost"); 171 | break; 172 | case 5: 173 | headers.put("User-Agent", "Apache-HttpClient/UNAVAILABLE (java 1.4)"); 174 | break; 175 | case 6: 176 | headers.put("User-Agent", "Mozilla/5.0 (iPad; CPU OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12B466 Safari/600.1.4"); 177 | break; 178 | case 7: 179 | headers.put("User-Agent", "okhttp/2.7.5"); 180 | break; 181 | case 10: 182 | headers.put("User-Agent", "Mozilla/5.0 (Linux; Android 5.1.1; oppo r11 plus Build/LMY48Z) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/39.0.0.0 Mobile Safari/537.36 SogouSearch Android1.0 version3.0"); 183 | break; 184 | default: 185 | headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36"); 186 | break; 187 | } 188 | request.addHeaders(headers); 189 | return request.execute(); 190 | } catch (Exception ex) { 191 | StaticLog.error(ex); 192 | return null; 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/main/java/com/luooqi/ocr/windows/MainForm.java: -------------------------------------------------------------------------------- 1 | package com.luooqi.ocr.windows; 2 | 3 | import java.awt.image.BufferedImage; 4 | import java.io.File; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | import com.luooqi.ocr.config.InitConfig; 9 | import com.luooqi.ocr.controller.ProcessController; 10 | import com.luooqi.ocr.local.PaddlePaddleOCRV4; 11 | import com.luooqi.ocr.model.CaptureInfo; 12 | import com.luooqi.ocr.model.StageInfo; 13 | import com.luooqi.ocr.snap.ScreenCapture; 14 | import com.luooqi.ocr.utils.CommUtils; 15 | import com.luooqi.ocr.utils.OcrUtils; 16 | 17 | import cn.hutool.core.io.FileTypeUtil; 18 | import cn.hutool.core.thread.ThreadUtil; 19 | import cn.hutool.core.util.StrUtil; 20 | import cn.hutool.log.StaticLog; 21 | import javafx.application.Platform; 22 | import javafx.beans.property.SimpleStringProperty; 23 | import javafx.geometry.Insets; 24 | import javafx.scene.Scene; 25 | import javafx.scene.control.Label; 26 | import javafx.scene.control.TextArea; 27 | import javafx.scene.control.ToolBar; 28 | import javafx.scene.input.Clipboard; 29 | import javafx.scene.input.DataFormat; 30 | import javafx.scene.layout.Border; 31 | import javafx.scene.layout.BorderPane; 32 | import javafx.scene.layout.BorderStroke; 33 | import javafx.scene.layout.BorderStrokeStyle; 34 | import javafx.scene.layout.BorderWidths; 35 | import javafx.scene.layout.CornerRadii; 36 | import javafx.scene.layout.HBox; 37 | import javafx.scene.paint.Color; 38 | import javafx.scene.text.Font; 39 | import javafx.scene.text.FontPosture; 40 | import javafx.stage.FileChooser; 41 | import javafx.stage.Stage; 42 | import lombok.extern.slf4j.Slf4j; 43 | 44 | /** 45 | * Created by litonglinux@qq.com on 12/9/2023_4:40 PM 46 | */ 47 | @Slf4j 48 | public class MainForm { 49 | private static StageInfo stageInfo; 50 | public static Stage stage; 51 | private static Scene mainScene; 52 | 53 | @Override 54 | public int hashCode() { 55 | return super.hashCode(); 56 | } 57 | 58 | private static ScreenCapture screenCapture; 59 | private static ProcessController processController; 60 | private static TextArea textArea; 61 | // private static boolean isSegment = true; 62 | // private static String ocrText = ""; 63 | 64 | public void init(Stage primaryStage) { 65 | 66 | log.info("primaryStage:{}", primaryStage); 67 | stage = primaryStage; 68 | setAutoResize(); 69 | screenCapture = new ScreenCapture(stage); 70 | processController = new ProcessController(); 71 | InitConfig.initKeyHook(); 72 | 73 | // ToggleGroup segmentGrp = new ToggleGroup(); 74 | // ToggleButton resetBtn = CommUtils.createToggleButton(segmentGrp, "resetBtn", this::resetText, "重置"); 75 | // ToggleButton segmentBtn = CommUtils.createToggleButton(segmentGrp, "segmentBtn", this::segmentText, "智能分段"); 76 | // resetBtn.setUserData("resetBtn"); 77 | // segmentBtn.setUserData("segmentBtn"); 78 | // 79 | // segmentGrp.selectToggle(segmentBtn); 80 | // segmentGrp.selectedToggleProperty().addListener((observable, oldValue, newValue) -> { 81 | // isSegment = newValue.getUserData().toString().equals("segmentBtn"); 82 | // }); 83 | 84 | HBox topBar = getTopBar(); 85 | textArea = getCenter(); 86 | ToolBar footerBar = getFooterBar(); 87 | BorderPane root = new BorderPane(); 88 | root.setTop(topBar); 89 | root.setCenter(textArea); 90 | root.setBottom(footerBar); 91 | root.getStylesheets().addAll(getClass().getResource("/css/main.css").toExternalForm()); 92 | CommUtils.initStage(primaryStage); 93 | mainScene = new Scene(root, 670, 470); 94 | stage.setScene(mainScene); 95 | // 启动引擎,加载模型,如果模型加载错误下屏幕显示错误 96 | try { 97 | PaddlePaddleOCRV4.init(); 98 | } catch (Exception e) { 99 | e.printStackTrace(); 100 | } 101 | } 102 | 103 | private TextArea getCenter() { 104 | TextArea textArea = new TextArea(); 105 | textArea.setId("ocrTextArea"); 106 | textArea.setWrapText(true); 107 | textArea.setBorder( 108 | new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT))); 109 | textArea.setFont(Font.font("Arial", FontPosture.REGULAR, 14)); 110 | return textArea; 111 | } 112 | 113 | private ToolBar getFooterBar() { 114 | ToolBar footerBar = new ToolBar(); 115 | footerBar.setId("statsToolbar"); 116 | Label statsLabel = new Label(); 117 | SimpleStringProperty statsProperty = new SimpleStringProperty("总字数:0"); 118 | textArea.textProperty().addListener((observable, oldValue, newValue) -> statsProperty 119 | .set("总字数:" + newValue.replaceAll(CommUtils.SPECIAL_CHARS, "").length())); 120 | statsLabel.textProperty().bind(statsProperty); 121 | footerBar.getItems().add(statsLabel); 122 | return footerBar; 123 | } 124 | 125 | private HBox getTopBar() { 126 | HBox topBar = new HBox(CommUtils.createButton("snapBtn", MainForm::screenShotOcr, "截图"), 127 | CommUtils.createButton("openImageBtn", this::openImageOcr, "打开"), 128 | CommUtils.createButton("copyBtn", this::copyText, "复制"), 129 | CommUtils.createButton("pasteBtn", this::pasteText, "粘贴"), 130 | CommUtils.createButton("clearBtn", this::clearText, "清空"), 131 | CommUtils.createButton("wrapBtn", this::wrapText, "换行") 132 | // CommUtils.SEPARATOR, resetBtn, segmentBtn 133 | ); 134 | topBar.setId("topBar"); 135 | topBar.setMinHeight(40); 136 | topBar.setSpacing(8); 137 | topBar.setPadding(new Insets(6, 8, 6, 8)); 138 | return topBar; 139 | } 140 | 141 | private void setAutoResize() { 142 | stageInfo = new StageInfo(); 143 | stage.xProperty().addListener((observable, oldValue, newValue) -> { 144 | if (stage.getX() > 0) { 145 | stageInfo.setX(stage.getX()); 146 | } 147 | }); 148 | stage.yProperty().addListener((observable, oldValue, newValue) -> { 149 | if (stage.getY() > 0) { 150 | stageInfo.setY(stage.getY()); 151 | } 152 | }); 153 | } 154 | 155 | private void wrapText() { 156 | textArea.setWrapText(!textArea.isWrapText()); 157 | } 158 | 159 | private void clearText() { 160 | textArea.setText(""); 161 | } 162 | 163 | private void pasteText() { 164 | String text = Clipboard.getSystemClipboard().getString(); 165 | if (StrUtil.isBlank(text)) { 166 | return; 167 | } 168 | textArea.setText(textArea.getText() + (StrUtil.isBlank(textArea.getText()) ? "" : "\n") 169 | + Clipboard.getSystemClipboard().getString()); 170 | } 171 | 172 | private void copyText() { 173 | String text = textArea.getSelectedText(); 174 | if (StrUtil.isBlank(text)) { 175 | text = textArea.getText(); 176 | } 177 | if (StrUtil.isBlank(text)) { 178 | return; 179 | } 180 | Map data = new HashMap<>(); 181 | data.put(DataFormat.PLAIN_TEXT, text); 182 | Clipboard.getSystemClipboard().setContent(data); 183 | } 184 | 185 | public static void screenShotOcr() { 186 | stageInfo.setWidth(stage.getWidth()); 187 | stageInfo.setHeight(stage.getHeight()); 188 | stageInfo.setFullScreenState(stage.isFullScreen()); 189 | Platform.runLater(screenCapture::prepareForCapture); 190 | } 191 | 192 | /** 193 | * 打开图片 194 | */ 195 | private void openImageOcr() { 196 | FileChooser fileChooser = new FileChooser(); 197 | fileChooser.setTitle("Please Select Image File"); 198 | String[] extensions = { "*.png", "*.jpg", "*.pdf", "*.PDF" }; 199 | fileChooser.getExtensionFilters().addAll(new FileChooser.ExtensionFilter("Image Files", extensions)); 200 | File selectedFile = fileChooser.showOpenDialog(stage); 201 | if (selectedFile == null || !selectedFile.isFile()) { 202 | return; 203 | } 204 | stageInfo = new StageInfo(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight(), stage.isFullScreen()); 205 | 206 | try { 207 | // BufferedImage image = ImageIO.read(selectedFile); 208 | doOcr(selectedFile); 209 | } catch (Exception e) { 210 | StaticLog.error(e); 211 | } 212 | } 213 | 214 | public static void cancelSnap() { 215 | Platform.runLater(screenCapture::cancelSnap); 216 | } 217 | 218 | public static void doOcr(BufferedImage image) { 219 | processController.setX(CaptureInfo.ScreenMinX + (CaptureInfo.ScreenWidth - 300) / 2); 220 | processController.setY(250); 221 | processController.show(); 222 | 223 | ThreadUtil.execute(() -> { 224 | String text = null; 225 | try { 226 | text = OcrUtils.recImgLocal(image); 227 | } catch (Exception e) { 228 | text = e.getMessage(); 229 | } 230 | 231 | String finalText = text; 232 | Platform.runLater(() -> { 233 | processController.close(); 234 | textArea.setText(finalText); 235 | restore(true); 236 | }); 237 | }); 238 | } 239 | 240 | public static void doOcr(File selectedFile) { 241 | processController.setX(CaptureInfo.ScreenMinX + (CaptureInfo.ScreenWidth - 300) / 2); 242 | processController.setY(250); 243 | processController.show(); 244 | ThreadUtil.execute(() -> { 245 | String text = null; 246 | try { 247 | String fileType = FileTypeUtil.getType(selectedFile); 248 | if ("pdf".equalsIgnoreCase(fileType)) { 249 | text = OcrUtils.recPdfLocal(selectedFile); 250 | } else { 251 | text = OcrUtils.recImgLocal(selectedFile); 252 | } 253 | 254 | } catch (Exception e) { 255 | text = e.getMessage(); 256 | e.printStackTrace(); 257 | } 258 | 259 | String finalText = text; 260 | Platform.runLater(() -> { 261 | processController.close(); 262 | textArea.setText(finalText); 263 | restore(true); 264 | }); 265 | }); 266 | } 267 | 268 | public static void restore(boolean focus) { 269 | stage.setAlwaysOnTop(false); 270 | stage.setScene(mainScene); 271 | stage.setFullScreen(stageInfo.isFullScreenState()); 272 | stage.setX(stageInfo.getX()); 273 | stage.setY(stageInfo.getY()); 274 | stage.setWidth(stageInfo.getWidth()); 275 | stage.setHeight(stageInfo.getHeight()); 276 | if (focus) { 277 | stage.setOpacity(1.0f); 278 | stage.requestFocus(); 279 | } else { 280 | stage.setOpacity(0.0f); 281 | } 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/main/resources/css/main.css: -------------------------------------------------------------------------------- 1 | .button.pressed { -fx-background-color: ghostwhite; } 2 | 3 | .button, .toggle-button { 4 | -fx-background-position: center; 5 | -fx-background-repeat: no-repeat; 6 | -fx-background-size: 18px 18px; 7 | -fx-cursor: pointer; 8 | } 9 | 10 | #snapBtn{ 11 | -fx-background-image: url(/img/screenshot.png); 12 | } 13 | 14 | #openImageBtn{ 15 | -fx-background-image: url(/img/add-image.png); 16 | } 17 | 18 | #clearBtn{ 19 | -fx-background-image: url(/img/clear.png); 20 | } 21 | 22 | #copyBtn{ 23 | -fx-background-image: url(/img/copy.png); 24 | } 25 | 26 | #pasteBtn{ 27 | -fx-background-image: url(/img/paste.png); 28 | } 29 | 30 | #wrapBtn{ 31 | -fx-background-image: url(/img/wrap.png); 32 | } 33 | 34 | /*#resetBtn{*/ 35 | /*-fx-background-image: url(/img/reset.png);*/ 36 | /*}*/ 37 | 38 | /*#segmentBtn{*/ 39 | /*-fx-background-image: url(/img/segment.png);*/ 40 | /*}*/ 41 | 42 | /*#topBar .separator{*/ 43 | /*-fx-padding: 2px -5px 2px -2px;*/ 44 | /*}*/ 45 | 46 | #ocrTextArea .text { 47 | -fx-line-spacing: 0px; 48 | -fx-background-color: #ffffff; 49 | } 50 | -------------------------------------------------------------------------------- /src/main/resources/images/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/tools-ocr/98c12ae01df864a81c89f3f2bd4ff97cefa4c381/src/main/resources/images/01.png -------------------------------------------------------------------------------- /src/main/resources/img/add-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/tools-ocr/98c12ae01df864a81c89f3f2bd4ff97cefa4c381/src/main/resources/img/add-image.png -------------------------------------------------------------------------------- /src/main/resources/img/clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/tools-ocr/98c12ae01df864a81c89f3f2bd4ff97cefa4c381/src/main/resources/img/clear.png -------------------------------------------------------------------------------- /src/main/resources/img/copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/tools-ocr/98c12ae01df864a81c89f3f2bd4ff97cefa4c381/src/main/resources/img/copy.png -------------------------------------------------------------------------------- /src/main/resources/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/tools-ocr/98c12ae01df864a81c89f3f2bd4ff97cefa4c381/src/main/resources/img/logo.png -------------------------------------------------------------------------------- /src/main/resources/img/paste.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/tools-ocr/98c12ae01df864a81c89f3f2bd4ff97cefa4c381/src/main/resources/img/paste.png -------------------------------------------------------------------------------- /src/main/resources/img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/tools-ocr/98c12ae01df864a81c89f3f2bd4ff97cefa4c381/src/main/resources/img/screenshot.png -------------------------------------------------------------------------------- /src/main/resources/img/wrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/tools-ocr/98c12ae01df864a81c89f3f2bd4ff97cefa4c381/src/main/resources/img/wrap.png -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ${CONSOLE_LOG_PATTERN} 12 | 13 | 14 | 15 | 16 | 17 | 18 | ${CONSOLE_LOG_PATTERN} 19 | 20 | 21 | 22 | ${LOG_HOME}/ocr-%d{yyyy-MM-dd}.log 23 | 24 | 180 25 | 26 | 27 | 28 | 10MB 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/test/java/com/litongjava/RapidOcrTest.java: -------------------------------------------------------------------------------- 1 | package com.litongjava; 2 | 3 | import com.benjaminwan.ocrlibrary.OcrResult; 4 | 5 | import io.github.mymonstercat.Model; 6 | import io.github.mymonstercat.ocr.InferenceEngine; 7 | import io.github.mymonstercat.ocr.config.HardwareConfig; 8 | 9 | public class RapidOcrTest { 10 | public static void main(String[] args) { 11 | String imagePath = "C:\\Users\\Administrator\\Desktop\\01.jpg"; 12 | 13 | // init 14 | HardwareConfig onnxConfig = HardwareConfig.getOnnxConfig(); 15 | onnxConfig.setNumThread(2); 16 | InferenceEngine engine = InferenceEngine.getInstance(Model.ONNX_PPOCR_V4_SERVER, onnxConfig); 17 | 18 | // run 19 | OcrResult ocrResult = engine.runOcr(imagePath); 20 | System.out.println(ocrResult.getStrRes().trim()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/com/litongjava/project/config/ProjectConfigTest.java: -------------------------------------------------------------------------------- 1 | package com.litongjava.project.config; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Created by litonglinux@qq.com on 10/11/2023_3:24 PM 9 | */ 10 | public class ProjectConfigTest { 11 | 12 | @Test 13 | public void getStr() { 14 | ProjectConfig projectConfig = new ProjectConfig(); 15 | projectConfig.put("model", "model"); 16 | } 17 | 18 | @Test 19 | public void getStr2() { 20 | ProjectConfig projectConfig = new ProjectConfig(); 21 | String model = projectConfig.getStr("model"); 22 | System.out.println(model); 23 | } 24 | } -------------------------------------------------------------------------------- /src/test/java/com/luooqi/ocr/utils/OcrUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.luooqi.ocr.utils; 2 | 3 | import java.awt.GraphicsConfiguration; 4 | import java.awt.GraphicsEnvironment; 5 | import java.awt.Point; 6 | import java.awt.geom.AffineTransform; 7 | import java.io.File; 8 | 9 | import org.junit.Test; 10 | 11 | public class OcrUtilsTest { 12 | 13 | String html = "{\"result\":[{\"groupID\":0,\"content\":\"目前,RFID(radio frequency identification\\n\",\"frame\":[\"537,11\",\"935,11\",\"935,41\",\"537,41\"]},{\"groupID\":1,\"content\":\"0引言\\n\",\"frame\":[\"23,22\",\"129,22\",\"129,54\",\"23,54\"]},{\"groupID\":2,\"content\":\"devices)技术、网络化信息管理及数据库技术在农\\n\",\"frame\":[\"496,43\",\"931,43\",\"931,65\",\"496,65\"]},{\"groupID\":3,\"content\":\"近年来,关于遗传育种的研究已从传统的常规\\n\",\"frame\":[\"64,68\",\"461,68\",\"461,93\",\"64,93\"]},{\"groupID\":2,\"content\":\"业领域的应用较广泛,三者相结合主要应用于农田\\n\",\"frame\":[\"496,68\",\"931,68\",\"931,93\",\"496,93\"]},{\"groupID\":2,\"content\":\"信息采集[9-12]、食品存储监测及安全跟踪[13-19]、畜\\n\",\"frame\":[\"499,94\",\"931,94\",\"931,121\",\"499,121\"]},{\"groupID\":3,\"content\":\"育种技术进入依靠生物技术育种阶段,研究内容也\\n\",\"frame\":[\"28,94\",\"460,94\",\"460,121\",\"28,121\"]},{\"groupID\":2,\"content\":\"禽养殖监测[20-2]等方面。关于采用编码的方法结合\\n\",\"frame\":[\"499,125\",\"931,125\",\"931,149\",\"499,149\"]},{\"groupID\":3,\"content\":\"从单个基因的测序转为有计划、大规模地检测水稻\\n\",\"frame\":[\"25,125\",\"460,125\",\"460,148\",\"25,148\"]},{\"groupID\":3,\"content\":\"等重要生物体的基因图谱。要实现水稻分子遗传育\\n\",\"frame\":[\"25,150\",\"460,150\",\"460,175\",\"25,175\"]},{\"groupID\":2,\"content\":\"数据库技术对农业中的动、植物进行标识、溯源和\\n\",\"frame\":[\"497,155\",\"929,155\",\"929,177\",\"497,177\"]},{\"groupID\":2,\"content\":\"建模方面的研究也有报道[242]。其在农作物育种方\\n\",\"frame\":[\"500,178\",\"929,178\",\"929,203\",\"500,203\"]},{\"groupID\":3,\"content\":\"种的目标,首要任务是了解、选择群体细胞的生理\\n\",\"frame\":[\"25,181\",\"460,181\",\"460,204\",\"25,204\"]},{\"groupID\":3,\"content\":\"生化特性以及与之相对应的表型现象,也就是要确\\n\",\"frame\":[\"25,207\",\"461,207\",\"461,232\",\"25,232\"]},{\"groupID\":2,\"content\":\"面的研究则主要涉及育种参数统计分析,育种遗传\\n\",\"frame\":[\"496,211\",\"931,211\",\"931,235\",\"496,235\"]},{\"groupID\":2,\"content\":\"过程的计算机数学模型,农作物育种专家系统,种\\n\",\"frame\":[\"496,237\",\"931,237\",\"931,262\",\"496,262\"]},{\"groupID\":3,\"content\":\"定植物群体实际基因或基因片段的表达与表型现\\n\",\"frame\":[\"25,237\",\"461,237\",\"461,262\",\"25,262\"]},{\"groupID\":3,\"content\":\"象的内在关联程度1一]。高通量的水稻种植试验成为\\n\",\"frame\":[\"25,263\",\"460,263\",\"460,288\",\"25,288\"]},{\"groupID\":2,\"content\":\"质资源数据库和育种信息管理等方面。由于长期以\\n\",\"frame\":[\"499,267\",\"931,267\",\"931,291\",\"499,291\"]},{\"groupID\":2,\"content\":\"来农作物育种信息采用人工方式记录管理,在育种\\n\",\"frame\":[\"499,293\",\"931,293\",\"931,318\",\"499,318\"]},{\"groupID\":3,\"content\":\"表型现象验证的重要手段。大规模、高效率的分子\\n\",\"frame\":[\"25,293\",\"461,293\",\"461,318\",\"25,318\"]},{\"groupID\":3,\"content\":\"遗传育种技术试验平台是实现高通量分子遗传育\\n\",\"frame\":[\"25,319\",\"461,319\",\"461,345\",\"25,345\"]},{\"groupID\":2,\"content\":\"试验量不大时,信息管理需求方面的矛眉并未突显,\\n\",\"frame\":[\"496,324\",\"928,324\",\"928,347\",\"496,347\"]},{\"groupID\":2,\"content\":\"因此国内关于农作物育种信息管理和数据库方面的\\n\",\"frame\":[\"496,349\",\"931,349\",\"931,374\",\"496,374\"]},{\"groupID\":3,\"content\":\"种试验的关键环节。温、湿度自动调节的现代化温\\n\",\"frame\":[\"25,350\",\"460,350\",\"460,373\",\"25,373\"]},{\"groupID\":3,\"content\":\"室以及全自动化的水稻种植、栽培、输送、检测试\\n\",\"frame\":[\"25,377\",\"460,377\",\"460,400\",\"25,400\"]},{\"groupID\":2,\"content\":\"研究报导较少[283]。随着分子遗传育种试验的中信\\n\",\"frame\":[\"496,376\",\"931,376\",\"931,404\",\"496,404\"]},{\"groupID\":2,\"content\":\"息量的倍增,如何运用计算机技术对育种试验信息\\n\",\"frame\":[\"499,406\",\"931,406\",\"931,431\",\"499,431\"]},{\"groupID\":3,\"content\":\"验环境的建立是保证高通重分子遗传育种试验完\\n\",\"frame\":[\"25,407\",\"460,407\",\"460,430\",\"25,430\"]},{\"groupID\":3,\"content\":\"成的基础条件。\\n\",\"frame\":[\"25,433\",\"154,433\",\"154,456\",\"25,456\"]},{\"groupID\":2,\"content\":\"进行科学、高效的管理成为一个亟待解决的问题。\\n\",\"frame\":[\"498,435\",\"914,435\",\"914,459\",\"498,459\"]},{\"groupID\":4,\"content\":\"本文以高通量水稻种植试验为研究对象,在已\\n\",\"frame\":[\"535,462\",\"931,462\",\"931,487\",\"535,487\"]},{\"groupID\":5,\"content\":\"修订日期:2014-02-26\\n\",\"frame\":[\"197,487\",\"336,487\",\"336,504\",\"197,504\"]},{\"groupID\":6,\"content\":\"收稿日期:2013-04-13\\n\",\"frame\":[\"27,487\",\"164,487\",\"164,504\",\"27,504\"]},{\"groupID\":4,\"content\":\"有的温室水稻盆栽自动化输送设备基础上,结合\\n\",\"frame\":[\"499,489\",\"931,489\",\"931,517\",\"499,517\"]},{\"groupID\":7,\"content\":\"基金项目:中央高校基本科研业务费专项资金资助(2013PY052):国\\n\",\"frame\":[\"27,511\",\"461,511\",\"461,527\",\"27,527\"]},{\"groupID\":4,\"content\":\"RFID技术、网络化信息管理及数据库技术,研究\\n\",\"frame\":[\"496,519\",\"934,519\",\"934,542\",\"496,542\"]},{\"groupID\":7,\"content\":\"家自然科学基金(61007058)\\n\",\"frame\":[\"27,530\",\"204,530\",\"204,549\",\"27,549\"]},{\"groupID\":4,\"content\":\"水稻的遗传育种试验中种植试验信息的管理方案,\\n\",\"frame\":[\"499,545\",\"922,545\",\"922,570\",\"499,570\"]},{\"groupID\":8,\"content\":\"作者简介:高云(1974一),女(汉),湖北鄂州,副教授,硕士,农\\n\",\"frame\":[\"27,554\",\"461,554\",\"461,570\",\"27,570\"]},{\"groupID\":8,\"content\":\"业智能检测与控制。武汉湖北省武汉市洪山区华中农业大学工学院,\\n\",\"frame\":[\"26,573\",\"455,573\",\"455,594\",\"26,594\"]},{\"groupID\":4,\"content\":\"针对水稻的突变体库的特点,提出了水稻种植试验\\n\",\"frame\":[\"496,576\",\"931,576\",\"931,599\",\"496,599\"]},{\"groupID\":9,\"content\":\"430070。Email:angelclouder@hotmail.com\\n\",\"frame\":[\"23,595\",\"284,595\",\"284,616\",\"23,616\"]},{\"groupID\":4,\"content\":\"家系可追溯的数据管理方法,实现水稻家系的自动\\n\",\"frame\":[\"499,603\",\"931,603\",\"931,626\",\"499,626\"]},{\"groupID\":10,\"content\":\"中国农业工程学会会员(E041700006M)\\n\",\"frame\":[\"27,621\",\"274,621\",\"274,637\",\"27,637\"]},{\"groupID\":4,\"content\":\"追溯和家系树的自动生成。该研究有效解决了高通\\n\",\"frame\":[\"496,632\",\"931,632\",\"931,655\",\"496,655\"]},{\"groupID\":11,\"content\":\"米通信作者:李小昱(1953一),女(汉),陕西西安,教授,博士生导\\n\",\"frame\":[\"26,639\",\"460,639\",\"460,660\",\"26,660\"]},{\"groupID\":4,\"content\":\"量下育种种植试验的信息量大、管理复杂的问题,\\n\",\"frame\":[\"499,658\",\"922,658\",\"922,682\",\"499,682\"]},{\"groupID\":11,\"content\":\"师,智能化检测技术。武汉湖北省武汉市洪山区华中农业大学工学院,\\n\",\"frame\":[\"25,664\",\"458,664\",\"458,680\",\"25,680\"]},{\"groupID\":12,\"content\":\"430070。Email:lixiaoyu@mail.hzau.edu.cn\\n\",\"frame\":[\"24,686\",\"280,686\",\"280,703\",\"24,703\"]},{\"groupID\":4,\"content\":\"提高了试验效率。对于其他与水稻相类似农作物育\\n\",\"frame\":[\"499,687\",\"931,687\",\"931,712\",\"499,712\"]},{\"groupID\":13,\"content\":\"中国农业工程学会高级会员(E041200068S)\\n\",\"frame\":[\"27,707\",\"297,707\",\"297,723\",\"27,723\"]}],\"success\":1,\"zly\":\"zly\",\"ocr_time\":1584.014892578125,\"id\":\"2274054490548600832\",\"lang\":\"zh-Chs\",\"direction\":0}"; 14 | 15 | @Test 16 | public void sogouMobileOcr() { 17 | 18 | } 19 | 20 | private Point frameToPoint(String text) { 21 | String[] arr = text.split(","); 22 | return new Point(Integer.valueOf(arr[0].trim()), Integer.valueOf(arr[1].trim())); 23 | } 24 | 25 | @Test 26 | public void sogouWebOcr() { 27 | GraphicsConfiguration asdf = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice() 28 | .getDefaultConfiguration(); 29 | AffineTransform asfd2 = asdf.getDefaultTransform(); 30 | double scaleX = asfd2.getScaleX(); 31 | double scaleY = asfd2.getScaleY(); 32 | } 33 | 34 | @Test 35 | public void recPdfLocal() { 36 | File file = new File("F:\\document\\dev-docs\\24.Internet_of_things\\02_C++\\2.1 面向C++模板库应用开发\\01 第一章C++.pdf"); 37 | String s = OcrUtils.recPdfLocal(file); 38 | System.out.println(s); 39 | } 40 | 41 | @Test 42 | public void recImageLocal() { 43 | OcrUtils.recImgLocal(new File("temp_1010298_4.png")); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/luooqi/ocr/utils/PdfTest.java: -------------------------------------------------------------------------------- 1 | package com.luooqi.ocr.utils; 2 | 3 | import org.apache.pdfbox.pdmodel.PDDocument; 4 | import org.apache.pdfbox.rendering.PDFRenderer; 5 | import org.junit.Test; 6 | 7 | import javax.imageio.ImageIO; 8 | import java.awt.image.BufferedImage; 9 | import java.io.File; 10 | import java.io.FileOutputStream; 11 | import java.io.IOException; 12 | 13 | public class PdfTest { 14 | 15 | @Test 16 | public void extraImageFromPdf() throws IOException { 17 | File pdfFile = new File("F:\\document\\dev-docs\\24.Internet_of_things\\02_C++\\2.1 面向C++模板库应用开发\\01 第一章C++.pdf"); 18 | try (PDDocument document = PDDocument.load(pdfFile)) { 19 | PDFRenderer renderer = new PDFRenderer(document); 20 | 21 | for (int i = 0; i < document.getNumberOfPages(); ++i) { 22 | BufferedImage bufferedImage = renderer.renderImageWithDPI(i, 300); 23 | FileOutputStream fileOutputStream = new FileOutputStream(i + ".png"); 24 | ImageIO.write(bufferedImage, "png", fileOutputStream); // 选择合适的格式,如 "png" 或 "jpg" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/resources/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/tools-ocr/98c12ae01df864a81c89f3f2bd4ff97cefa4c381/src/test/resources/03.png -------------------------------------------------------------------------------- /src/test/resources/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/tools-ocr/98c12ae01df864a81c89f3f2bd4ff97cefa4c381/src/test/resources/2.jpg --------------------------------------------------------------------------------