├── 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 | 9 | 10 | 14 | 15 | 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 | 8 | 9 | 20 | 21 | 22 | 23 | 28 | 29 | 30 | 31 | 32 | 33 | 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 | [![CI](https://github.com/jerrylususu/PaddleOCR-json-java-api/actions/workflows/ci.yml/badge.svg)](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 | --------------------------------------------------------------------------------