tencent_ocr_client.dart 4.89 KB
import 'dart:convert';
import 'dart:typed_data';

import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';

/// 腾讯云通用印刷体识别客户端。
///
/// 接口说明:https://cloud.tencent.com/document/product/866/33526
class TencentOcrClient {
  TencentOcrClient({
    required this.secretId,
    required this.secretKey,
  });

  final String secretId;
  final String secretKey;

  static const String _host = 'ocr.tencentcloudapi.com';
  static const String _service = 'ocr';
  static const String _action = 'GeneralBasicOCR';
  static const String _version = '2018-11-19';

  final Dio _dio = Dio(
    BaseOptions(
      connectTimeout: const Duration(seconds: 15),
      receiveTimeout: const Duration(seconds: 30),
      responseType: ResponseType.json,
    ),
  );

  /// 识别图片中的文字并返回多行文本。
  Future<String> recognize(Uint8List imageBytes) async {
    if (imageBytes.isEmpty) {
      throw const TencentOcrException(0, '图片内容为空');
    }

    final String payload = jsonEncode(<String, dynamic>{
      'ImageBase64': base64Encode(imageBytes),
      'LanguageType': 'zh',
    });
    final int timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
    final String date = _utcDate(timestamp);
    final String authorization = _authorization(timestamp, date, payload);

    final Response<Map<String, dynamic>> response;
    try {
      response = await _dio.post<Map<String, dynamic>>(
        'https://$_host',
        data: payload,
        options: Options(
          headers: <String, String>{
            'Authorization': authorization,
            'Content-Type': 'application/json; charset=utf-8',
            'Host': _host,
            'X-TC-Action': _action,
            'X-TC-Version': _version,
            'X-TC-Timestamp': '$timestamp',
            'X-TC-Region': 'ap-guangzhou',
          },
          contentType: 'application/json; charset=utf-8',
        ),
      );
    } on DioException catch (error) {
      throw TencentOcrException(
        error.response?.statusCode ?? 0,
        '图片识别网络请求失败,请检查网络',
      );
    }

    final Map<String, dynamic>? responseMap = response.data?['Response'] as Map<String, dynamic>?;
    if (responseMap == null) {
      throw TencentOcrException(response.statusCode ?? 0, '图片识别返回数据异常');
    }

    final Object? errorObj = responseMap['Error'];
    if (errorObj is Map) {
      throw TencentOcrException(
        errorObj['Code']?.toString().hashCode ?? 0,
        errorObj['Message']?.toString() ?? '图片识别失败',
      );
    }

    final List<dynamic>? detections = responseMap['TextDetections'] as List<dynamic>?;
    if (detections == null) {
      return '';
    }

    final List<String> lines = <String>[];
    for (final dynamic item in detections) {
      if (item is Map) {
        final String text = item['DetectedText']?.toString() ?? '';
        if (text.trim().isNotEmpty) {
          lines.add(text.trim());
        }
      }
    }
    return lines.join('\n');
  }

  String _authorization(int timestamp, String date, String payload) {
    final String canonicalRequest = <String>[
      'POST',
      '/',
      '',
      'content-type:application/json; charset=utf-8',
      'host:$_host',
      '',
      'content-type;host',
      _sha256Hex(payload),
    ].join('\n');

    final String credentialScope = '$date/$_service/tc3_request';
    final String stringToSign = <String>[
      'TC3-HMAC-SHA256',
      '$timestamp',
      credentialScope,
      _sha256Hex(canonicalRequest),
    ].join('\n');

    final List<int> secretDate = _hmacSha256(
      utf8.encode('TC3$secretKey'),
      date,
    );
    final List<int> secretService = _hmacSha256(secretDate, _service);
    final List<int> secretSigning = _hmacSha256(secretService, 'tc3_request');
    final String signature = _hexEncode(
      _hmacSha256(secretSigning, stringToSign),
    );

    return 'TC3-HMAC-SHA256 '
        'Credential=$secretId/$credentialScope, '
        'SignedHeaders=content-type;host, '
        'Signature=$signature';
  }

  String _utcDate(int timestamp) {
    final DateTime utc =
        DateTime.fromMillisecondsSinceEpoch(timestamp * 1000, isUtc: true);
    final String month = utc.month.toString().padLeft(2, '0');
    final String day = utc.day.toString().padLeft(2, '0');
    return '${utc.year}-$month-$day';
  }

  List<int> _hmacSha256(List<int> key, String data) {
    return Hmac(sha256, key).convert(utf8.encode(data)).bytes;
  }

  String _sha256Hex(String data) {
    return sha256.convert(utf8.encode(data)).toString();
  }

  String _hexEncode(List<int> bytes) {
    return bytes
        .map((int byte) => byte.toRadixString(16).padLeft(2, '0'))
        .join();
  }
}

class TencentOcrException implements Exception {
  const TencentOcrException(this.code, this.message);

  final int code;
  final String message;

  @override
  String toString() => 'TencentOcrException($code, $message)';
}