Pinenut666 发表于 2022-11-11 17:12

ShareX-OCRRedirector,自用的截图OCR本地化程序代码

本帖最后由 Pinenut666 于 2022-11-11 17:58 编辑

## 起因
前几天有个朋友问我有没有什么不需要登录QQ的OCR软件,我一琢磨:Steam上那个ShareX不就挺好的。
结果下载下来一看,旧版(13.X)用的是ocr.space的免费服务,新版是直接调用的本地OCR,总结下来就是一句话:识别不精准,需求达不到。

于是怒(不是)而写了一个对接百度OCR的Java程序,和大家一起分享。

(另:因为这个东西既涉及到对ShareX的反编译(修改OCR地址),又涉及到一个小Java项目的编写,但是考虑到ShareX是开源软件,而且修改的部分只占很小一部分,更多的是Java项目的实现思考,所以最后还是决定放在编程语言区……如果放错了区,本人万分抱歉)

(另2:在写的时候有一些问题,也希望和大家一起探讨~)

## 确定思路和对象

因为我实在懒得编译原版代码,于是我从ocr.space的入手,因为它是靠网络API来进行OCR识别,偷梁换柱比较方便。

~~(不是,我点了个保存草稿,怎么给我发出去了,算了那我接着写好了)~~

先用Dnspy进行一番查看,考虑到是OCR,我们直接搜Ocr,搜索到一个OcrSpace


毕竟不是商业软件,这逻辑一眼就能看懂。我们跑去ocr.space 的网站 https://ocr.space/OCRAPI , 不费吹灰之力我们就能得到它的API信息,请求和返回都有。

既然我们要用自己的OCR做OCR识别,(而我还不擅长C#,算了我怎么好像什么都不擅长)那么我们遵循这样一个思想:

让软件认为返回值是原本的接口返回,而实际上的OCR工作,由百度等其他接口实现。

粗俗点说就是:软件不动,我们改我们的服务器的项目,让服务器的返回值和原本的接口返回值保持一致(而OCR的内容由百度等其他接口实现)。

## 编写代码

先用Idea创建一个SpringBoot项目,引入fastjson,引入Baidu SDK。

```
      <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
      <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.17</version>
      </dependency>
      <!-- https://mvnrepository.com/artifact/com.baidu.aip/java-sdk -->
      <dependency>
            <groupId>com.baidu.aip</groupId>
            <artifactId>java-sdk</artifactId>
            <version>4.16.12</version>
            <exclusions>
                <exclusion>
                  <artifactId>slf4j-simple</artifactId>
                  <groupId>org.slf4j</groupId>
                </exclusion>
            </exclusions>
      </dependency>
```

然后我们观察一下dnspy里的接口:

```
                        Dictionary<string, string> dictionary = new Dictionary<string, string>();
                        dictionary.Add("apikey", this.APIKey);
                        dictionary.Add("language", this.Language.ToString());
                        dictionary.Add("isOverlayRequired", this.Overlay.ToString());
                        UploadResult uploadResult = base.SendRequestFile("https://apipro1.ocr.space/parse/image", stream, fileName, "file", dictionary, null, null, HttpMethod.POST, "multipart/form-data", null);
```
我们不难看出传值方式为POST,然后是multipart/form-data,显然传输的是二进制文件流,有了这些回到ocr.space查看接口定义,对应一下不难得到:



于是我们先写一个controller放着:

```
    @RequestMapping("/select_ocr")
    public ocrSpaceResponse selectOcr(HttpServletRequest request,
                                    @RequestParam(value = "apikey") String apikey,
                                    @RequestParam(value = "language") String language,
                                    @RequestParam(value = "isOverlayRequired") String required,
                                    @RequestParam(value = "file") MultipartFile file
    ){}
```
之后我们创建一个service,用spring管理service对象:

```
public class BaiduAPI {
    public static final String APP_ID = "";
    public static final String API_KEY = "";
    public static final String SECRET_KEY = "";
    public static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
    AipOcr client;
    public BaiduAPI()
    {
      client = new AipOcr(APP_ID, API_KEY, SECRET_KEY);
      // 可选:设置网络连接参数
      client.setConnectionTimeoutInMillis(2000);
      client.setSocketTimeoutInMillis(60000);
    }
```

**multipart file先转换成base64,这样方便我们处理。**

(结果根据百度SDK的说法,给百度SDK传值要么传图片地址,要么传图片数组,那么我们还需要一个base64转byte[]数组的代码。网上百度一下,就能找到对应的需求:

```
package com.example.demo.tools;

import com.example.demo.Demo1Application;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class ImageToBase64 {
    private static final org.slf4j.Logger logger = LoggerFactory.getLogger(Demo1Application.class);
    /**
   * 将MultipartFile 图片文件编码为base64
   * @Param file
   * @return
   * @throws Exception
   */
    public static String generateBase64(MultipartFile file){
      if (file == null || file.isEmpty()) {
            throw new RuntimeException("图片不能为空!");
      }
      String fileName = file.getOriginalFilename();
      String fileType = fileName.substring(fileName.lastIndexOf("."));
      String contentType = file.getContentType();
      byte[] imageBytes = null;
      String base64EncoderImg="";
      try {
            imageBytes = file.getBytes();
            BASE64Encoder base64Encoder =new BASE64Encoder();
            /**
             * 1.Java使用BASE64Encoder 需要添加图片头("data:" + contentType + ";base64,"),
             *   其中contentType是文件的内容格式。
             * 2.Java中在使用BASE64Enconder().encode()会出现字符串换行问题,这是因为RFC 822中规定,
             *   每72个字符中加一个换行符号,这样会造成在使用base64字符串时出现问题,
             *   所以我们在使用时要先用replaceAll("[\\s*\t\n\r]", "")解决换行的问题。
             */
            base64EncoderImg = "data:" + contentType + ";base64," + base64Encoder.encode(imageBytes);
            base64EncoderImg = base64EncoderImg.replaceAll("[\\s*\t\n\r]", "");
      } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
      }
      //logger.info("当前图片为:" + base64EncoderImg);
      return base64EncoderImg;
    }
    public static byte[] base64ToImgByteArray(String base64){
      try{
      sun.misc.BASE64Decoder decoder = new sun.misc.BASE64Decoder();
      //因为参数base64来源不一样,需要将附件数据替换清空掉。如果此入参来自canvas.toDataURL("image/png");
      base64 = base64.replaceAll("data:image/png;base64,", "");
      //base64解码并转为二进制数组
      byte[] bytes = decoder.decodeBuffer(base64);
      for (int i = 0; i < bytes.length; ++i) {
            if (bytes < 0) {// 调整异常数据
                bytes += 256;
            }
      }
      return bytes;
      }
      catch (Exception e)
      {
            return null;
      }
    }
//这里对参数属性,参数来自html img.src或者canvas.toDataURL("image/png");
//如果是其他类型的图片请做base64 = base64.replaceAll("data:image/png;base64,", "");里面的png修改
}

```

接下来把ocr.space的接口返回和百度接口的返回JSON拖到GSONFormatplus里,生成对应的对象:(对象名分别是ocrSpaceResponse和BaiduResponse)

之后编写获取百度OCR的代码:
```
    public ocrSpaceResponse getBaiduResult(String base64Pic,String language)
    {
      HashMap<String,String> options = new HashMap<>();
      //options.put("detect_language", "true");
                                //这里的JSONObject并不是fastjson,而是org.json,所以还要转换一下,才能用到GsonFormatplus的对象里
      JSONObject json = client.basicGeneral(base64ToImgByteArray(base64Pic),options);
      com.alibaba.fastjson.JSONObject c = com.alibaba.fastjson.JSONObject.parseObject(json.toString());
                                //这里
      BaiduResponse response1 = com.alibaba.fastjson.JSONObject.toJavaObject(c, BaiduResponse.class);
      if(response1.getErrorMsg()==null)
      {
            StringBuilder wordback = new StringBuilder();
            for(BaiduResponse.WordsResultDTO word:response1.getWordsResult())
            {
                //拼凑数据,因为百度返回和OCRSPACE对换行的处理不一样,这样就换成了OcrSpace的换行方式
                wordback.append(word.getWords()).append("\r\n");
            }
            return CreateResponse.createResponse(wordback);
      }
      else {
            return null;
      }
    }
```

修改一下client.basicGeneral,改成client.basicAccurateGeneral就是百度高精度了。
再看一眼这里的代码:
```
                        if (!uploadResult.IsSuccess)
                        {
                                uploadResult = base.SendRequestFile("https://apipro2.ocr.space/parse/image", stream, fileName, "file", dictionary, null, null, HttpMethod.POST, "multipart/form-data", null);
                        }
                        if (uploadResult.IsSuccess)
                        {
                                return JsonConvert.DeserializeObject<OCRSpaceResponse>(uploadResult.Response);
                        }
```
这个!IsSuccess就代表第一个接口失败。也是,毕竟百度免费SDK额度有限,用完了高精度的怎么办呢,再用低精度的就好。(毕竟加一起2000条,一个月用完属于是有点难度)

于是首先创建一个报错的异常
```
package com.example.demo.exceptions;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(code= HttpStatus.INTERNAL_SERVER_ERROR,reason="Failed so use another!")
public class ServerException extends Exception {

}
```

再从这里,检测到如果为空就抛出异常

```
    @RequestMapping("/baidu_high_ocr")
    public ocrSpaceResponse baidu_high_ocr(HttpServletRequest request,
                                           @RequestParam(value = "apikey") String apikey,
                                           @RequestParam(value = "language") String language,
                                           @RequestParam(value = "isOverlayRequired") String required,
                                           @RequestParam(value = "file") MultipartFile file
    ) throws ServerException {
      logger.info("使用百度高精度");
      String base = ImageToBase64.generateBase64(file);
      ocrSpaceResponse response = baidu.getBaiduHighResult(base, "schinese");
      if (response == null) {
            throw new ServerException();
      }
      return response;
    }
```

之后只需要修改一下dnspy这里的地址,再启动程序即可。(我的Springboot开的端口是8082)

```
                public OCRSpaceResponse DoOCR(Stream stream, string fileName)
                {
                        Dictionary<string, string> dictionary = new Dictionary<string, string>();
                        dictionary.Add("apikey", this.APIKey);
                        dictionary.Add("language", this.Language.ToString());
                        dictionary.Add("isOverlayRequired", this.Overlay.ToString());
                        UploadResult uploadResult = base.SendRequestFile("http://127.0.0.1:8082/baidu_low_ocr", stream, fileName, "file", dictionary, null, null, HttpMethod.POST, "multipart/form-data", null);
                        if (!uploadResult.IsSuccess)
                        {
                                uploadResult = base.SendRequestFile("http://127.0.0.1:8082/baidu_high_ocr", stream, fileName, "file", dictionary, null, null, HttpMethod.POST, "multipart/form-data", null);
                        }
                        if (uploadResult.IsSuccess)
                        {
                                return JsonConvert.DeserializeObject<OCRSpaceResponse>(uploadResult.Response);
                        }
                        return null;
                }
```

来一张成功的截图:



最后有几个问题想和各位探讨一下:

1. 在使用BaiduSdk的时候,它的JSON和我们的JSON库不一样,我是把它转换成了json文本,然后再用自己用的JSON库转换一次,才能让它直接赋值给对象。有没有比较好的方案去解决这个问题?
2. 可以看到BaiduSDK的key值等是写死的(具体看我项目文件),有没有比较合适的手段让他从文件中读写?之前尝试过使用Springboot的application.properties但是赋值就变成空了(@Value注解)

还望各位大佬不吝赐教,小弟在此感谢各位。





Pinenut666 发表于 2022-11-12 01:35

xouou 发表于 2022-11-11 20:37
百度的开源离线OCR库paddle-ocr, 不比其它在线的差
配合天若OCR的魔改版, 完美使用, 不需要申请api就能使 ...

受教了,有空试一试看

xouou 发表于 2022-11-11 20:37

百度的开源离线OCR库paddle-ocr, 不比其它在线的差
配合天若OCR的魔改版, 完美使用, 不需要申请api就能使用天若OCR翻译

eaglexiong 发表于 2022-11-13 17:28

自己动手还不错,用脚本或Acrobat 也行的

william_ni 发表于 2022-11-14 12:07

shareX 不是开源的吗?为什么还要 反编译?

Pinenut666 发表于 2022-11-14 15:46

william_ni 发表于 2022-11-14 12:07
shareX 不是开源的吗?为什么还要 反编译?

因为懒得下载工具去编译,电脑不是我的,下个dnspy还行,下个全套编译工具感觉没必要。
所以才扔到编程语言区啦

senlly 发表于 2023-3-12 07:36

给个成品啊

hesi2010bit 发表于 2023-4-18 15:45

感觉是个好东西,不过下下来不知道咋用。。。没有可执行程序

LOVEPOJIEcd 发表于 2023-4-21 09:36

不会用{:1_909:}——谢谢

xors 发表于 2023-4-21 16:59

{:1_904:} 2问题能不能从包外文件里去读取呢?在application.properties里定义配置文件路径变量,然后启动项目的时候再赋值
页: [1] 2
查看完整版本: ShareX-OCRRedirector,自用的截图OCR本地化程序代码