├── .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 | 
63 | 
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 | 
116 | - Settings-->Security and Privacy-->Screen Recording
117 | 
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 | 
62 |
63 | - result:
64 |
65 | 
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 | 
117 | - Settings --> Security and Privacy --> Screen Recording
118 | 
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
--------------------------------------------------------------------------------