baidu_ocr_client.dart
4.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import 'dart:convert';
import 'dart:typed_data';
import 'package:dio/dio.dart';
/// 百度智能云通用文字识别(标准版)客户端。
///
/// 免费额度:个人实名认证 1000 次/月,企业实名认证 2000 次/月。
/// 文档:https://cloud.baidu.com/doc/OCR/s/dk3iqnq51
class BaiduOcrClient {
BaiduOcrClient({
required this.apiKey,
required this.secretKey,
});
final String apiKey;
final String secretKey;
static const String _tokenHost = 'aip.baidubce.com';
static const String _ocrHost = 'aip.baidubce.com';
static const String _ocrPath = '/rest/2.0/ocr/v1/general_basic';
final Dio _dio = Dio(
BaseOptions(
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 30),
responseType: ResponseType.json,
),
);
String? _accessToken;
DateTime? _tokenExpiresAt;
/// 识别图片中的文字并返回多行文本。
Future<String> recognize(Uint8List imageBytes) async {
if (imageBytes.isEmpty) {
throw const BaiduOcrException(0, '图片内容为空');
}
final String accessToken = await _fetchAccessToken();
final String imageBase64 = base64Encode(imageBytes);
final Response<Map<String, dynamic>> response;
try {
response = await _dio.post<Map<String, dynamic>>(
'https://$_ocrHost$_ocrPath?access_token=$accessToken',
data: <String, String>{
'image': imageBase64,
'language_type': 'CHN_ENG',
'detect_direction': 'true',
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
} on DioException catch (error) {
throw BaiduOcrException(
error.response?.statusCode ?? 0,
'图片识别网络请求失败,请检查网络',
);
}
final Map<String, dynamic>? data = response.data;
if (data == null) {
throw BaiduOcrException(response.statusCode ?? 0, '图片识别返回数据异常');
}
final int errorCode = data['error_code'] as int? ?? 0;
if (errorCode != 0) {
throw BaiduOcrException(
errorCode,
_friendlyMessage(errorCode, data['error_msg']?.toString()),
);
}
final List<dynamic>? wordsResult = data['words_result'] as List<dynamic>?;
if (wordsResult == null) {
return '';
}
final List<String> lines = <String>[];
for (final dynamic item in wordsResult) {
if (item is Map) {
final String text = item['words']?.toString() ?? '';
if (text.trim().isNotEmpty) {
lines.add(text.trim());
}
}
}
return lines.join('\n');
}
String _friendlyMessage(int errorCode, String? serverMessage) {
switch (errorCode) {
case 17:
return '今日免费识别额度已用完,请明天再试';
case 18:
return '识别请求太频繁,请稍后重试';
case 19:
return '本月免费识别额度已用完';
default:
return serverMessage ?? '图片识别失败';
}
}
Future<String> _fetchAccessToken() async {
final DateTime now = DateTime.now();
if (_accessToken != null &&
_tokenExpiresAt != null &&
now.isBefore(_tokenExpiresAt!)) {
return _accessToken!;
}
final Response<Map<String, dynamic>> response;
try {
response = await _dio.post<Map<String, dynamic>>(
'https://$_tokenHost/oauth/2.0/token',
queryParameters: <String, String>{
'grant_type': 'client_credentials',
'client_id': apiKey,
'client_secret': secretKey,
},
);
} on DioException catch (error) {
throw BaiduOcrException(
error.response?.statusCode ?? 0,
'获取图片识别凭证失败,请检查网络',
);
}
final Map<String, dynamic>? data = response.data;
final String? token = data?['access_token']?.toString();
if (token == null || token.isEmpty) {
throw BaiduOcrException(
0,
data?['error_description']?.toString() ?? '图片识别凭证获取失败',
);
}
final int expiresIn = data?['expires_in'] as int? ?? 2592000;
_accessToken = token;
_tokenExpiresAt = now.add(Duration(seconds: expiresIn - 60));
return token;
}
}
class BaiduOcrException implements Exception {
const BaiduOcrException(this.code, this.message);
final int code;
final String message;
@override
String toString() => 'BaiduOcrException($code, $message)';
}