|
@@ -0,0 +1,171 @@
|
|
|
+package com.qmth.ops.biz.ai.client.aliyun;
|
|
|
+
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import com.qmth.boot.core.exception.ParameterException;
|
|
|
+import com.qmth.boot.core.exception.ReentrantException;
|
|
|
+import com.qmth.boot.core.exception.StatusException;
|
|
|
+import com.qmth.boot.core.exception.UnauthorizedException;
|
|
|
+import com.qmth.boot.tools.models.ByteArray;
|
|
|
+import com.qmth.boot.tools.uuid.FastUUID;
|
|
|
+import com.qmth.ops.biz.ai.client.OcrApiClient;
|
|
|
+import com.qmth.ops.biz.ai.client.OcrApiConfig;
|
|
|
+
|
|
|
+import javax.crypto.Mac;
|
|
|
+import javax.crypto.SecretKey;
|
|
|
+import javax.crypto.spec.SecretKeySpec;
|
|
|
+import java.io.File;
|
|
|
+import java.io.IOException;
|
|
|
+import java.io.UnsupportedEncodingException;
|
|
|
+import java.net.URL;
|
|
|
+import java.net.URLDecoder;
|
|
|
+import java.net.URLEncoder;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.time.Instant;
|
|
|
+import java.time.temporal.ChronoField;
|
|
|
+import java.util.*;
|
|
|
+
|
|
|
+public class AliyunOcrClient extends OcrApiClient {
|
|
|
+
|
|
|
+ private String action;
|
|
|
+
|
|
|
+ public AliyunOcrClient(OcrApiConfig config, String action) {
|
|
|
+ super(config);
|
|
|
+ this.action = action;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected String buildResult(byte[] data, ObjectMapper mapper) throws IOException {
|
|
|
+ return mapper.readValue(data, AliyunOcrResult.class).getDataContent(mapper);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected String handleError(byte[] data, int statusCode, ObjectMapper mapper) {
|
|
|
+ AliyunError error = null;
|
|
|
+ if (data != null) {
|
|
|
+ try {
|
|
|
+ error = mapper.readValue(data, AliyunError.class);
|
|
|
+ } catch (Exception e) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+ switch (statusCode) {
|
|
|
+ case 400:
|
|
|
+ case 413:
|
|
|
+ throw new ParameterException(error != null ? error.getMessage() : "ocr request parameter error");
|
|
|
+ case 401:
|
|
|
+ throw new UnauthorizedException(error != null ? error.getMessage() : "ocr api unauthorized");
|
|
|
+ case 503:
|
|
|
+ case 504:
|
|
|
+ throw new ReentrantException(error != null ? error.getMessage() : "ocr api temporary faile");
|
|
|
+ default:
|
|
|
+ throw new StatusException(error != null ? error.getMessage() : "ocr api error");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String percentEncode(String value) {
|
|
|
+ try {
|
|
|
+ return value != null ?
|
|
|
+ URLEncoder.encode(value, StandardCharsets.UTF_8.name()).replace("+", "%20").replace("*", "%2A")
|
|
|
+ .replace("%7E", "~") :
|
|
|
+ null;
|
|
|
+ } catch (UnsupportedEncodingException ignore) {
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String urlEncode(String value) {
|
|
|
+ try {
|
|
|
+ return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
|
|
|
+ } catch (UnsupportedEncodingException ignore) {
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String urlDecode(String value) {
|
|
|
+ try {
|
|
|
+ return URLDecoder.decode(value, StandardCharsets.UTF_8.name());
|
|
|
+ } catch (UnsupportedEncodingException ignore) {
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public String getSignature(String url, String httpMethod) throws Exception {
|
|
|
+ // 解析url中的参数部分
|
|
|
+ URL u = new URL(url);
|
|
|
+ String query = u.getQuery();
|
|
|
+ // 获取范化的请求字符串
|
|
|
+ String canonicalString = Arrays.stream(query.split("&")).map(s -> s.split("="))
|
|
|
+ // 去掉不合法的空参数(例如: https://example?Url=&AccessKeyId=)
|
|
|
+ .filter(arr -> arr.length > 1)
|
|
|
+ // 根据请求参数的字典顺序排序
|
|
|
+ .sorted(Comparator.comparing(arr -> arr[0]))
|
|
|
+ // 按照 RFC3986 规则编码参数名称、参数值
|
|
|
+ .map(arr -> String.format("%s=%s", percentEncode(arr[0]), percentEncode(urlDecode(arr[1]))))
|
|
|
+ // 用"&"拼接起来编码后的参数
|
|
|
+ .reduce((s1, s2) -> s1 + "&" + s2).orElse("");
|
|
|
+ // 将规范化字符串拼接成待签名的字符串
|
|
|
+ String stringToSign = httpMethod + "&" + percentEncode("/") + "&" + percentEncode(canonicalString);
|
|
|
+ // 把 AccessKeySecret加上"&"构成 HMAC-SHA1 算法的key
|
|
|
+ String secret = getConfig().getSecret() + "&";
|
|
|
+ // HMAC-SHA1 编码后的bytes
|
|
|
+ SecretKey secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA1");
|
|
|
+ Mac mac = Mac.getInstance("HmacSHA1");
|
|
|
+ mac.init(secretKey);
|
|
|
+ byte[] hashBytes = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
|
|
|
+ // 按照 base64 编码规则生成最后的签名字符串
|
|
|
+ return Base64.getEncoder().encodeToString(hashBytes);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取公共请求参数。公共请求参数详见文档:https://help.aliyun.com/document_detail/145074.html
|
|
|
+ *
|
|
|
+ * @return 公共请求参数组成的字典
|
|
|
+ */
|
|
|
+ private Map<String, String> getCommonParameters(String action) {
|
|
|
+ return new HashMap<String, String>() {{
|
|
|
+ put("Action", action); // 调用的接口名称,此处以 RecognizeGeneral 为例
|
|
|
+ put("Version", "2021-07-07"); // API版本。OCR的固定值:2021-07-07
|
|
|
+ put("Format", "JSON"); // 指定接口返回数据的格式,可以选择 JSON 或者 XML
|
|
|
+ put("AccessKeyId", getConfig().getKey()); // 您的AccessKeyId
|
|
|
+ put("SignatureNonce", FastUUID.get()); // 签名唯一随机数,每次调用不可重复
|
|
|
+ put("Timestamp", Instant.now().with(ChronoField.NANO_OF_SECOND, 0)
|
|
|
+ .toString()); // 需要Java8及以上版本。如果您需要使用更低版本Java,请更换获取时间戳的方法
|
|
|
+ put("SignatureMethod", "HMAC-SHA1"); // 签名方式。目前为固定值 HMAC-SHA1
|
|
|
+ put("SignatureVersion", "1.0"); // 签名方式。目前为固定值 1.0
|
|
|
+ }};
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 识别本地文件代码示例。以 RecognizeGeneral 接口为例。
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ protected String buildUrl() throws Exception {
|
|
|
+ // 获取公共请求参数
|
|
|
+ Map<String, String> parametersMap = getCommonParameters(action);
|
|
|
+ // 初始化请求URL
|
|
|
+ StringBuilder urlBuilder = new StringBuilder(getConfig().getUrl());
|
|
|
+ urlBuilder.append("?");
|
|
|
+ // 把业务参数拼接到请求链接中
|
|
|
+ for (Map.Entry<String, String> entry : parametersMap.entrySet()) {
|
|
|
+ // entry.getValue() 可能出现"&"等符号。 需要encode
|
|
|
+ urlBuilder.append(String.format("%s=%s", entry.getKey(), urlEncode(entry.getValue()))).append('&');
|
|
|
+ }
|
|
|
+ // 去掉最后的"&"
|
|
|
+ String url = urlBuilder.substring(0, urlBuilder.length() - 1);
|
|
|
+ // 获取签名
|
|
|
+ String signature = getSignature(url, "POST");
|
|
|
+ // 按照 RFC3986 规则编码签名,并添加到最终的请求链接上
|
|
|
+ url += String.format("&Signature=%s", percentEncode(signature));
|
|
|
+ return url;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static void main(String[] args) throws Exception {
|
|
|
+ OcrApiConfig config = new OcrApiConfig();
|
|
|
+ config.setUrl("https://ocr-api.cn-hangzhou.aliyuncs.com/");
|
|
|
+ config.setKey("LTAI5t6D5p62tDjYgwSz2mTR");
|
|
|
+ config.setSecret("twrXT7Dp1kG1bV5HZn6vgpoypu9PnZ");
|
|
|
+ config.setQps(0);
|
|
|
+ AliyunOcrClient client = new AliyunOcrClient(config, "RecognizeHandwriting");
|
|
|
+ System.out.println(client.call(ByteArray.fromFile(new File("/Users/luoshi/Downloads/cache/1-1.jpg")).value()));
|
|
|
+ }
|
|
|
+
|
|
|
+}
|