├── res
└── test.png
├── .idea
├── encodings.xml
├── .gitignore
├── vcs.xml
├── compiler.xml
├── jarRepositories.xml
└── misc.xml
├── LICENSE
├── pom.xml
├── src
├── main
│ └── java
│ │ └── org
│ │ └── example
│ │ ├── Main.java
│ │ └── Ocr.java
└── test
│ └── java
│ └── org
│ └── example
│ └── OcrTest.java
├── README.md
├── .gitignore
└── .github
└── workflows
└── ci.yml
/res/test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jerrylususu/PaddleOCR-json-java-api/HEAD/res/test.png
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # 默认忽略的文件
2 | /shelf/
3 | /workspace.xml
4 | # 基于编辑器的 HTTP 客户端请求
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Neko Null
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | org.example
8 | paddleocr-json-java
9 | 1.0-SNAPSHOT
10 |
11 |
12 | 8
13 | 8
14 |
15 |
16 |
17 |
18 |
19 | com.google.code.gson
20 | gson
21 | 2.9.1
22 |
23 |
24 |
25 |
26 | junit
27 | junit
28 | 4.13.2
29 | test
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
20 |
21 |
22 |
23 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/main/java/org/example/Main.java:
--------------------------------------------------------------------------------
1 | package org.example;
2 |
3 | import java.io.*;
4 | import java.nio.file.Files;
5 | import java.nio.file.Paths;
6 | import java.util.HashMap;
7 | import java.util.Map;
8 |
9 |
10 | public class Main {
11 | public static void main(String[] args) {
12 | // 可选的配置项
13 | Map arguments = new HashMap<>();
14 | // arguments.put("use_angle_cls", true);
15 |
16 | // 初始化 OCR:使用本地进程或者套接字服务器
17 | // 本地进程: new Ocr(new File(exePath), arguments)
18 | String exePath = "path_to_exe"; // paddleocr_json 的可执行文件所在路径
19 | try (Ocr ocr = new Ocr(new File(exePath), arguments)) {
20 | // 使用套接字服务器(仅作为客户端,不启动服务)
21 | // try (Ocr ocr = new Ocr(serverAddr, serverPort, arguments)) {
22 |
23 |
24 |
25 | // 对一张图片进行 OCR(使用路径)
26 | String imgPath = "path_to_img";
27 | OcrResponse resp = ocr.runOcr(new File(imgPath));
28 |
29 | // 或者使用图片数据(二进制或 base64)
30 | // byte[] fileBytes = Files.readAllBytes(Paths.get(imgPath));
31 | // OcrResponse resp = ocr.runOcrOnImgBytes(fileBytes);
32 |
33 | // OcrResponse resp = ocr.runOcrOnImgBase64("base64img");
34 |
35 | // 或者直接识别剪贴板中的图片
36 | // OcrResponse resp = ocr.runOcrOnClipboard();
37 |
38 | // 读取结果
39 | if (resp.code == OcrCode.OK) {
40 | for (OcrEntry entry : resp.data) {
41 | System.out.println(entry.text);
42 | }
43 | } else {
44 | System.out.println("error: code=" + resp.code + " msg=" + resp.msg);
45 | }
46 | } catch (IOException e) {
47 | e.printStackTrace();
48 | }
49 |
50 | }
51 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PaddleOCR-json-java-api
2 |
3 | [](https://github.com/jerrylususu/PaddleOCR-json-java-api/actions/workflows/ci.yml)
4 |
5 | [PaddleOCR-json](https://github.com/hiroi-sora/PaddleOCR-json) 的简单 Java 封装。
6 |
7 | v1.2 支持本地进程方式,v1.3 支持本地进程&套接字服务客户端方式,v1.4 支持 Linux 版本。
8 |
9 | ## 依赖
10 | - Gson
11 | - Java 8 或更新版本
12 |
13 | ## 使用
14 | 1. 在项目中引入 'Ocr.java'
15 | 2. 参考如下代码片段调用 OCR (或参考完整示例 [Main.java](https://github.com/jerrylususu/PaddleOCR-json-java-api/blob/main/src/main/java/org/example/Main.java))
16 |
17 | > 如果在 Linux 下使用,请参阅 [此 Issue](https://github.com/jerrylususu/PaddleOCR-json-java-api/issues/8) 。
18 |
19 |
20 | ```java
21 | // 可选的配置项
22 | Map arguments = new HashMap<>();
23 | // arguments.put("use_angle_cls", true);
24 |
25 | // 使用本地进程方式初始化 OCR
26 | String exePath = "path/to/executable"; // paddleocr_json 的可执行文件所在路径
27 | try (Ocr ocr = new Ocr(new File(exePath), arguments)) {
28 |
29 | // 使用套接字服务方式初始化 OCR(仅作为客户端,不启动服务)
30 | // try (Ocr ocr = new Ocr(serverAddr, serverPort, arguments)) {
31 |
32 | // 对一张图片进行 OCR
33 | String imgPath = "path/to/img";
34 | OcrResponse resp = ocr.runOcr(new File(imgPath));
35 |
36 | // 或者对图片的二进制数据/Base64后的图片进行 OCR
37 | // byte[] fileBytes = Files.readAllBytes(Paths.get(imgPath));
38 | // OcrResponse resp = ocr.runOcrOnImgBytes(fileBytes);
39 | // OcrResponse resp = ocr.runOcrOnImgBase64("base64img");
40 |
41 | // 或者直接识别剪贴板中的图片
42 | // OcrResponse resp = ocr.runOcrOnClipboard();
43 |
44 | // 读取结果
45 | if (resp.code == OcrCode.OK) {
46 | for (OcrEntry entry : resp.data) {
47 | System.out.println(entry.text);
48 | }
49 | } else {
50 | System.out.println("error: code=" + resp.code + " msg=" + resp.msg);
51 | }
52 | } catch (IOException e) {
53 | e.printStackTrace();
54 | }
55 | ```
56 |
57 | ## 参考
58 | - Unicode 转换为 ASCII 序列: [EscapedWriter (Soot Project)](https://github.com/soot-oss/soot/blob/3966f565db6dc2882c3538ffc39e44f4c14b5bcf/src/main/java/soot/util/EscapedWriter.java)
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled class file
2 | *.class
3 |
4 | # Log file
5 | *.log
6 |
7 | # BlueJ files
8 | *.ctxt
9 |
10 | # Mobile Tools for Java (J2ME)
11 | .mtj.tmp/
12 |
13 | # Package Files #
14 | *.jar
15 | *.war
16 | *.nar
17 | *.ear
18 | *.zip
19 | *.tar.gz
20 | *.rar
21 |
22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
23 | hs_err_pid*
24 | replay_pid*
25 |
26 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
27 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
28 |
29 | # User-specific stuff
30 | .idea/**/workspace.xml
31 | .idea/**/tasks.xml
32 | .idea/**/usage.statistics.xml
33 | .idea/**/dictionaries
34 | .idea/**/shelf
35 |
36 | # AWS User-specific
37 | .idea/**/aws.xml
38 |
39 | # Generated files
40 | .idea/**/contentModel.xml
41 |
42 | # Sensitive or high-churn files
43 | .idea/**/dataSources/
44 | .idea/**/dataSources.ids
45 | .idea/**/dataSources.local.xml
46 | .idea/**/sqlDataSources.xml
47 | .idea/**/dynamic.xml
48 | .idea/**/uiDesigner.xml
49 | .idea/**/dbnavigator.xml
50 |
51 | # Gradle
52 | .idea/**/gradle.xml
53 | .idea/**/libraries
54 |
55 | # Gradle and Maven with auto-import
56 | # When using Gradle or Maven with auto-import, you should exclude module files,
57 | # since they will be recreated, and may cause churn. Uncomment if using
58 | # auto-import.
59 | # .idea/artifacts
60 | # .idea/compiler.xml
61 | # .idea/jarRepositories.xml
62 | # .idea/modules.xml
63 | # .idea/*.iml
64 | # .idea/modules
65 | # *.iml
66 | # *.ipr
67 |
68 | # CMake
69 | cmake-build-*/
70 |
71 | # Mongo Explorer plugin
72 | .idea/**/mongoSettings.xml
73 |
74 | # File-based project format
75 | *.iws
76 |
77 | # IntelliJ
78 | out/
79 |
80 | # mpeltonen/sbt-idea plugin
81 | .idea_modules/
82 |
83 | # JIRA plugin
84 | atlassian-ide-plugin.xml
85 |
86 | # Cursive Clojure plugin
87 | .idea/replstate.xml
88 |
89 | # SonarLint plugin
90 | .idea/sonarlint/
91 |
92 | # Crashlytics plugin (for Android Studio and IntelliJ)
93 | com_crashlytics_export_strings.xml
94 | crashlytics.properties
95 | crashlytics-build.properties
96 | fabric.properties
97 |
98 | # Editor-based Rest Client
99 | .idea/httpRequests
100 |
101 | # Android studio 3.1+ serialized cache file
102 | .idea/caches/build_file_checksums.ser
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | push:
8 | branches:
9 | - main
10 | workflow_dispatch:
11 |
12 |
13 | jobs:
14 | build:
15 | runs-on: ${{ matrix.os }}
16 | strategy:
17 | matrix:
18 | os: [ubuntu-22.04, windows-latest]
19 |
20 | steps:
21 | - name: Checkout code
22 | uses: actions/checkout@v2
23 |
24 | - name: Set up JDK 8
25 | uses: actions/setup-java@v2
26 | with:
27 | java-version: '8'
28 | distribution: 'adopt'
29 |
30 | - name: Install 7-Zip on Windows
31 | if: matrix.os == 'windows-latest'
32 | run: choco install 7zip -y
33 | shell: powershell
34 |
35 | - name: Download and extract binary in Linux
36 | if: matrix.os == 'ubuntu-22.04'
37 | run: |
38 | wget --quiet -O binary-file.tar.xz https://github.com/hiroi-sora/PaddleOCR-json/releases/download/v1.4.0/PaddleOCR-json_v1.4.0_debian_gcc_x86-64.tar.xz
39 | mkdir -p extracted
40 | tar -xf binary-file.tar.xz -C extracted
41 | echo "PADDLEOCR_JSON_EXE_PATH=$(pwd)/extracted/PaddleOCR-json_v1.4.0_debian_gcc_x86-64/bin/PaddleOCR-json" >> $GITHUB_ENV
42 | echo "PADDLEOCR_JSON_TEST_IMG_PATH=$(pwd)/res/test.png" >> $GITHUB_ENV
43 | shell: bash
44 |
45 | - name: Download and extract binary in Windows
46 | if: matrix.os == 'windows-latest'
47 | run: |
48 | Invoke-WebRequest -Uri https://github.com/hiroi-sora/PaddleOCR-json/releases/download/v1.4.0/PaddleOCR-json_v1.4.0_windows_x86-64.7z -OutFile "binary-file.7z"
49 | New-Item -ItemType Directory -Path "extracted" -Force
50 | 7z x binary-file.7z -oextracted
51 | $PADDLEOCR_JSON_EXE_PATH = "$(pwd)\extracted\PaddleOCR-json_v1.4.0\PaddleOCR-json.exe"
52 | $PADDLEOCR_JSON_TEST_IMG_PATH = "$(pwd)\res\test.png"
53 | echo "PADDLEOCR_JSON_EXE_PATH=$PADDLEOCR_JSON_EXE_PATH" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
54 | echo "PADDLEOCR_JSON_TEST_IMG_PATH=$PADDLEOCR_JSON_TEST_IMG_PATH" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
55 | shell: pwsh
56 |
57 | - name: Run tests
58 | run: mvn -B test
59 | env:
60 | PADDLEOCR_JSON_EXE_PATH: ${{ env.PADDLEOCR_JSON_EXE_PATH }}
61 | PADDLEOCR_JSON_TEST_IMG_PATH: ${{ env.PADDLEOCR_JSON_TEST_IMG_PATH }}
62 |
--------------------------------------------------------------------------------
/src/test/java/org/example/OcrTest.java:
--------------------------------------------------------------------------------
1 | package org.example;
2 |
3 | import org.junit.Test;
4 |
5 | import java.io.*;
6 | import java.nio.file.Files;
7 | import java.nio.file.Paths;
8 | import java.util.HashMap;
9 | import java.util.Map;
10 |
11 | import static org.junit.Assert.assertEquals;
12 |
13 | public class OcrTest {
14 |
15 | private static final String exePath = System.getenv().getOrDefault("PADDLEOCR_JSON_EXE_PATH", "C:\\Temporary\\paddleocr\\PaddleOCR-json_v1.4.0\\PaddleOCR-json.exe");
16 | private static final String imgPath = System.getenv().getOrDefault("PADDLEOCR_JSON_TEST_IMG_PATH", "C:\\Projects\\PaddleOCR-json-java-api\\res\\test.png");
17 | private static final String kTestContent = "helloworld";
18 | private static final int kTestPort = Integer.parseInt(System.getenv().getOrDefault("PADDLEOCR_JSON_TEST_PORT", "23333"));
19 |
20 | @Test
21 | public void TestLocalMode() {
22 | System.out.println("using exePath: " + exePath);
23 |
24 | Map arguments = new HashMap<>();
25 |
26 | OcrResponse resp = null;
27 | try (Ocr ocr = new Ocr(new File(exePath), arguments)) {
28 |
29 | resp = ocr.runOcr(new File(imgPath));
30 |
31 | if (resp.code == OcrCode.OK) {
32 | for (OcrEntry entry : resp.data) {
33 | System.out.println(entry.text);
34 | }
35 | } else {
36 | System.out.println("error: code=" + resp.code + " msg=" + resp.msg);
37 | }
38 | } catch (IOException e) {
39 | e.printStackTrace();
40 | }
41 |
42 | assertEquals(resp.code, OcrCode.OK);
43 | assertEquals(resp.data.length, 1);
44 | assertEquals(resp.data[0].text, kTestContent);
45 | }
46 |
47 | @Test
48 | public void TestSocketMode() {
49 | System.out.println("using exePath: " + exePath);
50 |
51 | Map serverArguments = new HashMap<>();
52 | serverArguments.put("port", kTestPort);
53 | OcrResponse resp = null;
54 |
55 | // 在外层启动一个监听 kTestPort 的 paddleocr-json 服务
56 | try (Ocr ocrServer = new Ocr(new File(exePath), serverArguments)) {
57 |
58 | // 等开始监听
59 | System.out.println("sleep 5s to make sure the server is listening");
60 | Thread.sleep(5000);
61 |
62 | Map arugments = new HashMap<>();
63 | try (Ocr ocr = new Ocr("localhost", kTestPort, arugments)) {
64 |
65 | byte[] fileBytes = Files.readAllBytes(Paths.get(imgPath));
66 | resp = ocr.runOcrOnImgBytes(fileBytes);
67 |
68 | // 读取结果
69 | if (resp.code == OcrCode.OK) {
70 | for (OcrEntry entry : resp.data) {
71 | System.out.println(entry.text);
72 | }
73 | } else {
74 | System.out.println("error: code=" + resp.code + " msg=" + resp.msg);
75 | }
76 | } catch (IOException e) {
77 | e.printStackTrace();
78 | }
79 | } catch (IOException | InterruptedException e) {
80 | e.printStackTrace();
81 | }
82 |
83 | assertEquals(resp.code, OcrCode.OK);
84 | assertEquals(resp.data.length, 1);
85 | assertEquals(resp.data[0].text, kTestContent);
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/main/java/org/example/Ocr.java:
--------------------------------------------------------------------------------
1 | package org.example;
2 |
3 | import com.google.gson.Gson;
4 |
5 | import java.io.*;
6 | import java.net.*;
7 | import java.nio.charset.StandardCharsets;
8 | import java.util.*;
9 | import java.io.FilterWriter;
10 | import java.io.IOException;
11 | import java.io.Writer;
12 |
13 | // from: https://github.com/soot-oss/soot/blob/3966f565db6dc2882c3538ffc39e44f4c14b5bcf/src/main/java/soot/util/EscapedWriter.java
14 | /*-
15 | * #%L
16 | * Soot - a J*va Optimization Framework
17 | * %%
18 | * Copyright (C) 1997 - 1999 Raja Vallee-Rai
19 | * %%
20 | * This program is free software: you can redistribute it and/or modify
21 | * it under the terms of the GNU Lesser General Public License as
22 | * published by the Free Software Foundation, either version 2.1 of the
23 | * License, or (at your option) any later version.
24 | *
25 | * This program is distributed in the hope that it will be useful,
26 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
27 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28 | * GNU General Lesser Public License for more details.
29 | *
30 | * You should have received a copy of the GNU General Lesser Public
31 | * License along with this program. If not, see
32 | * .
33 | * #L%
34 | */
35 | /**
36 | * A FilterWriter which catches to-be-escaped characters (\\unnnn) in the input and substitutes their escaped
37 | * representation. Used for Soot output.
38 | */
39 | class EscapedWriter extends FilterWriter {
40 | /** Convenience field containing the system's line separator. */
41 | public final String lineSeparator = System.getProperty("line.separator");
42 | private final int cr = lineSeparator.charAt(0);
43 | private final int lf = (lineSeparator.length() == 2) ? lineSeparator.charAt(1) : -1;
44 |
45 | /** Constructs an EscapedWriter around the given Writer. */
46 | public EscapedWriter(Writer fos) {
47 | super(fos);
48 | }
49 |
50 | private final StringBuffer mini = new StringBuffer();
51 |
52 | /** Print a single character (unsupported). */
53 | public void print(int ch) throws IOException {
54 | write(ch);
55 | throw new RuntimeException();
56 | }
57 |
58 | /** Write a segment of the given String. */
59 | public void write(String s, int off, int len) throws IOException {
60 | for (int i = off; i < off + len; i++) {
61 | write(s.charAt(i));
62 | }
63 | }
64 |
65 | /** Write a single character. */
66 | public void write(int ch) throws IOException {
67 | if (ch >= 32 && ch <= 126 || ch == cr || ch == lf || ch == ' ') {
68 | super.write(ch);
69 | return;
70 | }
71 |
72 | mini.setLength(0);
73 | mini.append(Integer.toHexString(ch));
74 |
75 | while (mini.length() < 4) {
76 | mini.insert(0, "0");
77 | }
78 |
79 | mini.insert(0, "\\u");
80 | for (int i = 0; i < mini.length(); i++) {
81 | super.write(mini.charAt(i));
82 | }
83 | }
84 | }
85 |
86 | enum OcrMode {
87 | LOCAL_PROCESS, // 本地进程
88 | SOCKET_SERVER // 套接字服务器
89 | }
90 |
91 | class OcrCode {
92 | public static final int OK = 100;
93 | public static final int NO_TEXT = 101;
94 | }
95 |
96 | class OcrEntry {
97 | String text;
98 | int[][] box;
99 | double score;
100 |
101 | @Override
102 | public String toString() {
103 | return "RecognizedText{" +
104 | "text='" + text + '\'' +
105 | ", box=" + Arrays.toString(box) +
106 | ", score=" + score +
107 | '}';
108 | }
109 | }
110 |
111 | class OcrResponse {
112 | int code;
113 | OcrEntry[] data;
114 | String msg;
115 | String hotUpdate;
116 |
117 | @Override
118 | public String toString() {
119 | return "OcrResponse{" +
120 | "code=" + code +
121 | ", data=" + Arrays.toString(data) +
122 | ", msg='" + msg + '\'' +
123 | ", hotUpdate='" + hotUpdate + '\'' +
124 | '}';
125 | }
126 |
127 | public OcrResponse() {
128 | }
129 |
130 | public OcrResponse(int code, String msg) {
131 | this.code = code;
132 | this.msg = msg;
133 | }
134 | }
135 |
136 | public class Ocr implements AutoCloseable {
137 | // 公共
138 | Gson gson;
139 | boolean ocrReady = false;
140 | Map arguments;
141 | BufferedReader reader;
142 | BufferedWriter writer;
143 | OcrMode mode;
144 |
145 | // 本地进程模式
146 | Process process;
147 | File exePath;
148 |
149 |
150 | // 套接字服务器模式
151 | String serverAddr;
152 | int serverPort;
153 | Socket clientSocket;
154 | boolean isLoopback = false;
155 |
156 | /**
157 | * 使用套接字模式初始化
158 | * @param serverAddr
159 | * @param serverPort
160 | * @param arguments
161 | * @throws IOException
162 | */
163 | public Ocr(String serverAddr, int serverPort, Map arguments) throws IOException {
164 | this.mode = OcrMode.SOCKET_SERVER;
165 | this.arguments = arguments;
166 | this.serverAddr = serverAddr;
167 | this.serverPort = serverPort;
168 | checkIfLoopback();
169 | initOcr();
170 | }
171 |
172 | /**
173 | * 使用本地进程模式初始化
174 | * @param exePath
175 | * @param arguments
176 | * @throws IOException
177 | */
178 | public Ocr(File exePath, Map arguments) throws IOException {
179 | this.mode = OcrMode.LOCAL_PROCESS;
180 | this.arguments = arguments;
181 | this.exePath = exePath;
182 | initOcr();
183 | }
184 |
185 | private void initOcr() throws IOException {
186 | gson = new Gson();
187 |
188 | List commandList = new ArrayList<>();
189 | if (arguments != null) {
190 | for (Map.Entry entry : arguments.entrySet()) {
191 | commandList.add("--" + entry.getKey() + "=" + entry.getValue().toString());
192 | }
193 | }
194 |
195 | for (String c: commandList) {
196 | if (!StandardCharsets.US_ASCII.newEncoder().canEncode(c)) {
197 | throw new IllegalArgumentException("参数不能含有非 ASCII 字符");
198 | }
199 | }
200 |
201 | System.out.println("当前参数:" + (commandList.isEmpty() ? "空": commandList));
202 |
203 |
204 | switch (this.mode) {
205 | case LOCAL_PROCESS: {
206 | File workingDir = exePath.getParentFile();
207 | if (isLinux()) {
208 | // Linux 下解压后的默认布局是 ../bin/exe,需要再往上一层
209 | workingDir = workingDir.getParentFile();
210 | }
211 | commandList.add(0, exePath.toString());
212 | ProcessBuilder pb = new ProcessBuilder(commandList);
213 | pb.directory(workingDir);
214 | pb.redirectErrorStream(true);
215 |
216 | if (isLinux()) {
217 | // Linux 下启动,需要设置 LD_LIBRARY_PATH,见 https://github.com/hiroi-sora/PaddleOCR-json/blob/main/cpp/README-linux.md
218 | File libLocation = new File(workingDir, "lib");
219 | pb.environment().put("LD_LIBRARY_PATH", libLocation.getAbsolutePath());
220 | }
221 |
222 | process = pb.start();
223 |
224 | InputStream stdout = process.getInputStream();
225 | OutputStream stdin = process.getOutputStream();
226 | reader = new BufferedReader(new InputStreamReader(stdout, StandardCharsets.UTF_8));
227 | writer = new BufferedWriter(new OutputStreamWriter(stdin, StandardCharsets.UTF_8));
228 | String line = "";
229 | ocrReady = false;
230 | while (!ocrReady) {
231 | line = reader.readLine();
232 | if (isLinux() && line.contains("not found (required by")) {
233 | System.out.println("可能存在依赖库问题:" + line);
234 | break;
235 | }
236 | if (line.contains("OCR init completed")) {
237 | ocrReady = true;
238 | break;
239 | }
240 | }
241 | if (ocrReady) {
242 | System.out.println("初始化OCR成功");
243 | } else {
244 | System.out.println("初始化OCR失败,请检查输出");
245 | }
246 | break;
247 | }
248 | case SOCKET_SERVER: {
249 | clientSocket = new Socket(serverAddr, serverPort);
250 | reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), StandardCharsets.UTF_8));
251 | writer = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream(), StandardCharsets.UTF_8));
252 | ocrReady = true;
253 | System.out.println("已连接到OCR套接字服务器,假设服务器已初始化成功");
254 | break;
255 | }
256 | }
257 |
258 |
259 | }
260 |
261 | /**
262 | * 使用图片路径进行 OCR
263 | * @param imgFile
264 | * @return
265 | * @throws IOException
266 | */
267 | public OcrResponse runOcr(File imgFile) throws IOException {
268 | if (mode == OcrMode.SOCKET_SERVER && !isLoopback) {
269 | System.out.println("套接字模式下服务器不在本地,发送路径可能失败");
270 | }
271 | Map reqJson = new HashMap<>();
272 | reqJson.put("image_path", imgFile.toString());
273 | return this.sendJsonToOcr(reqJson);
274 | }
275 |
276 | /**
277 | * 使用剪贴板中图片进行 OCR
278 | * @return
279 | * @throws IOException
280 | */
281 | public OcrResponse runOcrOnClipboard() throws IOException {
282 | if (mode == OcrMode.SOCKET_SERVER && !isLoopback) {
283 | System.out.println("套接字模式下服务器不在本地,发送剪贴板可能失败");
284 | }
285 | Map reqJson = new HashMap<>();
286 | reqJson.put("image_path", "clipboard");
287 | return this.sendJsonToOcr(reqJson);
288 | }
289 |
290 | /**
291 | * 使用 Base64 编码的图片进行 OCR
292 | * @param base64str
293 | * @return
294 | * @throws IOException
295 | */
296 | public OcrResponse runOcrOnImgBase64(String base64str) throws IOException {
297 | Map reqJson = new HashMap<>();
298 | reqJson.put("image_base64", base64str);
299 | return this.sendJsonToOcr(reqJson);
300 | }
301 |
302 | /**
303 | * 使用图片 Byte 数组进行 OCR
304 | * @param fileBytes
305 | * @return
306 | * @throws IOException
307 | */
308 | public OcrResponse runOcrOnImgBytes(byte[] fileBytes) throws IOException {
309 | return this.runOcrOnImgBase64(Base64.getEncoder().encodeToString(fileBytes));
310 | }
311 |
312 | private OcrResponse sendJsonToOcr(Map reqJson) throws IOException {
313 | if (!isAlive()) {
314 | throw new RuntimeException("OCR进程已经退出或连接已断开");
315 | }
316 | StringWriter sw = new StringWriter();
317 | EscapedWriter ew = new EscapedWriter(sw);
318 | gson.toJson(reqJson, ew);
319 |
320 | // 重建 socket,修复长时间无请求时 socket 断开(Software caused connection abort: socket write error )
321 | // https://github.com/hiroi-sora/PaddleOCR-json/issues/106
322 | if (OcrMode.SOCKET_SERVER == mode) {
323 | writer.close();
324 | reader.close();
325 | clientSocket.close();
326 | clientSocket = new Socket(serverAddr, serverPort);
327 | clientSocket.setKeepAlive(true);
328 | reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), StandardCharsets.UTF_8));
329 | writer = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream(), StandardCharsets.UTF_8));
330 | }
331 |
332 | writer.write(sw.getBuffer().toString());
333 | writer.write("\r\n");
334 | writer.flush();
335 | String resp = reader.readLine();
336 | System.out.println(resp);
337 |
338 | Map rawJsonObj = gson.fromJson(resp, Map.class);
339 | if (rawJsonObj.get("data") instanceof String) {
340 | return new OcrResponse((int)Double.parseDouble(rawJsonObj.get("code").toString()), rawJsonObj.get("data").toString());
341 | }
342 |
343 | return gson.fromJson(resp, OcrResponse.class);
344 | }
345 |
346 |
347 | private void checkIfLoopback() {
348 | if (this.mode != OcrMode.SOCKET_SERVER) return;
349 | try {
350 | InetAddress address = InetAddress.getByName(serverAddr);
351 | NetworkInterface networkInterface = NetworkInterface.getByInetAddress(address);
352 | if (networkInterface != null && networkInterface.isLoopback()) {
353 | this.isLoopback = true;
354 | } else {
355 | this.isLoopback = false;
356 | }
357 | } catch (Exception e) {
358 | // 非关键路径
359 | System.out.println("套接字模式,未能确认服务端是否在本地");
360 | }
361 | System.out.println("套接字模式下,服务端在本地:" + isLoopback);
362 | }
363 |
364 | private boolean isAlive() {
365 | switch (this.mode) {
366 | case LOCAL_PROCESS:
367 | return process.isAlive();
368 | case SOCKET_SERVER:
369 | return clientSocket.isConnected();
370 | }
371 | return false;
372 | }
373 |
374 |
375 | private static boolean isLinux() {
376 | return System.getProperty("os.name").toLowerCase().contains("linux");
377 | }
378 |
379 | @Override
380 | public void close() {
381 | if (isAlive()) {
382 | switch (this.mode) {
383 | case LOCAL_PROCESS: {
384 | process.destroy();
385 | break;
386 | }
387 | case SOCKET_SERVER: {
388 | try {
389 | clientSocket.close();
390 | } catch (IOException e) {
391 | e.printStackTrace();
392 | }
393 | break;
394 | }
395 | }
396 | }
397 | }
398 |
399 | }
400 |
--------------------------------------------------------------------------------